diff --git a/nix-js/build.rs b/nix-js/build.rs index 33e13f5..ceca203 100644 --- a/nix-js/build.rs +++ b/nix-js/build.rs @@ -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") diff --git a/nix-js/runtime-ts/src/builtins/index.ts b/nix-js/runtime-ts/src/builtins/index.ts index dbab617..7fcf7ae 100644 --- a/nix-js/runtime-ts/src/builtins/index.ts +++ b/nix-js/runtime-ts/src/builtins/index.ts @@ -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 = ( + 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', diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index 63e5214..08f5eb4 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -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; diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index 1b9137c..7e7975a 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -16,7 +16,7 @@ impl Compile for Ir { match self { Ir::Const(Const { val }) => match val { crate::value::Const::Null => "null".to_string(), - crate::value::Const::Int(val) => format!("{}n", val), // Generate BigInt literal + crate::value::Const::Int(val) => format!("{}n", val), // Generate BigInt literal crate::value::Const::Float(val) => val.to_string(), crate::value::Const::Bool(val) => val.to_string(), }, diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 2e765c0..a516afb 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -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)) ); } diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 4e0fc72..a677eed 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -23,14 +23,17 @@ pub fn run(script: &str) -> Result { struct RuntimeContext<'a, 'b> { scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>, is_thunk_symbol: Option>, + is_primop_symbol: Option>, } 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> { + 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 { @@ -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 { + 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 { + 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!( diff --git a/nix-js/src/value.rs b/nix-js/src/value.rs index 31faf4a..3454653 100644 --- a/nix-js/src/value.rs +++ b/nix-js/src/value.rs @@ -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, ""), - Func => write!(f, ""), - PrimOp => write!(f, ""), - PrimOpApp => write!(f, ""), + 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, ""), } }