Compare commits

...

2 Commits

Author SHA1 Message Date
86953dd9d3 refactor: thunk scope 2026-01-27 10:45:40 +08:00
d1f87260a6 fix: infinite recursion on perl (WIP) 2026-01-27 10:45:40 +08:00
22 changed files with 307 additions and 665 deletions

View File

@@ -12,4 +12,4 @@
[no-exit-message] [no-exit-message]
@evalr expr: @evalr expr:
RUST_LOG=info cargo run --bin eval --release -- '{{expr}}' RUST_LOG=silent cargo run --bin eval --release -- '{{expr}}'

View File

@@ -75,7 +75,7 @@ name = "builtins"
harness = false harness = false
[[bench]] [[bench]]
name = "scc_optimization" name = "thunk_scope"
harness = false harness = false
[[bench]] [[bench]]

View File

@@ -31,7 +31,6 @@ export const hasAttr =
(set: NixValue): boolean => (set: NixValue): boolean =>
Object.hasOwn(forceAttrs(set), forceStringValue(s)); Object.hasOwn(forceAttrs(set), forceStringValue(s));
let counter = 0;
export const mapAttrs = export const mapAttrs =
(f: NixValue) => (f: NixValue) =>
(attrs: NixValue): NixAttrs => { (attrs: NixValue): NixAttrs => {
@@ -39,8 +38,7 @@ export const mapAttrs =
const forcedF = forceFunction(f); const forcedF = forceFunction(f);
const newAttrs: NixAttrs = {}; const newAttrs: NixAttrs = {};
for (const key in forcedAttrs) { for (const key in forcedAttrs) {
newAttrs[key] = createThunk(() => forceFunction(forcedF(key))(forcedAttrs[key]), `created by mapAttrs (${counter})`); newAttrs[key] = createThunk(() => forceFunction(forcedF(key))(forcedAttrs[key]), "created by mapAttrs");
counter += 1;
} }
return newAttrs; return newAttrs;
}; };

View File

@@ -351,6 +351,18 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
return result; return result;
}; };
const specialAttrs = new Set([
"name",
"builder",
"system",
"args",
"outputs",
"__structuredAttrs",
"__ignoreNulls",
"__contentAddressed",
"impure",
]);
export const derivation = (args: NixValue): NixAttrs => { export const derivation = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args); const attrs = forceAttrs(args);
const strict = derivationStrict(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 ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
const drvArgs = extractArgs(attrs, collectedContext); const drvArgs = extractArgs(attrs, collectedContext);
const specialAttrs = new Set([
"name",
"builder",
"system",
"args",
"outputs",
"__structuredAttrs",
"__ignoreNulls",
"__contentAddressed",
"impure",
]);
const baseAttrs: NixAttrs = { const baseAttrs: NixAttrs = {
type: "derivation", type: "derivation",
drvPath: strict.drvPath, drvPath: strict.drvPath,

View File

@@ -18,7 +18,8 @@ import * as misc from "./misc";
import * as derivation from "./derivation"; import * as derivation from "./derivation";
import type { NixValue } from "../types"; 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) * Symbol used to mark functions as primops (primitive operations)
@@ -263,4 +264,9 @@ export const builtins: any = {
nixPath: [], nixPath: [],
nixVersion: "2.31.2", nixVersion: "2.31.2",
storeDir: "INVALID_PATH", storeDir: "INVALID_PATH",
__traceCaller: (e: NixValue) => {
console.log(`traceCaller: ${getTos()}`)
return e
},
}; };

View File

@@ -12,7 +12,8 @@ import { getPathValue } from "../path";
import type { NixStringContext, StringWithContext } from "../string-context"; import type { NixStringContext, StringWithContext } from "../string-context";
import { mkStringWithContext } from "../string-context"; import { mkStringWithContext } from "../string-context";
import { isPath } from "./type-check"; import { isPath } from "./type-check";
import { getCorepkg } from "../corepkgs";
const importCache = new Map<string, NixValue>();
export const importFunc = (path: NixValue): NixValue => { export const importFunc = (path: NixValue): NixValue => {
const context: NixStringContext = new Set(); 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 // Call Rust op - returns JS code string
const code = Deno.core.ops.op_import(pathStr); 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 = export const scopedImport =
@@ -452,13 +461,8 @@ export const findFile =
} }
if (lookupPathStr.startsWith("nix/")) { if (lookupPathStr.startsWith("nix/")) {
const corepkgName = lookupPathStr.substring(4); // FIXME: special path type
const corepkgContent = getCorepkg(corepkgName); return { [IS_PATH]: true, value: `<${lookupPathStr}>` };
if (corepkgContent !== undefined) {
// FIXME: special path type
return { [IS_PATH]: true, value: `<nix/${corepkgName}>` };
}
} }
throw new CatchableError(`file '${lookupPathStr}' was not found in the Nix search path`); throw new CatchableError(`file '${lookupPathStr}' was not found in the Nix search path`);

View File

@@ -2,7 +2,7 @@
* Miscellaneous builtin functions * Miscellaneous builtin functions
*/ */
import { createThunk, force } from "../thunk"; import { force } from "../thunk";
import { CatchableError } from "../types"; import { CatchableError } from "../types";
import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types"; import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types";
import { forceList, forceAttrs, forceFunction, forceStringValue, forceString, forceStringNoCtx } from "../type-assert"; import { forceList, forceAttrs, forceFunction, forceStringValue, forceString, forceStringNoCtx } from "../type-assert";
@@ -20,7 +20,8 @@ import {
export const addErrorContext = export const addErrorContext =
(e1: NixValue) => (e1: NixValue) =>
(e2: NixValue): NixValue => { (e2: NixValue): NixValue => {
console.log("[WARNING]: addErrorContext not implemented"); // FIXME:
// console.log("[WARNING]: addErrorContext not implemented");
return e2; return e2;
}; };

View File

@@ -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; })
)
`;

View File

@@ -1,9 +0,0 @@
import { FETCHURL_NIX } from "./fetchurl.nix";
export const COREPKGS: Record<string, string> = {
"fetchurl.nix": FETCHURL_NIX,
};
export const getCorepkg = (name: string): string | undefined => {
return COREPKGS[name];
};

View File

@@ -19,12 +19,10 @@ interface StackFrame {
const callStack: StackFrame[] = []; const callStack: StackFrame[] = [];
const MAX_STACK_DEPTH = 1000; const MAX_STACK_DEPTH = 1000;
export const STACK_TRACE = { enabled: false };
function enrichError(error: unknown): Error { function enrichError(error: unknown): Error {
const err = error instanceof Error ? error : new Error(String(error)); const err = error instanceof Error ? error : new Error(String(error));
if (!STACK_TRACE.enabled || callStack.length === 0) { if (callStack.length === 0) {
return err; return err;
} }
@@ -38,13 +36,17 @@ function enrichError(error: unknown): Error {
return err; 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 * Push an error context onto the stack
* Used for tracking evaluation context (e.g., "while evaluating the condition") * Used for tracking evaluation context (e.g., "while evaluating the condition")
*/ */
export const pushContext = (message: string, span: string): void => { export const pushContext = (message: string, span: string): void => {
if (!STACK_TRACE.enabled) return;
if (callStack.length >= MAX_STACK_DEPTH) { if (callStack.length >= MAX_STACK_DEPTH) {
callStack.shift(); callStack.shift();
} }
@@ -55,7 +57,6 @@ export const pushContext = (message: string, span: string): void => {
* Pop an error context from the stack * Pop an error context from the stack
*/ */
export const popContext = (): void => { export const popContext = (): void => {
if (!STACK_TRACE.enabled) return;
callStack.pop(); callStack.pop();
}; };
@@ -64,10 +65,6 @@ export const popContext = (): void => {
* Automatically pushes context before execution and pops after * Automatically pushes context before execution and pops after
*/ */
export const withContext = <T>(message: string, span: string, fn: () => T): T => { export const withContext = <T>(message: string, span: string, fn: () => T): T => {
if (!STACK_TRACE.enabled) {
return fn();
}
pushContext(message, span); pushContext(message, span);
try { try {
return fn(); return fn();
@@ -183,7 +180,7 @@ export const resolvePath = (currentDir: string, path: NixValue): NixPath => {
}; };
export const select = (obj: NixValue, attrpath: NixValue[], span?: string): NixValue => { 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 pathStrings = attrpath.map((a) => forceStringValue(a));
const path = pathStrings.join("."); const path = pathStrings.join(".");
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute"; const message = path ? `while selecting attribute [${path}]` : "while selecting attribute";
@@ -229,7 +226,7 @@ export const selectWithDefault = (
default_val: NixValue, default_val: NixValue,
span?: string, span?: string,
): NixValue => { ): NixValue => {
if (STACK_TRACE.enabled && span) { if (span) {
const pathStrings = attrpath.map((a) => forceStringValue(a)); const pathStrings = attrpath.map((a) => forceStringValue(a));
const path = pathStrings.join("."); const path = pathStrings.join(".");
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute"; 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 => { export const call = (func: NixValue, arg: NixValue, span?: string): NixValue => {
if (STACK_TRACE.enabled && span) { if (span) {
if (callStack.length >= MAX_STACK_DEPTH) { if (callStack.length >= MAX_STACK_DEPTH) {
callStack.shift(); callStack.shift();
} }

View File

@@ -14,7 +14,6 @@ import {
concatStringsWithContext, concatStringsWithContext,
call, call,
assert, assert,
STACK_TRACE,
pushContext, pushContext,
popContext, popContext,
withContext, withContext,
@@ -41,7 +40,6 @@ export const Nix = {
HAS_CONTEXT, HAS_CONTEXT,
IS_PATH, IS_PATH,
DEBUG_THUNKS, DEBUG_THUNKS,
STACK_TRACE,
assert, assert,
call, call,

View File

@@ -224,13 +224,15 @@ export const op = {
const attrsA = av as NixAttrs; const attrsA = av as NixAttrs;
const attrsB = bv as NixAttrs; const attrsB = bv as NixAttrs;
// If both denote a derivation (type = "derivation"), compare their outPaths // Derivation comparison: compare outPaths only
const isDerivationA = "type" in attrsA && force(attrsA.type) === "derivation"; // Safe to force 'type' because it's always a string literal, never a computed value
const isDerivationB = "type" in attrsB && force(attrsB.type) === "derivation"; if ("type" in attrsA && "type" in attrsB) {
const typeValA = force(attrsA.type);
if (isDerivationA && isDerivationB) { const typeValB = force(attrsB.type);
if ("outPath" in attrsA && "outPath" in attrsB) { if (typeValA === "derivation" && typeValB === "derivation") {
return op.eq(attrsA.outPath, attrsB.outPath); if ("outPath" in attrsA && "outPath" in attrsB) {
return op.eq(attrsA.outPath, attrsB.outPath);
}
} }
} }

View File

@@ -12,8 +12,9 @@ import type { NixValue, NixThunkInterface, NixStrictValue } from "./types";
export const IS_THUNK = Symbol("is_thunk"); export const IS_THUNK = Symbol("is_thunk");
const forceStack: NixThunk[] = []; 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 * NixThunk class - represents a lazy, unevaluated expression
@@ -97,13 +98,28 @@ export const force = (value: NixValue): NixStrictValue => {
if (DEBUG_THUNKS.enabled) { if (DEBUG_THUNKS.enabled) {
forceStack.push(thunk); 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 { try {
const result = force(func()); const result = force(func());
thunk.result = result; thunk.result = result;
return result; return result;
} catch (e) {
thunk.func = func;
throw e;
} finally { } finally {
forceStack.pop(); if (DEBUG_THUNKS.enabled) {
forceStack.pop();
}
} }
}; };

View File

@@ -11,9 +11,6 @@ pub(crate) fn compile(expr: &Ir, ctx: &impl CodegenContext) -> String {
if std::env::var("NIX_JS_DEBUG_THUNKS").is_ok() { if std::env::var("NIX_JS_DEBUG_THUNKS").is_ok() {
debug_flags.push("Nix.DEBUG_THUNKS.enabled=true"); 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() { let debug_prefix = if debug_flags.is_empty() {
String::new() String::new()
} else { } else {
@@ -97,17 +94,11 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
let cond_code = ctx.get_ir(cond).compile(ctx); let cond_code = ctx.get_ir(cond).compile(ctx);
let consq = ctx.get_ir(consq).compile(ctx); let consq = ctx.get_ir(consq).compile(ctx);
let alter = ctx.get_ir(alter).compile(ctx); let alter = ctx.get_ir(alter).compile(ctx);
let cond_span = encode_span(ctx.get_ir(cond).span(), ctx);
// Only add context tracking if STACK_TRACE is enabled format!(
if std::env::var("NIX_JS_STACK_TRACE").is_ok() { "(Nix.withContext(\"while evaluating a branch condition\",{},()=>Nix.forceBool({})))?({}):({})",
let cond_span = encode_span(ctx.get_ir(cond).span(), ctx); cond_span, cond_code, consq, alter
format!( )
"(Nix.withContext(\"while evaluating a branch condition\",{},()=>Nix.forceBool({})))?({}):({})",
cond_span, cond_code, consq, alter
)
} else {
format!("Nix.forceBool({cond_code})?({consq}):({alter})")
}
} }
Ir::BinOp(x) => x.compile(ctx), Ir::BinOp(x) => x.compile(ctx),
Ir::UnOp(x) => x.compile(ctx), Ir::UnOp(x) => x.compile(ctx),
@@ -116,23 +107,9 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
Ir::List(x) => x.compile(ctx), Ir::List(x) => x.compile(ctx),
Ir::Call(x) => x.compile(ctx), Ir::Call(x) => x.compile(ctx),
Ir::Arg(x) => format!("arg{}", x.inner.0), Ir::Arg(x) => format!("arg{}", x.inner.0),
Ir::Let(x) => x.compile(ctx), Ir::TopLevel(x) => x.compile(ctx),
Ir::Select(x) => x.compile(ctx), Ir::Select(x) => x.compile(ctx),
&Ir::Thunk(Thunk { &Ir::Thunk(Thunk { inner: expr_id, .. }) => {
inner: expr_id,
span,
}) => {
let inner = ctx.get_ir(expr_id).compile(ctx);
format!(
"Nix.createThunk(()=>({}),\"expr{} {}:{}:{}\")",
inner,
expr_id.0,
ctx.get_current_source().get_name(),
usize::from(span.start()),
usize::from(span.end())
)
}
&Ir::ExprRef(ExprRef { inner: expr_id, .. }) => {
format!("expr{}", expr_id.0) format!("expr{}", expr_id.0)
} }
Ir::Builtins(_) => "Nix.builtins".to_string(), Ir::Builtins(_) => "Nix.builtins".to_string(),
@@ -149,27 +126,16 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
}) => { }) => {
let assertion_code = ctx.get_ir(assertion).compile(ctx); let assertion_code = ctx.get_ir(assertion).compile(ctx);
let expr = ctx.get_ir(expr).compile(ctx); let expr = ctx.get_ir(expr).compile(ctx);
let assertion_span = encode_span(ctx.get_ir(assertion).span(), ctx);
// Only add context tracking if STACK_TRACE is enabled let span = encode_span(span, ctx);
if std::env::var("NIX_JS_STACK_TRACE").is_ok() { format!(
let assertion_span = encode_span(ctx.get_ir(assertion).span(), ctx); "Nix.assert(Nix.withContext(\"while evaluating the condition of the assert statement\",{},()=>({})),{},{},{})",
let span = encode_span(span, ctx); assertion_span,
format!( assertion_code,
"Nix.assert(Nix.withContext(\"while evaluating the condition of the assert statement\",{},()=>({})),{},{},{})", expr,
assertion_span, assertion_raw.escape_quote(),
assertion_code, span
expr, )
assertion_raw.escape_quote(),
span
)
} else {
format!(
"Nix.assert({},{},{})",
assertion_code,
expr,
assertion_raw.escape_quote()
)
}
} }
Ir::CurPos(cur_pos) => { Ir::CurPos(cur_pos) => {
let span_str = encode_span(cur_pos.span, ctx); let span_str = encode_span(cur_pos.span, ctx);
@@ -186,20 +152,12 @@ impl<Ctx: CodegenContext> Compile<Ctx> for BinOp {
let lhs = ctx.get_ir(self.lhs).compile(ctx); let lhs = ctx.get_ir(self.lhs).compile(ctx);
let rhs = ctx.get_ir(self.rhs).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| { let with_ctx = |op_name: &str, op_call: String| {
if stack_trace_enabled { let span = encode_span(self.span, ctx);
let span = encode_span(self.span, ctx); format!(
format!( "Nix.withContext(\"while evaluating the {} operator\",{},()=>({}))",
"Nix.withContext(\"while evaluating the {} operator\",{},()=>({}))", op_name, span, op_call
op_name, span, op_call )
)
} else {
op_call
}
}; };
match self.kind { match self.kind {
@@ -248,7 +206,13 @@ impl<Ctx: CodegenContext> Compile<Ctx> for UnOp {
impl<Ctx: CodegenContext> Compile<Ctx> for Func { impl<Ctx: CodegenContext> Compile<Ctx> for Func {
fn compile(&self, ctx: &Ctx) -> String { fn compile(&self, ctx: &Ctx) -> String {
let id = ctx.get_ir(self.arg).as_ref().unwrap_arg().inner.0; let id = ctx.get_ir(self.arg).as_ref().unwrap_arg().inner.0;
let body = ctx.get_ir(self.body).compile(ctx); let thunk_defs = compile_thunks(&self.thunks, ctx);
let body_code = ctx.get_ir(self.body).compile(ctx);
let body = if thunk_defs.is_empty() {
body_code
} else {
format!("{{{}return {}}}", thunk_defs, body_code)
};
if let Some(Param { if let Some(Param {
required, required,
@@ -260,9 +224,17 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Func {
let required = format!("[{}]", required.join(",")); let required = format!("[{}]", required.join(","));
let mut optional = optional.iter().map(|&sym| ctx.get_sym(sym).escape_quote()); let mut optional = optional.iter().map(|&sym| ctx.get_sym(sym).escape_quote());
let optional = format!("[{}]", optional.join(",")); let optional = format!("[{}]", optional.join(","));
format!("Nix.mkFunction(arg{id}=>({body}),{required},{optional},{ellipsis})") if thunk_defs.is_empty() {
format!("Nix.mkFunction(arg{id}=>({body}),{required},{optional},{ellipsis})")
} else {
format!("Nix.mkFunction(arg{id}=>{body},{required},{optional},{ellipsis})")
}
} else { } else {
format!("arg{id}=>({body})") if thunk_defs.is_empty() {
format!("arg{id}=>({body})")
} else {
format!("arg{id}=>{body}")
}
} }
} }
} }
@@ -276,51 +248,38 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Call {
} }
} }
/// Determines if a Thunk should be kept (not unwrapped) for non-recursive let bindings. fn compile_thunks<Ctx: CodegenContext>(thunks: &[(ExprId, ExprId)], ctx: &Ctx) -> String {
/// Returns true for complex expressions that should remain lazy to preserve Nix semantics. if thunks.is_empty() {
fn should_keep_thunk(ir: &Ir) -> bool { return String::new();
match ir {
// Simple literals can be evaluated eagerly
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,
// Everything else should remain lazy:
_ => true,
} }
thunks
.iter()
.map(|&(slot, inner)| {
let inner_code = ctx.get_ir(inner).compile(ctx);
let inner_span = ctx.get_ir(inner).span();
format!(
"let expr{}=Nix.createThunk(()=>({}),\"expr{} {}:{}:{}\")",
slot.0,
inner_code,
slot.0,
ctx.get_current_source().get_name(),
usize::from(inner_span.start()),
usize::from(inner_span.end())
)
})
.join(";")
+ ";"
} }
fn unwrap_thunk(ir: &Ir, ctx: &impl CodegenContext) -> String { impl<Ctx: CodegenContext> Compile<Ctx> for TopLevel {
if let Ir::Thunk(Thunk { inner, .. }) = ir {
let inner_ir = ctx.get_ir(*inner);
if should_keep_thunk(inner_ir) {
ir.compile(ctx)
} else {
inner_ir.compile(ctx)
}
} else {
ir.compile(ctx)
}
}
impl<Ctx: CodegenContext> Compile<Ctx> for Let {
fn compile(&self, ctx: &Ctx) -> String { fn compile(&self, ctx: &Ctx) -> String {
let info = &self.binding_sccs; let thunk_defs = compile_thunks(&self.thunks, ctx);
let mut js_statements = Vec::new();
for (scc_exprs, is_recursive) in info.sccs.iter() {
for &expr in scc_exprs {
let value = if *is_recursive {
ctx.get_ir(expr).compile(ctx)
} else {
unwrap_thunk(ctx.get_ir(expr), ctx)
};
js_statements.push(format!("const expr{}={}", expr.0, value));
}
}
let body = ctx.get_ir(self.body).compile(ctx); let body = ctx.get_ir(self.body).compile(ctx);
format!("(()=>{{{};return {}}})()", js_statements.join(";"), body) if thunk_defs.is_empty() {
body
} else {
format!("(()=>{{{}return {}}})()", thunk_defs, body)
}
} }
} }
@@ -351,21 +310,16 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
fn compile(&self, ctx: &Ctx) -> String { fn compile(&self, ctx: &Ctx) -> String {
let mut attrs = Vec::new(); let mut attrs = Vec::new();
let mut attr_positions = 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 { for (&sym, &(expr, attr_span)) in &self.stcs {
let key = ctx.get_sym(sym); let key = ctx.get_sym(sym);
let value_code = ctx.get_ir(expr).compile(ctx); 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);
let value_span = encode_span(ctx.get_ir(expr).span(), ctx); let value = format!(
format!( "Nix.withContext(\"while evaluating the attribute '{}'\",{},()=>({}))",
"Nix.withContext(\"while evaluating the attribute '{}'\",{},()=>({}))", key, value_span, value_code
key, value_span, value_code );
)
} else {
value_code
};
attrs.push(format!("{}:{}", key.escape_quote(), value)); attrs.push(format!("{}:{}", key.escape_quote(), value));
let attr_pos_str = encode_span(attr_span, ctx); let attr_pos_str = encode_span(attr_span, ctx);
@@ -381,15 +335,11 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
let val_expr = ctx.get_ir(*val); let val_expr = ctx.get_ir(*val);
let val = val_expr.compile(ctx); let val = val_expr.compile(ctx);
let span = val_expr.span(); let span = val_expr.span();
let val = if stack_trace_enabled { let span = encode_span(span, ctx);
let span = encode_span(span, ctx); let val = format!(
format!( "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))",
"Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", span, val
span, val );
)
} else {
val
};
let dyn_span_str = encode_span(*attr_span, ctx); let dyn_span_str = encode_span(*attr_span, ctx);
(key, val, dyn_span_str) (key, val, dyn_span_str)
}) })
@@ -416,23 +366,17 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
impl<Ctx: CodegenContext> Compile<Ctx> for List { impl<Ctx: CodegenContext> Compile<Ctx> for List {
fn compile(&self, ctx: &Ctx) -> String { fn compile(&self, ctx: &Ctx) -> String {
let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok();
let list = self let list = self
.items .items
.iter() .iter()
.enumerate() .enumerate()
.map(|(idx, item)| { .map(|(idx, item)| {
let item_code = ctx.get_ir(*item).compile(ctx); let item_code = ctx.get_ir(*item).compile(ctx);
if stack_trace_enabled { let item_span = encode_span(ctx.get_ir(*item).span(), ctx);
let item_span = encode_span(ctx.get_ir(*item).span(), ctx); format!(
format!( "Nix.withContext(\"while evaluating list element {}\",{},()=>({}))",
"Nix.withContext(\"while evaluating list element {}\",{},()=>({}))", idx, item_span, item_code
idx, item_span, item_code )
)
} else {
item_code
}
}) })
.join(","); .join(",");
format!("[{list}]") format!("[{list}]")
@@ -441,22 +385,16 @@ impl<Ctx: CodegenContext> Compile<Ctx> for List {
impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings { impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
fn compile(&self, ctx: &Ctx) -> String { fn compile(&self, ctx: &Ctx) -> String {
let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok();
let parts: Vec<String> = self let parts: Vec<String> = self
.parts .parts
.iter() .iter()
.map(|part| { .map(|part| {
let part_code = ctx.get_ir(*part).compile(ctx); let part_code = ctx.get_ir(*part).compile(ctx);
if stack_trace_enabled { let part_span = encode_span(ctx.get_ir(*part).span(), ctx);
let part_span = encode_span(ctx.get_ir(*part).span(), ctx); format!(
format!( "Nix.withContext(\"while evaluating a path segment\",{},()=>({}))",
"Nix.withContext(\"while evaluating a path segment\",{},()=>({}))", part_span, part_code
part_span, part_code )
)
} else {
part_code
}
}) })
.collect(); .collect();

View File

@@ -1,28 +1,21 @@
use std::path::Path; use std::path::Path;
use std::ptr::NonNull; use std::ptr::NonNull;
use hashbrown::{HashMap, HashSet}; use hashbrown::HashMap;
use itertools::Itertools as _; use itertools::Itertools as _;
use petgraph::graphmap::DiGraphMap;
use rnix::TextRange; use rnix::TextRange;
use string_interner::DefaultStringInterner; use string_interner::DefaultStringInterner;
use crate::codegen::{CodegenContext, compile}; use crate::codegen::{CodegenContext, compile};
use crate::error::{Error, Result, Source}; use crate::error::{Error, Result, Source};
use crate::ir::{ use crate::ir::{
Arg, ArgId, Bool, Builtin, Downgrade as _, DowngradeContext, ExprId, ExprRef, Ir, Null, SymId, Arg, ArgId, Bool, Builtin, Downgrade as _, DowngradeContext, ExprId, Ir, Null, SymId, Thunk,
Thunk, ToIr as _, synthetic_span, ToIr as _, synthetic_span,
}; };
use crate::runtime::{Runtime, RuntimeContext}; use crate::runtime::{Runtime, RuntimeContext};
use crate::store::{Store, StoreBackend, StoreConfig}; use crate::store::{Store, StoreBackend, StoreConfig};
use crate::value::Value; use crate::value::Value;
#[derive(Debug)]
pub(crate) struct SccInfo {
/// list of SCCs (exprs, recursive)
pub(crate) sccs: Vec<(Vec<ExprId>, bool)>,
}
pub struct Context { pub struct Context {
ctx: Ctx, ctx: Ctx,
runtime: Runtime<Ctx>, runtime: Runtime<Ctx>,
@@ -256,14 +249,6 @@ impl RuntimeContext for Ctx {
} }
} }
struct DependencyTracker {
graph: DiGraphMap<ExprId, ()>,
current_binding: Option<ExprId>,
let_scope_exprs: HashSet<ExprId>,
// The outer binding that owns this tracker (for nested let scopes in function params)
owner_binding: Option<ExprId>,
}
enum Scope<'ctx> { enum Scope<'ctx> {
Global(&'ctx HashMap<SymId, ExprId>), Global(&'ctx HashMap<SymId, ExprId>),
Let(HashMap<SymId, ExprId>), Let(HashMap<SymId, ExprId>),
@@ -292,7 +277,7 @@ pub struct DowngradeCtx<'ctx> {
irs: Vec<Option<Ir>>, irs: Vec<Option<Ir>>,
scopes: Vec<Scope<'ctx>>, scopes: Vec<Scope<'ctx>>,
arg_id: usize, arg_id: usize,
dep_tracker_stack: Vec<DependencyTracker>, thunk_scopes: Vec<Vec<(ExprId, ExprId)>>,
} }
impl<'ctx> DowngradeCtx<'ctx> { impl<'ctx> DowngradeCtx<'ctx> {
@@ -301,7 +286,7 @@ impl<'ctx> DowngradeCtx<'ctx> {
scopes: vec![Scope::Global(global)], scopes: vec![Scope::Global(global)],
irs: vec![], irs: vec![],
arg_id: 0, arg_id: 0,
dep_tracker_stack: Vec::new(), thunk_scopes: vec![Vec::new()],
ctx, ctx,
} }
} }
@@ -346,14 +331,15 @@ impl DowngradeContext for DowngradeCtx<'_> {
| Ir::Float(_) | Ir::Float(_)
| Ir::Bool(_) | Ir::Bool(_)
| Ir::Null(_) | Ir::Null(_)
| Ir::Str(_) => id, | Ir::Str(_)
_ => self.new_expr( | Ir::Thunk(_) => id,
Thunk { _ => {
inner: id, let span = ir.span();
span: ir.span(), let slot = self.reserve_slots(1).next().expect("reserve_slots failed");
} self.replace_ir(slot, Thunk { inner: slot, span }.to_ir());
.to_ir(), self.register_thunk(slot, id);
), slot
}
} }
} }
@@ -375,45 +361,7 @@ impl DowngradeContext for DowngradeCtx<'_> {
} }
Scope::Let(let_scope) => { Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) { if let Some(&expr) = let_scope.get(&sym) {
// Find which tracker contains this expression return Ok(self.new_expr(Thunk { inner: expr, span }.to_ir()));
let expr_tracker_idx = self
.dep_tracker_stack
.iter()
.position(|t| t.let_scope_exprs.contains(&expr));
// Find the innermost tracker with a current_binding
let current_tracker_idx = self
.dep_tracker_stack
.iter()
.rposition(|t| t.current_binding.is_some());
// Record dependency if both exist
if let (Some(expr_idx), Some(curr_idx)) =
(expr_tracker_idx, current_tracker_idx)
{
let current_binding = self.dep_tracker_stack[curr_idx]
.current_binding
.expect("current_binding not set");
let owner_binding = self.dep_tracker_stack[curr_idx].owner_binding;
// If referencing from inner scope to outer scope
if curr_idx >= expr_idx {
let tracker = &mut self.dep_tracker_stack[expr_idx];
let from_node = current_binding;
let to_node = expr;
if curr_idx > expr_idx {
// Cross-scope reference: use owner_binding if available
if let Some(owner) = owner_binding {
tracker.graph.add_edge(owner, expr, ());
}
} else {
// Same-level reference: record directly
tracker.graph.add_edge(from_node, to_node, ());
}
}
}
return Ok(self.new_expr(ExprRef { inner: expr, span }.to_ir()));
} }
} }
&Scope::Param(param_sym, expr) => { &Scope::Param(param_sym, expr) => {
@@ -486,11 +434,15 @@ impl DowngradeContext for DowngradeCtx<'_> {
} }
fn downgrade(mut self, root: rnix::ast::Expr) -> Result<ExprId> { fn downgrade(mut self, root: rnix::ast::Expr) -> Result<ExprId> {
let root = root.downgrade(&mut self)?; use crate::ir::TopLevel;
let body = root.downgrade(&mut self)?;
let thunks = self.pop_thunk_scope();
let span = self.get_ir(body).span();
let top_level = self.new_expr(TopLevel { body, thunks, span }.to_ir());
self.ctx self.ctx
.irs .irs
.extend(self.irs.into_iter().map(Option::unwrap)); .extend(self.irs.into_iter().map(Option::unwrap));
Ok(root) Ok(top_level)
} }
fn with_let_scope<F, R>(&mut self, bindings: HashMap<SymId, ExprId>, f: F) -> R fn with_let_scope<F, R>(&mut self, bindings: HashMap<SymId, ExprId>, f: F) -> R
@@ -515,83 +467,26 @@ impl DowngradeContext for DowngradeCtx<'_> {
where where
F: FnOnce(&mut Self) -> R, F: FnOnce(&mut Self) -> R,
{ {
let namespace = self.maybe_thunk(namespace);
self.scopes.push(Scope::With(namespace)); self.scopes.push(Scope::With(namespace));
let mut guard = ScopeGuard { ctx: self }; let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx()) f(guard.as_ctx())
} }
fn push_dep_tracker(&mut self, slots: &[ExprId]) { fn push_thunk_scope(&mut self) {
let mut graph = DiGraphMap::new(); self.thunk_scopes.push(Vec::new());
let mut let_scope_exprs = HashSet::new();
for &expr in slots.iter() {
graph.add_node(expr);
let_scope_exprs.insert(expr);
}
self.dep_tracker_stack.push(DependencyTracker {
graph,
current_binding: None,
let_scope_exprs,
owner_binding: None,
});
} }
fn push_dep_tracker_with_owner(&mut self, slots: &[ExprId], owner: ExprId) { fn pop_thunk_scope(&mut self) -> Vec<(ExprId, ExprId)> {
let mut graph = DiGraphMap::new(); self.thunk_scopes
let mut let_scope_exprs = HashSet::new();
for &expr in slots.iter() {
graph.add_node(expr);
let_scope_exprs.insert(expr);
}
self.dep_tracker_stack.push(DependencyTracker {
graph,
current_binding: None,
let_scope_exprs,
owner_binding: Some(owner),
});
}
fn get_current_binding(&self) -> Option<ExprId> {
self.dep_tracker_stack
.last()
.and_then(|t| t.current_binding)
}
fn set_current_binding(&mut self, expr: Option<ExprId>) {
if let Some(tracker) = self.dep_tracker_stack.last_mut() {
tracker.current_binding = expr;
}
}
fn pop_dep_tracker(&mut self) -> Result<SccInfo> {
let tracker = self
.dep_tracker_stack
.pop() .pop()
.expect("pop_dep_tracker without active tracker"); .expect("pop_thunk_scope without active scope")
}
use petgraph::algo::kosaraju_scc; fn register_thunk(&mut self, slot: ExprId, inner: ExprId) {
let sccs = kosaraju_scc(&tracker.graph); self.thunk_scopes
.last_mut()
let mut sccs_topo = Vec::new(); .expect("register_thunk without active scope")
.push((slot, inner));
for scc_nodes in sccs.iter() {
let mut scc_exprs = Vec::new();
let mut is_recursive = scc_nodes.len() > 1;
for &expr in scc_nodes {
scc_exprs.push(expr);
if !is_recursive && tracker.graph.contains_edge(expr, expr) {
is_recursive = true;
}
}
sccs_topo.push((scc_exprs, is_recursive));
}
Ok(SccInfo { sccs: sccs_topo })
} }
} }

View File

@@ -214,8 +214,8 @@ pub struct StackFrame {
pub src: NamedSource<Arc<str>>, pub src: NamedSource<Arc<str>>,
} }
const MAX_STACK_FRAMES: usize = 10; const MAX_STACK_FRAMES: usize = 20;
const FRAMES_AT_START: usize = 5; const FRAMES_AT_START: usize = 15;
const FRAMES_AT_END: usize = 5; const FRAMES_AT_END: usize = 5;
pub(crate) fn parse_js_error(error: Box<JsError>, ctx: &impl RuntimeContext) -> Error { pub(crate) fn parse_js_error(error: Box<JsError>, ctx: &impl RuntimeContext) -> Error {
@@ -234,7 +234,11 @@ pub(crate) fn parse_js_error(error: Box<JsError>, ctx: &impl RuntimeContext) ->
} else { } else {
(None, None, Vec::new()) (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 message = error.get_message().to_string();
let js_backtrace = error.stack.map(|stack| { let js_backtrace = error.stack.map(|stack| {
stack stack
@@ -272,7 +276,7 @@ fn parse_frames(stack: &str, ctx: &impl RuntimeContext) -> Vec<NixStackFrame> {
let mut frames = Vec::new(); let mut frames = Vec::new();
for line in stack.lines() { 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 { let Some(rest) = line.strip_prefix("NIX_STACK_FRAME:") else {
continue; continue;
}; };

View File

@@ -3,7 +3,6 @@ use hashbrown::HashMap;
use rnix::{TextRange, ast}; use rnix::{TextRange, ast};
use string_interner::symbol::SymbolU32; use string_interner::symbol::SymbolU32;
use crate::context::SccInfo;
use crate::error::{Error, Result, Source}; use crate::error::{Error, Result, Source};
use crate::value::format_symbol; use crate::value::format_symbol;
use nix_js_macros::ir; use nix_js_macros::ir;
@@ -44,11 +43,9 @@ pub trait DowngradeContext {
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
fn push_dep_tracker(&mut self, slots: &[ExprId]); fn push_thunk_scope(&mut self);
fn push_dep_tracker_with_owner(&mut self, slots: &[ExprId], owner: ExprId); fn pop_thunk_scope(&mut self) -> Vec<(ExprId, ExprId)>;
fn get_current_binding(&self) -> Option<ExprId>; fn register_thunk(&mut self, slot: ExprId, inner: ExprId);
fn set_current_binding(&mut self, expr: Option<ExprId>);
fn pop_dep_tracker(&mut self) -> Result<SccInfo>;
} }
ir! { ir! {
@@ -71,10 +68,9 @@ ir! {
Assert { pub assertion: ExprId, pub expr: ExprId, pub assertion_raw: String }, Assert { pub assertion: ExprId, pub expr: ExprId, pub assertion_raw: String },
ConcatStrings { pub parts: Vec<ExprId> }, ConcatStrings { pub parts: Vec<ExprId> },
Path { pub expr: ExprId }, Path { pub expr: ExprId },
Func { pub body: ExprId, pub param: Option<Param>, pub arg: ExprId }, Func { pub body: ExprId, pub param: Option<Param>, pub arg: ExprId, pub thunks: Vec<(ExprId, ExprId)> },
Let { pub binding_sccs: SccInfo, pub body: ExprId }, TopLevel { pub body: ExprId, pub thunks: Vec<(ExprId, ExprId)> },
Arg(ArgId), Arg(ArgId),
ExprRef(ExprId),
Thunk(ExprId), Thunk(ExprId),
Builtins, Builtins,
Builtin(SymId), Builtin(SymId),
@@ -101,10 +97,9 @@ impl Ir {
Ir::ConcatStrings(c) => c.span, Ir::ConcatStrings(c) => c.span,
Ir::Path(p) => p.span, Ir::Path(p) => p.span,
Ir::Func(f) => f.span, Ir::Func(f) => f.span,
Ir::Let(l) => l.span, Ir::TopLevel(t) => t.span,
Ir::Arg(a) => a.span, Ir::Arg(a) => a.span,
Ir::ExprRef(e) => e.span, Ir::Thunk(e) => e.span,
Ir::Thunk(t) => t.span,
Ir::Builtins(b) => b.span, Ir::Builtins(b) => b.span,
Ir::Builtin(b) => b.span, Ir::Builtin(b) => b.span,
Ir::CurPos(c) => c.span, Ir::CurPos(c) => c.span,

View File

@@ -158,7 +158,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Str {
ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Str { val: lit, span }.to_ir())), ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Str { val: lit, span }.to_ir())),
ast::InterpolPart::Interpolation(interpol) => { ast::InterpolPart::Interpolation(interpol) => {
let inner = interpol.expr().unwrap().downgrade(ctx)?; let inner = interpol.expr().unwrap().downgrade(ctx)?;
Ok(ctx.new_expr(Thunk { inner, span }.to_ir())) Ok(ctx.maybe_thunk(inner))
} }
}) })
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
@@ -220,7 +220,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
// rec { a = 1; b = a; } => let a = 1; b = a; in { inherit a b; } // rec { a = 1; b = a; } => let a = 1; b = a; in { inherit a b; }
let entries: Vec<_> = self.entries().collect(); let entries: Vec<_> = self.entries().collect();
let (binding_sccs, body) = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| { downgrade_let_bindings(entries, ctx, span, |ctx, binding_keys| {
// Create plain attrset as body with inherit // Create plain attrset as body with inherit
let mut attrs = AttrSet { let mut attrs = AttrSet {
stcs: HashMap::new(), stcs: HashMap::new(),
@@ -229,22 +229,12 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
}; };
for sym in binding_keys { for sym in binding_keys {
// FIXME: span
let expr = ctx.lookup(*sym, synthetic_span())?; let expr = ctx.lookup(*sym, synthetic_span())?;
attrs.stcs.insert(*sym, (expr, synthetic_span())); attrs.stcs.insert(*sym, (expr, synthetic_span()));
} }
Ok(ctx.new_expr(attrs.to_ir())) Ok(ctx.new_expr(attrs.to_ir()))
})?; })
Ok(ctx.new_expr(
Let {
body,
binding_sccs,
span,
}
.to_ir(),
))
} }
} }
@@ -308,17 +298,8 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Select {
let expr = self.expr().unwrap().downgrade(ctx)?; let expr = self.expr().unwrap().downgrade(ctx)?;
let attrpath = downgrade_attrpath(self.attrpath().unwrap(), ctx)?; let attrpath = downgrade_attrpath(self.attrpath().unwrap(), ctx)?;
let default = if let Some(default) = self.default_expr() { let default = if let Some(default) = self.default_expr() {
let span = default.syntax().text_range();
let default_expr = default.downgrade(ctx)?; let default_expr = default.downgrade(ctx)?;
Some( Some(ctx.maybe_thunk(default_expr))
ctx.new_expr(
Thunk {
inner: default_expr,
span,
}
.to_ir(),
),
)
} else { } else {
None None
}; };
@@ -378,17 +359,9 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LetIn {
let body_expr = self.body().unwrap(); let body_expr = self.body().unwrap();
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let (binding_sccs, body) = downgrade_let_bindings(entries, ctx, span, |ctx, _binding_keys| {
downgrade_let_bindings(entries, ctx, |ctx, _binding_keys| body_expr.downgrade(ctx))?; body_expr.downgrade(ctx)
})
Ok(ctx.new_expr(
Let {
body,
binding_sccs,
span,
}
.to_ir(),
))
} }
} }
@@ -412,9 +385,10 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
let raw_param = self.param().unwrap(); let raw_param = self.param().unwrap();
let arg = ctx.new_arg(raw_param.syntax().text_range()); let arg = ctx.new_arg(raw_param.syntax().text_range());
ctx.push_thunk_scope();
let param; let param;
let body; let body;
let span = self.body().unwrap().syntax().text_range();
match raw_param { match raw_param {
ast::Param::IdentParam(id) => { ast::Param::IdentParam(id) => {
@@ -436,7 +410,6 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
let PatternBindings { let PatternBindings {
body: inner_body, body: inner_body,
scc_info,
required, required,
optional, optional,
} = downgrade_pattern_bindings(pat_entries, alias, arg, ctx, |ctx, _| { } = downgrade_pattern_bindings(pat_entries, alias, arg, ctx, |ctx, _| {
@@ -449,24 +422,18 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
ellipsis, ellipsis,
}); });
body = ctx.new_expr( body = inner_body;
Let {
body: inner_body,
binding_sccs: scc_info,
span,
}
.to_ir(),
);
} }
} }
let thunks = ctx.pop_thunk_scope();
let span = self.syntax().text_range(); let span = self.syntax().text_range();
// The function's body and parameters are now stored directly in the `Func` node.
Ok(ctx.new_expr( Ok(ctx.new_expr(
Func { Func {
body, body,
param, param,
arg, arg,
thunks,
span, span,
} }
.to_ir(), .to_ir(),

View File

@@ -5,6 +5,7 @@ use hashbrown::hash_map::Entry;
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use itertools::Itertools as _; use itertools::Itertools as _;
use rnix::ast; use rnix::ast;
use rnix::TextRange;
use rowan::ast::AstNode; use rowan::ast::AstNode;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
@@ -99,13 +100,7 @@ pub fn downgrade_inherit(
} }
.to_ir(), .to_ir(),
); );
ctx.new_expr( ctx.maybe_thunk(select_expr)
Thunk {
inner: select_expr,
span,
}
.to_ir(),
)
} else { } else {
ctx.lookup(ident, span)? ctx.lookup(ident, span)?
}; };
@@ -221,20 +216,12 @@ pub fn downgrade_static_attrpathvalue(
pub struct PatternBindings { pub struct PatternBindings {
pub body: ExprId, pub body: ExprId,
pub scc_info: SccInfo,
pub required: Vec<SymId>, pub required: Vec<SymId>,
pub optional: Vec<SymId>, pub optional: Vec<SymId>,
} }
/// Helper function for Lambda pattern parameters with SCC analysis. /// Helper function for Lambda pattern parameters.
/// Processes pattern entries like `{ a, b ? 2, ... }@alias` and creates optimized bindings. /// Processes pattern entries like `{ a, b ? 2, ... }@alias` and creates bindings.
///
/// # Parameters
/// - `pat_entries`: Iterator over pattern entries from the AST
/// - `alias`: Optional alias symbol (from @alias syntax)
/// - `arg`: The argument expression to extract from
///
/// Returns a tuple of (binding slots, body, SCC info, required params, allowed params)
pub fn downgrade_pattern_bindings<Ctx>( pub fn downgrade_pattern_bindings<Ctx>(
pat_entries: impl Iterator<Item = ast::PatEntry>, pat_entries: impl Iterator<Item = ast::PatEntry>,
alias: Option<SymId>, alias: Option<SymId>,
@@ -294,97 +281,6 @@ where
} }
}); });
// Get the owner from outer tracker's current_binding
let owner = ctx.get_current_binding();
let (scc_info, body) = downgrade_bindings_generic_with_owner(
ctx,
binding_keys,
|ctx, sym_to_slot| {
let mut bindings = HashMap::new();
for Param {
sym,
sym_span,
default,
span,
} in params
{
let slot = *sym_to_slot.get(&sym).unwrap();
ctx.set_current_binding(Some(slot));
let default = if let Some(default) = default {
Some(default.clone().downgrade(ctx)?)
} else {
None
};
let select_expr = ctx.new_expr(
Select {
expr: arg,
attrpath: vec![Attr::Str(sym, sym_span)],
default,
span,
}
.to_ir(),
);
bindings.insert(sym, select_expr);
ctx.set_current_binding(None);
}
if let Some(alias_sym) = alias {
bindings.insert(alias_sym, arg);
}
Ok(bindings)
},
body_fn,
owner, // Pass the owner to track cross-scope dependencies
)?;
Ok(PatternBindings {
body,
scc_info,
required,
optional,
})
}
/// Generic helper function to downgrade bindings with SCC analysis.
/// This is the core logic for let bindings, extracted for reuse.
///
/// # Parameters
/// - `binding_keys`: The symbols for all bindings
/// - `compute_bindings_fn`: Called in let scope with sym_to_slot mapping to compute binding values
/// - `body_fn`: Called in let scope to compute the body expression
///
/// Returns a tuple of (binding slots, body result, SCC info)
pub fn downgrade_bindings_generic<Ctx, B, F>(
ctx: &mut Ctx,
binding_keys: Vec<SymId>,
compute_bindings_fn: B,
body_fn: F,
) -> Result<(SccInfo, ExprId)>
where
Ctx: DowngradeContext,
B: FnOnce(&mut Ctx, &HashMap<SymId, ExprId>) -> Result<HashMap<SymId, ExprId>>,
F: FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>,
{
downgrade_bindings_generic_with_owner(ctx, binding_keys, compute_bindings_fn, body_fn, None)
}
pub fn downgrade_bindings_generic_with_owner<Ctx, B, F>(
ctx: &mut Ctx,
binding_keys: Vec<SymId>,
compute_bindings_fn: B,
body_fn: F,
owner: Option<ExprId>,
) -> Result<(SccInfo, ExprId)>
where
Ctx: DowngradeContext,
B: FnOnce(&mut Ctx, &HashMap<SymId, ExprId>) -> Result<HashMap<SymId, ExprId>>,
F: FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>,
{
let slots: Vec<_> = ctx.reserve_slots(binding_keys.len()).collect(); let slots: Vec<_> = ctx.reserve_slots(binding_keys.len()).collect();
let let_bindings: HashMap<_, _> = binding_keys let let_bindings: HashMap<_, _> = binding_keys
.iter() .iter()
@@ -392,53 +288,63 @@ where
.zip(slots.iter().copied()) .zip(slots.iter().copied())
.collect(); .collect();
if let Some(owner_binding) = owner { for &slot in &slots {
ctx.push_dep_tracker_with_owner(&slots, owner_binding); let span = synthetic_span();
} else { ctx.replace_ir(slot, Thunk { inner: slot, span }.to_ir());
ctx.push_dep_tracker(&slots);
} }
ctx.with_let_scope(let_bindings.clone(), |ctx| { ctx.with_let_scope(let_bindings.clone(), |ctx| {
let bindings = compute_bindings_fn(ctx, &let_bindings)?; for Param {
sym,
sym_span,
default,
span,
} in params
{
let slot = *let_bindings.get(&sym).unwrap();
let scc_info = ctx.pop_dep_tracker()?; let default = if let Some(default) = default {
let default = default.clone().downgrade(ctx)?;
for (sym, slot) in binding_keys.iter().copied().zip(slots.iter()) { Some(ctx.maybe_thunk(default))
if let Some(&expr) = bindings.get(&sym) {
ctx.replace_ir(
*slot,
Thunk {
inner: expr,
span: ctx.get_ir(expr).span(),
}
.to_ir(),
);
} else { } else {
return Err(Error::internal(format!( None
"binding '{}' not found", };
format_symbol(ctx.get_sym(sym))
))); let select_expr = ctx.new_expr(
} Select {
expr: arg,
attrpath: vec![Attr::Str(sym, sym_span)],
default,
span,
}
.to_ir(),
);
ctx.register_thunk(slot, select_expr);
}
if let Some(alias_sym) = alias {
let slot = *let_bindings.get(&alias_sym).unwrap();
ctx.register_thunk(slot, arg);
} }
let body = body_fn(ctx, &binding_keys)?; let body = body_fn(ctx, &binding_keys)?;
Ok((scc_info, body)) Ok(PatternBindings {
body,
required,
optional,
})
}) })
} }
/// Helper function to downgrade entries with let bindings semantics. /// Helper function to downgrade entries with let bindings semantics.
/// This extracts common logic for both `rec` attribute sets and `let...in` expressions. /// This extracts common logic for both `rec` attribute sets and `let...in` expressions.
///
/// Returns a tuple of (binding slots, body result, SCC info) where:
/// - binding slots: pre-allocated expression slots for the bindings
/// - body result: the result of calling `body_fn` in the let scope
/// - SCC info: strongly connected components information for optimization
pub fn downgrade_let_bindings<Ctx, F>( pub fn downgrade_let_bindings<Ctx, F>(
entries: Vec<ast::Entry>, entries: Vec<ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
_span: TextRange,
body_fn: F, body_fn: F,
) -> Result<(SccInfo, ExprId)> ) -> Result<ExprId>
where where
Ctx: DowngradeContext, Ctx: DowngradeContext,
F: FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>, F: FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>,
@@ -468,8 +374,6 @@ where
let attrpath = value.attrpath().unwrap(); let attrpath = value.attrpath().unwrap();
let attrs_vec: Vec<_> = attrpath.attrs().collect(); let attrs_vec: Vec<_> = attrpath.attrs().collect();
// Only check for duplicate definitions if this is a top-level binding (path length == 1)
// For nested paths (e.g., types.a, types.b), they will be merged into the same attrset
if attrs_vec.len() == 1 { if attrs_vec.len() == 1 {
if let Some(ast::Attr::Ident(ident)) = attrs_vec.first() { if let Some(ast::Attr::Ident(ident)) = attrs_vec.first() {
let sym = ctx.new_sym(ident.to_string()); let sym = ctx.new_sym(ident.to_string());
@@ -485,7 +389,6 @@ where
} }
} }
} else if attrs_vec.len() > 1 { } else if attrs_vec.len() > 1 {
// For nested paths, just record the first-level name without checking duplicates
if let Some(ast::Attr::Ident(ident)) = attrs_vec.first() { if let Some(ast::Attr::Ident(ident)) = attrs_vec.first() {
let sym = ctx.new_sym(ident.to_string()); let sym = ctx.new_sym(ident.to_string());
binding_syms.insert(sym); binding_syms.insert(sym);
@@ -496,51 +399,47 @@ where
} }
let binding_keys: Vec<_> = binding_syms.into_iter().collect(); let binding_keys: Vec<_> = binding_syms.into_iter().collect();
let slots: Vec<_> = ctx.reserve_slots(binding_keys.len()).collect();
let let_bindings: HashMap<_, _> = binding_keys
.iter()
.copied()
.zip(slots.iter().copied())
.collect();
downgrade_bindings_generic( for &slot in &slots {
ctx, let span = synthetic_span();
binding_keys, ctx.replace_ir(slot, Thunk { inner: slot, span }.to_ir());
|ctx, sym_to_slot| { }
let mut temp_attrs = AttrSet {
stcs: HashMap::new(),
dyns: Vec::new(),
span: synthetic_span(),
};
for entry in entries { ctx.with_let_scope(let_bindings.clone(), |ctx| {
match entry { let mut temp_attrs = AttrSet {
ast::Entry::Inherit(inherit) => { stcs: HashMap::new(),
for attr in inherit.attrs() { dyns: Vec::new(),
if let ast::Attr::Ident(ident) = attr { span: synthetic_span(),
let sym = ctx.new_sym(ident.to_string()); };
let slot = *sym_to_slot.get(&sym).unwrap();
ctx.set_current_binding(Some(slot)); for entry in entries {
} match entry {
} ast::Entry::Inherit(inherit) => {
downgrade_inherit(inherit, &mut temp_attrs.stcs, ctx)?; downgrade_inherit(inherit, &mut temp_attrs.stcs, ctx)?;
ctx.set_current_binding(None); }
} ast::Entry::AttrpathValue(value) => {
ast::Entry::AttrpathValue(value) => { downgrade_static_attrpathvalue(value, &mut temp_attrs, ctx)?;
let attrpath = value.attrpath().unwrap();
if let Some(first_attr) = attrpath.attrs().next()
&& let ast::Attr::Ident(ident) = first_attr
{
let sym = ctx.new_sym(ident.to_string());
let slot = *sym_to_slot.get(&sym).unwrap();
ctx.set_current_binding(Some(slot));
}
downgrade_static_attrpathvalue(value, &mut temp_attrs, ctx)?;
ctx.set_current_binding(None);
}
} }
} }
}
Ok(temp_attrs for (sym, slot) in binding_keys.iter().copied().zip(slots.iter()) {
.stcs if let Some(&(expr, _)) = temp_attrs.stcs.get(&sym) {
.into_iter() ctx.register_thunk(*slot, expr);
.map(|(k, (v, _))| (k, v)) } else {
.collect()) return Err(Error::internal(format!(
}, "binding '{}' not found",
body_fn, format_symbol(ctx.get_sym(sym))
) )));
}
}
body_fn(ctx, &binding_keys)
})
} }

View File

@@ -539,7 +539,11 @@ fn op_copy_path_to_store<Ctx: RuntimeContext>(
#[deno_core::op2] #[deno_core::op2]
#[string] #[string]
fn op_get_env(#[string] key: String) -> std::result::Result<String, NixError> { fn op_get_env(#[string] key: String) -> std::result::Result<String, NixError> {
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<Ctx: RuntimeContext> { pub(crate) struct Runtime<Ctx: RuntimeContext> {