From 9545b0fcaed129700ab8c1d93109eadb4f5951d9 Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Fri, 30 Jan 2026 17:03:31 +0800 Subject: [PATCH] fix: recursive attrs --- nix-js/runtime-ts/src/builtins/list.ts | 9 +- nix-js/src/ir.rs | 73 +++++++++++++-- nix-js/src/ir/downgrade.rs | 30 ++----- nix-js/src/ir/utils.rs | 118 ++++++++++++++++++------- nix-js/src/runtime.rs | 8 +- 5 files changed, 167 insertions(+), 71 deletions(-) diff --git a/nix-js/runtime-ts/src/builtins/list.ts b/nix-js/runtime-ts/src/builtins/list.ts index 7b79ef6..d8c99ef 100644 --- a/nix-js/runtime-ts/src/builtins/list.ts +++ b/nix-js/runtime-ts/src/builtins/list.ts @@ -141,10 +141,9 @@ export const all = export const any = (pred: NixValue) => (list: NixValue): boolean => { + // CppNix forces `pred` eagerly + const f = forceFunction(pred); const forcedList = forceList(list); - if (forcedList.length) { - const f = forceFunction(pred); - return forcedList.some((e) => forceBool(f(e))); - } - return true; + // `false` when no element + return forcedList.some((e) => forceBool(f(e))); }; diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index ae9e0b0..44406fb 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -108,6 +108,42 @@ impl Ir { } impl AttrSet { + fn merge(&mut self, other: &Self, ctx: &mut impl DowngradeContext) -> Result<()> { + for (&sym, &(val, sp)) in &other.stcs { + if let Some(&(existing_id, _)) = self.stcs.get(&sym) { + let mut existing_ir = ctx.extract_ir(existing_id); + let other_ir = ctx.extract_ir(val); + + match ( + existing_ir.as_mut().try_unwrap_attr_set(), + other_ir.as_ref().try_unwrap_attr_set(), + ) { + (Ok(existing_attrs), Ok(other_attrs)) => { + existing_attrs.merge(other_attrs, ctx)?; + ctx.replace_ir(existing_id, existing_ir); + ctx.replace_ir(val, other_ir); + } + _ => { + ctx.replace_ir(existing_id, existing_ir); + ctx.replace_ir(val, other_ir); + return Err(Error::downgrade_error( + format!( + "attribute '{}' already defined", + format_symbol(ctx.get_sym(sym)), + ), + ctx.get_current_source(), + sp, + )); + } + } + } else { + self.stcs.insert(sym, (val, sp)); + } + } + self.dyns.extend(other.dyns.iter().cloned()); + Ok(()) + } + fn _insert( &mut self, mut path: impl Iterator, @@ -171,15 +207,34 @@ impl AttrSet { // This is the final attribute in the path, so insert the value here. match name { Attr::Str(ident, span) => { - if self.stcs.insert(ident, (value, span)).is_some() { - return Err(Error::downgrade_error( - format!( - "attribute '{}' already defined", - format_symbol(ctx.get_sym(ident)), - ), - ctx.get_current_source(), - span, - )); + if let Some(&(existing_id, _)) = self.stcs.get(&ident) { + let mut existing_ir = ctx.extract_ir(existing_id); + let new_ir = ctx.extract_ir(value); + + match ( + existing_ir.as_mut().try_unwrap_attr_set(), + new_ir.as_ref().try_unwrap_attr_set(), + ) { + (Ok(existing_attrs), Ok(new_attrs)) => { + existing_attrs.merge(new_attrs, ctx)?; + ctx.replace_ir(existing_id, existing_ir); + ctx.replace_ir(value, new_ir); + } + _ => { + ctx.replace_ir(existing_id, existing_ir); + ctx.replace_ir(value, new_ir); + return Err(Error::downgrade_error( + format!( + "attribute '{}' already defined", + format_symbol(ctx.get_sym(ident)), + ), + ctx.get_current_source(), + span, + )); + } + } + } else { + self.stcs.insert(ident, (value, span)); } } Attr::Dynamic(dynamic, span) => { diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index bf84981..b11496d 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -220,21 +220,7 @@ impl Downgrade for ast::AttrSet { // rec { a = 1; b = a; } => let a = 1; b = a; in { inherit a b; } let entries: Vec<_> = self.entries().collect(); - downgrade_let_bindings(entries, ctx, span, |ctx, binding_keys| { - // Create plain attrset as body with inherit - let mut attrs = AttrSet { - stcs: HashMap::new(), - dyns: Vec::new(), - span, - }; - - for sym in binding_keys { - let expr = ctx.lookup(*sym, synthetic_span())?; - attrs.stcs.insert(*sym, (expr, synthetic_span())); - } - - Ok(ctx.new_expr(attrs.to_ir())) - }) + downgrade_rec_bindings(entries, ctx, span) } } @@ -321,10 +307,9 @@ impl Downgrade for ast::Select { impl Downgrade for ast::LegacyLet { fn downgrade(self, ctx: &mut Ctx) -> Result { let span = self.syntax().text_range(); - let bindings = downgrade_static_attrs(self, ctx)?; - let binding_keys: Vec<_> = bindings.keys().copied().collect(); - - let attrset_expr = ctx.with_let_scope(bindings, |ctx| { + let entries: Vec<_> = self.entries().collect(); + let attrset_expr = downgrade_let_bindings(entries, ctx, span, |ctx, binding_keys| { + // Create plain attrset as body with inherit let mut attrs = AttrSet { stcs: HashMap::new(), dyns: Vec::new(), @@ -332,12 +317,11 @@ impl Downgrade for ast::LegacyLet { }; for sym in binding_keys { - // FIXME: span - let expr = ctx.lookup(sym, synthetic_span())?; - attrs.stcs.insert(sym, (expr, synthetic_span())); + let expr = ctx.lookup(*sym, synthetic_span())?; + attrs.stcs.insert(*sym, (expr, synthetic_span())); } - Result::Ok(ctx.new_expr(attrs.to_ir())) + Ok(ctx.new_expr(attrs.to_ir())) })?; let body_sym = ctx.new_sym("body".to_string()); diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index e970982..8acc5be 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -37,32 +37,6 @@ pub fn downgrade_attrs( Ok(attrs) } -/// Downgrades attribute set entries for a `let...in` expression. -/// This is a stricter version of `downgrade_attrs` that disallows dynamic attributes, -/// as `let` bindings must be statically known. -pub fn downgrade_static_attrs( - attrs: impl ast::HasEntry + AstNode, - ctx: &mut impl DowngradeContext, -) -> Result> { - let entries = attrs.entries(); - let mut attrs = AttrSet { - stcs: HashMap::new(), - dyns: Vec::new(), - span: attrs.syntax().text_range(), - }; - - for entry in entries { - match entry { - ast::Entry::Inherit(inherit) => downgrade_inherit(inherit, &mut attrs.stcs, ctx)?, - ast::Entry::AttrpathValue(value) => { - downgrade_static_attrpathvalue(value, &mut attrs, ctx)? - } - } - } - - Ok(attrs.stcs.into_iter().map(|(k, (v, _))| (k, v)).collect()) -} - /// Downgrades an `inherit` statement. /// `inherit (from) a b;` is translated into `a = from.a; b = from.b;`. /// `inherit a b;` is translated into `a = a; b = b;` (i.e., bringing variables into scope). @@ -102,7 +76,8 @@ pub fn downgrade_inherit( ); ctx.maybe_thunk(select_expr) } else { - ctx.lookup(ident, span)? + let lookup_expr = ctx.lookup(ident, span)?; + ctx.maybe_thunk(lookup_expr) }; match stcs.entry(ident) { Entry::Occupied(occupied) => { @@ -338,20 +313,88 @@ where } /// 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 `let...in` expressions. +/// For `rec` attribute sets, use `downgrade_rec_bindings` instead. pub fn downgrade_let_bindings( entries: Vec, ctx: &mut Ctx, - _span: TextRange, + span: TextRange, body_fn: F, ) -> Result where Ctx: DowngradeContext, F: FnOnce(&mut Ctx, &[SymId]) -> Result, { + downgrade_let_bindings_impl( + entries, + ctx, + span, + |ctx, binding_keys, _dyns| body_fn(ctx, binding_keys), + false, + ) +} + +/// Helper function to downgrade `rec` attribute sets that may contain dynamic attributes. +/// Similar to `downgrade_let_bindings`, but allows dynamic attributes. +pub fn downgrade_rec_bindings( + entries: Vec, + ctx: &mut Ctx, + span: TextRange, +) -> Result +where + Ctx: DowngradeContext, +{ + downgrade_let_bindings_impl( + entries, + ctx, + span, + |ctx, binding_keys, dyns| { + let mut attrs = AttrSet { + stcs: HashMap::new(), + dyns: dyns.to_vec(), + span, + }; + + for sym in binding_keys { + let expr = ctx.lookup(*sym, synthetic_span())?; + attrs.stcs.insert(*sym, (expr, synthetic_span())); + } + + Ok(ctx.new_expr(attrs.to_ir())) + }, + true, + ) +} + +fn downgrade_let_bindings_impl( + entries: Vec, + ctx: &mut Ctx, + _span: TextRange, + body_fn: F, + allow_dynamic: bool, +) -> Result +where + Ctx: DowngradeContext, + F: FnOnce(&mut Ctx, &[SymId], &[(ExprId, ExprId, TextRange)]) -> Result, +{ + fn is_static_entry(entry: &ast::Entry) -> bool { + match entry { + ast::Entry::Inherit(_) => true, + ast::Entry::AttrpathValue(value) => { + let attrpath = value.attrpath().unwrap(); + let first_attr = attrpath.attrs().next(); + matches!(first_attr, Some(ast::Attr::Ident(_))) + } + } + } + let mut binding_syms = HashSet::new(); for entry in &entries { + if !is_static_entry(entry) && allow_dynamic { + continue; + } + match entry { ast::Entry::Inherit(inherit) => { for attr in inherit.attrs() { @@ -438,13 +481,18 @@ where }; for entry in entries { + if !is_static_entry(&entry) && allow_dynamic { + if let ast::Entry::AttrpathValue(value) = entry { + downgrade_attrpathvalue(value, &mut temp_attrs, ctx)?; + } + continue; + } + match entry { ast::Entry::Inherit(inherit) => { if inherit.from().is_some() { - // `inherit (from) x` - process normally, `from` may reference current scope downgrade_inherit(inherit, &mut temp_attrs.stcs, ctx)?; } else { - // `inherit x` - use pre-looked-up expressions from outer scope for attr in inherit.attrs() { if let ast::Attr::Ident(ident) = attr { let sym = ctx.new_sym(ident.to_string()); @@ -456,7 +504,11 @@ where } } ast::Entry::AttrpathValue(value) => { - downgrade_static_attrpathvalue(value, &mut temp_attrs, ctx)?; + if allow_dynamic { + downgrade_attrpathvalue(value, &mut temp_attrs, ctx)?; + } else { + downgrade_static_attrpathvalue(value, &mut temp_attrs, ctx)?; + } } } } @@ -472,6 +524,6 @@ where } } - body_fn(ctx, &binding_keys) + body_fn(ctx, &binding_keys, &temp_attrs.dyns) }) } diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index f4bd0b7..73685cb 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -572,7 +572,13 @@ impl Runtime { ..Default::default() }); - let (is_thunk_symbol, primop_metadata_symbol, has_context_symbol, is_path_symbol, is_cycle_symbol) = { + let ( + is_thunk_symbol, + primop_metadata_symbol, + has_context_symbol, + is_path_symbol, + is_cycle_symbol, + ) = { deno_core::scope!(scope, &mut js_runtime); Self::get_symbols(scope)? };