Files
nix-js/nix-js/src/ir/utils.rs

571 lines
20 KiB
Rust

// Assume no parse error
#![allow(clippy::unwrap_used)]
use std::sync::Arc;
use hashbrown::hash_map::Entry;
use hashbrown::{HashMap, HashSet};
use rnix::ast;
use rowan::ast::AstNode;
use crate::error::{Error, Result};
use crate::ir::{Attr, AttrSet, ConcatStrings, ExprId, Ir, Select, Str, SymId};
use crate::value::format_symbol;
use super::*;
pub fn maybe_thunk(mut expr: ast::Expr, ctx: &mut impl DowngradeContext) -> Result<ExprId> {
use ast::Expr::*;
let expr = loop {
expr = match expr {
Paren(paren) => paren.expr().unwrap(),
Root(root) => root.expr().unwrap(),
expr => break expr,
}
};
match expr {
Error(error) => {
let span = error.syntax().text_range();
return Err(self::Error::downgrade_error(error.to_string())
.with_span(span)
.with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from(""))));
}
Ident(ident) => return ident.downgrade(ctx),
Literal(lit) => return lit.downgrade(ctx),
Str(str) => return str.downgrade(ctx),
Path(path) => return path.downgrade(ctx),
_ => (),
}
let id = match expr {
Apply(apply) => apply.downgrade(ctx),
Assert(assert) => assert.downgrade(ctx),
IfElse(ifelse) => ifelse.downgrade(ctx),
Select(select) => select.downgrade(ctx),
Lambda(lambda) => lambda.downgrade(ctx),
LegacyLet(let_) => let_.downgrade(ctx),
LetIn(letin) => letin.downgrade(ctx),
List(list) => list.downgrade(ctx),
BinOp(op) => op.downgrade(ctx),
AttrSet(attrs) => attrs.downgrade(ctx),
UnaryOp(op) => op.downgrade(ctx),
With(with) => with.downgrade(ctx),
HasAttr(has) => has.downgrade(ctx),
_ => unreachable!(),
}?;
Ok(ctx.new_expr(
Thunk {
inner: id,
// span: ctx.get_span(id),
// FIXME: span
span: synthetic_span()
}
.to_ir(),
))
}
/// 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<AttrSet> {
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_attrpathvalue(value, &mut attrs, ctx)?,
}
}
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<HashMap<SymId, ExprId>> {
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)
}
/// 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<SymId, ExprId>,
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.
return Err(Error::downgrade_error(
"dynamic attributes not allowed in inherit".to_string(),
)
.with_span(span)
.with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from(""))));
}
};
let expr = if let Some(expr) = from {
let select_expr = ctx.new_expr(
Select {
expr,
attrpath: vec![Attr::Str(ident)],
default: None,
span,
}
.to_ir(),
);
ctx.new_expr(
Thunk {
inner: select_expr,
span,
}
.to_ir(),
)
} else {
ctx.lookup(ident, span)?
};
match stcs.entry(ident) {
Entry::Occupied(occupied) => {
return Err(Error::downgrade_error(format!(
"attribute '{}' already defined",
format_symbol(ctx.get_sym(*occupied.key()))
))
.with_span(span)
.with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from(""))));
}
Entry::Vacant(vacant) => vacant.insert(expr),
};
}
Ok(())
}
/// 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<Attr> {
use ast::Attr::*;
use ast::InterpolPart::*;
let span = attr.syntax().text_range();
match attr {
Ident(ident) => Ok(Attr::Str(ctx.new_sym(ident.to_string()))),
Str(string) => {
let parts = string.normalized_parts();
if parts.is_empty() {
Ok(Attr::Str(ctx.new_sym("".to_string())))
} 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))),
Interpolation(interpol) => {
Ok(Attr::Dynamic(interpol.expr().unwrap().downgrade(ctx)?))
}
}
} 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 {
Literal(lit) => Ok(ctx.new_expr(self::Str { val: lit, span }.to_ir())),
Interpolation(interpol) => interpol.expr().unwrap().downgrade(ctx),
})
.collect::<Result<Vec<_>>>()?;
Ok(Attr::Dynamic(
ctx.new_expr(ConcatStrings { parts, span }.to_ir()),
))
}
}
Dynamic(dynamic) => Ok(Attr::Dynamic(dynamic.expr().unwrap().downgrade(ctx)?)),
}
}
/// Downgrades an attribute path (e.g., `a.b."${c}".d`) into a `Vec<Attr>`.
pub fn downgrade_attrpath(
attrpath: ast::Attrpath,
ctx: &mut impl DowngradeContext,
) -> Result<Vec<Attr>> {
attrpath
.attrs()
.map(|attr| downgrade_attr(attr, ctx))
.collect::<Result<Vec<_>>>()
}
/// 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 = maybe_thunk(value.value().unwrap(), ctx)?;
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 path.iter().any(|attr| matches!(attr, Attr::Dynamic(_))) {
return Err(Error::downgrade_error(
"dynamic attributes not allowed in let bindings".to_string(),
)
.with_span(attrpath_node.syntax().text_range())
.with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from(""))));
}
let value = value.value().unwrap().downgrade(ctx)?;
attrs.insert(path, value, ctx)
}
pub struct PatternBindings {
pub body: ExprId,
pub scc_info: SccInfo,
pub required_params: Vec<SymId>,
pub allowed_params: Option<HashSet<SymId>>,
}
/// Helper function for Lambda pattern parameters with SCC analysis.
/// Processes pattern entries like `{ a, b ? 2, ... }@alias` and creates optimized 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>(
pat_entries: impl Iterator<Item = ast::PatEntry>,
alias: Option<SymId>,
arg: ExprId,
has_ellipsis: bool,
ctx: &mut Ctx,
body_fn: impl FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>,
) -> Result<PatternBindings>
where
Ctx: DowngradeContext,
{
let mut param_syms = Vec::new();
let mut param_defaults = Vec::new();
let mut param_spans = Vec::new();
let mut seen_params = HashSet::new();
for entry in pat_entries {
let sym = ctx.new_sym(entry.ident().unwrap().to_string());
let span = entry.ident().unwrap().syntax().text_range();
if !seen_params.insert(sym) {
return Err(Error::downgrade_error(format!(
"duplicate parameter '{}'",
format_symbol(ctx.get_sym(sym))
))
.with_span(span)
.with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from(""))));
}
let default_ast = entry.default();
param_syms.push(sym);
param_defaults.push(default_ast);
param_spans.push(span);
}
let mut binding_keys: Vec<SymId> = param_syms.clone();
if let Some(alias_sym) = alias {
binding_keys.push(alias_sym);
}
let required: Vec<SymId> = param_syms
.iter()
.zip(param_defaults.iter())
.filter_map(|(&sym, default)| if default.is_none() { Some(sym) } else { None })
.collect();
let allowed: Option<HashSet<SymId>> = if has_ellipsis {
None
} else {
Some(param_syms.iter().copied().collect())
};
// 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 ((sym, default_ast), span) in param_syms
.iter()
.zip(param_defaults.iter())
.zip(param_spans.iter())
{
let slot = *sym_to_slot.get(sym).unwrap();
ctx.set_current_binding(Some(slot));
let default = if let Some(default_expr) = default_ast {
Some(default_expr.clone().downgrade(ctx)?)
} else {
None
};
let select_expr = ctx.new_expr(
Select {
expr: arg,
attrpath: vec![Attr::Str(*sym)],
default,
span: *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_params: required,
allowed_params: allowed,
})
}
/// 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 let_bindings: HashMap<_, _> = binding_keys
.iter()
.copied()
.zip(slots.iter().copied())
.collect();
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)?;
let scc_info = ctx.pop_dep_tracker()?;
for (sym, slot) in binding_keys.iter().copied().zip(slots.iter()) {
if let Some(&expr) = bindings.get(&sym) {
ctx.replace_expr(
*slot,
Thunk {
inner: expr,
// span: ctx.get_span(expr),
// FIXME: span
span: synthetic_span()
}
.to_ir(),
);
} else {
return Err(Error::downgrade_error(format!(
"binding '{}' not found",
format_symbol(ctx.get_sym(sym))
))
.with_span(synthetic_span())
.with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from(""))));
}
}
let body = body_fn(ctx, &binding_keys)?;
Ok((scc_info, body))
})
}
/// Helper function to downgrade entries with let bindings semantics.
/// 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>(
entries: Vec<ast::Entry>,
ctx: &mut Ctx,
body_fn: F,
) -> Result<(SccInfo, ExprId)>
where
Ctx: DowngradeContext,
F: FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>,
{
let mut binding_syms = HashSet::new();
for entry in &entries {
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))
))
.with_span(ident.syntax().text_range())
.with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from(""))));
}
}
}
}
ast::Entry::AttrpathValue(value) => {
let attrpath = value.attrpath().unwrap();
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 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))
))
.with_span(ident.syntax().text_range())
.with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from(""))));
}
}
} 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() {
let sym = ctx.new_sym(ident.to_string());
binding_syms.insert(sym);
}
}
}
}
}
let binding_keys: Vec<_> = binding_syms.into_iter().collect();
downgrade_bindings_generic(
ctx,
binding_keys,
|ctx, sym_to_slot| {
let mut temp_attrs = AttrSet {
stcs: HashMap::new(),
dyns: Vec::new(),
span: synthetic_span()
};
for entry in entries {
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());
let slot = *sym_to_slot.get(&sym).unwrap();
ctx.set_current_binding(Some(slot));
}
}
downgrade_inherit(inherit, &mut temp_attrs.stcs, ctx)?;
ctx.set_current_binding(None);
}
ast::Entry::AttrpathValue(value) => {
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.stcs)
},
body_fn,
)
}