diff --git a/Justfile b/Justfile index b501a0e..c3e6bae 100644 --- a/Justfile +++ b/Justfile @@ -12,4 +12,4 @@ [no-exit-message] @evalr expr: - RUST_LOG=info cargo run --bin eval --release -- '{{expr}}' + RUST_LOG=silent cargo run --bin eval --release -- '{{expr}}' diff --git a/nix-js/runtime-ts/src/builtins/attrs.ts b/nix-js/runtime-ts/src/builtins/attrs.ts index 93755da..548b544 100644 --- a/nix-js/runtime-ts/src/builtins/attrs.ts +++ b/nix-js/runtime-ts/src/builtins/attrs.ts @@ -31,7 +31,6 @@ export const hasAttr = (set: NixValue): boolean => Object.hasOwn(forceAttrs(set), forceStringValue(s)); -let counter = 0; export const mapAttrs = (f: NixValue) => (attrs: NixValue): NixAttrs => { @@ -39,8 +38,7 @@ export const mapAttrs = const forcedF = forceFunction(f); const newAttrs: NixAttrs = {}; for (const key in forcedAttrs) { - newAttrs[key] = createThunk(() => forceFunction(forcedF(key))(forcedAttrs[key]), `created by mapAttrs (${counter})`); - counter += 1; + newAttrs[key] = createThunk(() => forceFunction(forcedF(key))(forcedAttrs[key]), "created by mapAttrs"); } return newAttrs; }; diff --git a/nix-js/runtime-ts/src/builtins/derivation.ts b/nix-js/runtime-ts/src/builtins/derivation.ts index c15e012..1641c37 100644 --- a/nix-js/runtime-ts/src/builtins/derivation.ts +++ b/nix-js/runtime-ts/src/builtins/derivation.ts @@ -351,6 +351,18 @@ export const derivationStrict = (args: NixValue): NixAttrs => { return result; }; +const specialAttrs = new Set([ + "name", + "builder", + "system", + "args", + "outputs", + "__structuredAttrs", + "__ignoreNulls", + "__contentAddressed", + "impure", +]); + export const derivation = (args: NixValue): NixAttrs => { const attrs = forceAttrs(args); const strict = derivationStrict(args); @@ -364,18 +376,6 @@ export const derivation = (args: NixValue): NixAttrs => { const ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false; const drvArgs = extractArgs(attrs, collectedContext); - const specialAttrs = new Set([ - "name", - "builder", - "system", - "args", - "outputs", - "__structuredAttrs", - "__ignoreNulls", - "__contentAddressed", - "impure", - ]); - const baseAttrs: NixAttrs = { type: "derivation", drvPath: strict.drvPath, diff --git a/nix-js/runtime-ts/src/builtins/index.ts b/nix-js/runtime-ts/src/builtins/index.ts index 2dd3c35..0c976e0 100644 --- a/nix-js/runtime-ts/src/builtins/index.ts +++ b/nix-js/runtime-ts/src/builtins/index.ts @@ -18,7 +18,8 @@ import * as misc from "./misc"; import * as derivation from "./derivation"; import type { NixValue } from "../types"; -import { createThunk, force } from "../thunk"; +import { createThunk, force, isThunk } from "../thunk"; +import { getTos } from "../helpers"; /** * Symbol used to mark functions as primops (primitive operations) @@ -263,4 +264,9 @@ export const builtins: any = { nixPath: [], nixVersion: "2.31.2", storeDir: "INVALID_PATH", + + __traceCaller: (e: NixValue) => { + console.log(`traceCaller: ${getTos()}`) + return e + }, }; diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index 89429d6..764a39a 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -12,7 +12,8 @@ import { getPathValue } from "../path"; import type { NixStringContext, StringWithContext } from "../string-context"; import { mkStringWithContext } from "../string-context"; import { isPath } from "./type-check"; -import { getCorepkg } from "../corepkgs"; + +const importCache = new Map(); export const importFunc = (path: NixValue): NixValue => { const context: NixStringContext = new Set(); @@ -30,9 +31,17 @@ Dependency tracking for imported derivations may be incomplete.`, ); } + const cached = importCache.get(pathStr); + if (cached !== undefined) { + return cached; + } + // Call Rust op - returns JS code string const code = Deno.core.ops.op_import(pathStr); - return Function(`return (${code})`)(); + const result = Function(`return (${code})`)(); + + importCache.set(pathStr, result); + return result; }; export const scopedImport = @@ -452,13 +461,8 @@ export const findFile = } if (lookupPathStr.startsWith("nix/")) { - const corepkgName = lookupPathStr.substring(4); - const corepkgContent = getCorepkg(corepkgName); - - if (corepkgContent !== undefined) { - // FIXME: special path type - return { [IS_PATH]: true, value: `` }; - } + // FIXME: special path type + return { [IS_PATH]: true, value: `<${lookupPathStr}>` }; } throw new CatchableError(`file '${lookupPathStr}' was not found in the Nix search path`); diff --git a/nix-js/runtime-ts/src/builtins/misc.ts b/nix-js/runtime-ts/src/builtins/misc.ts index e895e2b..a3a488d 100644 --- a/nix-js/runtime-ts/src/builtins/misc.ts +++ b/nix-js/runtime-ts/src/builtins/misc.ts @@ -2,7 +2,7 @@ * Miscellaneous builtin functions */ -import { createThunk, force } from "../thunk"; +import { force } from "../thunk"; import { CatchableError } from "../types"; import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types"; import { forceList, forceAttrs, forceFunction, forceStringValue, forceString, forceStringNoCtx } from "../type-assert"; @@ -20,7 +20,8 @@ import { export const addErrorContext = (e1: NixValue) => (e2: NixValue): NixValue => { - console.log("[WARNING]: addErrorContext not implemented"); + // FIXME: + // console.log("[WARNING]: addErrorContext not implemented"); return e2; }; diff --git a/nix-js/runtime-ts/src/corepkgs/fetchurl.nix.ts b/nix-js/runtime-ts/src/corepkgs/fetchurl.nix.ts deleted file mode 100644 index 35be5cf..0000000 --- a/nix-js/runtime-ts/src/corepkgs/fetchurl.nix.ts +++ /dev/null @@ -1,73 +0,0 @@ -export const FETCHURL_NIX = `{ - system ? "", # obsolete - url, - hash ? "", # an SRI hash - - # Legacy hash specification - md5 ? "", - sha1 ? "", - sha256 ? "", - sha512 ? "", - outputHash ? - if hash != "" then - hash - else if sha512 != "" then - sha512 - else if sha1 != "" then - sha1 - else if md5 != "" then - md5 - else - sha256, - outputHashAlgo ? - if hash != "" then - "" - else if sha512 != "" then - "sha512" - else if sha1 != "" then - "sha1" - else if md5 != "" then - "md5" - else - "sha256", - - executable ? false, - unpack ? false, - name ? baseNameOf (toString url), - impure ? false, -}: - -derivation ( - { - builder = "builtin:fetchurl"; - - # New-style output content requirements. - outputHashMode = if unpack || executable then "recursive" else "flat"; - - inherit - name - url - executable - unpack - ; - - system = "builtin"; - - # No need to double the amount of network traffic - preferLocalBuild = true; - - # This attribute does nothing; it's here to avoid changing evaluation results. - impureEnvVars = [ - "http_proxy" - "https_proxy" - "ftp_proxy" - "all_proxy" - "no_proxy" - ]; - - # To make "nix-prefetch-url" work. - urls = [ url ]; - } - // (if impure then { __impure = true; } else { inherit outputHashAlgo outputHash; }) -) -`; diff --git a/nix-js/runtime-ts/src/corepkgs/index.ts b/nix-js/runtime-ts/src/corepkgs/index.ts deleted file mode 100644 index 261f35b..0000000 --- a/nix-js/runtime-ts/src/corepkgs/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FETCHURL_NIX } from "./fetchurl.nix"; - -export const COREPKGS: Record = { - "fetchurl.nix": FETCHURL_NIX, -}; - -export const getCorepkg = (name: string): string | undefined => { - return COREPKGS[name]; -}; diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index 647095a..8734f9a 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -19,12 +19,10 @@ interface StackFrame { const callStack: StackFrame[] = []; const MAX_STACK_DEPTH = 1000; -export const STACK_TRACE = { enabled: false }; - function enrichError(error: unknown): Error { const err = error instanceof Error ? error : new Error(String(error)); - if (!STACK_TRACE.enabled || callStack.length === 0) { + if (callStack.length === 0) { return err; } @@ -38,13 +36,17 @@ function enrichError(error: unknown): Error { return err; } +export const getTos = (): string => { + const tos = callStack[callStack.length - 2]; + const { file, line, column } = Deno.core.ops.op_decode_span(tos.span); + return `${tos.message} at ${file}:${line}:${column}`; +} + /** * Push an error context onto the stack * Used for tracking evaluation context (e.g., "while evaluating the condition") */ export const pushContext = (message: string, span: string): void => { - if (!STACK_TRACE.enabled) return; - if (callStack.length >= MAX_STACK_DEPTH) { callStack.shift(); } @@ -55,7 +57,6 @@ export const pushContext = (message: string, span: string): void => { * Pop an error context from the stack */ export const popContext = (): void => { - if (!STACK_TRACE.enabled) return; callStack.pop(); }; @@ -64,10 +65,6 @@ export const popContext = (): void => { * Automatically pushes context before execution and pops after */ export const withContext = (message: string, span: string, fn: () => T): T => { - if (!STACK_TRACE.enabled) { - return fn(); - } - pushContext(message, span); try { return fn(); @@ -183,7 +180,7 @@ export const resolvePath = (currentDir: string, path: NixValue): NixPath => { }; export const select = (obj: NixValue, attrpath: NixValue[], span?: string): NixValue => { - if (STACK_TRACE.enabled && span) { + if (span) { const pathStrings = attrpath.map((a) => forceStringValue(a)); const path = pathStrings.join("."); const message = path ? `while selecting attribute [${path}]` : "while selecting attribute"; @@ -229,7 +226,7 @@ export const selectWithDefault = ( default_val: NixValue, span?: string, ): NixValue => { - if (STACK_TRACE.enabled && span) { + if (span) { const pathStrings = attrpath.map((a) => forceStringValue(a)); const path = pathStrings.join("."); const message = path ? `while selecting attribute [${path}]` : "while selecting attribute"; @@ -337,7 +334,7 @@ export const validateParams = ( }; export const call = (func: NixValue, arg: NixValue, span?: string): NixValue => { - if (STACK_TRACE.enabled && span) { + if (span) { if (callStack.length >= MAX_STACK_DEPTH) { callStack.shift(); } diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index 88cd884..1e57f51 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -14,7 +14,6 @@ import { concatStringsWithContext, call, assert, - STACK_TRACE, pushContext, popContext, withContext, @@ -41,7 +40,6 @@ export const Nix = { HAS_CONTEXT, IS_PATH, DEBUG_THUNKS, - STACK_TRACE, assert, call, diff --git a/nix-js/runtime-ts/src/operators.ts b/nix-js/runtime-ts/src/operators.ts index 8f30b38..e361300 100644 --- a/nix-js/runtime-ts/src/operators.ts +++ b/nix-js/runtime-ts/src/operators.ts @@ -224,13 +224,15 @@ export const op = { const attrsA = av as NixAttrs; const attrsB = bv as NixAttrs; - // If both denote a derivation (type = "derivation"), compare their outPaths - const isDerivationA = "type" in attrsA && force(attrsA.type) === "derivation"; - const isDerivationB = "type" in attrsB && force(attrsB.type) === "derivation"; - - if (isDerivationA && isDerivationB) { - if ("outPath" in attrsA && "outPath" in attrsB) { - return op.eq(attrsA.outPath, attrsB.outPath); + // Derivation comparison: compare outPaths only + // Safe to force 'type' because it's always a string literal, never a computed value + if ("type" in attrsA && "type" in attrsB) { + const typeValA = force(attrsA.type); + const typeValB = force(attrsB.type); + if (typeValA === "derivation" && typeValB === "derivation") { + if ("outPath" in attrsA && "outPath" in attrsB) { + return op.eq(attrsA.outPath, attrsB.outPath); + } } } diff --git a/nix-js/runtime-ts/src/thunk.ts b/nix-js/runtime-ts/src/thunk.ts index e16bf15..681ace5 100644 --- a/nix-js/runtime-ts/src/thunk.ts +++ b/nix-js/runtime-ts/src/thunk.ts @@ -12,8 +12,9 @@ import type { NixValue, NixThunkInterface, NixStrictValue } from "./types"; export const IS_THUNK = Symbol("is_thunk"); const forceStack: NixThunk[] = []; +const MAX_FORCE_DEPTH = 1000; -export const DEBUG_THUNKS = { enabled: false }; +export const DEBUG_THUNKS = { enabled: true }; /** * NixThunk class - represents a lazy, unevaluated expression @@ -97,13 +98,28 @@ export const force = (value: NixValue): NixStrictValue => { if (DEBUG_THUNKS.enabled) { forceStack.push(thunk); + if (forceStack.length > MAX_FORCE_DEPTH) { + let msg = `force depth exceeded ${MAX_FORCE_DEPTH} at ${thunk}\n`; + msg += "Force chain (most recent first):\n"; + for (let i = forceStack.length - 1; i >= Math.max(0, forceStack.length - 20); i--) { + const t = forceStack[i]; + msg += ` ${i + 1}. ${t}`; + msg += "\n"; + } + throw new Error(msg); + } } try { const result = force(func()); thunk.result = result; return result; + } catch (e) { + thunk.func = func; + throw e; } finally { - forceStack.pop(); + if (DEBUG_THUNKS.enabled) { + forceStack.pop(); + } } }; diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index f5fec30..e055611 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -11,9 +11,6 @@ pub(crate) fn compile(expr: &Ir, ctx: &impl CodegenContext) -> String { if std::env::var("NIX_JS_DEBUG_THUNKS").is_ok() { debug_flags.push("Nix.DEBUG_THUNKS.enabled=true"); } - if std::env::var("NIX_JS_STACK_TRACE").is_ok() { - debug_flags.push("Nix.STACK_TRACE.enabled=true"); - } let debug_prefix = if debug_flags.is_empty() { String::new() } else { @@ -97,17 +94,11 @@ impl Compile for Ir { let cond_code = ctx.get_ir(cond).compile(ctx); let consq = ctx.get_ir(consq).compile(ctx); let alter = ctx.get_ir(alter).compile(ctx); - - // Only add context tracking if STACK_TRACE is enabled - if std::env::var("NIX_JS_STACK_TRACE").is_ok() { - let cond_span = encode_span(ctx.get_ir(cond).span(), ctx); - format!( - "(Nix.withContext(\"while evaluating a branch condition\",{},()=>Nix.forceBool({})))?({}):({})", - cond_span, cond_code, consq, alter - ) - } else { - format!("Nix.forceBool({cond_code})?({consq}):({alter})") - } + let cond_span = encode_span(ctx.get_ir(cond).span(), ctx); + format!( + "(Nix.withContext(\"while evaluating a branch condition\",{},()=>Nix.forceBool({})))?({}):({})", + cond_span, cond_code, consq, alter + ) } Ir::BinOp(x) => x.compile(ctx), Ir::UnOp(x) => x.compile(ctx), @@ -149,27 +140,16 @@ impl Compile for Ir { }) => { let assertion_code = ctx.get_ir(assertion).compile(ctx); let expr = ctx.get_ir(expr).compile(ctx); - - // Only add context tracking if STACK_TRACE is enabled - if std::env::var("NIX_JS_STACK_TRACE").is_ok() { - let assertion_span = encode_span(ctx.get_ir(assertion).span(), ctx); - let span = encode_span(span, ctx); - format!( - "Nix.assert(Nix.withContext(\"while evaluating the condition of the assert statement\",{},()=>({})),{},{},{})", - assertion_span, - assertion_code, - expr, - assertion_raw.escape_quote(), - span - ) - } else { - format!( - "Nix.assert({},{},{})", - assertion_code, - expr, - assertion_raw.escape_quote() - ) - } + let assertion_span = encode_span(ctx.get_ir(assertion).span(), ctx); + let span = encode_span(span, ctx); + format!( + "Nix.assert(Nix.withContext(\"while evaluating the condition of the assert statement\",{},()=>({})),{},{},{})", + assertion_span, + assertion_code, + expr, + assertion_raw.escape_quote(), + span + ) } Ir::CurPos(cur_pos) => { let span_str = encode_span(cur_pos.span, ctx); @@ -186,20 +166,12 @@ impl Compile for BinOp { let lhs = ctx.get_ir(self.lhs).compile(ctx); let rhs = ctx.get_ir(self.rhs).compile(ctx); - // Only add context tracking if STACK_TRACE is enabled - let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok(); - - // Helper to wrap operation with context (only if enabled) let with_ctx = |op_name: &str, op_call: String| { - if stack_trace_enabled { - let span = encode_span(self.span, ctx); - format!( - "Nix.withContext(\"while evaluating the {} operator\",{},()=>({}))", - op_name, span, op_call - ) - } else { - op_call - } + let span = encode_span(self.span, ctx); + format!( + "Nix.withContext(\"while evaluating the {} operator\",{},()=>({}))", + op_name, span, op_call + ) }; match self.kind { @@ -284,7 +256,7 @@ fn should_keep_thunk(ir: &Ir) -> bool { Ir::Int(_) | Ir::Float(_) | Ir::Bool(_) | Ir::Null(_) | Ir::Str(_) => false, // Builtin references are safe to evaluate eagerly Ir::Builtin(_) | Ir::Builtins(_) => false, - Ir::ExprRef(_) => false, + Ir::ExprRef(_) => true, // Everything else should remain lazy: _ => true, } @@ -351,21 +323,16 @@ impl Compile for AttrSet { fn compile(&self, ctx: &Ctx) -> String { let mut attrs = Vec::new(); let mut attr_positions = Vec::new(); - let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok(); for (&sym, &(expr, attr_span)) in &self.stcs { let key = ctx.get_sym(sym); let value_code = ctx.get_ir(expr).compile(ctx); - let value = if stack_trace_enabled { - let value_span = encode_span(ctx.get_ir(expr).span(), ctx); - format!( - "Nix.withContext(\"while evaluating the attribute '{}'\",{},()=>({}))", - key, value_span, value_code - ) - } else { - value_code - }; + let value_span = encode_span(ctx.get_ir(expr).span(), ctx); + let value = format!( + "Nix.withContext(\"while evaluating the attribute '{}'\",{},()=>({}))", + key, value_span, value_code + ); attrs.push(format!("{}:{}", key.escape_quote(), value)); let attr_pos_str = encode_span(attr_span, ctx); @@ -381,15 +348,11 @@ impl Compile for AttrSet { let val_expr = ctx.get_ir(*val); let val = val_expr.compile(ctx); let span = val_expr.span(); - let val = if stack_trace_enabled { - let span = encode_span(span, ctx); - format!( - "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", - span, val - ) - } else { - val - }; + let span = encode_span(span, ctx); + let val = format!( + "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", + span, val + ); let dyn_span_str = encode_span(*attr_span, ctx); (key, val, dyn_span_str) }) @@ -416,23 +379,17 @@ impl Compile for AttrSet { impl Compile for List { fn compile(&self, ctx: &Ctx) -> String { - let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok(); - let list = self .items .iter() .enumerate() .map(|(idx, item)| { let item_code = ctx.get_ir(*item).compile(ctx); - if stack_trace_enabled { - let item_span = encode_span(ctx.get_ir(*item).span(), ctx); - format!( - "Nix.withContext(\"while evaluating list element {}\",{},()=>({}))", - idx, item_span, item_code - ) - } else { - item_code - } + let item_span = encode_span(ctx.get_ir(*item).span(), ctx); + format!( + "Nix.withContext(\"while evaluating list element {}\",{},()=>({}))", + idx, item_span, item_code + ) }) .join(","); format!("[{list}]") @@ -441,22 +398,16 @@ impl Compile for List { impl Compile for ConcatStrings { fn compile(&self, ctx: &Ctx) -> String { - let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok(); - let parts: Vec = self .parts .iter() .map(|part| { let part_code = ctx.get_ir(*part).compile(ctx); - if stack_trace_enabled { - let part_span = encode_span(ctx.get_ir(*part).span(), ctx); - format!( - "Nix.withContext(\"while evaluating a path segment\",{},()=>({}))", - part_span, part_code - ) - } else { - part_code - } + let part_span = encode_span(ctx.get_ir(*part).span(), ctx); + format!( + "Nix.withContext(\"while evaluating a path segment\",{},()=>({}))", + part_span, part_code + ) }) .collect(); diff --git a/nix-js/src/error.rs b/nix-js/src/error.rs index ba977f3..1f8ead5 100644 --- a/nix-js/src/error.rs +++ b/nix-js/src/error.rs @@ -214,8 +214,8 @@ pub struct StackFrame { pub src: NamedSource>, } -const MAX_STACK_FRAMES: usize = 10; -const FRAMES_AT_START: usize = 5; +const MAX_STACK_FRAMES: usize = 20; +const FRAMES_AT_START: usize = 15; const FRAMES_AT_END: usize = 5; pub(crate) fn parse_js_error(error: Box, ctx: &impl RuntimeContext) -> Error { @@ -234,7 +234,11 @@ pub(crate) fn parse_js_error(error: Box, ctx: &impl RuntimeContext) -> } else { (None, None, Vec::new()) }; - let stack_trace = truncate_stack_trace(frames); + let stack_trace = if std::env::var("NIX_JS_STACK_TRACE").is_ok() { + truncate_stack_trace(frames) + } else { + Vec::new() + }; let message = error.get_message().to_string(); let js_backtrace = error.stack.map(|stack| { stack @@ -272,7 +276,7 @@ fn parse_frames(stack: &str, ctx: &impl RuntimeContext) -> Vec { let mut frames = Vec::new(); for line in stack.lines() { - // Format: NIX_STACK_FRAME:start:end[:extra_data] + // Format: NIX_STACK_FRAME:source_id:start:end[:extra_data] let Some(rest) = line.strip_prefix("NIX_STACK_FRAME:") else { continue; }; diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index e46ed3a..0462f81 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -314,7 +314,8 @@ where ctx.set_current_binding(Some(slot)); let default = if let Some(default) = default { - Some(default.clone().downgrade(ctx)?) + let default = default.clone().downgrade(ctx)?; + Some(ctx.maybe_thunk(default)) } else { None }; diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index f64da72..1d2c107 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -539,7 +539,11 @@ fn op_copy_path_to_store( #[deno_core::op2] #[string] fn op_get_env(#[string] key: String) -> std::result::Result { - Ok(std::env::var(key).map_err(|err| format!("Failed to read env var: {err}"))?) + match std::env::var(key) { + Ok(val) => Ok(val), + Err(std::env::VarError::NotPresent) => Ok("".into()), + Err(err) => Err(format!("Failed to read env var: {err}").into()) + } } pub(crate) struct Runtime {