feat: IS_PRIMOP

This commit is contained in:
2026-01-02 23:07:43 +08:00
parent f1670e8397
commit 073c95f2c3
7 changed files with 307 additions and 139 deletions

View File

@@ -1,6 +1,6 @@
use std::process::Command;
use std::path::Path;
use std::env;
use std::path::Path;
use std::process::Command;
fn main() {
let runtime_ts_dir = Path::new("runtime-ts");
@@ -17,7 +17,11 @@ fn main() {
if !runtime_ts_dir.join("node_modules").exists() {
println!("Installing npm dependencies...");
let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
let npm_cmd = if cfg!(target_os = "windows") {
"npm.cmd"
} else {
"npm"
};
let status = Command::new(npm_cmd)
.arg("install")
.current_dir(runtime_ts_dir)
@@ -30,7 +34,11 @@ fn main() {
}
println!("Running TypeScript type checking...");
let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
let npm_cmd = if cfg!(target_os = "windows") {
"npm.cmd"
} else {
"npm"
};
let status = Command::new(npm_cmd)
.arg("run")
.arg("typecheck")

View File

@@ -5,6 +5,98 @@
import { create_thunk } from '../thunk';
/**
* Symbol used to mark functions as primops (primitive operations)
* This is similar to IS_THUNK but for builtin functions
*/
export const IS_PRIMOP = Symbol('is_primop');
/**
* Metadata interface for primop functions
*/
export interface PrimopMetadata {
/** The name of the primop (e.g., "add", "map") */
name: string;
/** Total arity of the function (number of arguments it expects) */
arity: number;
/** Number of arguments already applied (for partial applications) */
applied: number;
}
/**
* Mark a function as a primop with metadata
* For curried functions, this recursively marks each layer
*
* @param func - The function to mark
* @param name - Name of the primop
* @param arity - Total number of arguments expected
* @param applied - Number of arguments already applied (default: 0)
* @returns The marked function
*/
export const markPrimop = <T extends Function>(
func: T,
name: string,
arity: number,
applied: number = 0
): T => {
// Mark this function as a primop
(func as any)[IS_PRIMOP] = {
name,
arity,
applied,
} as PrimopMetadata;
// If this is a curried function and not fully applied,
// wrap it to mark the next layer too
if (applied < arity - 1) {
const wrappedFunc = ((...args: any[]) => {
const result = func(...args);
// If result is a function, mark it as the next layer
if (typeof result === 'function') {
return markPrimop(result, name, arity, applied + args.length);
}
return result;
}) as any;
// Copy the primop metadata to the wrapper
wrappedFunc[IS_PRIMOP] = {
name,
arity,
applied,
} as PrimopMetadata;
return wrappedFunc as T;
}
return func;
};
/**
* Type guard to check if a value is a primop
* @param value - Value to check
* @returns true if value is marked as a primop
*/
export const is_primop = (value: unknown): value is Function & { [IS_PRIMOP]: PrimopMetadata } => {
return (
typeof value === 'function' &&
IS_PRIMOP in value &&
typeof value[IS_PRIMOP] === 'object' &&
value[IS_PRIMOP] !== null
);
};
/**
* Get primop metadata from a function
* @param func - Function to get metadata from
* @returns Metadata if function is a primop, undefined otherwise
*/
export const get_primop_metadata = (func: unknown): PrimopMetadata | undefined => {
if (is_primop(func)) {
return func[IS_PRIMOP];
}
return undefined;
};
// Import all builtin categories
import * as arithmetic from './arithmetic';
import * as math from './math';
@@ -24,144 +116,133 @@ import * as misc from './misc';
* All functions are curried for Nix semantics:
* - Single argument functions: (a) => result
* - Multi-argument functions: (a) => (b) => result
*
* All primop functions are marked with IS_PRIMOP symbol for runtime introspection
*/
export const builtins: any = {
// Arithmetic (curried binary functions)
add: arithmetic.add,
sub: arithmetic.sub,
mul: arithmetic.mul,
div: arithmetic.div,
bitAnd: arithmetic.bitAnd,
bitOr: arithmetic.bitOr,
bitXor: arithmetic.bitXor,
lessThan: arithmetic.lessThan,
add: markPrimop(arithmetic.add, 'add', 2),
sub: markPrimop(arithmetic.sub, 'sub', 2),
mul: markPrimop(arithmetic.mul, 'mul', 2),
div: markPrimop(arithmetic.div, 'div', 2),
bitAnd: markPrimop(arithmetic.bitAnd, 'bitAnd', 2),
bitOr: markPrimop(arithmetic.bitOr, 'bitOr', 2),
bitXor: markPrimop(arithmetic.bitXor, 'bitXor', 2),
lessThan: markPrimop(arithmetic.lessThan, 'lessThan', 2),
// Math
ceil: math.ceil,
floor: math.floor,
ceil: markPrimop(math.ceil, 'ceil', 1),
floor: markPrimop(math.floor, 'floor', 1),
// Type checking
isAttrs: typeCheck.isAttrs,
isBool: typeCheck.isBool,
isFloat: typeCheck.isFloat,
isFunction: typeCheck.isFunction,
isInt: typeCheck.isInt,
isList: typeCheck.isList,
isNull: typeCheck.isNull,
isPath: typeCheck.isPath,
isString: typeCheck.isString,
typeOf: typeCheck.typeOf,
isAttrs: markPrimop(typeCheck.isAttrs, 'isAttrs', 1),
isBool: markPrimop(typeCheck.isBool, 'isBool', 1),
isFloat: markPrimop(typeCheck.isFloat, 'isFloat', 1),
isFunction: markPrimop(typeCheck.isFunction, 'isFunction', 1),
isInt: markPrimop(typeCheck.isInt, 'isInt', 1),
isList: markPrimop(typeCheck.isList, 'isList', 1),
isNull: markPrimop(typeCheck.isNull, 'isNull', 1),
isPath: markPrimop(typeCheck.isPath, 'isPath', 1),
isString: markPrimop(typeCheck.isString, 'isString', 1),
typeOf: markPrimop(typeCheck.typeOf, 'typeOf', 1),
// List operations
map: list.map,
filter: list.filter,
length: list.length,
head: list.head,
tail: list.tail,
elem: list.elem,
elemAt: list.elemAt,
concatLists: list.concatLists,
concatMap: list.concatMap,
'foldl\'': list.foldlPrime,
sort: list.sort,
partition: list.partition,
genList: list.genList,
all: list.all,
any: list.any,
map: markPrimop(list.map, 'map', 2),
filter: markPrimop(list.filter, 'filter', 2),
length: markPrimop(list.length, 'length', 1),
head: markPrimop(list.head, 'head', 1),
tail: markPrimop(list.tail, 'tail', 1),
elem: markPrimop(list.elem, 'elem', 2),
elemAt: markPrimop(list.elemAt, 'elemAt', 2),
concatLists: markPrimop(list.concatLists, 'concatLists', 1),
concatMap: markPrimop(list.concatMap, 'concatMap', 2),
'foldl\'': markPrimop(list.foldlPrime, 'foldl\'', 3),
sort: markPrimop(list.sort, 'sort', 2),
partition: markPrimop(list.partition, 'partition', 2),
genList: markPrimop(list.genList, 'genList', 2),
all: markPrimop(list.all, 'all', 2),
any: markPrimop(list.any, 'any', 2),
// Attribute set operations
attrNames: attrs.attrNames,
attrValues: attrs.attrValues,
getAttr: attrs.getAttr,
hasAttr: attrs.hasAttr,
mapAttrs: attrs.mapAttrs,
listToAttrs: attrs.listToAttrs,
intersectAttrs: attrs.intersectAttrs,
catAttrs: attrs.catAttrs,
groupBy: attrs.groupBy,
attrNames: markPrimop(attrs.attrNames, 'attrNames', 1),
attrValues: markPrimop(attrs.attrValues, 'attrValues', 1),
getAttr: markPrimop(attrs.getAttr, 'getAttr', 2),
hasAttr: markPrimop(attrs.hasAttr, 'hasAttr', 2),
mapAttrs: markPrimop(attrs.mapAttrs, 'mapAttrs', 2),
listToAttrs: markPrimop(attrs.listToAttrs, 'listToAttrs', 1),
intersectAttrs: markPrimop(attrs.intersectAttrs, 'intersectAttrs', 2),
catAttrs: markPrimop(attrs.catAttrs, 'catAttrs', 2),
groupBy: markPrimop(attrs.groupBy, 'groupBy', 2),
// String operations
stringLength: string.stringLength,
substring: string.substring,
concatStringsSep: string.concatStringsSep,
baseNameOf: string.baseNameOf,
stringLength: markPrimop(string.stringLength, 'stringLength', 1),
substring: markPrimop(string.substring, 'substring', 3),
concatStringsSep: markPrimop(string.concatStringsSep, 'concatStringsSep', 2),
baseNameOf: markPrimop(string.baseNameOf, 'baseNameOf', 1),
// Functional
seq: functional.seq,
deepSeq: functional.deepSeq,
abort: functional.abort,
throw: functional.throwFunc,
trace: functional.trace,
warn: functional.warn,
break: functional.breakFunc,
seq: markPrimop(functional.seq, 'seq', 2),
deepSeq: markPrimop(functional.deepSeq, 'deepSeq', 2),
abort: markPrimop(functional.abort, 'abort', 1),
throw: markPrimop(functional.throwFunc, 'throw', 1),
trace: markPrimop(functional.trace, 'trace', 2),
warn: markPrimop(functional.warn, 'warn', 2),
break: markPrimop(functional.breakFunc, 'break', 1),
// I/O (unimplemented)
import: io.importFunc,
scopedImport: io.scopedImport,
fetchClosure: io.fetchClosure,
fetchGit: io.fetchGit,
fetchTarball: io.fetchTarball,
fetchTree: io.fetchTree,
fetchurl: io.fetchurl,
readDir: io.readDir,
readFile: io.readFile,
readFileType: io.readFileType,
pathExists: io.pathExists,
path: io.path,
toFile: io.toFile,
toPath: io.toPath,
filterSource: io.filterSource,
findFile: io.findFile,
getEnv: io.getEnv,
import: markPrimop(io.importFunc, 'import', 1),
scopedImport: markPrimop(io.scopedImport, 'scopedImport', 2),
fetchClosure: markPrimop(io.fetchClosure, 'fetchClosure', 1),
fetchGit: markPrimop(io.fetchGit, 'fetchGit', 1),
fetchTarball: markPrimop(io.fetchTarball, 'fetchTarball', 1),
fetchTree: markPrimop(io.fetchTree, 'fetchTree', 1),
fetchurl: markPrimop(io.fetchurl, 'fetchurl', 1),
readDir: markPrimop(io.readDir, 'readDir', 1),
readFile: markPrimop(io.readFile, 'readFile', 1),
readFileType: markPrimop(io.readFileType, 'readFileType', 1),
pathExists: markPrimop(io.pathExists, 'pathExists', 1),
path: markPrimop(io.path, 'path', 1),
toFile: markPrimop(io.toFile, 'toFile', 2),
toPath: markPrimop(io.toPath, 'toPath', 1),
filterSource: markPrimop(io.filterSource, 'filterSource', 2),
findFile: markPrimop(io.findFile, 'findFile', 2),
getEnv: markPrimop(io.getEnv, 'getEnv', 1),
// Conversion (unimplemented)
fromJSON: conversion.fromJSON,
fromTOML: conversion.fromTOML,
toJSON: conversion.toJSON,
toXML: conversion.toXML,
toString: conversion.toString,
fromJSON: markPrimop(conversion.fromJSON, 'fromJSON', 1),
fromTOML: markPrimop(conversion.fromTOML, 'fromTOML', 1),
toJSON: markPrimop(conversion.toJSON, 'toJSON', 1),
toXML: markPrimop(conversion.toXML, 'toXML', 1),
toString: markPrimop(conversion.toString, 'toString', 1),
// Miscellaneous (unimplemented)
getContext: misc.getContext,
hasContext: misc.hasContext,
hashFile: misc.hashFile,
hashString: misc.hashString,
convertHash: misc.convertHash,
unsafeDiscardOutputDependency: misc.unsafeDiscardOutputDependency,
unsafeDiscardStringContext: misc.unsafeDiscardStringContext,
unsafeGetAttrPos: misc.unsafeGetAttrPos,
addDrvOutputDependencies: misc.addDrvOutputDependencies,
compareVersions: misc.compareVersions,
dirOf: misc.dirOf,
flakeRefToString: misc.flakeRefToString,
functionArgs: misc.functionArgs,
genericClosure: misc.genericClosure,
getFlake: misc.getFlake,
match: misc.match,
outputOf: misc.outputOf,
parseDrvName: misc.parseDrvName,
parseFlakeName: misc.parseFlakeName,
placeholder: misc.placeholder,
replaceStrings: misc.replaceStrings,
split: misc.split,
splitVersion: misc.splitVersion,
traceVerbose: misc.traceVerbose,
tryEval: misc.tryEval,
zipAttrsWith: misc.zipAttrsWith,
getContext: markPrimop(misc.getContext, 'getContext', 1),
hasContext: markPrimop(misc.hasContext, 'hasContext', 1),
hashFile: markPrimop(misc.hashFile, 'hashFile', 2),
hashString: markPrimop(misc.hashString, 'hashString', 2),
convertHash: markPrimop(misc.convertHash, 'convertHash', 2),
unsafeDiscardOutputDependency: markPrimop(misc.unsafeDiscardOutputDependency, 'unsafeDiscardOutputDependency', 1),
unsafeDiscardStringContext: markPrimop(misc.unsafeDiscardStringContext, 'unsafeDiscardStringContext', 1),
unsafeGetAttrPos: markPrimop(misc.unsafeGetAttrPos, 'unsafeGetAttrPos', 2),
addDrvOutputDependencies: markPrimop(misc.addDrvOutputDependencies, 'addDrvOutputDependencies', 2),
compareVersions: markPrimop(misc.compareVersions, 'compareVersions', 2),
dirOf: markPrimop(misc.dirOf, 'dirOf', 1),
flakeRefToString: markPrimop(misc.flakeRefToString, 'flakeRefToString', 1),
functionArgs: markPrimop(misc.functionArgs, 'functionArgs', 1),
genericClosure: markPrimop(misc.genericClosure, 'genericClosure', 1),
getFlake: markPrimop(misc.getFlake, 'getFlake', 1),
match: markPrimop(misc.match, 'match', 2),
outputOf: markPrimop(misc.outputOf, 'outputOf', 2),
parseDrvName: markPrimop(misc.parseDrvName, 'parseDrvName', 1),
parseFlakeName: markPrimop(misc.parseFlakeName, 'parseFlakeName', 1),
placeholder: markPrimop(misc.placeholder, 'placeholder', 1),
replaceStrings: markPrimop(misc.replaceStrings, 'replaceStrings', 3),
split: markPrimop(misc.split, 'split', 2),
splitVersion: markPrimop(misc.splitVersion, 'splitVersion', 1),
traceVerbose: markPrimop(misc.traceVerbose, 'traceVerbose', 2),
tryEval: markPrimop(misc.tryEval, 'tryEval', 1),
zipAttrsWith: markPrimop(misc.zipAttrsWith, 'zipAttrsWith', 2),
// Meta - self-reference and constants
builtins: create_thunk(() => builtins), // Recursive reference
builtins: create_thunk(() => builtins),
currentSystem: create_thunk(() => {
throw 'Not implemented: currentSystem';
}),
currentTime: create_thunk(() => Date.now()),
// Constants (special keys)
false: false,
true: true,
null: null,
// Version information
langVersion: 6,
nixPath: [],
nixVersion: 'NIX_JS_VERSION',

View File

@@ -7,7 +7,7 @@
import { create_thunk, force, is_thunk, IS_THUNK } from './thunk';
import { select, select_with_default, validate_params } from './helpers';
import { op } from './operators';
import { builtins } from './builtins';
import { builtins, IS_PRIMOP } from './builtins';
export type NixRuntime = typeof Nix;
@@ -26,6 +26,7 @@ export const Nix = {
op,
builtins,
IS_PRIMOP,
};
globalThis.Nix = Nix;

View File

@@ -44,7 +44,6 @@ impl Default for Context {
"true",
"false",
"null",
"abort",
"baseNameOf",
"break",
@@ -681,10 +680,7 @@ mod test {
Value::Const(Const::Float(3.5))
);
assert_eq!(
ctx.eval("(-7) / 3").unwrap(),
Value::Const(Const::Int(-2))
);
assert_eq!(ctx.eval("(-7) / 3").unwrap(), Value::Const(Const::Int(-2)));
}
#[test]
@@ -700,8 +696,7 @@ mod test {
// Test builtin mul with large numbers
assert_eq!(
ctx.eval("builtins.mul 1000000000 1000000000")
.unwrap(),
ctx.eval("builtins.mul 1000000000 1000000000").unwrap(),
Value::Const(Const::Int(1000000000000000000i64))
);
}

View File

@@ -23,14 +23,17 @@ pub fn run(script: &str) -> Result<Value> {
struct RuntimeContext<'a, 'b> {
scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>,
is_thunk_symbol: Option<v8::Local<'a, v8::Symbol>>,
is_primop_symbol: Option<v8::Local<'a, v8::Symbol>>,
}
impl<'a, 'b> RuntimeContext<'a, 'b> {
fn new(scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>) -> Self {
let is_thunk_symbol = Self::get_is_thunk_symbol(scope);
let is_primop_symbol = Self::get_is_primop_symbol(scope);
Self {
scope,
is_thunk_symbol,
is_primop_symbol,
}
}
@@ -50,6 +53,23 @@ impl<'a, 'b> RuntimeContext<'a, 'b> {
None
}
}
fn get_is_primop_symbol(
scope: &v8::PinnedRef<'a, v8::HandleScope<'b>>,
) -> Option<v8::Local<'a, v8::Symbol>> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")?;
let nix_obj = global.get(scope, nix_key.into())?.to_object(scope)?;
let is_primop_sym_key = v8::String::new(scope, "IS_PRIMOP")?;
let is_primop_sym = nix_obj.get(scope, is_primop_sym_key.into())?;
if is_primop_sym.is_symbol() {
is_primop_sym.try_cast().ok()
} else {
None
}
}
}
fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result<Value> {
@@ -127,8 +147,11 @@ fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
_ if val.is_number() => {
let val = val.to_number(scope).unwrap().value();
// Heuristic: convert whole numbers to Int (for backward compatibility and JS interop)
if val.is_finite() && val.fract() == 0.0
&& val >= i64::MIN as f64 && val <= i64::MAX as f64 {
if val.is_finite()
&& val.fract() == 0.0
&& val >= i64::MIN as f64
&& val <= i64::MAX as f64
{
Value::Const(Const::Int(val as i64))
} else {
Value::Const(Const::Float(val))
@@ -152,7 +175,15 @@ fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
.collect();
Value::List(List::new(list))
}
_ if val.is_function() => Value::Func,
_ if val.is_function() => {
if let Some(name) = primop_app_name(val, ctx) {
Value::PrimOpApp(name)
} else if let Some(name) = primop_name(val, ctx) {
Value::PrimOp(name)
} else {
Value::Func
}
}
_ if val.is_object() => {
if is_thunk(val, ctx) {
return Value::Thunk;
@@ -193,6 +224,58 @@ fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
matches!(obj.get(scope, is_thunk_sym.into()), Some(v) if v.is_true())
}
/// Check if a function is a primop
fn primop_name<'a, 'b>(
val: v8::Local<'a, v8::Value>,
ctx: &RuntimeContext<'a, 'b>,
) -> Option<String> {
if !val.is_function() {
return None;
}
// Use cached IS_PRIMOP symbol from context
let is_primop_sym = ctx.is_primop_symbol?;
let scope = ctx.scope;
let obj = val.to_object(scope).unwrap();
if let Some(metadata) = obj.get(scope, is_primop_sym.into())
&& let Some(metadata_obj) = metadata.to_object(scope)
&& let Some(name_key) = v8::String::new(scope, "name")
&& let Some(name_val) = metadata_obj.get(scope, name_key.into())
{
Some(name_val.to_rust_string_lossy(scope))
} else {
None
}
}
/// Check if a primop is partially applied (has applied > 0)
fn primop_app_name<'a, 'b>(
val: v8::Local<'a, v8::Value>,
ctx: &RuntimeContext<'a, 'b>,
) -> Option<String> {
let name = primop_name(val, ctx)?;
// Get cached IS_PRIMOP symbol
let is_primop_sym = ctx.is_primop_symbol?;
let scope = ctx.scope;
let obj = val.to_object(scope).unwrap();
if let Some(metadata) = obj.get(scope, is_primop_sym.into())
&& let Some(metadata_obj) = metadata.to_object(scope)
&& let Some(applied_key) = v8::String::new(scope, "applied")
&& let Some(applied_val) = metadata_obj.get(scope, applied_key.into())
&& let Some(applied_num) = applied_val.to_number(scope)
&& applied_num.value() > 0.0
{
Some(name)
} else {
None
}
}
#[test]
fn to_value_working() {
assert_eq!(

View File

@@ -185,9 +185,9 @@ pub enum Value {
/// A function (lambda).
Func,
/// A primitive (built-in) operation.
PrimOp,
PrimOp(String),
/// A partially applied primitive operation.
PrimOpApp,
PrimOpApp(String),
/// A marker for a value that has been seen before during serialization, to break cycles.
/// This is used to prevent infinite recursion when printing or serializing cyclic data structures.
Repeated,
@@ -201,10 +201,10 @@ impl Display for Value {
String(x) => write!(f, r#""{x}""#),
AttrSet(x) => write!(f, "{x}"),
List(x) => write!(f, "{x}"),
Thunk => write!(f, "<CODE>"),
Func => write!(f, "<LAMBDA>"),
PrimOp => write!(f, "<PRIMOP>"),
PrimOpApp => write!(f, "<PRIMOP-APP>"),
Thunk => write!(f, "«code»"),
Func => write!(f, "«lambda»"),
PrimOp(name) => write!(f, "«primop {name}»"),
PrimOpApp(name) => write!(f, "«partially applied primop {name}»"),
Repeated => write!(f, "<REPEATED>"),
}
}