diff --git a/nix-js/runtime-ts/src/builtins/string.ts b/nix-js/runtime-ts/src/builtins/string.ts index 7b90688..31da0bb 100644 --- a/nix-js/runtime-ts/src/builtins/string.ts +++ b/nix-js/runtime-ts/src/builtins/string.ts @@ -2,10 +2,10 @@ * String operation builtin functions */ -import type { NixValue } from "../types"; +import type { NixInt, NixValue } from "../types"; import { forceString, forceList, forceInt } from "../type-assert"; -export const stringLength = (e: NixValue): number => forceString(e).length; +export const stringLength = (e: NixValue): NixInt => BigInt(forceString(e).length); export const substring = (start: NixValue) => diff --git a/nix-js/src/context/downgrade.rs b/nix-js/src/context/downgrade.rs index 5eb5ce0..11fc1ed 100644 --- a/nix-js/src/context/downgrade.rs +++ b/nix-js/src/context/downgrade.rs @@ -15,6 +15,8 @@ struct DependencyTracker { graph: Graph, current_binding: Option, let_scope_exprs: HashSet, + // The outer binding that owns this tracker (for nested let scopes in function params) + owner_binding: Option, } enum Scope<'ctx> { @@ -90,14 +92,44 @@ impl DowngradeContext for DowngradeCtx<'_> { } Scope::Let(let_scope) => { if let Some(&expr) = let_scope.get(&sym) { - if let Some(tracker) = self.dep_tracker_stack.last_mut() - && let Some(current) = tracker.current_binding - && tracker.let_scope_exprs.contains(¤t) - && tracker.let_scope_exprs.contains(&expr) - { - let from = tracker.expr_to_node[¤t]; - let to = tracker.expr_to_node[&expr]; - tracker.graph.add_edge(from, to, ()); + // Find which tracker contains this expression + 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.unwrap(); + 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]; + if let (Some(&from_node), Some(&to_node)) = ( + tracker.expr_to_node.get(¤t_binding), + tracker.expr_to_node.get(&expr), + ) { + // Same-level reference: record directly + tracker.graph.add_edge(from_node, to_node, ()); + } else if curr_idx > expr_idx { + // Cross-scope reference: use owner_binding if available + if let Some(owner) = owner_binding { + if let (Some(&from_node), Some(&to_node)) = ( + tracker.expr_to_node.get(&owner), + tracker.expr_to_node.get(&expr), + ) { + tracker.graph.add_edge(from_node, to_node, ()); + } + } + } + } } return Ok(self.new_expr(Ir::ExprRef(expr))); @@ -216,9 +248,34 @@ impl DowngradeContext for DowngradeCtx<'_> { graph, current_binding: None, let_scope_exprs, + owner_binding: None, }); } + fn push_dep_tracker_with_owner(&mut self, slots: &[ExprId], owner: ExprId) { + let mut graph = Graph::new(); + let mut expr_to_node = HashMap::new(); + let mut let_scope_exprs = HashSet::new(); + + for &expr in slots.iter() { + let node = graph.add_node(expr); + expr_to_node.insert(expr, node); + let_scope_exprs.insert(expr); + } + + self.dep_tracker_stack.push(DependencyTracker { + expr_to_node, + graph, + current_binding: None, + let_scope_exprs, + owner_binding: Some(owner), + }); + } + + fn get_current_binding(&self) -> Option { + self.dep_tracker_stack.last().and_then(|t| t.current_binding) + } + fn set_current_binding(&mut self, expr: Option) { if let Some(tracker) = self.dep_tracker_stack.last_mut() { tracker.current_binding = expr; diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index 8931aed..9e023cf 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -41,6 +41,8 @@ pub trait DowngradeContext { fn get_current_dir(&self) -> std::path::PathBuf; fn push_dep_tracker(&mut self, slots: &[ExprId]); + fn push_dep_tracker_with_owner(&mut self, slots: &[ExprId], owner: ExprId); + fn get_current_binding(&self) -> Option; fn set_current_binding(&mut self, expr: Option); fn pop_dep_tracker(&mut self) -> Result; } diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index c0553c3..f2654bd 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -283,7 +283,10 @@ where Some(param_syms.iter().copied().collect()) }; - let (scc_info, body) = downgrade_bindings_generic( + // 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| { @@ -318,6 +321,7 @@ where Ok(bindings) }, body_fn, + owner, // Pass the owner to track cross-scope dependencies )?; Ok(PatternBindings { @@ -343,6 +347,21 @@ pub fn downgrade_bindings_generic( compute_bindings_fn: B, body_fn: F, ) -> Result<(SccInfo, ExprId)> +where + Ctx: DowngradeContext, + B: FnOnce(&mut Ctx, &HashMap) -> Result>, + F: FnOnce(&mut Ctx, &[SymId]) -> Result, +{ + downgrade_bindings_generic_with_owner(ctx, binding_keys, compute_bindings_fn, body_fn, None) +} + +pub fn downgrade_bindings_generic_with_owner( + ctx: &mut Ctx, + binding_keys: Vec, + compute_bindings_fn: B, + body_fn: F, + owner: Option, +) -> Result<(SccInfo, ExprId)> where Ctx: DowngradeContext, B: FnOnce(&mut Ctx, &HashMap) -> Result>, @@ -355,7 +374,11 @@ where .zip(slots.iter().copied()) .collect(); - ctx.push_dep_tracker(&slots); + if let Some(owner_binding) = owner { + ctx.push_dep_tracker_with_owner(&slots, owner_binding); + } else { + ctx.push_dep_tracker(&slots); + } ctx.with_let_scope(let_bindings.clone(), |ctx| { let bindings = compute_bindings_fn(ctx, &let_bindings)?;