diff --git a/Cargo.toml b/Cargo.toml index ed51c07..6e7ea10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,7 @@ members = [ "nix-js", "nix-js-macros" ] + +[profile.profiling] +inherits = "release" +debug = true diff --git a/flake.nix b/flake.nix index 7dcfafd..51eaf58 100644 --- a/flake.nix +++ b/flake.nix @@ -31,6 +31,7 @@ valgrind hyperfine just + samply nodejs nodePackages.npm diff --git a/nix-js-macros/src/ir.rs b/nix-js-macros/src/ir.rs index 0bee8ea..6bd37e3 100644 --- a/nix-js-macros/src/ir.rs +++ b/nix-js-macros/src/ir.rs @@ -92,6 +92,7 @@ pub fn ir_impl(input: TokenStream) -> TokenStream { let mut mut_variants = Vec::new(); let mut as_ref_arms = Vec::new(); let mut as_mut_arms = Vec::new(); + let mut span_arms = Vec::new(); let mut from_impls = Vec::new(); let mut to_trait_impls = Vec::new(); @@ -112,6 +113,7 @@ pub fn ir_impl(input: TokenStream) -> TokenStream { mut_variants.push(quote! { #name(&'a mut #inner_type) }); as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) }); as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) }); + span_arms.push(quote! { Self::#name(inner) => inner.span }); from_impls.push(quote! { impl From<#inner_type> for #base_name { fn from(val: #inner_type) -> Self { #base_name::#name(val) } @@ -140,6 +142,7 @@ pub fn ir_impl(input: TokenStream) -> TokenStream { mut_variants.push(quote! { #name(&'a mut #inner_type) }); as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) }); as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) }); + span_arms.push(quote! { Self::#name(inner) => inner.span }); from_impls.push(quote! { impl From<#inner_type> for #base_name { fn from(val: #inner_type) -> Self { #base_name::#name(val) } @@ -172,6 +175,7 @@ pub fn ir_impl(input: TokenStream) -> TokenStream { mut_variants.push(quote! { #name(&'a mut #inner_type) }); as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) }); as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) }); + span_arms.push(quote! { Self::#name(inner) => inner.span }); from_impls.push(quote! { impl From<#inner_type> for #base_name { fn from(val: #inner_type) -> Self { #base_name::#name(val) } @@ -223,6 +227,12 @@ pub fn ir_impl(input: TokenStream) -> TokenStream { #( #as_mut_arms ),* } } + + pub fn span(&self) -> rnix::TextRange { + match self { + #( #span_arms ),* + } + } } // `From` implementations for converting variant structs into the main enum. diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 8f4dda3..e9004cd 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -274,7 +274,7 @@ impl<'a, 'ctx> ScopeGuard<'a, 'ctx> { pub struct DowngradeCtx<'ctx> { ctx: &'ctx mut Ctx, - irs: Vec>, + irs: Vec, scopes: Vec>, arg_id: usize, thunk_scopes: Vec>, @@ -294,18 +294,18 @@ impl<'ctx> DowngradeCtx<'ctx> { impl DowngradeContext for DowngradeCtx<'_> { fn new_expr(&mut self, expr: Ir) -> ExprId { - self.irs.push(Some(expr)); + self.irs.push(expr); ExprId(self.ctx.irs.len() + self.irs.len() - 1) } fn new_arg(&mut self, span: TextRange) -> ExprId { - self.irs.push(Some( + self.irs.push( Arg { inner: ArgId(self.arg_id), span, } .to_ir(), - )); + ); self.arg_id += 1; ExprId(self.ctx.irs.len() + self.irs.len() - 1) } @@ -317,8 +317,6 @@ impl DowngradeContext for DowngradeCtx<'_> { self.irs .get(id.0 - self.ctx.irs.len()) .expect("ExprId out of bounds") - .as_ref() - .expect("maybe_thunk called on an extracted expr") } } @@ -404,22 +402,9 @@ impl DowngradeContext for DowngradeCtx<'_> { }) } - fn extract_ir(&mut self, id: ExprId) -> Ir { - let local_id = id.0 - self.ctx.irs.len(); - self.irs - .get_mut(local_id) - .expect("ExprId out of bounds") - .take() - .expect("extract_expr called on an already extracted expr") - } - fn replace_ir(&mut self, id: ExprId, expr: Ir) { let local_id = id.0 - self.ctx.irs.len(); - let _ = self - .irs - .get_mut(local_id) - .expect("ExprId out of bounds") - .insert(expr); + *self.irs.get_mut(local_id).expect("ExprId out of bounds") = expr; } fn get_current_source(&self) -> Source { @@ -429,8 +414,11 @@ impl DowngradeContext for DowngradeCtx<'_> { #[allow(refining_impl_trait)] fn reserve_slots(&mut self, slots: usize) -> impl Iterator + Clone + use<> { let start = self.ctx.irs.len() + self.irs.len(); - self.irs.extend(std::iter::repeat_with(|| None).take(slots)); - (start..start + slots).map(ExprId) + let range = (start..start + slots).map(ExprId); + let span = synthetic_span(); + // Fill reserved slots with placeholder value + self.irs.extend(range.clone().map(|slot| Thunk { inner: slot, span }.to_ir())); + range } fn downgrade(mut self, root: rnix::ast::Expr) -> Result { @@ -441,7 +429,7 @@ impl DowngradeContext for DowngradeCtx<'_> { let top_level = self.new_expr(TopLevel { body, thunks, span }.to_ir()); self.ctx .irs - .extend(self.irs.into_iter().map(Option::unwrap)); + .extend(self.irs); Ok(top_level) } diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index 44406fb..c0740c7 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -3,8 +3,7 @@ use hashbrown::HashMap; use rnix::{TextRange, ast}; use string_interner::symbol::SymbolU32; -use crate::error::{Error, Result, Source}; -use crate::value::format_symbol; +use crate::error::{Result, Source}; use nix_js_macros::ir; mod downgrade; @@ -28,7 +27,6 @@ pub trait DowngradeContext { fn lookup(&mut self, sym: SymId, span: TextRange) -> Result; fn get_ir(&self, id: ExprId) -> &Ir; - fn extract_ir(&mut self, id: ExprId) -> Ir; fn replace_ir(&mut self, id: ExprId, expr: Ir); fn reserve_slots(&mut self, slots: usize) -> impl Iterator + Clone + use; fn get_current_source(&self) -> Source; @@ -77,189 +75,6 @@ ir! { CurPos, } -impl Ir { - pub fn span(&self) -> TextRange { - match self { - Ir::Int(i) => i.span, - Ir::Float(f) => f.span, - Ir::Bool(b) => b.span, - Ir::Null(n) => n.span, - Ir::Str(s) => s.span, - Ir::AttrSet(a) => a.span, - Ir::List(l) => l.span, - Ir::HasAttr(h) => h.span, - Ir::BinOp(b) => b.span, - Ir::UnOp(u) => u.span, - Ir::Select(s) => s.span, - Ir::If(i) => i.span, - Ir::Call(c) => c.span, - Ir::Assert(a) => a.span, - Ir::ConcatStrings(c) => c.span, - Ir::Path(p) => p.span, - Ir::Func(f) => f.span, - Ir::TopLevel(t) => t.span, - Ir::Arg(a) => a.span, - Ir::Thunk(e) => e.span, - Ir::Builtins(b) => b.span, - Ir::Builtin(b) => b.span, - Ir::CurPos(c) => c.span, - } - } -} - -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, - name: Attr, - value: ExprId, - ctx: &mut impl DowngradeContext, - ) -> Result<()> { - if let Some(attr) = path.next() { - // If the path is not yet exhausted, we need to recurse deeper. - match attr { - Attr::Str(ident, span) => { - // If the next attribute is a static string. - if let Some(&(id, _)) = self.stcs.get(&ident) { - // If a sub-attrset already exists, recurse into it. - let mut ir = ctx.extract_ir(id); - let result = ir - .as_mut() - .try_unwrap_attr_set() - .map_err(|_| { - // This path segment exists but is not an attrset, which is an error. - Error::downgrade_error(format!( - "attribute '{}' already defined but is not an attribute set", - format_symbol(ctx.get_sym(ident)), - ), - ctx.get_current_source(), - span - ) - }) - .and_then(|attrs| attrs._insert(path, name, value, ctx)); - ctx.replace_ir(id, ir); - result?; - } else { - // Create a new sub-attrset because this path doesn't exist yet. - // FIXME: span - let mut attrs = AttrSet { - stcs: Default::default(), - dyns: Default::default(), - span, - }; - attrs._insert(path, name, value, ctx)?; - let attrs_expr = ctx.new_expr(attrs.to_ir()); - self.stcs.insert(ident, (attrs_expr, span)); - } - Ok(()) - } - Attr::Dynamic(dynamic, span) => { - // If the next attribute is a dynamic expression, we must create a new sub-attrset. - // We cannot merge with existing dynamic attributes at this stage. - // FIXME: span - let mut attrs = AttrSet { - stcs: Default::default(), - dyns: Default::default(), - span, - }; - attrs._insert(path, name, value, ctx)?; - self.dyns.push((dynamic, ctx.new_expr(attrs.to_ir()), span)); - Ok(()) - } - } - } else { - // This is the final attribute in the path, so insert the value here. - match name { - Attr::Str(ident, 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) => { - self.dyns.push((dynamic, value, span)); - } - } - Ok(()) - } - } - - fn insert( - &mut self, - path: Vec, - value: ExprId, - ctx: &mut impl DowngradeContext, - ) -> Result<()> { - let mut path = path.into_iter(); - // The last part of the path is the name of the attribute to be inserted. - let name = path - .next_back() - .expect("empty attrpath passed. this is a bug"); - self._insert(path, name, value, ctx) - } -} - #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExprId(pub usize); @@ -271,6 +86,7 @@ pub type SymId = SymbolU32; pub struct ArgId(pub usize); /// Represents a key in an attribute path. +#[allow(unused)] #[derive(Debug, TryUnwrap)] pub enum Attr { /// A dynamic attribute key, which is an expression that must evaluate to a string. diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index 8acc5be..fc01f0d 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -5,7 +5,7 @@ use hashbrown::hash_map::Entry; use hashbrown::{HashMap, HashSet}; use itertools::Itertools as _; use rnix::TextRange; -use rnix::ast; +use rnix::ast::{self, HasEntry}; use rowan::ast::AstNode; use crate::error::{Error, Result}; @@ -14,92 +14,404 @@ use crate::value::format_symbol; use super::*; -/// Downgrades the entries of an attribute set. -/// This handles `inherit` and `attrpath = value;` entries. -pub fn downgrade_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(), - }; +enum PendingValue { + Expr(ast::Expr), + InheritFrom(ast::Expr, SymId, TextRange), + InheritScope(SymId, TextRange), + Set(PendingAttrSet), + ExtendedRecAttrSet { + base: ast::AttrSet, + extensions: Vec, + span: TextRange, + }, +} - for entry in entries { - match entry { - ast::Entry::Inherit(inherit) => downgrade_inherit(inherit, &mut attrs.stcs, ctx)?, - ast::Entry::AttrpathValue(value) => downgrade_attrpathvalue(value, &mut attrs, ctx)?, +struct PendingAttrSet { + stcs: HashMap, + dyns: Vec<(ast::Attr, PendingValue, TextRange)>, + span: TextRange, +} + +impl PendingAttrSet { + fn new(span: TextRange) -> Self { + Self { + stcs: HashMap::new(), + dyns: Vec::new(), + span, } } - Ok(attrs) -} + fn insert( + &mut self, + path: &[ast::Attr], + value: ast::Expr, + ctx: &mut impl DowngradeContext, + ) -> Result<()> { + let first = path.first().expect("empty attrpath passed. this is a bug"); + let rest = &path[1..]; + let span = first.syntax().text_range(); -/// 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). -pub fn downgrade_inherit( - inherit: ast::Inherit, - stcs: &mut HashMap, - ctx: &mut impl DowngradeContext, -) -> Result<()> { - // Downgrade the `from` expression if it exists. - let from = if let Some(from) = inherit.from() { - Some(from.expr().unwrap().downgrade(ctx)?) - } else { - None - }; - for attr in inherit.attrs() { - let span = attr.syntax().text_range(); - let ident = match downgrade_attr(attr, ctx)? { - Attr::Str(ident, _) => ident, - _ => { - // `inherit` does not allow dynamic attributes. + match first { + ast::Attr::Ident(ident) => { + let sym = ctx.new_sym(ident.to_string()); + self.insert_static(sym, span, rest, value, ctx) + } + ast::Attr::Str(string) => { + let parts = string.normalized_parts(); + if parts.len() == 1 + && let ast::InterpolPart::Literal(lit) = parts.into_iter().next().unwrap() + { + let sym = ctx.new_sym(lit); + return self.insert_static(sym, span, rest, value, ctx); + } + self.insert_dynamic(first.clone(), span, rest, value) + } + ast::Attr::Dynamic(_) => self.insert_dynamic(first.clone(), span, rest, value), + } + } + + fn insert_static( + &mut self, + sym: SymId, + span: TextRange, + path: &[ast::Attr], + value: ast::Expr, + ctx: &mut impl DowngradeContext, + ) -> Result<()> { + if !path.is_empty() { + match self.stcs.entry(sym) { + Entry::Occupied(mut entry) => { + let (existing, existing_span) = entry.get_mut(); + + if let PendingValue::Expr(ast::Expr::AttrSet(attrset)) = existing + && attrset.rec_token().is_some() + { + let base = attrset.clone(); + let base_span = attrset.syntax().text_range(); + let ext_entry = make_attrpath_value_entry(path.to_vec(), value); + *existing = PendingValue::ExtendedRecAttrSet { + base, + extensions: vec![ext_entry], + span: base_span, + }; + return Ok(()); + } + + if let PendingValue::ExtendedRecAttrSet { extensions, .. } = existing { + let ext_entry = make_attrpath_value_entry(path.to_vec(), value); + extensions.push(ext_entry); + return Ok(()); + } + + let nested = Self::ensure_pending_set(existing, ctx, *existing_span)?; + nested.insert(path, value, ctx) + } + Entry::Vacant(entry) => { + let mut nested = PendingAttrSet::new(span); + nested.insert(path, value, ctx)?; + entry.insert((PendingValue::Set(nested), span)); + Ok(()) + } + } + } else { + match self.stcs.entry(sym) { + Entry::Occupied(mut entry) => { + let (existing, existing_span) = entry.get_mut(); + Self::merge_value(existing, *existing_span, value, span, ctx) + } + Entry::Vacant(entry) => { + entry.insert((PendingValue::Expr(value), span)); + Ok(()) + } + } + } + } + + fn insert_dynamic( + &mut self, + attr: ast::Attr, + span: TextRange, + path: &[ast::Attr], + value: ast::Expr, + ) -> Result<()> { + if !path.is_empty() { + let mut nested = PendingAttrSet::new(span); + nested.insert_dynamic( + path[0].clone(), + path[0].syntax().text_range(), + &path[1..], + value, + )?; + self.dyns.push((attr, PendingValue::Set(nested), span)); + } else { + self.dyns.push((attr, PendingValue::Expr(value), span)); + } + Ok(()) + } + + fn ensure_pending_set<'a>( + value: &'a mut PendingValue, + ctx: &mut impl DowngradeContext, + span: TextRange, + ) -> Result<&'a mut PendingAttrSet> { + match value { + PendingValue::Set(set) => Ok(set), + PendingValue::Expr(expr) => { + if let ast::Expr::AttrSet(attrset) = expr { + let mut nested = PendingAttrSet::new(attrset.syntax().text_range()); + nested.collect_entries(attrset.entries(), ctx)?; + *value = PendingValue::Set(nested); + match value { + PendingValue::Set(set) => Ok(set), + _ => unreachable!(), + } + } else { + Err(Error::downgrade_error( + "attribute already defined but is not an attribute set".to_string(), + ctx.get_current_source(), + span, + )) + } + } + PendingValue::ExtendedRecAttrSet { .. } => Err(Error::downgrade_error( + "cannot add nested attributes to rec attribute set with extensions".to_string(), + ctx.get_current_source(), + span, + )), + PendingValue::InheritFrom(..) | PendingValue::InheritScope(..) => { + Err(Error::downgrade_error( + "attribute already defined (inherited)".to_string(), + ctx.get_current_source(), + span, + )) + } + } + } + + fn merge_value( + existing: &mut PendingValue, + existing_span: TextRange, + new_value: ast::Expr, + new_span: TextRange, + ctx: &mut impl DowngradeContext, + ) -> Result<()> { + match existing { + PendingValue::Set(existing_set) => { + if let ast::Expr::AttrSet(new_attrset) = new_value { + existing_set.collect_entries(new_attrset.entries(), ctx) + } else { + Err(Error::downgrade_error( + "attribute already defined as attribute set".to_string(), + ctx.get_current_source(), + new_span, + )) + } + } + PendingValue::Expr(existing_expr) => { + if let ast::Expr::AttrSet(existing_attrset) = existing_expr { + if let ast::Expr::AttrSet(new_attrset) = new_value { + let is_rec = existing_attrset.rec_token().is_some(); + if is_rec { + let base = existing_attrset.clone(); + let extensions: Vec<_> = new_attrset.entries().collect(); + *existing = PendingValue::ExtendedRecAttrSet { + base, + extensions, + span: existing_attrset.syntax().text_range(), + }; + Ok(()) + } else { + let mut merged = + PendingAttrSet::new(existing_attrset.syntax().text_range()); + merged.collect_entries(existing_attrset.entries(), ctx)?; + merged.collect_entries(new_attrset.entries(), ctx)?; + *existing = PendingValue::Set(merged); + Ok(()) + } + } else { + Err(Error::downgrade_error( + "attribute already defined as attribute set".to_string(), + ctx.get_current_source(), + new_span, + )) + } + } else { + Err(Error::downgrade_error( + "attribute already defined".to_string(), + ctx.get_current_source(), + existing_span, + )) + } + } + PendingValue::ExtendedRecAttrSet { extensions, .. } => { + if let ast::Expr::AttrSet(new_attrset) = new_value { + extensions.extend(new_attrset.entries()); + Ok(()) + } else { + Err(Error::downgrade_error( + "attribute already defined as attribute set".to_string(), + ctx.get_current_source(), + new_span, + )) + } + } + PendingValue::InheritFrom(..) | PendingValue::InheritScope(..) => { + Err(Error::downgrade_error( + "attribute already defined (inherited)".to_string(), + ctx.get_current_source(), + existing_span, + )) + } + } + } + + fn collect_entries( + &mut self, + entries: impl Iterator, + ctx: &mut impl DowngradeContext, + ) -> Result<()> { + for entry in entries { + match entry { + ast::Entry::Inherit(inherit) => { + self.collect_inherit(inherit, ctx)?; + } + ast::Entry::AttrpathValue(value) => { + let attrpath = value.attrpath().unwrap(); + let path: Vec<_> = attrpath.attrs().collect(); + let expr = value.value().unwrap(); + self.insert(&path, expr, ctx)?; + } + } + } + Ok(()) + } + + fn collect_inherit( + &mut self, + inherit: ast::Inherit, + ctx: &mut impl DowngradeContext, + ) -> Result<()> { + let from = inherit.from().map(|f| f.expr().unwrap()); + for attr in inherit.attrs() { + let span = attr.syntax().text_range(); + let sym = match &attr { + ast::Attr::Ident(ident) => ctx.new_sym(ident.to_string()), + ast::Attr::Str(s) => { + let parts = s.normalized_parts(); + if parts.len() == 1 + && let ast::InterpolPart::Literal(lit) = parts.into_iter().next().unwrap() + { + ctx.new_sym(lit) + } else { + return Err(Error::downgrade_error( + "dynamic attributes not allowed in inherit".to_string(), + ctx.get_current_source(), + span, + )); + } + } + ast::Attr::Dynamic(_) => { + return Err(Error::downgrade_error( + "dynamic attributes not allowed in inherit".to_string(), + ctx.get_current_source(), + span, + )); + } + }; + + if self.stcs.contains_key(&sym) { return Err(Error::downgrade_error( - "dynamic attributes not allowed in inherit".to_string(), + format!( + "attribute '{}' already defined", + format_symbol(ctx.get_sym(sym)) + ), ctx.get_current_source(), span, )); } - }; - let expr = if let Some(expr) = from { - let select_expr = ctx.new_expr( - Select { - expr, - attrpath: vec![Attr::Str(ident, span)], - default: None, - span, - } - .to_ir(), - ); - ctx.maybe_thunk(select_expr) - } else { - let lookup_expr = ctx.lookup(ident, span)?; - ctx.maybe_thunk(lookup_expr) - }; - match stcs.entry(ident) { - Entry::Occupied(occupied) => { - return Err(Error::downgrade_error( - format!( - "attribute '{}' already defined", - format_symbol(ctx.get_sym(*occupied.key())) - ), - ctx.get_current_source(), - span, - ) - .with_span(span) - .with_source(ctx.get_current_source())); - } - Entry::Vacant(vacant) => vacant.insert((expr, span)), - }; + + let pending_value = if let Some(ref from_expr) = from { + PendingValue::InheritFrom(from_expr.clone(), sym, span) + } else { + PendingValue::InheritScope(sym, span) + }; + + self.stcs.insert(sym, (pending_value, span)); + } + Ok(()) } - Ok(()) +} + +fn make_attrpath_value_entry(path: Vec, value: ast::Expr) -> ast::Entry { + use rnix::{NixLanguage, SyntaxKind}; + use rowan::{GreenNodeBuilder, Language, NodeOrToken}; + + let mut builder = GreenNodeBuilder::new(); + builder.start_node(NixLanguage::kind_to_raw(SyntaxKind::NODE_ATTRPATH_VALUE)); + + builder.start_node(NixLanguage::kind_to_raw(SyntaxKind::NODE_ATTRPATH)); + for attr in path { + fn add_node(builder: &mut GreenNodeBuilder, node: &rowan::SyntaxNode) { + for child in node.children_with_tokens() { + match child { + NodeOrToken::Node(n) => { + builder.start_node(NixLanguage::kind_to_raw(n.kind())); + add_node(builder, &n); + builder.finish_node(); + } + NodeOrToken::Token(t) => { + builder.token(NixLanguage::kind_to_raw(t.kind()), t.text()); + } + } + } + } + builder.start_node(NixLanguage::kind_to_raw(attr.syntax().kind())); + add_node(&mut builder, attr.syntax()); + builder.finish_node(); + } + builder.finish_node(); + + builder.token(NixLanguage::kind_to_raw(SyntaxKind::TOKEN_ASSIGN), "="); + + builder.start_node(NixLanguage::kind_to_raw(value.syntax().kind())); + fn add_node_value(builder: &mut GreenNodeBuilder, node: &rowan::SyntaxNode) { + for child in node.children_with_tokens() { + match child { + NodeOrToken::Node(n) => { + builder.start_node(NixLanguage::kind_to_raw(n.kind())); + add_node_value(builder, &n); + builder.finish_node(); + } + NodeOrToken::Token(t) => { + builder.token(NixLanguage::kind_to_raw(t.kind()), t.text()); + } + } + } + } + add_node_value(&mut builder, value.syntax()); + builder.finish_node(); + + builder.token(NixLanguage::kind_to_raw(SyntaxKind::TOKEN_SEMICOLON), ";"); + + builder.finish_node(); + + let green = builder.finish(); + let node = rowan::SyntaxNode::::new_root(green); + ast::Entry::cast(node).unwrap() +} + +/// Downgrades the entries of a non-recursive attribute set. +pub fn downgrade_attrs( + attrs: impl ast::HasEntry + AstNode, + ctx: &mut impl DowngradeContext, +) -> Result { + let span = attrs.syntax().text_range(); + let mut pending = PendingAttrSet::new(span); + pending.collect_entries(attrs.entries(), ctx)?; + finalize_pending_set::<_, true>(pending, &HashMap::new(), ctx) } /// Downgrades a single attribute key (part of an attribute path). -/// An attribute can be a static identifier, an interpolated string, or a dynamic expression. pub fn downgrade_attr(attr: ast::Attr, ctx: &mut impl DowngradeContext) -> Result { use ast::Attr::*; use ast::InterpolPart::*; @@ -114,7 +426,6 @@ pub fn downgrade_attr(attr: ast::Attr, ctx: &mut impl DowngradeContext) -> Resul if parts.is_empty() { Ok(Attr::Str(ctx.new_sym("".to_string()), span)) } else if parts.len() == 1 { - // If the string has only one part, it's either a literal or a single interpolation. match parts.into_iter().next().unwrap() { Literal(ident) => Ok(Attr::Str(ctx.new_sym(ident), span)), Interpolation(interpol) => Ok(Attr::Dynamic( @@ -123,7 +434,6 @@ pub fn downgrade_attr(attr: ast::Attr, ctx: &mut impl DowngradeContext) -> Resul )), } } else { - // If the string has multiple parts, it's an interpolated string that must be concatenated. let parts = parts .into_iter() .map(|part| match part { @@ -155,40 +465,6 @@ pub fn downgrade_attrpath( .collect::>>() } -/// Downgrades an `attrpath = value;` expression and inserts it into an `AttrSet`. -pub fn downgrade_attrpathvalue( - value: ast::AttrpathValue, - attrs: &mut AttrSet, - ctx: &mut impl DowngradeContext, -) -> Result<()> { - let path = downgrade_attrpath(value.attrpath().unwrap(), ctx)?; - let value = value.value().unwrap().downgrade(ctx)?; - let value = ctx.maybe_thunk(value); - attrs.insert(path, value, ctx) -} - -/// A stricter version of `downgrade_attrpathvalue` for `let...in` bindings. -/// It ensures that the attribute path contains no dynamic parts. -pub fn downgrade_static_attrpathvalue( - value: ast::AttrpathValue, - attrs: &mut AttrSet, - ctx: &mut impl DowngradeContext, -) -> Result<()> { - let attrpath_node = value.attrpath().unwrap(); - let path = downgrade_attrpath(attrpath_node.clone(), ctx)?; - if let Some(&Attr::Dynamic(_, span)) = - path.iter().find(|attr| matches!(attr, Attr::Dynamic(..))) - { - return Err(Error::downgrade_error( - "dynamic attributes not allowed in let bindings".to_string(), - ctx.get_current_source(), - span, - )); - } - let value = value.value().unwrap().downgrade(ctx)?; - attrs.insert(path, value, ctx) -} - pub struct PatternBindings { pub body: ExprId, pub required: Vec, @@ -196,7 +472,6 @@ pub struct PatternBindings { } /// Helper function for Lambda pattern parameters. -/// Processes pattern entries like `{ a, b ? 2, ... }@alias` and creates bindings. pub fn downgrade_pattern_bindings( pat_entries: impl Iterator, alias: Option, @@ -312,9 +587,8 @@ where }) } -/// Helper function to downgrade entries with let bindings semantics. -/// This extracts common logic for `let...in` expressions. -/// For `rec` attribute sets, use `downgrade_rec_bindings` instead. +/// Downgrades a `let...in` expression. This is a special case of rec attrs +/// that disallows dynamic attributes and has a body expression. pub fn downgrade_let_bindings( entries: Vec, ctx: &mut Ctx, @@ -325,17 +599,12 @@ 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, - ) + downgrade_rec_attrs_impl::<_, _, false>(entries, ctx, span, |ctx, binding_keys, _dyns| { + body_fn(ctx, binding_keys) + }) } -/// Helper function to downgrade `rec` attribute sets that may contain dynamic attributes. -/// Similar to `downgrade_let_bindings`, but allows dynamic attributes. +/// Downgrades a `rec` attribute set. pub fn downgrade_rec_bindings( entries: Vec, ctx: &mut Ctx, @@ -344,121 +613,40 @@ pub fn downgrade_rec_bindings( 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, - }; + downgrade_rec_attrs_impl::<_, _, true>(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())); - } + 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, - ) + Ok(ctx.new_expr(attrs.to_ir())) + }) } -fn downgrade_let_bindings_impl( +/// Core implementation for recursive bindings (rec attrs and let-in). +/// ALLOW_DYN controls whether dynamic attributes are allowed. +fn downgrade_rec_attrs_impl( entries: Vec, ctx: &mut Ctx, - _span: TextRange, + 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 pending = PendingAttrSet::new(span); + pending.collect_entries(entries.iter().cloned(), ctx)?; - let mut binding_syms = HashSet::new(); + let binding_syms = collect_binding_syms::<_, ALLOW_DYN>(&pending, ctx)?; - for entry in &entries { - if !is_static_entry(entry) && allow_dynamic { - continue; - } - - match entry { - ast::Entry::Inherit(inherit) => { - for attr in inherit.attrs() { - if let ast::Attr::Ident(ident) = attr { - let sym = ctx.new_sym(ident.to_string()); - if !binding_syms.insert(sym) { - return Err(Error::downgrade_error( - format!( - "attribute '{}' already defined", - format_symbol(ctx.get_sym(sym)) - ), - ctx.get_current_source(), - ident.syntax().text_range(), - )); - } - } - } - } - ast::Entry::AttrpathValue(value) => { - let attrpath = value.attrpath().unwrap(); - let attrs_vec: Vec<_> = attrpath.attrs().collect(); - - if attrs_vec.len() == 1 { - if let Some(ast::Attr::Ident(ident)) = attrs_vec.first() { - let sym = ctx.new_sym(ident.to_string()); - if !binding_syms.insert(sym) { - return Err(Error::downgrade_error( - format!( - "attribute '{}' already defined", - format_symbol(ctx.get_sym(sym)) - ), - ctx.get_current_source(), - ident.syntax().text_range(), - )); - } - } - } else if attrs_vec.len() > 1 - && let Some(ast::Attr::Ident(ident)) = attrs_vec.first() - { - let sym = ctx.new_sym(ident.to_string()); - binding_syms.insert(sym); - } - } - } - } - - // IMPORTANT: For `inherit x` (without `from`), we must look up `x` in the OUTER scope - // BEFORE adding the rec's slots to the scope. Otherwise, `x` would resolve to its own - // slot, causing infinite recursion. - let mut inherit_lookups: HashMap = HashMap::new(); - for entry in &entries { - if let ast::Entry::Inherit(inherit) = entry - && inherit.from().is_none() - { - for attr in inherit.attrs() { - if let ast::Attr::Ident(ident) = attr { - let span = ident.syntax().text_range(); - let sym = ctx.new_sym(ident.to_string()); - let expr = ctx.lookup(sym, span)?; - inherit_lookups.insert(sym, (expr, span)); - } - } - } - } + let inherit_lookups = collect_inherit_lookups(&entries, ctx)?; let binding_keys: Vec<_> = binding_syms.into_iter().collect(); let slots: Vec<_> = ctx.reserve_slots(binding_keys.len()).collect(); @@ -469,52 +657,22 @@ where .collect(); for &slot in &slots { - let span = synthetic_span(); - ctx.replace_ir(slot, Thunk { inner: slot, span }.to_ir()); + let slot_span = synthetic_span(); + ctx.replace_ir( + slot, + Thunk { + inner: slot, + span: slot_span, + } + .to_ir(), + ); } ctx.with_let_scope(let_bindings.clone(), |ctx| { - let mut temp_attrs = AttrSet { - stcs: HashMap::new(), - dyns: Vec::new(), - span: synthetic_span(), - }; - - 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() { - downgrade_inherit(inherit, &mut temp_attrs.stcs, ctx)?; - } else { - for attr in inherit.attrs() { - if let ast::Attr::Ident(ident) = attr { - let sym = ctx.new_sym(ident.to_string()); - if let Some(&(expr, span)) = inherit_lookups.get(&sym) { - temp_attrs.stcs.insert(sym, (expr, span)); - } - } - } - } - } - ast::Entry::AttrpathValue(value) => { - if allow_dynamic { - downgrade_attrpathvalue(value, &mut temp_attrs, ctx)?; - } else { - downgrade_static_attrpathvalue(value, &mut temp_attrs, ctx)?; - } - } - } - } + let finalized = finalize_pending_set::<_, ALLOW_DYN>(pending, &inherit_lookups, ctx)?; for (sym, slot) in binding_keys.iter().copied().zip(slots.iter()) { - if let Some(&(expr, _)) = temp_attrs.stcs.get(&sym) { + if let Some(&(expr, _)) = finalized.stcs.get(&sym) { ctx.register_thunk(*slot, expr); } else { return Err(Error::internal(format!( @@ -524,6 +682,163 @@ where } } - body_fn(ctx, &binding_keys, &temp_attrs.dyns) + body_fn(ctx, &binding_keys, &finalized.dyns) }) } + +/// Collects `inherit x` lookups from the outer scope before entering the rec scope. +fn collect_inherit_lookups( + entries: &[ast::Entry], + ctx: &mut Ctx, +) -> Result> { + let mut inherit_lookups = HashMap::new(); + for entry in entries { + if let ast::Entry::Inherit(inherit) = entry + && inherit.from().is_none() + { + for attr in inherit.attrs() { + if let ast::Attr::Ident(ident) = attr { + let attr_span = ident.syntax().text_range(); + let sym = ctx.new_sym(ident.to_string()); + let expr = ctx.lookup(sym, attr_span)?; + inherit_lookups.insert(sym, (expr, attr_span)); + } + } + } + } + Ok(inherit_lookups) +} + +/// Collects binding symbols from a pending set, checking for duplicates. +fn collect_binding_syms( + pending: &PendingAttrSet, + ctx: &mut Ctx, +) -> Result> { + let mut binding_syms = HashSet::new(); + + for (sym, (_, span)) in &pending.stcs { + if !binding_syms.insert(*sym) { + return Err(Error::downgrade_error( + format!( + "attribute '{}' already defined", + format_symbol(ctx.get_sym(*sym)) + ), + ctx.get_current_source(), + *span, + )); + } + } + + Ok(binding_syms) +} + +/// Unified finalize function for PendingAttrSet. +/// ALLOW_DYN controls whether dynamic attributes are allowed. +fn finalize_pending_set( + pending: PendingAttrSet, + inherit_lookups: &HashMap, + ctx: &mut Ctx, +) -> Result { + let mut stcs = HashMap::new(); + let mut dyns = Vec::new(); + + for (sym, (value, value_span)) in pending.stcs { + let expr_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?; + stcs.insert(sym, (expr_id, value_span)); + } + + if ALLOW_DYN { + for (attr, value, value_span) in pending.dyns { + let key_id = downgrade_attr(attr, ctx)?; + let key_expr = match key_id { + Attr::Dynamic(id, _) => id, + Attr::Str(sym, attr_span) => ctx.new_expr( + Str { + val: ctx.get_sym(sym).to_string(), + span: attr_span, + } + .to_ir(), + ), + }; + let value_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?; + dyns.push((key_expr, value_id, value_span)); + } + } + + Ok(AttrSet { + stcs, + dyns, + span: pending.span, + }) +} + +/// Unified finalize function for PendingValue. +/// ALLOW_DYN controls whether dynamic attributes are allowed. +fn finalize_pending_value( + value: PendingValue, + inherit_lookups: &HashMap, + ctx: &mut Ctx, +) -> Result { + match value { + PendingValue::Expr(expr) => { + if !ALLOW_DYN { + check_no_dynamic_attrs(&expr, ctx)?; + } + let id = Downgrade::downgrade(expr, ctx)?; + Ok(ctx.maybe_thunk(id)) + } + PendingValue::InheritFrom(from_expr, sym, span) => { + let from_id = Downgrade::downgrade(from_expr, ctx)?; + let select = Select { + expr: from_id, + attrpath: vec![Attr::Str(sym, span)], + default: None, + span, + }; + let select_id = ctx.new_expr(select.to_ir()); + Ok(ctx.maybe_thunk(select_id)) + } + PendingValue::InheritScope(sym, span) => { + if let Some(&(expr, _)) = inherit_lookups.get(&sym) { + Ok(ctx.maybe_thunk(expr)) + } else { + let lookup_id = ctx.lookup(sym, span)?; + Ok(ctx.maybe_thunk(lookup_id)) + } + } + PendingValue::Set(set) => { + let attrset = finalize_pending_set::<_, ALLOW_DYN>(set, inherit_lookups, ctx)?; + Ok(ctx.new_expr(attrset.to_ir())) + } + PendingValue::ExtendedRecAttrSet { + base, + extensions, + span, + } => { + let mut all_entries: Vec<_> = base.entries().collect(); + all_entries.extend(extensions); + downgrade_rec_bindings(all_entries, ctx, span) + } + } +} + +/// Check that an expression doesn't contain dynamic attributes (for let bindings). +fn check_no_dynamic_attrs(expr: &ast::Expr, ctx: &impl DowngradeContext) -> Result<()> { + let ast::Expr::AttrSet(attrset) = expr else { + return Ok(()) + }; + for v in attrset.attrpath_values() { + v.attrpath().unwrap().attrs().try_for_each(|attr| { + if let ast::Attr::Dynamic(dyn_attr) = attr { + Err(Error::downgrade_error( + "dynamic attributes not allowed in let bindings".to_string(), + ctx.get_current_source(), + dyn_attr.syntax().text_range(), + )) + } else { + Ok(()) + } + })?; + } + Ok(()) +} diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 73685cb..385a37b 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -739,7 +739,7 @@ fn to_value<'a>( } if is_cycle(val, scope, is_cycle_symbol) { - return Value::Thunk; + return Value::Repeated; } if let Some(path_val) = extract_path(val, scope, is_path_symbol) { diff --git a/nix-js/src/value.rs b/nix-js/src/value.rs index 30b8622..ad3ca89 100644 --- a/nix-js/src/value.rs +++ b/nix-js/src/value.rs @@ -214,7 +214,7 @@ impl Display for Value { Func => write!(f, "«lambda»"), PrimOp(name) => write!(f, "«primop {name}»"), PrimOpApp(name) => write!(f, "«partially applied primop {name}»"), - Repeated => write!(f, ""), + Repeated => write!(f, "«repeated»"), } } } diff --git a/nix-js/tests/lang.rs b/nix-js/tests/lang.rs index 827e6d9..7cf2c2a 100644 --- a/nix-js/tests/lang.rs +++ b/nix-js/tests/lang.rs @@ -139,7 +139,7 @@ eval_okay_test!(getenv, || { std::env::set_var("TEST_VAR", "foo") }; }); -eval_okay_test!(groupBy); +eval_okay_test!(#[ignore = "not implemented: hashString"] groupBy); eval_okay_test!(r#if); eval_okay_test!(ind_string); eval_okay_test!(#[ignore = "not implemented: scopedImport"] import); @@ -156,7 +156,7 @@ eval_okay_test!(merge_dynamic_attrs); eval_okay_test!(nested_with); eval_okay_test!(new_let); eval_okay_test!(null_dynamic_attrs); -eval_okay_test!(overrides); +eval_okay_test!(#[ignore = "__overrides is not supported"] overrides); eval_okay_test!(#[ignore = "not implemented: parseFlakeRef"] parse_flake_ref); eval_okay_test!(partition); eval_okay_test!(path);