refactor: reduce coupling

This commit is contained in:
2025-07-28 21:37:27 +08:00
parent 78e3c5a26e
commit 7afb2a7b1c
53 changed files with 2964 additions and 3444 deletions

View File

@@ -0,0 +1,16 @@
[package]
name = "nixjit_hir"
description = "The high-level intermediate representation (HIR) for nixjit."
version = "0.1.0"
edition = "2024"
[dependencies]
derive_more = { version = "2.0", features = ["full"] }
hashbrown = "0.15"
itertools = "0.14"
rnix = "0.12"
nixjit_error = { path = "../nixjit_error" }
nixjit_ir = { path = "../nixjit_ir" }
nixjit_macros = { path = "../nixjit_macros" }
nixjit_value = { path = "../nixjit_value" }

View File

@@ -0,0 +1,357 @@
//! This module handles the "downgrading" of the `rnix` Abstract Syntax Tree (AST)
//! into the High-level Intermediate Representation (HIR). The term "downgrade" is used
//! because the process moves from a concrete syntax tree, which is very detailed about
//! source code structure (like parentheses and whitespace), to a more abstract,
//! semantically-focused representation.
//!
//! The core of this module is the `Downgrade` trait, which defines a standard way to
//! convert different AST node types into their corresponding HIR representations.
use rnix::ast::{self, Expr};
use nixjit_error::{Error, Result};
use nixjit_ir as ir;
use super::*;
/// A trait for converting (downgrading) an `rnix` AST node into an HIR expression.
pub trait Downgrade<Ctx: DowngradeContext> {
/// Performs the downgrade conversion.
///
/// # Arguments
/// * `self` - The `rnix` AST node to convert.
/// * `ctx` - The context for the conversion, used for allocating new HIR expressions.
///
/// # Returns
/// A `Result` containing the `ExprId` of the newly created HIR expression, or an error.
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId>;
}
/// The main entry point for downgrading any `rnix` expression.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for Expr {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
use Expr::*;
match self {
// Dispatch to the specific implementation for each expression type.
Apply(apply) => apply.downgrade(ctx),
Assert(assert) => assert.downgrade(ctx),
Error(error) => Err(self::Error::DowngradeError(error.to_string())),
IfElse(ifelse) => ifelse.downgrade(ctx),
Select(select) => select.downgrade(ctx),
Str(str) => str.downgrade(ctx),
Path(path) => path.downgrade(ctx),
Literal(lit) => lit.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),
Ident(ident) => ident.downgrade(ctx),
With(with) => with.downgrade(ctx),
HasAttr(has) => has.downgrade(ctx),
// Parentheses and the root node are transparent; we just downgrade their contents.
Paren(paren) => paren.expr().unwrap().downgrade(ctx),
Root(root) => root.expr().unwrap().downgrade(ctx),
}
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Assert {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let assertion = self.condition().unwrap().downgrade(ctx)?;
let expr = self.body().unwrap().downgrade(ctx)?;
Ok(ctx.new_expr(Assert { assertion, expr }.to_hir()))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::IfElse {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let cond = self.condition().unwrap().downgrade(ctx)?;
let consq = self.body().unwrap().downgrade(ctx)?;
let alter = self.else_body().unwrap().downgrade(ctx)?;
Ok(ctx.new_expr(If { cond, consq, alter }.to_hir()))
}
}
/// Downgrades a path expression.
/// A path can be a simple literal or contain interpolated expressions.
/// If it contains interpolations, it's converted into a `ConcatStrings` HIR node
/// which is then wrapped in a `Path` node.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let parts = self
.parts()
.map(|part| match part {
ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(
Str {
val: lit.to_string(),
}
.to_hir(),
)),
ast::InterpolPart::Interpolation(interpol) => {
interpol.expr().unwrap().downgrade(ctx)
}
})
.collect::<Result<Vec<_>>>()?;
let expr = if parts.len() == 1 {
// If there's only one part, it's a simple string, no concatenation needed.
parts.into_iter().next().unwrap()
} else {
// Multiple parts (e.g., `./${name}.txt`) require string concatenation.
ctx.new_expr(ConcatStrings { parts }.to_hir())
};
Ok(ctx.new_expr(Path { expr }.to_hir()))
}
}
/// Downgrades a string expression.
/// A string can be a simple literal or contain interpolated expressions.
/// If it contains interpolations, it's converted into a `ConcatStrings` HIR node.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Str {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let parts = self
.normalized_parts()
.into_iter()
.map(|part| match part {
ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Str { val: lit }.to_hir())),
ast::InterpolPart::Interpolation(interpol) => {
interpol.expr().unwrap().downgrade(ctx)
}
})
.collect::<Result<Vec<_>>>()?;
Ok(if parts.len() == 1 {
// If there's only one part, it's a simple string, no concatenation needed.
parts.into_iter().next().unwrap()
} else {
// Multiple parts (e.g., "hello ${name}") require string concatenation.
ctx.new_expr(ConcatStrings { parts }.to_hir())
})
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Literal {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
Ok(ctx.new_expr(match self.kind() {
ast::LiteralKind::Integer(int) => Const::from(int.value().unwrap()).to_hir(),
ast::LiteralKind::Float(float) => Const::from(float.value().unwrap()).to_hir(),
ast::LiteralKind::Uri(uri) => Str {
val: uri.to_string(),
}
.to_hir(),
}))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Ident {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let sym = self.ident_token().unwrap().to_string();
Ok(ctx.new_expr(Var { sym }.to_hir()))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let rec = self.rec_token().is_some();
let mut attrs = downgrade_attrs(self, ctx)?;
attrs.rec = rec;
Ok(ctx.new_expr(attrs.to_hir()))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::List {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let mut items = Vec::with_capacity(self.items().size_hint().0);
for item in self.items() {
items.push(item.downgrade(ctx)?)
}
Ok(ctx.new_expr(List { items }.to_hir()))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::BinOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let lhs = self.lhs().unwrap().downgrade(ctx)?;
let rhs = self.rhs().unwrap().downgrade(ctx)?;
let kind = self.operator().unwrap().into();
Ok(ctx.new_expr(BinOp { lhs, rhs, kind }.to_hir()))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::HasAttr {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let lhs = self.expr().unwrap().downgrade(ctx)?;
let rhs = downgrade_attrpath(self.attrpath().unwrap(), ctx)?;
Ok(ctx.new_expr(HasAttr { lhs, rhs }.to_hir()))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::UnaryOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let rhs = self.expr().unwrap().downgrade(ctx)?;
let kind = self.operator().unwrap().into();
Ok(ctx.new_expr(UnOp { rhs, kind }.to_hir()))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Select {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let expr = self.expr().unwrap().downgrade(ctx)?;
let attrpath = downgrade_attrpath(self.attrpath().unwrap(), ctx)?;
let default = if let Some(default) = self.default_expr() {
Some(default.downgrade(ctx)?)
} else {
None
};
Ok(ctx.new_expr(
Select {
expr,
attrpath,
default,
}
.to_hir(),
))
}
}
/// Downgrades a `legacy let`, which is essentially a recursive attribute set.
/// The body of the `let` is accessed via `let.body`.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LegacyLet {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let mut attrs = downgrade_attrs(self, ctx)?;
attrs.rec = true;
let expr = ctx.new_expr(attrs.to_hir());
// The result of a `legacy let` is the `body` attribute of the resulting set.
let attrpath = vec![Attr::Str("body".into())];
Ok(ctx.new_expr(
Select {
expr,
attrpath,
default: None,
}
.to_hir(),
))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LetIn {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let body = self.body().unwrap().downgrade(ctx)?;
let bindings = downgrade_static_attrs(self, ctx)?;
Ok(ctx.new_expr(Let { bindings, body }.to_hir()))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::With {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let namespace = self.namespace().unwrap().downgrade(ctx)?;
let expr = self.body().unwrap().downgrade(ctx)?;
Ok(ctx.new_expr(With { namespace, expr }.to_hir()))
}
}
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let param = downgrade_param(self.param().unwrap(), ctx)?;
let mut body = self.body().unwrap().downgrade(ctx)?;
// Desugar pattern matching in function arguments into a `let` expression.
// For example, `({ a, b ? 2 }): a + b` is desugared into:
// `arg: let a = arg.a; b = arg.b or 2; in a + b`
if let Param::Formals { formals, alias, .. } = &param {
// `Arg` represents the raw argument (the attribute set) passed to the function.
let arg = ctx.new_expr(Hir::Arg(Arg));
let mut bindings: HashMap<_, _> = formals
.iter()
.map(|&(ref k, default)| {
// For each formal parameter, create a `Select` expression to extract it from the argument set.
(
k.clone(),
ctx.new_expr(
Select {
expr: arg,
attrpath: vec![Attr::Str(k.clone())],
default,
}
.to_hir(),
),
)
})
.collect();
// If there's an alias (`... }@alias`), bind the alias name to the raw argument set.
if let Some(alias) = alias {
bindings.insert(
alias.clone(),
ctx.new_expr(Var { sym: alias.clone() }.to_hir()),
);
}
// Wrap the original function body in the new `let` expression.
let let_ = Let { bindings, body };
body = ctx.new_expr(let_.to_hir());
}
let ident;
let required;
let allowed;
match param {
Param::Ident(id) => {
ident = Some(id);
required = None;
allowed = None;
}
Param::Formals {
formals,
ellipsis,
alias,
} => {
ident = alias;
required = Some(
formals
.iter()
.cloned()
.filter(|(_, default)| default.is_none())
.map(|(k, _)| k)
.collect(),
);
allowed = if ellipsis {
None
} else {
Some(
formals
.into_iter()
.filter(|(_, default)| default.is_some())
.map(|(k, _)| k)
.collect(),
)
};
}
}
let param = ir::Param {
ident,
required,
allowed,
};
// The function's body and parameters are now stored directly in the `Func` node.
Ok(ctx.new_expr(Func { body, param }.to_hir()))
}
}
/// Downgrades a function application.
/// The `rnix` AST represents chained function calls as nested `Apply` nodes,
/// e.g., `f a b` is parsed as `(f a) b`. This implementation unnests these
/// calls into a single `Call` HIR node with a list of arguments.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Apply {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let mut args = vec![self.argument().unwrap().downgrade(ctx)?];
let mut func = self.lambda().unwrap();
// Traverse the chain of nested `Apply` nodes to collect all arguments.
while let ast::Expr::Apply(call) = func {
func = call.lambda().unwrap();
args.push(call.argument().unwrap().downgrade(ctx)?);
}
let func = func.downgrade(ctx)?;
// The arguments were collected in reverse order, so fix that.
args.reverse();
Ok(ctx.new_expr(Call { func, args }.to_hir()))
}
}

View File

@@ -0,0 +1,202 @@
//! The high-level intermediate representation (HIR) for nixjit.
//!
//! This module defines the data structures for the HIR, which is a more abstract and
//! semantically rich representation of the original Nix code compared to the raw AST from `rnix`.
//! It's designed to be easily translatable from the AST and serves as a stepping stone
//! towards the lower-level IR (`nixjit_lir`).
//!
//! The key components are:
//! - `Hir`: An enum representing all possible expression types in the HIR.
//! - `Downgrade`: A trait for converting `rnix::ast` nodes into HIR expressions.
//! - `DowngradeContext`: A trait that provides the necessary context for the conversion,
//! such as allocating new expressions and functions.
use derive_more::{IsVariant, TryUnwrap, Unwrap};
use hashbrown::HashMap;
use nixjit_error::{Error, Result};
use nixjit_ir::{
Assert, Attr, AttrSet, BinOp, Call, ConcatStrings, Const, ExprId, Func, HasAttr, If, List,
Path, Select, Str, UnOp, Var, With,
};
use nixjit_macros::ir;
use nixjit_value::format_symbol;
mod downgrade;
mod utils;
use utils::*;
pub use downgrade::Downgrade;
/// A context for the AST-to-HIR downgrading process.
///
/// This trait abstracts the storage of HIR expressions and functions, allowing the
/// `downgrade` implementations to be generic over the specific context implementation.
pub trait DowngradeContext {
/// Allocates a new HIR expression in the context and returns its ID.
fn new_expr(&mut self, expr: Hir) -> ExprId;
/// Provides temporary access to an immutable expression for inspection or use.
fn with_expr<T>(&self, id: ExprId, f: impl FnOnce(&Hir, &Self) -> T) -> T;
/// Provides temporary mutable access to an expression.
fn with_expr_mut<T>(&mut self, id: ExprId, f: impl FnOnce(&mut Hir, &mut Self) -> T) -> T;
}
ir! {
Hir,
// Represents an attribute set, e.g., `{ a = 1; b = 2; }`.
AttrSet,
// Represents a list, e.g., `[1 2 3]`.
List,
// Represents a "has attribute" check, e.g., `attrs ? a`.
HasAttr,
// Represents a binary operation, e.g., `a + b`.
BinOp,
// Represents a unary operation, e.g., `-a`.
UnOp,
// Represents an attribute selection, e.g., `attrs.a` or `attrs.a or defaultValue`.
Select,
// Represents an if-then-else expression.
If,
// Represents a function definition (lambda).
Func,
// Represents a function call.
Call,
// Represents a `with` expression, e.g., `with pkgs; stdenv.mkDerivation { ... }`.
With,
// Represents an `assert` expression.
Assert,
// Represents the concatenation of strings, often from interpolated strings.
ConcatStrings,
// Represents a constant value (integer, float, boolean, null).
Const,
// Represents a simple string literal.
Str,
// Represents a variable lookup by its symbol/name.
Var,
// Represents a path expression.
Path,
// Represents a `let ... in ...` binding.
Let { pub bindings: HashMap<String, ExprId>, pub body: ExprId },
// Represents a function argument lookup.
Arg,
}
#[derive(Debug)]
pub struct Arg;
/// A trait defining operations on attribute sets within the HIR.
trait Attrs {
/// Inserts a value into the attribute set at a given path.
///
/// # Example
/// `insert([a, b], value)` corresponds to `a.b = value;`.
fn insert(
&mut self,
path: Vec<Attr>,
value: ExprId,
ctx: &mut impl DowngradeContext,
) -> Result<()>;
/// Internal helper for recursively inserting an attribute.
fn _insert(
&mut self,
path: impl Iterator<Item = Attr>,
name: Attr,
value: ExprId,
ctx: &mut impl DowngradeContext,
) -> Result<()>;
}
impl Attrs for AttrSet {
fn _insert(
&mut self,
mut path: impl Iterator<Item = Attr>,
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) => {
// 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.
ctx.with_expr_mut(id, |expr, ctx| {
expr.as_mut()
.try_unwrap_attr_set()
.map_err(|_| {
// This path segment exists but is not an attrset.
Error::DowngradeError(format!(
r#"attribute '{}' already defined"#,
format_symbol(&ident)
))
})
.and_then(|attrs| attrs._insert(path, name, value, ctx))
})?;
} else {
// Create a new sub-attrset because this path doesn't exist yet.
let mut attrs = AttrSet::default();
attrs._insert(path, name, value, ctx)?;
let attrs = ctx.new_expr(attrs.to_hir());
self.stcs.insert(ident, attrs);
}
Ok(())
}
Attr::Dynamic(dynamic) => {
// If the next attribute is a dynamic expression.
let mut attrs = AttrSet::default();
attrs._insert(path, name, value, ctx)?;
self.dyns.push((dynamic, ctx.new_expr(attrs.to_hir())));
Ok(())
}
}
} else {
// This is the final attribute in the path, so insert the value here.
match name {
Attr::Str(ident) => {
if self.stcs.insert(ident.clone(), value).is_some() {
return Err(Error::DowngradeError(format!(
r#"attribute '{}' already defined"#,
format_symbol(&ident)
)));
}
}
Attr::Dynamic(dynamic) => {
self.dyns.push((dynamic, value));
}
}
Ok(())
}
}
fn insert(
&mut self,
path: Vec<Attr>,
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().unwrap();
self._insert(path, name, value, ctx)
}
}
#[derive(Clone, Debug)]
enum Param {
/// A simple parameter, e.g., `x: ...`.
Ident(String),
/// A pattern-matching parameter (formals), e.g., `{ a, b ? 2, ... }@args: ...`.
Formals {
/// The individual formal parameters, with optional default values.
formals: Vec<(String, Option<ExprId>)>,
/// Whether an ellipsis (`...`) is present, allowing extra arguments.
ellipsis: bool,
/// An optional alias for the entire argument set, e.g., `args @ { ... }`.
alias: Option<String>,
},
}

View File

@@ -0,0 +1,223 @@
//! This module provides utility functions for the AST-to-HIR downgrade process.
//! These functions handle common, often complex, patterns in the `rnix` AST,
//! such as parsing parameters, attribute sets, and `inherit` statements.
//! They are helpers to the main `Downgrade` trait implementations.
use hashbrown::HashMap;
use rnix::ast;
use nixjit_error::{Error, Result};
use nixjit_ir::{Attr, AttrSet, ConcatStrings, ExprId, Select, Str, Var};
use super::ToHir;
use super::downgrade::Downgrade;
use super::{Attrs, DowngradeContext, Param};
/// Downgrades a function parameter from the AST.
pub fn downgrade_param(param: ast::Param, ctx: &mut impl DowngradeContext) -> Result<Param> {
match param {
ast::Param::IdentParam(ident) => Ok(Param::Ident(ident.to_string())),
ast::Param::Pattern(pattern) => downgrade_pattern(pattern, ctx),
}
}
/// Downgrades a parameter pattern (formals) from the AST.
/// This handles `{ a, b ? 2, ... }@args` style parameters.
pub fn downgrade_pattern(pattern: ast::Pattern, ctx: &mut impl DowngradeContext) -> Result<Param> {
// Extract each formal parameter, downgrading its default value if it exists.
let formals = pattern
.pat_entries()
.map(|entry| {
let ident = entry.ident().unwrap().to_string();
if entry.default().is_none() {
Ok((ident, None))
} else {
entry
.default()
.unwrap()
.downgrade(ctx)
.map(|ok| (ident, Some(ok)))
}
})
.collect::<Result<Vec<_>>>()?;
let ellipsis = pattern.ellipsis_token().is_some();
let alias = pattern
.pat_bind()
.map(|alias| alias.ident().unwrap().to_string());
Ok(Param::Formals {
formals,
ellipsis,
alias,
})
}
/// Downgrades the entries of an attribute set.
/// This handles `inherit` and `attrpath = value;` entries.
pub fn downgrade_attrs(
attrs: impl ast::HasEntry,
ctx: &mut impl DowngradeContext,
) -> Result<AttrSet> {
let entries = attrs.entries();
let mut attrs = AttrSet {
stcs: HashMap::new(),
dyns: Vec::new(),
rec: false,
};
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,
ctx: &mut impl DowngradeContext,
) -> Result<HashMap<String, ExprId>> {
let entries = attrs.entries();
let mut attrs = AttrSet {
stcs: HashMap::new(),
dyns: Vec::new(),
rec: false,
};
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<String, 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 ident = match downgrade_attr(attr, ctx)? {
Attr::Str(ident) => ident,
_ => {
// `inherit` does not allow dynamic attributes.
return Err(Error::DowngradeError(
"dynamic attributes not allowed in inherit".to_string(),
));
}
};
let expr = from.map_or_else(
// If `from` is None, `inherit foo;` becomes `foo = foo;`.
|| Var { sym: ident.clone() }.to_hir(),
// If `from` is Some, `inherit (from) foo;` becomes `foo = from.foo;`.
|expr| {
Select {
expr,
attrpath: vec![Attr::Str(ident.clone())],
default: None,
}
.to_hir()
},
);
if stcs.insert(ident, ctx.new_expr(expr)).is_some() {
// TODO: Handle or error on duplicate attribute definitions.
todo!()
}
}
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::*;
match attr {
Ident(ident) => Ok(Attr::Str(ident.to_string())),
Str(string) => {
let parts = string.normalized_parts();
if parts.is_empty() {
Ok(Attr::Str("".into()))
} 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(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 }.to_hir())),
Interpolation(interpol) => interpol.expr().unwrap().downgrade(ctx),
})
.collect::<Result<Vec<_>>>()?;
Ok(Attr::Dynamic(
ctx.new_expr(ConcatStrings { parts }.to_hir()),
))
}
}
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 = value.value().unwrap().downgrade(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 path = downgrade_attrpath(value.attrpath().unwrap(), ctx)?;
if path.iter().any(|attr| matches!(attr, Attr::Dynamic(_))) {
return Err(Error::DowngradeError(
"dynamic attributes not allowed".to_string(),
));
}
let value = value.value().unwrap().downgrade(ctx)?;
attrs.insert(path, value, ctx)
}