Skip to main content

graphcal_compiler/hir/
expr.rs

1//! HIR expression/value reference types and lowering.
2//!
3//! This module is the expression-side counterpart to [`super::types`]. It
4//! lowers the desugared syntax AST into a HIR expression tree whose reference
5//! positions use canonical module identities or lexical local IDs. This is the
6//! single name-resolution stage of the pipeline: syntactic reference paths
7//! ([`crate::syntax::ast::UnresolvedRef`]) are classified and resolved here,
8//! in one pass, against the lexical scope and the module-aware resolver.
9//! Source paths (`NamePath` / `IdentPath` / `ScopedName`) are consumed at
10//! this boundary and are not stored in HIR reference fields.
11//!
12//! Lowering is diagnostic-accumulating: a reference that cannot be resolved
13//! becomes an explicit [`ExprKind::Error`] node and its diagnostic is
14//! recorded, so IDE consumers can keep working on incomplete code. The strict
15//! entry points ([`lower_expr`], [`lower_assert_body`]) reject any tree that
16//! contains an error node, so the batch pipeline never sees one.
17
18use std::collections::{BTreeSet, HashMap};
19
20use thiserror::Error;
21
22use crate::dag_id::DagId;
23use crate::desugar::desugared_ast as ast;
24use crate::registry::resolve_types::{
25    AggregationFn, ConstructorFn, DatetimeExtractFn, DatetimeFromFn, DatetimeToFn, SpecialFnKind,
26    TypeConversionFn,
27};
28use crate::registry::time_scale::TimeScale;
29use crate::syntax::ast::{Ident, IdentPath, UnresolvedRef};
30use crate::syntax::module_resolve::{DeclSymbolKind, ModuleResolveError, ModuleResolver};
31use crate::syntax::names::{
32    DeclName, FieldName, GenericParamName, IndexName, IndexVariantName, LocalName, NameAtom,
33    NameAtomError, NameNamespace, NamePath, ResolvedIndexVariant, ResolvedName, ScopedName,
34    namespace,
35};
36use crate::syntax::non_empty::NonEmpty;
37use crate::syntax::phase::never;
38use crate::syntax::span::{Span, Spanned};
39
40use super::lower::{
41    GenericScope, HirLowerError, PreludeTypeScope, TypeLoweringContext, lower_nat_expr,
42    lower_type_expr,
43};
44use super::types::{NatExpr, TypeExpr};
45
46/// Errors produced while lowering syntax expressions into HIR.
47#[derive(Debug, Clone, PartialEq, Eq, Error)]
48pub enum ExprLowerError {
49    /// A type-level generic argument failed to lower.
50    #[error(transparent)]
51    Type(#[from] HirLowerError),
52    /// A module-aware lookup failed at an expression use site.
53    #[error("{source}")]
54    ModuleResolve {
55        #[source]
56        source: ModuleResolveError,
57        span: Span,
58    },
59    /// A structured diagnostic name contained a segment that cannot be a source name atom.
60    #[error("invalid scoped-name segment `{segment}`: {source}")]
61    InvalidScopedNameSegment {
62        segment: String,
63        #[source]
64        source: NameAtomError,
65        span: Span,
66    },
67    /// A local reference had no lexical binding in scope.
68    #[error("unknown local variable `{name}`")]
69    UnknownLocalRef { name: LocalName, span: Span },
70    /// A graph reference (`@name`) did not resolve to any declaration.
71    #[error("unknown graph reference `@{name}`")]
72    UnknownGraphRef { name: ScopedName, span: Span },
73    /// A single expression tree introduced more local bindings than HIR can index.
74    #[error("too many local bindings in one expression")]
75    TooManyLocals { span: Span },
76    /// A map literal entry unexpectedly had no keys after syntax lowering.
77    #[error("map literal entry has no keys")]
78    EmptyMapEntry { span: Span },
79    /// A map literal used a key variant that is not declared by its index.
80    #[error("extra variant `{variant_name}` in map literal for index `{index_name}`")]
81    ExtraMapVariant {
82        index_name: IndexName,
83        variant_name: IndexVariantName,
84        span: Span,
85    },
86    /// One lexical scope introduced the same local name twice.
87    #[error("duplicate local binding `{name}`")]
88    DuplicateLocalBinding {
89        name: LocalName,
90        first: Span,
91        duplicate: Span,
92    },
93    /// A function call could not be resolved to a built-in function.
94    #[error("unknown function `{path}`")]
95    UnknownFunction { path: String, span: Span },
96    /// A built-in function was called with the wrong number of arguments.
97    #[error("function `{name}` expects {expected} argument(s), got {got}")]
98    WrongArity {
99        name: crate::syntax::names::FnName,
100        expected: usize,
101        got: usize,
102        span: Span,
103    },
104    /// A path-pattern could not be resolved to a constructor or index label.
105    #[error("unknown match pattern `{path}`")]
106    UnknownPattern { path: String, span: Span },
107}
108
109/// Context required to lower one expression tree into HIR.
110#[derive(Debug, Clone, Copy)]
111pub struct ExprLoweringContext<'a> {
112    pub owner: &'a DagId,
113    pub resolver: &'a ModuleResolver,
114    pub generic_scope: &'a GenericScope,
115    pub prelude: Option<&'a PreludeTypeScope>,
116    pub decl_bindings: Option<&'a HashMap<ScopedName, ResolvedName<namespace::Decl>>>,
117}
118
119impl<'a> ExprLoweringContext<'a> {
120    /// Create an expression-lowering context.
121    #[must_use]
122    pub const fn new(
123        owner: &'a DagId,
124        resolver: &'a ModuleResolver,
125        generic_scope: &'a GenericScope,
126    ) -> Self {
127        Self {
128            owner,
129            resolver,
130            generic_scope,
131            prelude: None,
132            decl_bindings: None,
133        }
134    }
135
136    /// Add implicit prelude type symbols for lowering type arguments.
137    #[must_use]
138    pub const fn with_prelude(self, prelude: &'a PreludeTypeScope) -> Self {
139        Self {
140            owner: self.owner,
141            resolver: self.resolver,
142            generic_scope: self.generic_scope,
143            prelude: Some(prelude),
144            decl_bindings: self.decl_bindings,
145        }
146    }
147
148    /// Add canonical declaration bindings for declarations already visible in
149    /// the lowered IR, such as prefixed dependency entries and DAG self-imports.
150    #[must_use]
151    pub const fn with_decl_bindings(
152        self,
153        decl_bindings: &'a HashMap<ScopedName, ResolvedName<namespace::Decl>>,
154    ) -> Self {
155        Self {
156            owner: self.owner,
157            resolver: self.resolver,
158            generic_scope: self.generic_scope,
159            prelude: self.prelude,
160            decl_bindings: Some(decl_bindings),
161        }
162    }
163
164    const fn type_context(self) -> TypeLoweringContext<'a> {
165        let ctx = TypeLoweringContext::new(self.owner, self.resolver, self.generic_scope);
166        match self.prelude {
167            Some(prelude) => ctx.with_prelude(prelude),
168            None => ctx,
169        }
170    }
171}
172
173/// Lower a syntax expression into HIR, accumulating diagnostics.
174///
175/// References that cannot be resolved become [`ExprKind::Error`] nodes and
176/// their diagnostics are returned alongside the lowered tree, so consumers
177/// that must keep working on incomplete code (the LSP) still get a tree with
178/// spans for every position that did resolve.
179#[must_use]
180pub fn lower_expr_tolerant(
181    expr: &ast::Expr,
182    ctx: ExprLoweringContext<'_>,
183) -> (Expr, Vec<ExprLowerError>) {
184    let mut lowerer = ExprLowerer::new(ctx);
185    let hir_expr = lowerer.lower_expr(expr);
186    (hir_expr, lowerer.diagnostics)
187}
188
189/// Lower a syntax expression into HIR, rejecting unresolved references.
190///
191/// This is the batch-pipeline boundary: the lowered tree is guaranteed to
192/// contain no [`ExprKind::Error`] node.
193///
194/// # Errors
195///
196/// Returns the first [`ExprLowerError`] if any expression-level reference
197/// cannot be resolved to a canonical module identity or lexical local binding.
198pub fn lower_expr(expr: &ast::Expr, ctx: ExprLoweringContext<'_>) -> Result<Expr, ExprLowerError> {
199    let (lowered, mut diagnostics) = lower_expr_tolerant(expr, ctx);
200    if diagnostics.is_empty() {
201        Ok(lowered)
202    } else {
203        Err(diagnostics.swap_remove(0))
204    }
205}
206
207/// Lower a syntax assertion body into HIR, accumulating diagnostics.
208///
209/// Each assertion body owns an independent lexical local-id space. Assertion
210/// expressions cannot share locals across the `actual`/`expected`/`tolerance`
211/// slots of a tolerance assertion, so each slot is lowered with a fresh lowerer.
212#[must_use]
213pub fn lower_assert_body_tolerant(
214    body: &ast::AssertBody,
215    ctx: ExprLoweringContext<'_>,
216) -> (AssertBody, Vec<ExprLowerError>) {
217    match body {
218        ast::AssertBody::Expr(expr) => {
219            let (lowered, diagnostics) = lower_expr_tolerant(expr, ctx);
220            (AssertBody::Expr(lowered), diagnostics)
221        }
222        ast::AssertBody::Tolerance {
223            actual,
224            expected,
225            tolerance,
226            is_relative,
227        } => {
228            let (actual, mut diagnostics) = lower_expr_tolerant(actual, ctx);
229            let (expected, expected_diags) = lower_expr_tolerant(expected, ctx);
230            let (tolerance, tolerance_diags) = lower_expr_tolerant(tolerance, ctx);
231            diagnostics.extend(expected_diags);
232            diagnostics.extend(tolerance_diags);
233            (
234                AssertBody::Tolerance {
235                    actual: Box::new(actual),
236                    expected: Box::new(expected),
237                    tolerance: Box::new(tolerance),
238                    is_relative: *is_relative,
239                },
240                diagnostics,
241            )
242        }
243    }
244}
245
246/// Lower a syntax assertion body into HIR, rejecting unresolved references.
247///
248/// # Errors
249///
250/// Returns the first [`ExprLowerError`] if any reference cannot be resolved.
251pub fn lower_assert_body(
252    body: &ast::AssertBody,
253    ctx: ExprLoweringContext<'_>,
254) -> Result<AssertBody, ExprLowerError> {
255    let (lowered, mut diagnostics) = lower_assert_body_tolerant(body, ctx);
256    if diagnostics.is_empty() {
257        Ok(lowered)
258    } else {
259        Err(diagnostics.swap_remove(0))
260    }
261}
262
263/// Stable lexical identity for a local expression binding.
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
265pub struct LocalId(u32);
266
267impl LocalId {
268    /// Numeric index unique within one lowered expression tree.
269    #[must_use]
270    pub const fn index(self) -> u32 {
271        self.0
272    }
273}
274
275/// A lexical local binding introduced by an expression form.
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct LocalDef {
278    pub id: LocalId,
279    pub name: LocalName,
280    pub span: Span,
281}
282
283/// A layered lexical environment for HIR locals.
284///
285/// Each binder (for-comp, scan, unfold, match arm) layers a child frame
286/// holding its few bindings over the enclosing environment instead of cloning
287/// the full local map; lookup walks the parent chain. [`LocalId`]s are unique
288/// within one lowered body, so frames never shadow one another — the chain is
289/// purely an ownership layering, and nested binders cost O(own bindings)
290/// instead of O(visible locals).
291#[derive(Debug)]
292pub struct LocalEnv<'a, V> {
293    parent: Option<&'a Self>,
294    bindings: Vec<(LocalId, V)>,
295}
296
297impl<'a, V> LocalEnv<'a, V> {
298    /// Create an empty root environment.
299    #[must_use]
300    pub const fn root() -> Self {
301        Self {
302            parent: None,
303            bindings: Vec::new(),
304        }
305    }
306
307    /// Create a root environment holding the given bindings.
308    #[must_use]
309    pub const fn from_bindings(bindings: Vec<(LocalId, V)>) -> Self {
310        Self {
311            parent: None,
312            bindings,
313        }
314    }
315
316    /// Layer a child frame holding `bindings` over this environment.
317    #[must_use]
318    pub const fn child<'b>(&'b self, bindings: Vec<(LocalId, V)>) -> LocalEnv<'b, V>
319    where
320        'a: 'b,
321    {
322        LocalEnv {
323            parent: Some(self),
324            bindings,
325        }
326    }
327
328    /// Look up a local by its lexical identity, innermost frame first.
329    #[must_use]
330    pub fn get(&self, id: LocalId) -> Option<&V> {
331        self.bindings
332            .iter()
333            .rev()
334            .find(|(bound, _)| *bound == id)
335            .map(|(_, value)| value)
336            .or_else(|| self.parent.and_then(|parent| parent.get(id)))
337    }
338
339    /// Bind or update a local in this frame.
340    ///
341    /// Iterating binders (for-comp elements, scan/unfold steps) rebind the
342    /// same `LocalId` once per iteration without growing the frame.
343    pub fn bind(&mut self, id: LocalId, value: V) {
344        match self.bindings.iter_mut().find(|(bound, _)| *bound == id) {
345            Some((_, slot)) => *slot = value,
346            None => self.bindings.push((id, value)),
347        }
348    }
349}
350
351impl<V> Default for LocalEnv<'_, V> {
352    fn default() -> Self {
353        Self::root()
354    }
355}
356
357/// Define a closed set of built-in names: the enum, the `parse` boundary
358/// crossing, the canonical `as_str` rendering, and an `ALL` listing for
359/// cross-table consistency tests — all generated from a single table so the
360/// spellings can never drift apart.
361macro_rules! define_builtin_names {
362    (
363        $(#[$meta:meta])*
364        $vis:vis enum $name:ident { $($variant:ident => $text:literal),+ $(,)? }
365    ) => {
366        $(#[$meta])*
367        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
368        $vis enum $name { $($variant),+ }
369
370        impl $name {
371            /// Every variant, for cross-table consistency tests.
372            $vis const ALL: &'static [Self] = &[$(Self::$variant),+];
373
374            /// Parse a source name into the typed variant — the only place
375            /// these strings cross into the typed core.
376            #[must_use]
377            $vis fn parse(name: &str) -> Option<Self> {
378                match name {
379                    $($text => Some(Self::$variant),)+
380                    _ => None,
381                }
382            }
383
384            /// Canonical source spelling.
385            #[must_use]
386            $vis const fn as_str(self) -> &'static str {
387                match self {
388                    $(Self::$variant => $text),+
389                }
390            }
391        }
392    };
393}
394
395define_builtin_names! {
396    /// Built-in constants with closed semantic meaning.
397    pub enum BuiltinConst {
398        Pi => "PI",
399        E => "E",
400        Tau => "TAU",
401        Sqrt2 => "SQRT2",
402        Ln2 => "LN2",
403        Ln10 => "LN10",
404    }
405}
406
407impl BuiltinConst {
408    /// Numeric value of the constant. Must agree with
409    /// [`crate::registry::builtins::builtin_constants`] (enforced by test).
410    #[must_use]
411    pub const fn value(self) -> f64 {
412        match self {
413            Self::Pi => std::f64::consts::PI,
414            Self::E => std::f64::consts::E,
415            Self::Tau => std::f64::consts::TAU,
416            Self::Sqrt2 => std::f64::consts::SQRT_2,
417            Self::Ln2 => std::f64::consts::LN_2,
418            Self::Ln10 => std::f64::consts::LN_10,
419        }
420    }
421}
422
423define_builtin_names! {
424    /// Built-in function names with closed semantic meaning.
425    pub enum BuiltinFnName {
426        Sqrt => "sqrt",
427        Cbrt => "cbrt",
428        Exp => "exp",
429        Expm1 => "expm1",
430        Ln => "ln",
431        Log10 => "log10",
432        Log2 => "log2",
433        Log => "log",
434        Log1p => "log1p",
435        Sin => "sin",
436        Cos => "cos",
437        Tan => "tan",
438        Asin => "asin",
439        Acos => "acos",
440        Atan => "atan",
441        Atan2 => "atan2",
442        Sinh => "sinh",
443        Cosh => "cosh",
444        Tanh => "tanh",
445        Asinh => "asinh",
446        Acosh => "acosh",
447        Atanh => "atanh",
448        Abs => "abs",
449        Floor => "floor",
450        Ceil => "ceil",
451        Round => "round",
452        Trunc => "trunc",
453        Sign => "sign",
454        Min => "min",
455        Max => "max",
456        Hypot => "hypot",
457        Clamp => "clamp",
458        Sum => "sum",
459        Mean => "mean",
460        Count => "count",
461        ToFloat => "to_float",
462        ToInt => "to_int",
463        ToUtc => "to_utc",
464        ToTai => "to_tai",
465        ToTt => "to_tt",
466        ToTdb => "to_tdb",
467        ToEt => "to_et",
468        ToGpst => "to_gpst",
469        ToGst => "to_gst",
470        ToBdt => "to_bdt",
471        ToQzsst => "to_qzsst",
472        Datetime => "datetime",
473        Epoch => "epoch",
474        Year => "year",
475        Month => "month",
476        Day => "day",
477        Hour => "hour",
478        Minute => "minute",
479        Second => "second",
480        Weekday => "weekday",
481        DayOfYear => "day_of_year",
482        FromJd => "from_jd",
483        FromMjd => "from_mjd",
484        FromUnix => "from_unix",
485        ToJd => "to_jd",
486        ToMjd => "to_mjd",
487        ToUnix => "to_unix",
488    }
489}
490
491impl BuiltinFnName {
492    /// Return the existing typed special-function classification when this
493    /// built-in is one of the special categories.
494    #[must_use]
495    pub const fn special_kind(self) -> Option<SpecialFnKind> {
496        match self {
497            Self::Sum => Some(SpecialFnKind::Aggregation(AggregationFn::Sum)),
498            Self::Min => Some(SpecialFnKind::Aggregation(AggregationFn::Min)),
499            Self::Max => Some(SpecialFnKind::Aggregation(AggregationFn::Max)),
500            Self::Mean => Some(SpecialFnKind::Aggregation(AggregationFn::Mean)),
501            Self::Count => Some(SpecialFnKind::Aggregation(AggregationFn::Count)),
502            Self::ToFloat => Some(SpecialFnKind::TypeConversion(TypeConversionFn::ToFloat)),
503            Self::ToInt => Some(SpecialFnKind::TypeConversion(TypeConversionFn::ToInt)),
504            Self::ToUtc => Some(SpecialFnKind::TimeScaleConversion(TimeScale::UTC)),
505            Self::ToTai => Some(SpecialFnKind::TimeScaleConversion(TimeScale::TAI)),
506            Self::ToTt => Some(SpecialFnKind::TimeScaleConversion(TimeScale::TT)),
507            Self::ToTdb => Some(SpecialFnKind::TimeScaleConversion(TimeScale::TDB)),
508            Self::ToEt => Some(SpecialFnKind::TimeScaleConversion(TimeScale::ET)),
509            Self::ToGpst => Some(SpecialFnKind::TimeScaleConversion(TimeScale::GPST)),
510            Self::ToGst => Some(SpecialFnKind::TimeScaleConversion(TimeScale::GST)),
511            Self::ToBdt => Some(SpecialFnKind::TimeScaleConversion(TimeScale::BDT)),
512            Self::ToQzsst => Some(SpecialFnKind::TimeScaleConversion(TimeScale::QZSST)),
513            Self::Datetime => Some(SpecialFnKind::Constructor(ConstructorFn::Datetime)),
514            Self::Epoch => Some(SpecialFnKind::Constructor(ConstructorFn::Epoch)),
515            Self::Year => Some(SpecialFnKind::DatetimeExtract(DatetimeExtractFn::Year)),
516            Self::Month => Some(SpecialFnKind::DatetimeExtract(DatetimeExtractFn::Month)),
517            Self::Day => Some(SpecialFnKind::DatetimeExtract(DatetimeExtractFn::Day)),
518            Self::Hour => Some(SpecialFnKind::DatetimeExtract(DatetimeExtractFn::Hour)),
519            Self::Minute => Some(SpecialFnKind::DatetimeExtract(DatetimeExtractFn::Minute)),
520            Self::Second => Some(SpecialFnKind::DatetimeExtract(DatetimeExtractFn::Second)),
521            Self::Weekday => Some(SpecialFnKind::DatetimeExtract(DatetimeExtractFn::Weekday)),
522            Self::DayOfYear => Some(SpecialFnKind::DatetimeExtract(DatetimeExtractFn::DayOfYear)),
523            Self::FromJd => Some(SpecialFnKind::DatetimeFrom(DatetimeFromFn::FromJd)),
524            Self::FromMjd => Some(SpecialFnKind::DatetimeFrom(DatetimeFromFn::FromMjd)),
525            Self::FromUnix => Some(SpecialFnKind::DatetimeFrom(DatetimeFromFn::FromUnix)),
526            Self::ToJd => Some(SpecialFnKind::DatetimeTo(DatetimeToFn::ToJd)),
527            Self::ToMjd => Some(SpecialFnKind::DatetimeTo(DatetimeToFn::ToMjd)),
528            Self::ToUnix => Some(SpecialFnKind::DatetimeTo(DatetimeToFn::ToUnix)),
529            _ => None,
530        }
531    }
532}
533
534/// HIR expression node.
535#[derive(Debug)]
536pub struct Expr {
537    pub kind: ExprKind,
538    pub span: Span,
539}
540
541// Manual impl instead of `#[derive(Clone)]`: derived clone glue recurses
542// once per tree level without any stack-growth guard, so cloning a long
543// left-nested operator chain overflows the stack. Routing each level
544// through `with_stack_growth` lets the stack grow on demand (the derived
545// `ExprKind` clone calls back into this impl through `Box<Expr>`).
546impl Clone for Expr {
547    fn clone(&self) -> Self {
548        crate::stack::with_stack_growth(|| Self {
549            kind: self.kind.clone(),
550            span: self.span,
551        })
552    }
553}
554
555impl Expr {
556    #[must_use]
557    pub const fn new(kind: ExprKind, span: Span) -> Self {
558        Self { kind, span }
559    }
560}
561
562/// A resolved index-variant reference with the source spans of its two
563/// written segments kept separate.
564///
565/// The index segment and the variant segment are not necessarily adjacent:
566/// table desugaring reuses the `table[Axis]` axis token's span as the index
567/// span of every row key, so a single merged span would cover unrelated
568/// source (and make different rows' spans contain each other). Keeping the
569/// segments separate lets diagnostics on contiguous `Index.Variant` paths
570/// use the whole written path ([`IndexVariantRef::path_span`]) while
571/// span-precise consumers (rename, find-references) address exactly the
572/// variant segment.
573#[derive(Debug, Clone, PartialEq, Eq)]
574pub struct IndexVariantRef {
575    /// The resolved index variant.
576    pub variant: ResolvedIndexVariant,
577    /// Span of the index path as written (`Maneuver` in `Maneuver.Departure`,
578    /// or the axis token inside `table[...]` for desugared table rows).
579    /// `None` when the variant is written without an index segment (a bare
580    /// label in a match pattern whose index is inferred).
581    pub index_span: Option<Span>,
582    /// Span of just the variant segment (the final path segment / row label).
583    pub variant_span: Span,
584}
585
586impl IndexVariantRef {
587    /// Whole-reference span for diagnostics on contiguous `Index.Variant`
588    /// paths. For desugared table rows the index segment lives inside
589    /// `table[...]`, so prefer [`Self::variant_span`] there.
590    #[must_use]
591    pub fn path_span(&self) -> Span {
592        self.index_span
593            .map_or(self.variant_span, |index| index.merge(self.variant_span))
594    }
595}
596
597/// Resolved expression shape.
598#[derive(Debug, Clone)]
599pub enum ExprKind {
600    /// A reference that failed to resolve.
601    ///
602    /// Produced only by tolerant lowering; the diagnostic for the failure is
603    /// reported alongside the lowered tree. The strict entry points reject
604    /// trees containing this node, so the batch pipeline never observes it.
605    Error,
606    Number(f64),
607    Integer(i64),
608    Bool(bool),
609    StringLiteral(String),
610    TypeSystemRef(Spanned<TypeSystemRef>),
611    GraphRef(Spanned<ResolvedName<namespace::Decl>>),
612    ConstRef(Spanned<ConstRef>),
613    LocalRef(Spanned<LocalId>),
614    BinOp {
615        op: ast::BinOp,
616        lhs: Box<Expr>,
617        rhs: Box<Expr>,
618    },
619    UnaryOp {
620        op: ast::UnaryOp,
621        operand: Box<Expr>,
622    },
623    FnCall {
624        callee: Spanned<FunctionRef>,
625        type_args: Vec<GenericArg>,
626        args: Vec<Expr>,
627    },
628    If {
629        condition: Box<Expr>,
630        then_branch: Box<Expr>,
631        else_branch: Box<Expr>,
632    },
633    UnitLiteral {
634        value: f64,
635        unit: ast::UnitExpr,
636    },
637    Convert {
638        expr: Box<Expr>,
639        target: ast::UnitExpr,
640    },
641    DisplayTimezone {
642        expr: Box<Expr>,
643        timezone: String,
644    },
645    FieldAccess {
646        expr: Box<Expr>,
647        field: Spanned<FieldName>,
648    },
649    ConstructorCall {
650        callee: Spanned<ResolvedName<namespace::Constructor>>,
651        generic_args: Vec<GenericArg>,
652        fields: Vec<FieldInit>,
653    },
654    MapLiteral {
655        entries: Vec<MapEntry>,
656    },
657    ForComp {
658        bindings: Vec<ForBinding>,
659        body: Box<Expr>,
660    },
661    IndexAccess {
662        expr: Box<Expr>,
663        args: Vec<IndexArg>,
664    },
665    Scan {
666        source: Box<Expr>,
667        init: Box<Expr>,
668        acc: LocalDef,
669        val: LocalDef,
670        body: Box<Expr>,
671    },
672    Unfold {
673        init: Box<Expr>,
674        prev: LocalDef,
675        curr: LocalDef,
676        body: Box<Expr>,
677    },
678    Match {
679        scrutinee: Box<Expr>,
680        arms: Vec<MatchArm>,
681    },
682    VariantLiteral(IndexVariantRef),
683    InlineDagRef {
684        target: Spanned<DagId>,
685        args: Vec<ParamBinding>,
686        output: Spanned<ResolvedName<namespace::Decl>>,
687    },
688}
689
690/// Canonical declaration dependencies observed in one HIR expression tree.
691#[derive(Debug, Clone, Default, PartialEq, Eq)]
692pub struct ExprDependencies {
693    /// Runtime graph dependencies reached through `@name` references.
694    pub graph_refs: BTreeSet<ResolvedName<namespace::Decl>>,
695    /// Compile-time const dependencies reached through const-like value refs.
696    pub const_refs: BTreeSet<ResolvedName<namespace::Decl>>,
697}
698
699/// Collect canonical declaration dependencies from an already-lowered HIR expression.
700#[must_use]
701pub fn collect_expr_dependencies(expr: &Expr) -> ExprDependencies {
702    let mut deps = ExprDependencies::default();
703    collect_expr_dependencies_into(expr, &mut deps);
704    deps
705}
706
707fn collect_expr_dependencies_into(expr: &Expr, deps: &mut ExprDependencies) {
708    // Recursion choke point: recurses once per tree level (unbounded for
709    // left-nested operator chains).
710    crate::stack::with_stack_growth(|| collect_expr_dependencies_into_inner(expr, deps));
711}
712
713fn collect_expr_dependencies_into_inner(expr: &Expr, deps: &mut ExprDependencies) {
714    match &expr.kind {
715        ExprKind::Error
716        | ExprKind::Number(_)
717        | ExprKind::Integer(_)
718        | ExprKind::Bool(_)
719        | ExprKind::StringLiteral(_)
720        | ExprKind::TypeSystemRef(_)
721        | ExprKind::LocalRef(_)
722        | ExprKind::VariantLiteral(_)
723        | ExprKind::UnitLiteral { .. } => {}
724        ExprKind::GraphRef(target) => {
725            deps.graph_refs.insert(target.value.clone());
726        }
727        ExprKind::ConstRef(target) => {
728            if let ConstRef::Decl(resolved) = &target.value {
729                deps.const_refs.insert(resolved.clone());
730            }
731        }
732        ExprKind::BinOp { lhs, rhs, .. } => {
733            collect_expr_dependencies_into(lhs, deps);
734            collect_expr_dependencies_into(rhs, deps);
735        }
736        ExprKind::UnaryOp { operand, .. }
737        | ExprKind::Convert { expr: operand, .. }
738        | ExprKind::DisplayTimezone { expr: operand, .. }
739        | ExprKind::FieldAccess { expr: operand, .. } => {
740            collect_expr_dependencies_into(operand, deps);
741        }
742        ExprKind::FnCall { args, .. } => {
743            for arg in args {
744                collect_expr_dependencies_into(arg, deps);
745            }
746        }
747        ExprKind::If {
748            condition,
749            then_branch,
750            else_branch,
751        } => {
752            collect_expr_dependencies_into(condition, deps);
753            collect_expr_dependencies_into(then_branch, deps);
754            collect_expr_dependencies_into(else_branch, deps);
755        }
756        ExprKind::ConstructorCall { fields, .. } => {
757            for field in fields {
758                collect_expr_dependencies_into(&field.value, deps);
759            }
760        }
761        ExprKind::MapLiteral { entries } => {
762            for entry in entries {
763                collect_expr_dependencies_into(&entry.value, deps);
764            }
765        }
766        ExprKind::ForComp { body, .. } => collect_expr_dependencies_into(body, deps),
767        ExprKind::IndexAccess { expr, args } => {
768            collect_expr_dependencies_into(expr, deps);
769            for arg in args {
770                if let IndexArg::Expr(expr) = arg {
771                    collect_expr_dependencies_into(expr, deps);
772                }
773            }
774        }
775        ExprKind::Scan {
776            source, init, body, ..
777        } => {
778            collect_expr_dependencies_into(source, deps);
779            collect_expr_dependencies_into(init, deps);
780            collect_expr_dependencies_into(body, deps);
781        }
782        ExprKind::Unfold { init, body, .. } => {
783            collect_expr_dependencies_into(init, deps);
784            collect_expr_dependencies_into(body, deps);
785        }
786        ExprKind::Match { scrutinee, arms } => {
787            collect_expr_dependencies_into(scrutinee, deps);
788            for arm in arms {
789                collect_expr_dependencies_into(&arm.body, deps);
790            }
791        }
792        ExprKind::InlineDagRef { args, .. } => {
793            for arg in args {
794                collect_expr_dependencies_into(&arg.value, deps);
795            }
796        }
797    }
798}
799
800/// Returns `true` if `expr` contains a graph reference to `name` that is
801/// not dominated by an `Unfold` ancestor.
802///
803/// Unfold self-references access the previous step and are therefore not
804/// true cyclic dependencies; a self-reference *outside* any unfold subtree
805/// is a genuine cycle. Used to decide whether a declaration's self-edge can
806/// be dropped from the dependency graph.
807#[must_use]
808pub fn has_ref_outside_unfold(expr: &Expr, name: &ResolvedName<namespace::Decl>) -> bool {
809    // Recursion choke point: recurses once per tree level (unbounded for
810    // left-nested operator chains).
811    crate::stack::with_stack_growth(|| match &expr.kind {
812        ExprKind::GraphRef(target) => target.value == *name,
813        // Unfold: anything inside accesses the previous step. The rest are
814        // leaves without graph references.
815        ExprKind::Unfold { .. }
816        | ExprKind::Error
817        | ExprKind::Number(_)
818        | ExprKind::Integer(_)
819        | ExprKind::Bool(_)
820        | ExprKind::StringLiteral(_)
821        | ExprKind::TypeSystemRef(_)
822        | ExprKind::LocalRef(_)
823        | ExprKind::VariantLiteral(_)
824        | ExprKind::UnitLiteral { .. }
825        | ExprKind::ConstRef(_) => false,
826        ExprKind::BinOp { lhs, rhs, .. } => {
827            has_ref_outside_unfold(lhs, name) || has_ref_outside_unfold(rhs, name)
828        }
829        ExprKind::UnaryOp { operand, .. }
830        | ExprKind::Convert { expr: operand, .. }
831        | ExprKind::DisplayTimezone { expr: operand, .. }
832        | ExprKind::FieldAccess { expr: operand, .. } => has_ref_outside_unfold(operand, name),
833        ExprKind::FnCall { args, .. } => args.iter().any(|arg| has_ref_outside_unfold(arg, name)),
834        ExprKind::If {
835            condition,
836            then_branch,
837            else_branch,
838        } => {
839            has_ref_outside_unfold(condition, name)
840                || has_ref_outside_unfold(then_branch, name)
841                || has_ref_outside_unfold(else_branch, name)
842        }
843        ExprKind::ConstructorCall { fields, .. } => fields
844            .iter()
845            .any(|field| has_ref_outside_unfold(&field.value, name)),
846        ExprKind::MapLiteral { entries } => entries
847            .iter()
848            .any(|entry| has_ref_outside_unfold(&entry.value, name)),
849        ExprKind::ForComp { body, .. } => has_ref_outside_unfold(body, name),
850        ExprKind::IndexAccess { expr, args } => {
851            has_ref_outside_unfold(expr, name)
852                || args.iter().any(|arg| match arg {
853                    IndexArg::Expr(expr) => has_ref_outside_unfold(expr, name),
854                    _ => false,
855                })
856        }
857        ExprKind::Scan {
858            source, init, body, ..
859        } => {
860            has_ref_outside_unfold(source, name)
861                || has_ref_outside_unfold(init, name)
862                || has_ref_outside_unfold(body, name)
863        }
864        ExprKind::Match { scrutinee, arms } => {
865            has_ref_outside_unfold(scrutinee, name)
866                || arms
867                    .iter()
868                    .any(|arm| has_ref_outside_unfold(&arm.body, name))
869        }
870        ExprKind::InlineDagRef { args, .. } => args
871            .iter()
872            .any(|arg| has_ref_outside_unfold(&arg.value, name)),
873    })
874}
875
876/// Type-system identifier used as a value expression, usually in include bindings.
877#[derive(Debug, Clone, PartialEq, Eq)]
878pub enum TypeSystemRef {
879    Type(ResolvedName<namespace::StructType>),
880    Dimension(ResolvedName<namespace::Dim>),
881    Index(ResolvedName<namespace::Index>),
882    IndexVariant(ResolvedIndexVariant),
883}
884
885impl TypeSystemRef {
886    #[must_use]
887    pub fn surface_description(&self) -> String {
888        match self {
889            Self::Type(name) => format!("type `{}`", name.as_str()),
890            Self::Dimension(name) => format!("dimension `{}`", name.as_str()),
891            Self::Index(name) => format!("index `{}`", name.as_str()),
892            Self::IndexVariant(variant) => format!(
893                "index label `{}.{}`",
894                variant.index().as_str(),
895                variant.variant()
896            ),
897        }
898    }
899
900    #[must_use]
901    pub fn value_position_error(&self) -> String {
902        format!("{} cannot be used as a value", self.surface_description())
903    }
904}
905
906/// Resolved constant-like expression target.
907#[derive(Debug, Clone, PartialEq, Eq)]
908pub enum ConstRef {
909    Decl(ResolvedName<namespace::Decl>),
910    Constructor(ResolvedName<namespace::Constructor>),
911    Builtin(BuiltinConst),
912    TimeScale(TimeScale),
913    GenericNatParam(super::types::GenericParamId),
914}
915
916/// Function call target.
917#[derive(Debug, Clone, Copy, PartialEq, Eq)]
918pub enum FunctionRef {
919    Builtin(BuiltinFnName),
920}
921
922/// A lowered assertion body.
923#[derive(Debug, Clone)]
924pub enum AssertBody {
925    Expr(Expr),
926    Tolerance {
927        actual: Box<Expr>,
928        expected: Box<Expr>,
929        tolerance: Box<Expr>,
930        is_relative: bool,
931    },
932}
933
934/// Generic argument at an expression call site.
935#[derive(Debug, Clone)]
936pub enum GenericArg {
937    Type(TypeExpr),
938    Nat(NatExpr),
939}
940
941/// Field initializer after expression lowering.
942#[derive(Debug, Clone)]
943pub struct FieldInit {
944    pub name: Spanned<FieldName>,
945    pub value: Expr,
946}
947
948/// A param binding in an inline DAG invocation.
949#[derive(Debug, Clone)]
950pub struct ParamBinding {
951    pub target: Spanned<ResolvedName<namespace::Decl>>,
952    pub value: Expr,
953    pub span: Span,
954}
955
956/// A resolved map literal entry.
957#[derive(Debug, Clone)]
958pub struct MapEntry {
959    pub keys: NonEmpty<MapEntryKey>,
960    pub value: Expr,
961}
962
963/// A single resolved map key.
964#[derive(Debug, Clone, PartialEq, Eq)]
965pub enum MapEntryKey {
966    IndexVariant(IndexVariantRef),
967    NatRangeVariant {
968        size: u64,
969        variant: Spanned<IndexVariantName>,
970    },
971}
972
973/// A resolved for-comprehension binding.
974#[derive(Debug, Clone)]
975pub struct ForBinding {
976    pub local: LocalDef,
977    pub index: ForBindingIndex,
978}
979
980/// Index target in a for-comprehension binding.
981#[derive(Debug, Clone, PartialEq, Eq)]
982pub enum ForBindingIndex {
983    Named(Spanned<ResolvedName<namespace::Index>>),
984    Range { arg: NatExpr, span: Span },
985}
986
987/// A resolved index-access argument.
988#[derive(Debug, Clone)]
989pub enum IndexArg {
990    Variant(IndexVariantRef),
991    Var(Spanned<LocalId>),
992    Expr(Box<Expr>),
993}
994
995/// One lowered match arm.
996#[derive(Debug, Clone)]
997pub struct MatchArm {
998    pub pattern: MatchPattern,
999    pub body: Expr,
1000    pub span: Span,
1001}
1002
1003/// Resolved match pattern.
1004#[derive(Debug, Clone)]
1005pub enum MatchPattern {
1006    Constructor {
1007        constructor: Spanned<ResolvedName<namespace::Constructor>>,
1008        bindings: Vec<PatternBinding>,
1009        span: Span,
1010    },
1011    IndexLabel {
1012        variant: IndexVariantRef,
1013        span: Span,
1014    },
1015}
1016
1017impl MatchPattern {
1018    fn bound_locals(&self) -> Vec<LocalDef> {
1019        match self {
1020            Self::Constructor { bindings, .. } => bindings
1021                .iter()
1022                .filter_map(|binding| match binding {
1023                    PatternBinding::Bind { local, .. } => Some(local.clone()),
1024                    PatternBinding::Wildcard { .. } => None,
1025                })
1026                .collect(),
1027            Self::IndexLabel { .. } => Vec::new(),
1028        }
1029    }
1030}
1031
1032/// Binding inside a constructor match pattern.
1033#[derive(Debug, Clone)]
1034pub enum PatternBinding {
1035    Bind {
1036        field: Spanned<FieldName>,
1037        local: LocalDef,
1038    },
1039    Wildcard {
1040        field: Spanned<FieldName>,
1041        span: Span,
1042    },
1043}
1044
1045struct ExprLowerer<'a> {
1046    ctx: ExprLoweringContext<'a>,
1047    local_scopes: Vec<HashMap<LocalName, LocalDef>>,
1048    next_local: u32,
1049    diagnostics: Vec<ExprLowerError>,
1050}
1051
1052impl<'a> ExprLowerer<'a> {
1053    const fn new(ctx: ExprLoweringContext<'a>) -> Self {
1054        Self {
1055            ctx,
1056            local_scopes: Vec::new(),
1057            next_local: 0,
1058            diagnostics: Vec::new(),
1059        }
1060    }
1061
1062    /// Lower one expression level, localizing failures.
1063    ///
1064    /// A subtree whose references cannot be resolved becomes a single
1065    /// [`ExprKind::Error`] node with its diagnostic recorded; sibling
1066    /// subtrees keep lowering.
1067    fn lower_expr(&mut self, expr: &ast::Expr) -> Expr {
1068        // Recursion choke point: lowering recurses once per tree level
1069        // (unbounded for left-nested operator chains).
1070        crate::stack::with_stack_growth(|| match self.lower_expr_inner(expr) {
1071            Ok(lowered) => lowered,
1072            Err(err) => {
1073                self.diagnostics.push(err);
1074                Expr::new(ExprKind::Error, expr.span)
1075            }
1076        })
1077    }
1078
1079    #[expect(clippy::too_many_lines, reason = "exhaustive ExprKind lowering")]
1080    fn lower_expr_inner(&mut self, expr: &ast::Expr) -> Result<Expr, ExprLowerError> {
1081        let kind = match &expr.kind {
1082            ast::ExprKind::Number(value) => ExprKind::Number(*value),
1083            ast::ExprKind::Integer(value) => ExprKind::Integer(*value),
1084            ast::ExprKind::Bool(value) => ExprKind::Bool(*value),
1085            ast::ExprKind::StringLiteral(value) => ExprKind::StringLiteral(value.clone()),
1086            ast::ExprKind::UnresolvedRef(unresolved) => {
1087                let UnresolvedRef::Path(path) = unresolved;
1088                self.lower_unresolved_path(path)?
1089            }
1090            ast::ExprKind::GraphRef(name) => ExprKind::GraphRef(Spanned::new(
1091                self.resolve_decl_scoped_name(&name.value, name.span)?,
1092                name.span,
1093            )),
1094            ast::ExprKind::BinOp { op, lhs, rhs } => ExprKind::BinOp {
1095                op: *op,
1096                lhs: Box::new(self.lower_expr(lhs)),
1097                rhs: Box::new(self.lower_expr(rhs)),
1098            },
1099            ast::ExprKind::UnaryOp { op, operand } => ExprKind::UnaryOp {
1100                op: *op,
1101                operand: Box::new(self.lower_expr(operand)),
1102            },
1103            ast::ExprKind::FnCall {
1104                callee,
1105                type_args,
1106                args,
1107            } => ExprKind::FnCall {
1108                callee: {
1109                    let function_ref = Self::lower_function_ref(callee)?;
1110                    Self::check_function_arity(function_ref, args.len(), callee.span())?;
1111                    Spanned::new(function_ref, callee.span())
1112                },
1113                type_args: type_args
1114                    .iter()
1115                    .map(|arg| self.lower_generic_arg(arg))
1116                    .collect::<Result<Vec<_>, _>>()?,
1117                args: args.iter().map(|arg| self.lower_expr(arg)).collect(),
1118            },
1119            ast::ExprKind::If {
1120                condition,
1121                then_branch,
1122                else_branch,
1123            } => ExprKind::If {
1124                condition: Box::new(self.lower_expr(condition)),
1125                then_branch: Box::new(self.lower_expr(then_branch)),
1126                else_branch: Box::new(self.lower_expr(else_branch)),
1127            },
1128            ast::ExprKind::UnitLiteral { value, unit } => ExprKind::UnitLiteral {
1129                value: *value,
1130                unit: unit.clone(),
1131            },
1132            ast::ExprKind::Convert { expr, target } => ExprKind::Convert {
1133                expr: Box::new(self.lower_expr(expr)),
1134                target: target.clone(),
1135            },
1136            ast::ExprKind::DisplayTimezone { expr, timezone } => ExprKind::DisplayTimezone {
1137                expr: Box::new(self.lower_expr(expr)),
1138                timezone: timezone.clone(),
1139            },
1140            // `@alias.member` parses as `FieldAccess(GraphRef(alias), member)`.
1141            // When `alias.member` resolves as a module-qualified declaration,
1142            // promote the access to a qualified graph reference — this is the
1143            // same promotion the project pipeline applies before evaluation,
1144            // done here so every consumer of HIR (LSP included) sees the
1145            // resolved identity. Otherwise it is a struct-field access.
1146            ast::ExprKind::FieldAccess { expr, field } => self
1147                .resolve_alias_field_access(expr, field)
1148                .unwrap_or_else(|| ExprKind::FieldAccess {
1149                    expr: Box::new(self.lower_expr(expr)),
1150                    field: field.clone(),
1151                }),
1152            ast::ExprKind::ConstructorCall {
1153                callee,
1154                generic_args,
1155                fields,
1156            } => ExprKind::ConstructorCall {
1157                callee: Spanned::new(
1158                    self.ctx
1159                        .resolver
1160                        .resolve_constructor_ident_path(self.ctx.owner, callee)
1161                        .map_err(|source| ExprLowerError::ModuleResolve {
1162                            source,
1163                            span: callee.span(),
1164                        })?,
1165                    callee.span(),
1166                ),
1167                generic_args: generic_args
1168                    .iter()
1169                    .map(|arg| self.lower_generic_arg(arg))
1170                    .collect::<Result<Vec<_>, _>>()?,
1171                fields: fields
1172                    .iter()
1173                    .map(|field| self.lower_field_init(field))
1174                    .collect(),
1175            },
1176            ast::ExprKind::MapLiteral { entries } => ExprKind::MapLiteral {
1177                entries: entries
1178                    .iter()
1179                    .map(|entry| self.lower_map_entry(entry, expr.span))
1180                    .collect::<Result<Vec<_>, _>>()?,
1181            },
1182            ast::ExprKind::ForComp { bindings, body } => {
1183                let bindings = bindings
1184                    .iter()
1185                    .map(|binding| self.lower_for_binding(binding))
1186                    .collect::<Result<Vec<_>, _>>()?;
1187                let locals = bindings
1188                    .iter()
1189                    .map(|binding| binding.local.clone())
1190                    .collect::<Vec<_>>();
1191                self.push_scope(locals)?;
1192                let body = Box::new(self.lower_expr(body));
1193                self.pop_scope();
1194                ExprKind::ForComp { bindings, body }
1195            }
1196            ast::ExprKind::IndexAccess { expr, args } => ExprKind::IndexAccess {
1197                expr: Box::new(self.lower_expr(expr)),
1198                args: args
1199                    .iter()
1200                    .map(|arg| self.lower_index_arg(arg))
1201                    .collect::<Result<Vec<_>, _>>()?,
1202            },
1203            ast::ExprKind::Scan {
1204                source,
1205                init,
1206                acc_name,
1207                val_name,
1208                body,
1209            } => {
1210                let source = Box::new(self.lower_expr(source));
1211                let init = Box::new(self.lower_expr(init));
1212                let acc = self.allocate_local(acc_name.value.clone(), acc_name.span)?;
1213                let val = self.allocate_local(val_name.value.clone(), val_name.span)?;
1214                self.push_scope(vec![acc.clone(), val.clone()])?;
1215                let body = Box::new(self.lower_expr(body));
1216                self.pop_scope();
1217                ExprKind::Scan {
1218                    source,
1219                    init,
1220                    acc,
1221                    val,
1222                    body,
1223                }
1224            }
1225            ast::ExprKind::Unfold {
1226                init,
1227                prev_name,
1228                curr_name,
1229                body,
1230            } => {
1231                let init = Box::new(self.lower_expr(init));
1232                let prev = self.allocate_local(prev_name.value.clone(), prev_name.span)?;
1233                let curr = self.allocate_local(curr_name.value.clone(), curr_name.span)?;
1234                self.push_scope(vec![prev.clone(), curr.clone()])?;
1235                let body = Box::new(self.lower_expr(body));
1236                self.pop_scope();
1237                ExprKind::Unfold {
1238                    init,
1239                    prev,
1240                    curr,
1241                    body,
1242                }
1243            }
1244            ast::ExprKind::Match { scrutinee, arms } => ExprKind::Match {
1245                scrutinee: Box::new(self.lower_expr(scrutinee)),
1246                arms: arms
1247                    .iter()
1248                    .map(|arm| self.lower_match_arm(arm))
1249                    .collect::<Result<Vec<_>, _>>()?,
1250            },
1251            ast::ExprKind::InlineDagRef { path, args, output } => {
1252                let target = self
1253                    .ctx
1254                    .resolver
1255                    .resolve_module_path(self.ctx.owner, path)
1256                    .map_err(|source| ExprLowerError::ModuleResolve {
1257                        source,
1258                        span: path.span(),
1259                    })?;
1260                let lowered_args = args
1261                    .iter()
1262                    .map(|arg| self.lower_param_binding(&target, arg))
1263                    .collect::<Result<Vec<_>, _>>()?;
1264                let output_path = NamePath::local(output.value.atom().clone());
1265                let lowered_output = self
1266                    .ctx
1267                    .resolver
1268                    .resolve_decl_path(&target, &output_path)
1269                    .map_err(|source| ExprLowerError::ModuleResolve {
1270                        source,
1271                        span: output.span,
1272                    })?;
1273                ExprKind::InlineDagRef {
1274                    target: Spanned::new(target, path.span()),
1275                    args: lowered_args,
1276                    output: Spanned::new(lowered_output, output.span),
1277                }
1278            }
1279            // `Sugar(_)` payload is `Infallible` post-desugar.
1280            #[expect(
1281                clippy::uninhabited_references,
1282                reason = "Sugar(Infallible) proves this arm unreachable"
1283            )]
1284            ast::ExprKind::Sugar(s) => never(*s),
1285        };
1286        Ok(Expr::new(kind, expr.span))
1287    }
1288
1289    /// Resolve a syntactic reference path in value position.
1290    ///
1291    /// This is the single classification point of the compiler: it decides,
1292    /// in one pass, whether a path names a lexical local, a built-in constant
1293    /// or time scale, a constructor, a type-system entity, a generic `Nat`
1294    /// parameter, or a declaration — and resolves it to its canonical
1295    /// identity at the same time. Lexical scope shadows module symbols.
1296    fn lower_unresolved_path(&self, path: &IdentPath) -> Result<ExprKind, ExprLowerError> {
1297        path.as_bare().map_or_else(
1298            || self.lower_dotted_path_ref(path),
1299            |ident| self.lower_bare_name_ref(ident),
1300        )
1301    }
1302
1303    /// Resolve a bare identifier in value position.
1304    ///
1305    /// Priority:
1306    /// 1. Lexical locals (for/scan/unfold/match bindings)
1307    /// 2. Built-in constants (`PI`, `E`, ...) and time scales (`UTC`, ...)
1308    /// 3. Constructors (a bare constructor name is a nullary call)
1309    /// 4. Type-system names (struct types, dimensions, indexes, variants)
1310    /// 5. Generic `Nat` parameters and declarations (const/node/param)
1311    fn lower_bare_name_ref(&self, ident: &Ident) -> Result<ExprKind, ExprLowerError> {
1312        let span = ident.span;
1313        if let Ok(local) = self.lookup_local(&LocalName::from_atom(ident.name.clone()), span) {
1314            return Ok(ExprKind::LocalRef(Spanned::new(local, span)));
1315        }
1316        if let Some(builtin) = BuiltinConst::parse(ident.name.as_str()) {
1317            return Ok(ExprKind::ConstRef(Spanned::new(
1318                ConstRef::Builtin(builtin),
1319                span,
1320            )));
1321        }
1322        if let Ok(scale) = ident.name.as_str().parse::<TimeScale>() {
1323            return Ok(ExprKind::ConstRef(Spanned::new(
1324                ConstRef::TimeScale(scale),
1325                span,
1326            )));
1327        }
1328        let path = NamePath::local(ident.name.clone());
1329        if let Ok(constructor) = self
1330            .ctx
1331            .resolver
1332            .resolve_constructor_path(self.ctx.owner, &path)
1333        {
1334            return Ok(ExprKind::ConstructorCall {
1335                callee: Spanned::new(constructor, span),
1336                generic_args: Vec::new(),
1337                fields: Vec::new(),
1338            });
1339        }
1340        if let Some(type_system_ref) =
1341            self.resolve_bare_type_system_name(&path, &ident.name, span)?
1342        {
1343            return Ok(ExprKind::TypeSystemRef(Spanned::new(type_system_ref, span)));
1344        }
1345        self.lower_const_ref(&ScopedName::local(ident.name.as_str()), span)
1346            .map(|const_ref| ExprKind::ConstRef(Spanned::new(const_ref, span)))
1347    }
1348
1349    /// Resolve a bare identifier against the type-system namespaces.
1350    ///
1351    /// Bare type-system names appear in value position in include-binding
1352    /// RHSs (`Speed: Velocity`); downstream type checking rejects them in
1353    /// genuine value positions with a precise diagnostic.
1354    fn resolve_bare_type_system_name(
1355        &self,
1356        path: &NamePath,
1357        name: &NameAtom,
1358        span: Span,
1359    ) -> Result<Option<TypeSystemRef>, ExprLowerError> {
1360        if let Ok(struct_type) = self
1361            .ctx
1362            .resolver
1363            .resolve_struct_type_path(self.ctx.owner, path)
1364        {
1365            return Ok(Some(TypeSystemRef::Type(struct_type)));
1366        }
1367        if let Ok(dimension) = self
1368            .ctx
1369            .resolver
1370            .resolve_dimension_path(self.ctx.owner, path)
1371        {
1372            return Ok(Some(TypeSystemRef::Dimension(dimension)));
1373        }
1374        if let Some(prelude) = self.ctx.prelude
1375            && let Some(dimension) = prelude.resolve_dimension_path(path)
1376        {
1377            return Ok(Some(TypeSystemRef::Dimension(dimension)));
1378        }
1379        if let Ok(index) = self.ctx.resolver.resolve_index_path(self.ctx.owner, path) {
1380            return Ok(Some(TypeSystemRef::Index(index)));
1381        }
1382        let variant_name = IndexVariantName::from_atom(name.clone());
1383        match self
1384            .ctx
1385            .resolver
1386            .resolve_bare_index_variant(self.ctx.owner, &variant_name)
1387        {
1388            Ok(variant) => Ok(Some(TypeSystemRef::IndexVariant(variant))),
1389            Err(ModuleResolveError::UnknownName { .. }) => Ok(None),
1390            Err(source) => Err(ExprLowerError::ModuleResolve { source, span }),
1391        }
1392    }
1393
1394    /// Resolve a dotted reference path (`a.b`, `a.b.c`, ...) in value position.
1395    ///
1396    /// A two-segment path whose head names an index in scope is a variant
1397    /// literal; anything else is a qualified constant-like reference
1398    /// (declaration, constructor, or index variant of an imported module).
1399    fn lower_dotted_path_ref(&self, path: &IdentPath) -> Result<ExprKind, ExprLowerError> {
1400        let span = path.span();
1401        if let [qualifier, member] = path.segments() {
1402            let index_path = NamePath::local(qualifier.name.clone());
1403            if self
1404                .ctx
1405                .resolver
1406                .resolve_index_path(self.ctx.owner, &index_path)
1407                .is_ok()
1408            {
1409                let variant = IndexVariantName::from_atom(member.name.clone());
1410                let resolved = self.resolve_index_variant_parts(
1411                    &index_path,
1412                    &variant,
1413                    qualifier.span,
1414                    member.span,
1415                )?;
1416                return Ok(ExprKind::VariantLiteral(IndexVariantRef {
1417                    variant: resolved,
1418                    index_span: Some(qualifier.span),
1419                    variant_span: member.span,
1420                }));
1421            }
1422        }
1423
1424        let (qualifier, member) = path.split_last();
1425        let scoped = ScopedName::qualified_path(
1426            qualifier.iter().map(|segment| segment.name.to_string()),
1427            member.name.to_string(),
1428        );
1429        match self.lower_const_ref(&scoped, span) {
1430            Ok(const_ref) => Ok(ExprKind::ConstRef(Spanned::new(const_ref, span))),
1431            // A qualified path that is not a const-like declaration can still
1432            // be an index variant (`m.Season.Winter`). Resolving it here keeps
1433            // the segment spans of the written path available for the literal.
1434            Err(const_err) => self
1435                .resolve_variant_literal_path(path)
1436                .ok_or(const_err)
1437                .map(ExprKind::VariantLiteral),
1438        }
1439    }
1440
1441    /// Promote `FieldAccess(GraphRef(alias), member)` to a qualified graph
1442    /// reference when `alias.member` resolves as a module-qualified
1443    /// declaration. Returns `None` when it does not (a struct-field access).
1444    ///
1445    /// Resolution goes through [`ModuleResolver::resolve_decl_path`] only —
1446    /// it applies the alias boundary's visibility rule, so a private member
1447    /// of an imported/included module does not get promoted (and therefore
1448    /// stays an unresolved reference, exactly like writing the qualified
1449    /// path directly).
1450    fn resolve_alias_field_access(
1451        &self,
1452        inner: &ast::Expr,
1453        field: &Spanned<FieldName>,
1454    ) -> Option<ExprKind> {
1455        let ast::ExprKind::GraphRef(name) = &inner.kind else {
1456            return None;
1457        };
1458        if name.value.is_qualified() {
1459            return None;
1460        }
1461        let scoped = ScopedName::qualified(name.value.member(), field.value.as_str());
1462        let span = name.span.merge(field.span);
1463        let path = scoped_name_to_path(&scoped, span).ok()?;
1464        let resolved = self
1465            .ctx
1466            .resolver
1467            .resolve_decl_path(self.ctx.owner, &path)
1468            .ok()?;
1469        Some(ExprKind::GraphRef(Spanned::new(resolved, span)))
1470    }
1471
1472    /// Resolve a dotted path as an index-variant literal, keeping the index
1473    /// and variant segment spans separate. Returns `None` when the path does
1474    /// not name an index variant in scope.
1475    fn resolve_variant_literal_path(&self, path: &IdentPath) -> Option<IndexVariantRef> {
1476        let (qualifier, member) = path.split_last();
1477        let (first, rest) = qualifier.split_first()?;
1478        let index_span = rest
1479            .iter()
1480            .fold(first.span, |merged, segment| merged.merge(segment.span));
1481        let resolved = self
1482            .ctx
1483            .resolver
1484            .resolve_index_variant_path(self.ctx.owner, &path.to_name_path())
1485            .ok()?;
1486        Some(IndexVariantRef {
1487            variant: resolved,
1488            index_span: Some(index_span),
1489            variant_span: member.span,
1490        })
1491    }
1492
1493    fn lower_generic_arg(&self, arg: &ast::GenericArg) -> Result<GenericArg, ExprLowerError> {
1494        match arg {
1495            ast::GenericArg::Type(type_expr) => Ok(GenericArg::Type(lower_type_expr(
1496                type_expr,
1497                self.ctx.type_context(),
1498            )?)),
1499            ast::GenericArg::Nat(nat_expr) => Ok(GenericArg::Nat(lower_nat_expr(
1500                nat_expr,
1501                self.ctx.type_context(),
1502            )?)),
1503        }
1504    }
1505
1506    fn lower_field_init(&mut self, field: &ast::FieldInit) -> FieldInit {
1507        FieldInit {
1508            name: field.name.clone(),
1509            value: self.lower_expr(&field.value),
1510        }
1511    }
1512
1513    fn lower_param_binding(
1514        &mut self,
1515        target: &DagId,
1516        binding: &ast::ParamBinding,
1517    ) -> Result<ParamBinding, ExprLowerError> {
1518        let path = NamePath::local(binding.name.name.clone());
1519        let target_name = self
1520            .ctx
1521            .resolver
1522            .resolve_decl_path(target, &path)
1523            .map_err(|source| ExprLowerError::ModuleResolve {
1524                source,
1525                span: binding.name.span,
1526            })?;
1527        Ok(ParamBinding {
1528            target: Spanned::new(target_name, binding.name.span),
1529            value: self.lower_expr(&binding.value),
1530            span: binding.span,
1531        })
1532    }
1533
1534    fn lower_const_ref(&self, name: &ScopedName, span: Span) -> Result<ConstRef, ExprLowerError> {
1535        if !name.is_qualified() {
1536            if let Some(builtin) = BuiltinConst::parse(name.member()) {
1537                return Ok(ConstRef::Builtin(builtin));
1538            }
1539            if let Ok(scale) = name.member().parse::<TimeScale>() {
1540                return Ok(ConstRef::TimeScale(scale));
1541            }
1542            let generic_name = GenericParamName::new(name.member());
1543            if let Some(binding) = self.ctx.generic_scope.get(&generic_name)
1544                && binding.constraint == ast::GenericConstraint::Nat
1545            {
1546                return Ok(ConstRef::GenericNatParam(binding.id.clone()));
1547            }
1548        }
1549
1550        let path = scoped_name_to_path(name, span)?;
1551        let mut first_error = None;
1552
1553        if let Some(resolved) = self
1554            .ctx
1555            .decl_bindings
1556            .and_then(|bindings| bindings.get(name))
1557            .cloned()
1558        {
1559            return Ok(ConstRef::Decl(resolved));
1560        }
1561
1562        match self
1563            .ctx
1564            .resolver
1565            .resolve_const_decl_path(self.ctx.owner, &path)
1566        {
1567            Ok(resolved) => return Ok(ConstRef::Decl(resolved)),
1568            Err(err) => first_error.get_or_insert(err),
1569        };
1570        if let Some(resolved) = self.resolve_synthetic_child_decl_path(&path)
1571            && self
1572                .ctx
1573                .resolver
1574                .decl_symbol_kind(&resolved)
1575                .is_ok_and(DeclSymbolKind::is_const)
1576        {
1577            return Ok(ConstRef::Decl(resolved));
1578        }
1579        match self
1580            .ctx
1581            .resolver
1582            .resolve_constructor_path(self.ctx.owner, &path)
1583        {
1584            Ok(resolved) => return Ok(ConstRef::Constructor(resolved)),
1585            Err(err) => first_error.get_or_insert(err),
1586        };
1587
1588        first_error.map_or_else(
1589            || {
1590                Err(ExprLowerError::ModuleResolve {
1591                    source: ModuleResolveError::UnknownName {
1592                        owner: self.ctx.owner.clone(),
1593                        namespace: namespace::Decl::DISPLAY_NAME,
1594                        name: name.to_string(),
1595                    },
1596                    span,
1597                })
1598            },
1599            |source| Err(ExprLowerError::ModuleResolve { source, span }),
1600        )
1601    }
1602
1603    fn resolve_decl_scoped_name(
1604        &self,
1605        name: &ScopedName,
1606        span: Span,
1607    ) -> Result<ResolvedName<namespace::Decl>, ExprLowerError> {
1608        let path = scoped_name_to_path(name, span)?;
1609        if let Some(resolved) = self
1610            .ctx
1611            .decl_bindings
1612            .and_then(|bindings| bindings.get(name))
1613            .cloned()
1614        {
1615            return Ok(resolved);
1616        }
1617        self.ctx
1618            .resolver
1619            .resolve_decl_path(self.ctx.owner, &path)
1620            .or_else(|err| self.resolve_synthetic_child_decl_path(&path).ok_or(err))
1621            .map_err(|source| match source {
1622                ModuleResolveError::UnknownName { .. } => ExprLowerError::UnknownGraphRef {
1623                    name: name.clone(),
1624                    span,
1625                },
1626                source => ExprLowerError::ModuleResolve { source, span },
1627            })
1628    }
1629
1630    fn resolve_synthetic_child_decl_path(
1631        &self,
1632        path: &NamePath,
1633    ) -> Option<ResolvedName<namespace::Decl>> {
1634        let (qualifier, leaf) = path.qualifier_and_leaf()?;
1635        let owner = qualifier
1636            .iter()
1637            .fold(self.ctx.owner.clone(), |owner, segment| {
1638                owner.child(segment.as_str())
1639            });
1640        self.ctx
1641            .resolver
1642            .modules()
1643            .contains_key(&owner)
1644            .then(|| ResolvedName::from_def(owner, DeclName::from_atom(leaf.clone())))
1645    }
1646
1647    /// Validate a built-in call's argument count against the registry's
1648    /// arity table. Aggregations are variadic over collections and skip the
1649    /// check; their argument shapes are validated during type checking.
1650    fn check_function_arity(
1651        function_ref: FunctionRef,
1652        got: usize,
1653        span: Span,
1654    ) -> Result<(), ExprLowerError> {
1655        let FunctionRef::Builtin(builtin) = function_ref;
1656        if builtin.special_kind().is_some_and(|kind| {
1657            matches!(
1658                kind,
1659                crate::registry::resolve_types::SpecialFnKind::Aggregation(_)
1660            )
1661        }) {
1662            return Ok(());
1663        }
1664        let Some(function) = crate::registry::builtins::builtin_functions().get(builtin.as_str())
1665        else {
1666            return Ok(());
1667        };
1668        if got != function.arity() {
1669            return Err(ExprLowerError::WrongArity {
1670                name: crate::syntax::names::FnName::new(builtin.as_str()),
1671                expected: function.arity(),
1672                got,
1673                span,
1674            });
1675        }
1676        Ok(())
1677    }
1678
1679    fn lower_function_ref(
1680        callee: &crate::syntax::ast::IdentPath,
1681    ) -> Result<FunctionRef, ExprLowerError> {
1682        let Some(ident) = callee.as_bare() else {
1683            return Err(ExprLowerError::UnknownFunction {
1684                path: callee.display_path(),
1685                span: callee.span(),
1686            });
1687        };
1688        BuiltinFnName::parse(ident.name.as_str())
1689            .map(FunctionRef::Builtin)
1690            .ok_or_else(|| ExprLowerError::UnknownFunction {
1691                path: callee.display_path(),
1692                span: callee.span(),
1693            })
1694    }
1695
1696    fn lower_map_entry(
1697        &mut self,
1698        entry: &ast::MapEntry,
1699        map_span: Span,
1700    ) -> Result<MapEntry, ExprLowerError> {
1701        let keys = entry
1702            .keys
1703            .iter()
1704            .map(|key| self.lower_map_entry_key(key, map_span))
1705            .collect::<Result<Vec<_>, _>>()?;
1706        let mut keys = keys.into_iter();
1707        let Some(first) = keys.next() else {
1708            return Err(ExprLowerError::EmptyMapEntry {
1709                span: entry.value.span,
1710            });
1711        };
1712        Ok(MapEntry {
1713            keys: NonEmpty::new(first, keys.collect()),
1714            value: self.lower_expr(&entry.value),
1715        })
1716    }
1717
1718    fn lower_map_entry_key(
1719        &self,
1720        key: &ast::MapEntryKey,
1721        map_span: Span,
1722    ) -> Result<MapEntryKey, ExprLowerError> {
1723        match &key.index.value {
1724            crate::syntax::ast::MapEntryIndex::Named(index_path) => {
1725                let variant = self
1726                    .resolve_index_variant_parts(
1727                        index_path,
1728                        &key.variant.value,
1729                        key.index.span,
1730                        key.variant.span,
1731                    )
1732                    .map_err(|err| match err {
1733                        ExprLowerError::ModuleResolve {
1734                            source: ModuleResolveError::UnknownIndexVariant { index, variant },
1735                            ..
1736                        } => ExprLowerError::ExtraMapVariant {
1737                            index_name: index.to_unowned_def_name(),
1738                            variant_name: variant,
1739                            span: map_span,
1740                        },
1741                        err => err,
1742                    })?;
1743                Ok(MapEntryKey::IndexVariant(IndexVariantRef {
1744                    variant,
1745                    index_span: Some(key.index.span),
1746                    variant_span: key.variant.span,
1747                }))
1748            }
1749            crate::syntax::ast::MapEntryIndex::NatRange(size) => Ok(MapEntryKey::NatRangeVariant {
1750                size: *size,
1751                variant: key.variant.clone(),
1752            }),
1753        }
1754    }
1755
1756    fn lower_for_binding(
1757        &mut self,
1758        binding: &ast::ForBinding,
1759    ) -> Result<ForBinding, ExprLowerError> {
1760        let local = self.allocate_local(binding.var.value.clone(), binding.var.span)?;
1761        let index = match &binding.index {
1762            ast::ForBindingIndex::Named(index) => {
1763                let resolved = self
1764                    .ctx
1765                    .resolver
1766                    .resolve_index_path(self.ctx.owner, &index.value)
1767                    .map_err(|source| ExprLowerError::ModuleResolve {
1768                        source,
1769                        span: index.span,
1770                    })?;
1771                ForBindingIndex::Named(Spanned::new(resolved, index.span))
1772            }
1773            ast::ForBindingIndex::Range { arg, span } => ForBindingIndex::Range {
1774                arg: lower_nat_expr(arg, self.ctx.type_context())?,
1775                span: *span,
1776            },
1777        };
1778        Ok(ForBinding { local, index })
1779    }
1780
1781    fn lower_index_arg(&mut self, arg: &ast::IndexArg) -> Result<IndexArg, ExprLowerError> {
1782        match arg {
1783            ast::IndexArg::Variant { index, variant } => {
1784                let resolved = self.resolve_index_variant_parts(
1785                    &index.value,
1786                    &variant.value,
1787                    index.span,
1788                    variant.span,
1789                )?;
1790                Ok(IndexArg::Variant(IndexVariantRef {
1791                    variant: resolved,
1792                    index_span: Some(index.span),
1793                    variant_span: variant.span,
1794                }))
1795            }
1796            ast::IndexArg::Var(ident) => Ok(IndexArg::Var(Spanned::new(
1797                self.lookup_local(&LocalName::from_atom(ident.name.clone()), ident.span)?,
1798                ident.span,
1799            ))),
1800            ast::IndexArg::Expr(expr) => Ok(IndexArg::Expr(Box::new(self.lower_expr(expr)))),
1801        }
1802    }
1803
1804    fn lower_match_arm(&mut self, arm: &ast::MatchArm) -> Result<MatchArm, ExprLowerError> {
1805        let pattern = self.lower_match_pattern(&arm.pattern)?;
1806        self.push_scope(pattern.bound_locals())?;
1807        let body = self.lower_expr(&arm.body);
1808        self.pop_scope();
1809        Ok(MatchArm {
1810            pattern,
1811            body,
1812            span: arm.span,
1813        })
1814    }
1815
1816    fn lower_match_pattern(
1817        &mut self,
1818        pattern: &ast::MatchPattern,
1819    ) -> Result<MatchPattern, ExprLowerError> {
1820        match pattern {
1821            ast::MatchPattern::Constructor {
1822                name,
1823                bindings,
1824                span,
1825            } => Ok(MatchPattern::Constructor {
1826                constructor: Spanned::new(
1827                    self.ctx
1828                        .resolver
1829                        .resolve_constructor_path(
1830                            self.ctx.owner,
1831                            &NamePath::local(name.value.atom().clone()),
1832                        )
1833                        .map_err(|source| ExprLowerError::ModuleResolve {
1834                            source,
1835                            span: name.span,
1836                        })?,
1837                    name.span,
1838                ),
1839                bindings: bindings
1840                    .iter()
1841                    .map(|binding| self.lower_pattern_binding(binding))
1842                    .collect::<Result<Vec<_>, _>>()?,
1843                span: *span,
1844            }),
1845            ast::MatchPattern::IndexLabel {
1846                index,
1847                variant,
1848                span,
1849            } => {
1850                let resolved = self.resolve_index_variant_parts(
1851                    &index.value,
1852                    &variant.value,
1853                    index.span,
1854                    variant.span,
1855                )?;
1856                Ok(MatchPattern::IndexLabel {
1857                    variant: IndexVariantRef {
1858                        variant: resolved,
1859                        index_span: Some(index.span),
1860                        variant_span: variant.span,
1861                    },
1862                    span: *span,
1863                })
1864            }
1865            ast::MatchPattern::Path {
1866                path,
1867                bindings,
1868                span,
1869            } => self.lower_path_pattern(path, bindings, *span),
1870        }
1871    }
1872
1873    fn lower_path_pattern(
1874        &mut self,
1875        path: &crate::syntax::ast::IdentPath,
1876        bindings: &[ast::PatternBinding],
1877        span: Span,
1878    ) -> Result<MatchPattern, ExprLowerError> {
1879        let name_path = path.to_name_path();
1880        if bindings.is_empty()
1881            && let Ok(variant) = self
1882                .ctx
1883                .resolver
1884                .resolve_index_variant_path(self.ctx.owner, &name_path)
1885        {
1886            let (qualifier, member) = path.split_last();
1887            let index_span = qualifier.split_first().map(|(first, rest)| {
1888                rest.iter()
1889                    .fold(first.span, |merged, segment| merged.merge(segment.span))
1890            });
1891            return Ok(MatchPattern::IndexLabel {
1892                variant: IndexVariantRef {
1893                    variant,
1894                    index_span,
1895                    variant_span: member.span,
1896                },
1897                span,
1898            });
1899        }
1900
1901        match self
1902            .ctx
1903            .resolver
1904            .resolve_constructor_path(self.ctx.owner, &name_path)
1905        {
1906            Ok(constructor) => Ok(MatchPattern::Constructor {
1907                constructor: Spanned::new(constructor, path.span()),
1908                bindings: bindings
1909                    .iter()
1910                    .map(|binding| self.lower_pattern_binding(binding))
1911                    .collect::<Result<Vec<_>, _>>()?,
1912                span,
1913            }),
1914            Err(source) => match source {
1915                ModuleResolveError::UnknownName { .. }
1916                | ModuleResolveError::UnknownModuleAlias { .. }
1917                | ModuleResolveError::UnknownModule { .. } => Err(ExprLowerError::UnknownPattern {
1918                    path: path.display_path(),
1919                    span,
1920                }),
1921                source => Err(ExprLowerError::ModuleResolve { source, span }),
1922            },
1923        }
1924    }
1925
1926    fn lower_pattern_binding(
1927        &mut self,
1928        binding: &ast::PatternBinding,
1929    ) -> Result<PatternBinding, ExprLowerError> {
1930        match binding {
1931            ast::PatternBinding::Bind { field, var } => Ok(PatternBinding::Bind {
1932                field: field.clone(),
1933                local: self.allocate_local(LocalName::from_atom(var.name.clone()), var.span)?,
1934            }),
1935            ast::PatternBinding::Wildcard { field, span } => Ok(PatternBinding::Wildcard {
1936                field: field.clone(),
1937                span: *span,
1938            }),
1939        }
1940    }
1941
1942    fn resolve_index_variant_parts(
1943        &self,
1944        index_path: &NamePath,
1945        variant: &IndexVariantName,
1946        index_span: Span,
1947        variant_span: Span,
1948    ) -> Result<ResolvedIndexVariant, ExprLowerError> {
1949        self.ctx
1950            .resolver
1951            .resolve_index_variant_parts(self.ctx.owner, index_path, variant)
1952            .map_err(|source| {
1953                let span = match source {
1954                    ModuleResolveError::UnknownIndexVariant { .. } => variant_span,
1955                    _ => index_span,
1956                };
1957                ExprLowerError::ModuleResolve { source, span }
1958            })
1959    }
1960
1961    fn allocate_local(&mut self, name: LocalName, span: Span) -> Result<LocalDef, ExprLowerError> {
1962        let id = LocalId(self.next_local);
1963        let Some(next_local) = self.next_local.checked_add(1) else {
1964            return Err(ExprLowerError::TooManyLocals { span });
1965        };
1966        self.next_local = next_local;
1967        Ok(LocalDef { id, name, span })
1968    }
1969
1970    fn push_scope(&mut self, bindings: Vec<LocalDef>) -> Result<(), ExprLowerError> {
1971        let mut scope = HashMap::new();
1972        for binding in bindings {
1973            if let Some(first) = scope.insert(binding.name.clone(), binding.clone()) {
1974                return Err(ExprLowerError::DuplicateLocalBinding {
1975                    name: binding.name,
1976                    first: first.span,
1977                    duplicate: binding.span,
1978                });
1979            }
1980        }
1981        self.local_scopes.push(scope);
1982        Ok(())
1983    }
1984
1985    fn pop_scope(&mut self) {
1986        self.local_scopes.pop();
1987    }
1988
1989    fn lookup_local(&self, name: &LocalName, span: Span) -> Result<LocalId, ExprLowerError> {
1990        self.local_scopes
1991            .iter()
1992            .rev()
1993            .find_map(|scope| scope.get(name.as_str()))
1994            .map(|def| def.id)
1995            .ok_or_else(|| ExprLowerError::UnknownLocalRef {
1996                name: name.clone(),
1997                span,
1998            })
1999    }
2000}
2001
2002fn scoped_name_to_path(name: &ScopedName, span: Span) -> Result<NamePath, ExprLowerError> {
2003    let qualifier = name
2004        .qualifier()
2005        .iter()
2006        .map(|segment| parse_atom(segment, span))
2007        .collect::<Result<Vec<_>, _>>()?;
2008    let leaf = parse_atom(name.member(), span)?;
2009    Ok(NamePath::qualified_path(qualifier, leaf))
2010}
2011
2012fn parse_atom(segment: &str, span: Span) -> Result<NameAtom, ExprLowerError> {
2013    NameAtom::parse(segment).map_err(|source| ExprLowerError::InvalidScopedNameSegment {
2014        segment: segment.to_string(),
2015        source,
2016        span,
2017    })
2018}
2019
2020#[cfg(test)]
2021mod tests {
2022    use super::*;
2023    use crate::syntax::parser::Parser;
2024
2025    fn desugared_source(source: &str) -> ast::File {
2026        let raw = Parser::new(source).parse_file().unwrap();
2027        crate::syntax::desugar::desugar_multi_decls_in_file(raw)
2028    }
2029
2030    #[test]
2031    fn local_env_layers_frames_without_cloning() {
2032        let a = LocalId(0);
2033        let b = LocalId(1);
2034        let c = LocalId(2);
2035
2036        let root: LocalEnv<'_, i32> = LocalEnv::root();
2037        assert_eq!(root.get(a), None);
2038
2039        let outer = root.child(vec![(a, 1)]);
2040        assert_eq!(outer.get(a), Some(&1));
2041        assert_eq!(outer.get(b), None);
2042
2043        let inner = outer.child(vec![(b, 2)]);
2044        assert_eq!(inner.get(a), Some(&1));
2045        assert_eq!(inner.get(b), Some(&2));
2046
2047        // A child frame never leaks into its parent.
2048        assert_eq!(outer.get(b), None);
2049
2050        let seeded = LocalEnv::from_bindings(vec![(c, 7)]);
2051        assert_eq!(seeded.get(c), Some(&7));
2052    }
2053
2054    #[test]
2055    fn local_env_bind_rebinds_in_place() {
2056        let a = LocalId(0);
2057        let b = LocalId(1);
2058        let root: LocalEnv<'_, i32> = LocalEnv::root();
2059        let mut frame = root.child(Vec::new());
2060
2061        // Iterating binders rebind the same id once per element.
2062        for value in 0..3 {
2063            frame.bind(a, value);
2064            assert_eq!(frame.get(a), Some(&value));
2065        }
2066        frame.bind(b, 10);
2067        assert_eq!(frame.get(a), Some(&2));
2068        assert_eq!(frame.get(b), Some(&10));
2069    }
2070
2071    fn node_value<'a>(file: &'a ast::File, name: &str) -> &'a ast::Expr {
2072        file.declarations
2073            .iter()
2074            .find_map(|decl| match &decl.kind {
2075                ast::DeclKind::Node(node) if node.name.value.as_str() == name => Some(&node.value),
2076                _ => None,
2077            })
2078            .expect("source should contain requested node")
2079    }
2080
2081    fn resolver_with_import(
2082        lib_id: &DagId,
2083        main_id: &DagId,
2084        lib: &ast::File,
2085        main: &ast::File,
2086    ) -> ModuleResolver {
2087        let mut resolver = ModuleResolver::default();
2088        resolver
2089            .add_module(lib_id.clone(), &lib.declarations)
2090            .unwrap();
2091        resolver
2092            .add_module(main_id.clone(), &main.declarations)
2093            .unwrap();
2094        for decl in &main.declarations {
2095            let ast::DeclKind::Import(import) = &decl.kind else {
2096                continue;
2097            };
2098            resolver
2099                .register_import(main_id, &import.path, &import.kind, lib_id)
2100                .unwrap();
2101        }
2102        resolver
2103    }
2104
2105    #[test]
2106    fn lowers_qualified_index_variant_literal_to_canonical_owner() {
2107        let lib_id = DagId::root("lib");
2108        let main_id = DagId::root("main");
2109        let lib = desugared_source("pub index Phase = { Burn, Coast };");
2110        let main_source = "import lib as mission; node phase: Dimensionless = mission.Phase.Burn;";
2111        let main = desugared_source(main_source);
2112        let resolver = resolver_with_import(&lib_id, &main_id, &lib, &main);
2113        let scope = GenericScope::new();
2114
2115        let expr = lower_expr(
2116            node_value(&main, "phase"),
2117            ExprLoweringContext::new(&main_id, &resolver, &scope),
2118        )
2119        .unwrap();
2120
2121        let ExprKind::VariantLiteral(variant) = expr.kind else {
2122            panic!("expected variant literal, got {expr:?}");
2123        };
2124        assert_eq!(variant.variant.index().owner(), &lib_id);
2125        assert_eq!(variant.variant.index().as_str(), "Phase");
2126        assert_eq!(variant.variant.variant().as_str(), "Burn");
2127        // Segment spans address exactly the written path parts.
2128        let slice = |span: crate::syntax::span::Span| {
2129            &main_source[span.offset()..span.offset() + span.len()]
2130        };
2131        assert_eq!(slice(variant.variant_span), "Burn");
2132        assert_eq!(
2133            slice(variant.index_span.expect("written index path")),
2134            "mission.Phase"
2135        );
2136    }
2137
2138    #[test]
2139    fn lowers_qualified_nullary_constructor_const_ref_to_canonical_owner() {
2140        let lib_id = DagId::root("lib");
2141        let main_id = DagId::root("main");
2142        let lib = desugared_source("pub type BurnKind { Impulsive, Coast }");
2143        let main = desugared_source(
2144            "import lib as mission; node burn: Dimensionless = mission.Impulsive;",
2145        );
2146        let resolver = resolver_with_import(&lib_id, &main_id, &lib, &main);
2147        let scope = GenericScope::new();
2148
2149        let expr = lower_expr(
2150            node_value(&main, "burn"),
2151            ExprLoweringContext::new(&main_id, &resolver, &scope),
2152        )
2153        .unwrap();
2154
2155        let ExprKind::ConstRef(target) = expr.kind else {
2156            panic!("expected const-like ref, got {expr:?}");
2157        };
2158        let ConstRef::Constructor(constructor) = target.value else {
2159            panic!("expected constructor, got {target:?}");
2160        };
2161        assert_eq!(constructor.owner(), &lib_id);
2162        assert_eq!(constructor.as_str(), "Impulsive");
2163    }
2164
2165    #[test]
2166    fn lowers_for_locals_to_lexical_ids() {
2167        let owner = DagId::root("main");
2168        let file = desugared_source(
2169            "index Phase = { Burn }; node x: Dimensionless[Phase] = for p: Phase { p };",
2170        );
2171        let mut resolver = ModuleResolver::default();
2172        resolver
2173            .add_module(owner.clone(), &file.declarations)
2174            .unwrap();
2175        let scope = GenericScope::new();
2176
2177        let expr = lower_expr(
2178            node_value(&file, "x"),
2179            ExprLoweringContext::new(&owner, &resolver, &scope),
2180        )
2181        .unwrap();
2182
2183        let ExprKind::ForComp { bindings, body } = expr.kind else {
2184            panic!("expected for comp, got {expr:?}");
2185        };
2186        let [binding] = bindings.as_slice() else {
2187            panic!("expected one binding, got {bindings:?}");
2188        };
2189        let ExprKind::LocalRef(local) = body.kind else {
2190            panic!("expected local ref, got {body:?}");
2191        };
2192        assert_eq!(binding.local.id, local.value);
2193    }
2194
2195    #[test]
2196    fn lowers_qualified_constructor_match_pattern_and_binding() {
2197        let lib_id = DagId::root("lib");
2198        let main_id = DagId::root("main");
2199        let lib =
2200            desugared_source("pub type BurnKind { Impulsive(delta_v: Dimensionless), Coast }");
2201        let main = desugared_source(
2202            "import lib as mission; param burn: Dimensionless; \
2203             node dv: Dimensionless = match @burn { mission.Impulsive(delta_v: dv) => dv, mission.Coast => 0.0 };",
2204        );
2205        let resolver = resolver_with_import(&lib_id, &main_id, &lib, &main);
2206        let scope = GenericScope::new();
2207
2208        let expr = lower_expr(
2209            node_value(&main, "dv"),
2210            ExprLoweringContext::new(&main_id, &resolver, &scope),
2211        )
2212        .unwrap();
2213
2214        let ExprKind::Match { arms, .. } = expr.kind else {
2215            panic!("expected match, got {expr:?}");
2216        };
2217        let [first, _second] = arms.as_slice() else {
2218            panic!("expected two arms, got {arms:?}");
2219        };
2220        let MatchPattern::Constructor {
2221            constructor,
2222            bindings,
2223            ..
2224        } = &first.pattern
2225        else {
2226            panic!("expected constructor pattern, got {:?}", first.pattern);
2227        };
2228        assert_eq!(constructor.value.owner(), &lib_id);
2229        assert_eq!(constructor.value.as_str(), "Impulsive");
2230        let [PatternBinding::Bind { local, .. }] = bindings.as_slice() else {
2231            panic!("expected one field binding, got {bindings:?}");
2232        };
2233        let ExprKind::LocalRef(body_ref) = &first.body.kind else {
2234            panic!("expected local ref body, got {:?}", first.body);
2235        };
2236        assert_eq!(local.id, body_ref.value);
2237    }
2238
2239    #[test]
2240    fn collects_canonical_decl_dependencies_from_hir_expr() {
2241        let lib_id = DagId::root("lib");
2242        let main_id = DagId::root("main");
2243        let lib =
2244            desugared_source("pub const node C: Dimensionless = 1.0; param p: Dimensionless;");
2245        let main = desugared_source(
2246            "import lib as mission; import lib.{p}; node x: Dimensionless = @p + mission.C;",
2247        );
2248        let resolver = resolver_with_import(&lib_id, &main_id, &lib, &main);
2249        let scope = GenericScope::new();
2250
2251        let expr = lower_expr(
2252            node_value(&main, "x"),
2253            ExprLoweringContext::new(&main_id, &resolver, &scope),
2254        )
2255        .unwrap();
2256        let deps = collect_expr_dependencies(&expr);
2257
2258        let graph_refs = deps.graph_refs.into_iter().collect::<Vec<_>>();
2259        let const_refs = deps.const_refs.into_iter().collect::<Vec<_>>();
2260        let [graph_ref] = graph_refs.as_slice() else {
2261            panic!("expected one graph dep, got {graph_refs:?}");
2262        };
2263        let [const_ref] = const_refs.as_slice() else {
2264            panic!("expected one const dep, got {const_refs:?}");
2265        };
2266        assert_eq!(graph_ref.owner(), &lib_id);
2267        assert_eq!(graph_ref.as_str(), "p");
2268        assert_eq!(const_ref.owner(), &lib_id);
2269        assert_eq!(const_ref.as_str(), "C");
2270    }
2271
2272    #[test]
2273    fn const_ref_to_runtime_decl_is_rejected_by_decl_kind() {
2274        let owner = DagId::root("main");
2275        let file = desugared_source("param p: Dimensionless; node x: Dimensionless = p;");
2276        let mut resolver = ModuleResolver::default();
2277        resolver
2278            .add_module(owner.clone(), &file.declarations)
2279            .unwrap();
2280        let scope = GenericScope::new();
2281
2282        let err = lower_expr(
2283            node_value(&file, "x"),
2284            ExprLoweringContext::new(&owner, &resolver, &scope),
2285        )
2286        .unwrap_err();
2287
2288        assert!(
2289            err.to_string()
2290                .contains("expected const declaration `main.p`, found param"),
2291            "unexpected error: {err}"
2292        );
2293    }
2294}