Skip to main content

aver/ir/hir/
classify.rs

1//! Shared structural classifiers over [`ResolvedExpr`] /
2//! [`ResolvedPattern`].
3//!
4//! The shared IR classifiers in `crate::ir::{calls, leaf, matches,
5//! body}` still operate on the pre-Phase-E `Expr` shape — they're
6//! consumed by the backends that haven't migrated yet (wasm-gc,
7//! Lean, Dafny, self-host). The VM compiler (#147 phase E PR 7) and
8//! the Rust codegen (#147 phase E PR 8) consume `ResolvedExpr` /
9//! `ResolvedPattern` directly, so they share this module's
10//! classifiers instead of threading a `&Expr` conversion through
11//! every fast path.
12//!
13//! These mirror the IR classifiers' optimisation menu 1-for-1
14//! (`Vector.get` → `LeafOp::FieldAccess`, dispatch-table arms →
15//! `MATCH_DISPATCH`, etc.) so the bytecode / Rust source the
16//! backends emit is byte-identical to the pre-migration build.
17//! The IR classifiers stay the source of truth for the recognised
18//! shapes; this file re-encodes that menu against the resolved AST
19//! without re-doing the identity work the resolver pass already
20//! finished.
21
22use crate::ast::{Literal, Spanned};
23use crate::ir::hir::{
24    BuiltinCtor, ResolvedCallee, ResolvedCtor, ResolvedExpr, ResolvedFnBody, ResolvedFnDef,
25    ResolvedMatchArm, ResolvedPattern, ResolvedStmt,
26};
27use crate::ir::{
28    BoolCompareOp, BoolMatchShape, DispatchArmPlan, DispatchBindingPlan, DispatchDefaultPlan,
29    DispatchLiteral, DispatchTableShape, ListMatchShape, MatchDispatchPlan,
30    SemanticDispatchPattern, WrapperKind,
31};
32
33/// Mirror of [`crate::ir::LeafOp`] keyed against `ResolvedExpr`.
34///
35/// Each variant's fields are deliberately preserved (even when the
36/// VM compiler currently consumes the leaf by walking the original
37/// `ResolvedExpr` instead of these borrows) — keeps the shape
38/// documentation aligned with `crate::ir::LeafOp` for cross-reading
39/// and leaves room for future consumers that prefer the typed
40/// borrows over re-walking. Suppress the dead-code lint at the type
41/// level rather than per-field so the docs stay flat.
42#[allow(dead_code)]
43pub enum ResolvedLeafOp<'a> {
44    FieldAccess {
45        object: &'a crate::ast::Spanned<ResolvedExpr>,
46        field_name: &'a str,
47    },
48    MapGet {
49        map: &'a crate::ast::Spanned<ResolvedExpr>,
50        key: &'a crate::ast::Spanned<ResolvedExpr>,
51    },
52    MapSet {
53        map: &'a crate::ast::Spanned<ResolvedExpr>,
54        key: &'a crate::ast::Spanned<ResolvedExpr>,
55        value: &'a crate::ast::Spanned<ResolvedExpr>,
56    },
57    VectorNew {
58        size: &'a crate::ast::Spanned<ResolvedExpr>,
59        fill: &'a crate::ast::Spanned<ResolvedExpr>,
60    },
61    VectorSetOrDefaultSameVector {
62        vector: &'a crate::ast::Spanned<ResolvedExpr>,
63        index: &'a crate::ast::Spanned<ResolvedExpr>,
64        value: &'a crate::ast::Spanned<ResolvedExpr>,
65    },
66    VectorGetOrDefaultLiteral {
67        vector: &'a crate::ast::Spanned<ResolvedExpr>,
68        index: &'a crate::ast::Spanned<ResolvedExpr>,
69        default_literal: &'a Literal,
70    },
71    IntModOrDefaultLiteral {
72        a: &'a crate::ast::Spanned<ResolvedExpr>,
73        b: &'a crate::ast::Spanned<ResolvedExpr>,
74        default_literal: &'a Literal,
75    },
76    ListIndexGet {
77        list: &'a crate::ast::Spanned<ResolvedExpr>,
78        index: &'a crate::ast::Spanned<ResolvedExpr>,
79    },
80    NoneValue,
81    VariantConstructor {
82        qualified_type_name: String,
83        variant_name: String,
84    },
85    StaticRef(String),
86}
87
88/// Bool-subject classifier output, mirror of
89/// [`crate::ir::BoolSubjectPlan`].
90#[allow(dead_code)]
91pub enum ResolvedBoolSubjectPlan<'a> {
92    Expr(&'a ResolvedExpr),
93    Compare {
94        lhs: &'a crate::ast::Spanned<ResolvedExpr>,
95        rhs: &'a crate::ast::Spanned<ResolvedExpr>,
96        op: BoolCompareOp,
97        invert: bool,
98    },
99}
100
101/// Forward-call shape mirror — the resolved callee + per-arg
102/// forwarding slot when every arg is a `Resolved` local. `args`
103/// carries the original [`crate::ast::Spanned<ResolvedExpr>`] borrows
104/// so the emitter can reuse the existing name + last-use stamp
105/// (which `ForwardSlot::Local { slot }` alone wouldn't preserve).
106#[allow(dead_code)]
107pub struct ResolvedForwardCallPlan<'a> {
108    pub callee: &'a ResolvedCallee,
109    pub forward_slots: Vec<ForwardSlot>,
110    pub args: &'a [crate::ast::Spanned<ResolvedExpr>],
111}
112
113pub enum ForwardSlot {
114    Local { slot: u16 },
115}
116
117/// Recognise an expression as one of the fused leaf shapes.
118/// Mirrors [`crate::ir::classify_leaf_op`] structurally; identity
119/// classification (`Builtin` / `Fn` / ...) is read directly off
120/// [`ResolvedCallee`] instead of re-derived from a string.
121pub fn classify_leaf_op_resolved<'a>(
122    expr: &'a ResolvedExpr,
123    is_user_type: &impl Fn(&str) -> bool,
124) -> Option<ResolvedLeafOp<'a>> {
125    match expr {
126        ResolvedExpr::Attr(object, field_name) => {
127            classify_field_access(expr, object, field_name, is_user_type)
128        }
129        ResolvedExpr::Call(callee, args) => classify_leaf_call(callee, args, is_user_type),
130        ResolvedExpr::Ctor(ctor, args) if args.is_empty() => match ctor {
131            ResolvedCtor::Builtin(BuiltinCtor::OptionNone) => Some(ResolvedLeafOp::NoneValue),
132            ResolvedCtor::User {
133                type_id: _,
134                name,
135                ctor_id: _,
136            } => {
137                // Nullary user ctor in non-call position. Reconstruct
138                // the qualified type name from the surrounding
139                // expression context — the ctor carries only the
140                // variant name and a TypeId, not the source dotted
141                // form. The VM compiler's `leaf_op` consumer uses
142                // the dotted name to look up the variant in the
143                // arena; the typecheck-time qualified name is
144                // recoverable via the resolver's stored type_id but
145                // here we don't have access to it. Skip for now;
146                // the call-shape compiler path catches nullary
147                // ctors anyway via `compile_constructor`.
148                let _ = name;
149                None
150            }
151            _ => None,
152        },
153        _ => None,
154    }
155}
156
157fn classify_field_access<'a>(
158    full_expr: &'a ResolvedExpr,
159    object: &'a crate::ast::Spanned<ResolvedExpr>,
160    field_name: &'a str,
161    _is_user_type: &impl Fn(&str) -> bool,
162) -> Option<ResolvedLeafOp<'a>> {
163    // Walk to detect an uppercase dotted path (matches the IR
164    // classifier's heuristic). If the field access is on a value
165    // expression (e.g. a record), emit the generic FieldAccess.
166    let dotted = resolved_to_dotted(full_expr);
167    let starts_upper = dotted
168        .as_deref()
169        .and_then(|d| d.chars().next())
170        .is_some_and(|c| c.is_uppercase());
171
172    if !starts_upper {
173        return Some(ResolvedLeafOp::FieldAccess { object, field_name });
174    }
175    // Uppercase dotted path: classify as a static module/builtin
176    // reference. The VM has full namespace state in its symbol
177    // table, so the consumer (`compile_leaf_op`) can resolve the
178    // dotted path at emit time. We pass through the dotted form so
179    // the existing emitter logic stays unchanged. The Rust codegen
180    // refines this to `VariantConstructor` / `NoneValue` at emit
181    // time when the prefix is known to name a user type.
182    dotted.map(ResolvedLeafOp::StaticRef)
183}
184
185fn classify_leaf_call<'a>(
186    callee: &'a ResolvedCallee,
187    args: &'a [crate::ast::Spanned<ResolvedExpr>],
188    is_user_type: &impl Fn(&str) -> bool,
189) -> Option<ResolvedLeafOp<'a>> {
190    let builtin_name = match callee {
191        ResolvedCallee::Builtin(name) => name.as_str(),
192        _ => return None,
193    };
194    match (builtin_name, args.len()) {
195        ("Map.get", 2) => Some(ResolvedLeafOp::MapGet {
196            map: &args[0],
197            key: &args[1],
198        }),
199        ("Map.set", 3) => Some(ResolvedLeafOp::MapSet {
200            map: &args[0],
201            key: &args[1],
202            value: &args[2],
203        }),
204        ("Vector.new", 2) => Some(ResolvedLeafOp::VectorNew {
205            size: &args[0],
206            fill: &args[1],
207        }),
208        ("Vector.get", 2) => classify_list_index_get(&args[0], &args[1]),
209        ("Option.withDefault", 2) => classify_vector_set_or_default(&args[0], &args[1])
210            .or_else(|| classify_vector_get_or_default(&args[0], &args[1])),
211        ("Result.withDefault", 2) => classify_int_mod_or_default(&args[0], &args[1], is_user_type),
212        _ => None,
213    }
214}
215
216fn classify_vector_set_or_default<'a>(
217    option_expr: &'a crate::ast::Spanned<ResolvedExpr>,
218    default_expr: &'a crate::ast::Spanned<ResolvedExpr>,
219) -> Option<ResolvedLeafOp<'a>> {
220    let ResolvedExpr::Call(inner_callee, inner_args) = &option_expr.node else {
221        return None;
222    };
223    if inner_args.len() != 3 {
224        return None;
225    }
226    let is_vector_set =
227        matches!(inner_callee, ResolvedCallee::Builtin(name) if name == "Vector.set");
228    if !is_vector_set {
229        return None;
230    }
231    if default_expr.node != inner_args[0].node {
232        return None;
233    }
234    Some(ResolvedLeafOp::VectorSetOrDefaultSameVector {
235        vector: &inner_args[0],
236        index: &inner_args[1],
237        value: &inner_args[2],
238    })
239}
240
241fn classify_vector_get_or_default<'a>(
242    option_expr: &'a crate::ast::Spanned<ResolvedExpr>,
243    default_expr: &'a crate::ast::Spanned<ResolvedExpr>,
244) -> Option<ResolvedLeafOp<'a>> {
245    let default_literal = match &default_expr.node {
246        ResolvedExpr::Literal(lit) => lit,
247        _ => return None,
248    };
249    let ResolvedExpr::Call(inner_callee, inner_args) = &option_expr.node else {
250        return None;
251    };
252    if inner_args.len() != 2 {
253        return None;
254    }
255    let is_vector_get =
256        matches!(inner_callee, ResolvedCallee::Builtin(name) if name == "Vector.get");
257    if !is_vector_get {
258        return None;
259    }
260    Some(ResolvedLeafOp::VectorGetOrDefaultLiteral {
261        vector: &inner_args[0],
262        index: &inner_args[1],
263        default_literal,
264    })
265}
266
267fn classify_list_index_get<'a>(
268    vector_expr: &'a crate::ast::Spanned<ResolvedExpr>,
269    index: &'a crate::ast::Spanned<ResolvedExpr>,
270) -> Option<ResolvedLeafOp<'a>> {
271    let ResolvedExpr::Call(inner_callee, inner_args) = &vector_expr.node else {
272        return None;
273    };
274    if inner_args.len() != 1 {
275        return None;
276    }
277    let is_from_list =
278        matches!(inner_callee, ResolvedCallee::Builtin(name) if name == "Vector.fromList");
279    if !is_from_list {
280        return None;
281    }
282    Some(ResolvedLeafOp::ListIndexGet {
283        list: &inner_args[0],
284        index,
285    })
286}
287
288fn classify_int_mod_or_default<'a>(
289    result_expr: &'a crate::ast::Spanned<ResolvedExpr>,
290    default_expr: &'a crate::ast::Spanned<ResolvedExpr>,
291    _is_user_type: &impl Fn(&str) -> bool,
292) -> Option<ResolvedLeafOp<'a>> {
293    let default_literal = match &default_expr.node {
294        ResolvedExpr::Literal(lit) => lit,
295        _ => return None,
296    };
297    let ResolvedExpr::Call(inner_callee, inner_args) = &result_expr.node else {
298        return None;
299    };
300    if inner_args.len() != 2 {
301        return None;
302    }
303    let is_int_mod = matches!(inner_callee, ResolvedCallee::Builtin(name) if name == "Int.mod");
304    if !is_int_mod {
305        return None;
306    }
307    Some(ResolvedLeafOp::IntModOrDefaultLiteral {
308        a: &inner_args[0],
309        b: &inner_args[1],
310        default_literal,
311    })
312}
313
314/// Classify a [`ResolvedExpr`] as a bool subject for the
315/// `match X { true -> _; false -> _ }` shortcut.
316pub fn classify_bool_subject_plan_resolved(subject: &ResolvedExpr) -> ResolvedBoolSubjectPlan<'_> {
317    let ResolvedExpr::BinOp(op, lhs, rhs) = subject else {
318        return ResolvedBoolSubjectPlan::Expr(subject);
319    };
320    use crate::ast::BinOp;
321    match op {
322        BinOp::Eq => ResolvedBoolSubjectPlan::Compare {
323            lhs,
324            rhs,
325            op: BoolCompareOp::Eq,
326            invert: false,
327        },
328        BinOp::Lt => ResolvedBoolSubjectPlan::Compare {
329            lhs,
330            rhs,
331            op: BoolCompareOp::Lt,
332            invert: false,
333        },
334        BinOp::Gt => ResolvedBoolSubjectPlan::Compare {
335            lhs,
336            rhs,
337            op: BoolCompareOp::Gt,
338            invert: false,
339        },
340        BinOp::Neq => ResolvedBoolSubjectPlan::Compare {
341            lhs,
342            rhs,
343            op: BoolCompareOp::Eq,
344            invert: true,
345        },
346        BinOp::Gte => ResolvedBoolSubjectPlan::Compare {
347            lhs,
348            rhs,
349            op: BoolCompareOp::Lt,
350            invert: true,
351        },
352        BinOp::Lte => ResolvedBoolSubjectPlan::Compare {
353            lhs,
354            rhs,
355            op: BoolCompareOp::Gt,
356            invert: true,
357        },
358        BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div => ResolvedBoolSubjectPlan::Expr(subject),
359    }
360}
361
362/// Classify a single arm pattern as dispatchable in the VM's
363/// MATCH_DISPATCH table. Mirrors [`crate::ir::classify_dispatch_pattern`].
364pub fn classify_dispatch_pattern_resolved(
365    pattern: &ResolvedPattern,
366) -> Option<SemanticDispatchPattern> {
367    match pattern {
368        ResolvedPattern::Literal(lit) => Some(SemanticDispatchPattern::Literal(
369            dispatch_literal_from_ast(lit),
370        )),
371        ResolvedPattern::EmptyList => Some(SemanticDispatchPattern::EmptyList),
372        ResolvedPattern::Ctor(ctor, bindings) => match ctor {
373            ResolvedCtor::Builtin(BuiltinCtor::OptionNone) if bindings.is_empty() => {
374                Some(SemanticDispatchPattern::NoneValue)
375            }
376            ResolvedCtor::Builtin(BuiltinCtor::ResultOk) if bindings.len() <= 1 => {
377                Some(SemanticDispatchPattern::WrapperTag(WrapperKind::ResultOk))
378            }
379            ResolvedCtor::Builtin(BuiltinCtor::ResultErr) if bindings.len() <= 1 => {
380                Some(SemanticDispatchPattern::WrapperTag(WrapperKind::ResultErr))
381            }
382            ResolvedCtor::Builtin(BuiltinCtor::OptionSome) if bindings.len() <= 1 => {
383                Some(SemanticDispatchPattern::WrapperTag(WrapperKind::OptionSome))
384            }
385            _ => None,
386        },
387        _ => None,
388    }
389}
390
391/// Mirror of [`crate::ir::classify_list_match_shape`] / `_from_patterns`.
392pub fn classify_list_match_shape_resolved(arms: &[ResolvedMatchArm]) -> Option<ListMatchShape> {
393    if arms.len() != 2 {
394        return None;
395    }
396    match (&arms[0].pattern, &arms[1].pattern) {
397        (ResolvedPattern::EmptyList, ResolvedPattern::Cons(_, _)) => Some(ListMatchShape {
398            empty_arm_index: 0,
399            cons_arm_index: 1,
400        }),
401        (ResolvedPattern::Cons(_, _), ResolvedPattern::EmptyList) => Some(ListMatchShape {
402            empty_arm_index: 1,
403            cons_arm_index: 0,
404        }),
405        _ => None,
406    }
407}
408
409/// Mirror of [`crate::ir::classify_bool_match_shape`] / `_from_patterns`.
410pub fn classify_bool_match_shape_resolved(arms: &[ResolvedMatchArm]) -> Option<BoolMatchShape> {
411    if arms.len() != 2 {
412        return None;
413    }
414    use crate::ast::Literal as Lit;
415    match (&arms[0].pattern, &arms[1].pattern) {
416        (ResolvedPattern::Literal(Lit::Bool(true)), ResolvedPattern::Literal(Lit::Bool(false))) => {
417            Some(BoolMatchShape {
418                true_arm_index: 0,
419                false_arm_index: 1,
420            })
421        }
422        (ResolvedPattern::Literal(Lit::Bool(false)), ResolvedPattern::Literal(Lit::Bool(true))) => {
423            Some(BoolMatchShape {
424                true_arm_index: 1,
425                false_arm_index: 0,
426            })
427        }
428        (
429            ResolvedPattern::Literal(Lit::Bool(true)),
430            ResolvedPattern::Wildcard | ResolvedPattern::Ident(_),
431        ) => Some(BoolMatchShape {
432            true_arm_index: 0,
433            false_arm_index: 1,
434        }),
435        _ => None,
436    }
437}
438
439/// Mirror of [`crate::ir::classify_dispatch_table_shape`] /
440/// `_from_patterns`.
441pub fn classify_dispatch_table_shape_resolved(
442    arms: &[ResolvedMatchArm],
443) -> Option<DispatchTableShape> {
444    if arms.len() < 2 {
445        return None;
446    }
447    let has_default = matches!(
448        arms.last().map(|a| &a.pattern),
449        Some(ResolvedPattern::Wildcard | ResolvedPattern::Ident(_))
450    );
451    let dispatchable_end = if has_default {
452        arms.len() - 1
453    } else {
454        arms.len()
455    };
456
457    let mut entries = Vec::new();
458    for (arm_index, arm) in arms[..dispatchable_end].iter().enumerate() {
459        let semantic = classify_dispatch_pattern_resolved(&arm.pattern)?;
460        entries.push(DispatchArmPlan {
461            binding: classify_dispatch_binding_resolved(&arm.pattern, &semantic),
462            pattern: semantic,
463            arm_index,
464        });
465    }
466
467    if entries.len() < 2 {
468        return None;
469    }
470
471    let default_arm = has_default.then(|| {
472        let arm_idx = arms.len() - 1;
473        let binding_name = match &arms[arm_idx].pattern {
474            ResolvedPattern::Ident(name) if name != "_" => Some(name.clone()),
475            _ => None,
476        };
477        DispatchDefaultPlan {
478            arm_index: arm_idx,
479            binding_name,
480        }
481    });
482
483    Some(DispatchTableShape {
484        entries,
485        default_arm,
486    })
487}
488
489/// Mirror of [`crate::ir::classify_match_dispatch_plan`] /
490/// `_from_patterns`.
491pub fn classify_match_dispatch_plan_resolved(
492    arms: &[ResolvedMatchArm],
493) -> Option<MatchDispatchPlan> {
494    if let Some(shape) = classify_bool_match_shape_resolved(arms) {
495        return Some(MatchDispatchPlan::Bool(shape));
496    }
497    if let Some(shape) = classify_list_match_shape_resolved(arms) {
498        return Some(MatchDispatchPlan::List(shape));
499    }
500    classify_dispatch_table_shape_resolved(arms).map(MatchDispatchPlan::Table)
501}
502
503fn classify_dispatch_binding_resolved(
504    pattern: &ResolvedPattern,
505    semantic: &SemanticDispatchPattern,
506) -> DispatchBindingPlan {
507    match (pattern, semantic) {
508        (ResolvedPattern::Ctor(_, bindings), SemanticDispatchPattern::WrapperTag(_))
509            if !bindings.is_empty() && bindings[0] != "_" =>
510        {
511            DispatchBindingPlan::WrapperPayload(bindings[0].clone())
512        }
513        _ => DispatchBindingPlan::None,
514    }
515}
516
517fn dispatch_literal_from_ast(lit: &Literal) -> DispatchLiteral {
518    match lit {
519        Literal::Int(i) => DispatchLiteral::Int(*i),
520        Literal::Float(f) => DispatchLiteral::Float(f.to_string()),
521        Literal::Bool(b) => DispatchLiteral::Bool(*b),
522        Literal::Str(s) => DispatchLiteral::Str(s.clone()),
523        Literal::Unit => DispatchLiteral::Unit,
524    }
525}
526
527/// Reconstruct an `Module.member.sub` dotted path from a chain of
528/// `ResolvedExpr::Ident` / `ResolvedExpr::Attr` nodes. Used by the
529/// VM compiler's leaf-op recognition for static module/builtin
530/// references (`Fibonacci.fib`, `Domain.Tag.Active`, ...) and by
531/// the `Unresolved`-callee fallback path so the existing namespace
532/// dispatch logic can keep operating on dotted names. Returns
533/// `None` for anything whose root isn't an `Ident` (resolved
534/// slots, calls, etc.).
535pub fn resolved_to_dotted(expr: &ResolvedExpr) -> Option<String> {
536    match expr {
537        ResolvedExpr::Ident(name) => Some(name.clone()),
538        ResolvedExpr::Resolved { name, .. } => Some(name.clone()),
539        ResolvedExpr::Attr(obj, field) => {
540            let head = resolved_to_dotted(&obj.node)?;
541            Some(format!("{head}.{field}"))
542        }
543        _ => None,
544    }
545}
546
547/// Map a [`ResolvedCallee`] (call-position) to a forward-call plan
548/// when every supplied arg is a slot-based `Resolved` reference or a
549/// bare `Ident` (the latter for `EmitCtx::is_local_value` style
550/// classification — local idents in the source-shape resolver).
551/// Returns `None` if the callee is dynamic / a wrapper with the
552/// wrong arity / has any non-local argument. `forward_slots` carries
553/// the resolved arg borrows so the emitter can reuse the original
554/// name + last-use stamp without synthesizing empty `Resolved` nodes.
555pub fn classify_forward_call_resolved<'a>(
556    callee: &'a ResolvedCallee,
557    args: &'a [crate::ast::Spanned<ResolvedExpr>],
558) -> Option<ResolvedForwardCallPlan<'a>> {
559    match callee {
560        ResolvedCallee::Unresolved { .. } => return None,
561        ResolvedCallee::LocalSlot { .. } => return None,
562        ResolvedCallee::Intrinsic(_) => return None,
563        _ => {}
564    }
565
566    let forward_slots = args
567        .iter()
568        .map(classify_forward_arg_resolved)
569        .collect::<Option<Vec<_>>>()?;
570
571    Some(ResolvedForwardCallPlan {
572        callee,
573        forward_slots,
574        args,
575    })
576}
577
578fn classify_forward_arg_resolved(expr: &crate::ast::Spanned<ResolvedExpr>) -> Option<ForwardSlot> {
579    match &expr.node {
580        ResolvedExpr::Resolved { slot, .. } => Some(ForwardSlot::Local { slot: *slot }),
581        _ => None,
582    }
583}
584
585// ---------------------------------------------------------------------------
586// Body-shape classifiers — mirror of `crate::ir::body` against `ResolvedExpr`.
587// ---------------------------------------------------------------------------
588//
589// The Rust codegen (#147 phase E PR 8) uses these to drive its body-plan +
590// thin-fn-def emission paths. Same recognition menu as `body.rs`; identity
591// classification of callees / ctors is read off the resolved enums directly.
592
593pub use crate::ir::body::ThinKind;
594
595/// Resolved-form mirror of [`crate::ir::BodyExprPlan`].
596pub enum ResolvedBodyExprPlan<'a> {
597    Expr(&'a ResolvedExpr),
598    Leaf(ResolvedLeafOp<'a>),
599    Call {
600        callee: &'a ResolvedCallee,
601        args: &'a [Spanned<ResolvedExpr>],
602    },
603    ForwardCall(ResolvedForwardCallPlan<'a>),
604}
605
606/// Resolved-form mirror of [`crate::ir::BodyBindingPlan`].
607pub struct ResolvedBodyBindingPlan<'a> {
608    pub name: &'a str,
609    pub expr: ResolvedBodyExprPlan<'a>,
610}
611
612/// Resolved-form mirror of [`crate::ir::BodyPlan`].
613pub enum ResolvedBodyPlan<'a> {
614    SingleExpr(ResolvedBodyExprPlan<'a>),
615    Block {
616        stmts: &'a [ResolvedStmt],
617        bindings: Vec<ResolvedBodyBindingPlan<'a>>,
618        tail: ResolvedBodyExprPlan<'a>,
619    },
620}
621
622/// Resolved-form mirror of [`crate::ir::ThinBodyPlan`]. `params` mirrors
623/// `ResolvedFnDef::params` shape so consumers that look at param types
624/// see the resolved [`crate::ast::Type`] form rather than the source
625/// annotation string.
626pub struct ResolvedThinBodyPlan<'a> {
627    pub params: &'a [(String, crate::ast::Type)],
628    pub body: ResolvedBodyPlan<'a>,
629    pub kind: ThinKind,
630}
631
632pub fn classify_body_expr_plan_resolved<'a>(
633    expr: &'a ResolvedExpr,
634    is_user_type: &impl Fn(&str) -> bool,
635) -> ResolvedBodyExprPlan<'a> {
636    if let Some(leaf) = classify_leaf_op_resolved(expr, is_user_type) {
637        return ResolvedBodyExprPlan::Leaf(leaf);
638    }
639    if let ResolvedExpr::Call(callee, args) = expr {
640        if let Some(plan) = classify_forward_call_resolved(callee, args) {
641            return ResolvedBodyExprPlan::ForwardCall(plan);
642        }
643        // LocalSlot is "dynamic" — fall through to Expr passthrough.
644        // Unresolved keeps the source-shape "call" shape so the thin-fn
645        // classifier still recognises it as a direct call (matches
646        // pre-migration `classify_call_plan` returning `Function(name)`
647        // for any unknown bare ident).
648        if !matches!(callee, ResolvedCallee::LocalSlot { .. }) {
649            return ResolvedBodyExprPlan::Call { callee, args };
650        }
651    }
652    ResolvedBodyExprPlan::Expr(expr)
653}
654
655pub fn classify_body_plan_resolved<'a>(
656    body: &'a ResolvedFnBody,
657    is_user_type: &impl Fn(&str) -> bool,
658) -> Option<ResolvedBodyPlan<'a>> {
659    let stmts = body.stmts();
660    let (tail_stmt, prefix) = stmts.split_last()?;
661
662    let ResolvedStmt::Expr(tail_expr) = tail_stmt else {
663        return None;
664    };
665
666    if prefix.is_empty() {
667        return Some(ResolvedBodyPlan::SingleExpr(
668            classify_body_expr_plan_resolved(&tail_expr.node, is_user_type),
669        ));
670    }
671
672    let mut bindings = Vec::with_capacity(prefix.len());
673    for stmt in prefix {
674        let ResolvedStmt::Binding { name, value, .. } = stmt else {
675            return None;
676        };
677        bindings.push(ResolvedBodyBindingPlan {
678            name: name.as_str(),
679            expr: classify_body_expr_plan_resolved(&value.node, is_user_type),
680        });
681    }
682
683    Some(ResolvedBodyPlan::Block {
684        stmts,
685        bindings,
686        tail: classify_body_expr_plan_resolved(&tail_expr.node, is_user_type),
687    })
688}
689
690pub fn classify_thin_fn_def_resolved<'a>(
691    fd: &'a ResolvedFnDef,
692    is_user_type: &impl Fn(&str) -> bool,
693) -> Option<ResolvedThinBodyPlan<'a>> {
694    let body = classify_body_plan_resolved(&fd.body, is_user_type)?;
695    Some(ResolvedThinBodyPlan {
696        params: &fd.params,
697        kind: classify_thin_kind_resolved(&body, is_user_type)?,
698        body,
699    })
700}
701
702fn classify_thin_kind_resolved(
703    plan: &ResolvedBodyPlan<'_>,
704    is_user_type: &impl Fn(&str) -> bool,
705) -> Option<ThinKind> {
706    match plan {
707        ResolvedBodyPlan::SingleExpr(expr) => classify_thin_expr_kind_resolved(expr, is_user_type),
708        ResolvedBodyPlan::Block { bindings, tail, .. } => {
709            if bindings
710                .iter()
711                .all(|binding| body_expr_is_thin_binding_resolved(&binding.expr))
712            {
713                classify_thin_expr_kind_resolved(tail, is_user_type)
714            } else {
715                None
716            }
717        }
718    }
719}
720
721fn classify_thin_expr_kind_resolved(
722    plan: &ResolvedBodyExprPlan<'_>,
723    _is_user_type: &impl Fn(&str) -> bool,
724) -> Option<ThinKind> {
725    match plan {
726        ResolvedBodyExprPlan::Leaf(_) => Some(ThinKind::Leaf),
727        ResolvedBodyExprPlan::Call { .. } => Some(ThinKind::Direct),
728        ResolvedBodyExprPlan::ForwardCall(_) => Some(ThinKind::Forward),
729        ResolvedBodyExprPlan::Expr(expr) => match expr {
730            ResolvedExpr::Match { arms, .. }
731                if classify_match_dispatch_plan_resolved(arms).is_some() =>
732            {
733                Some(ThinKind::Dispatch)
734            }
735            ResolvedExpr::TailCall { .. } => Some(ThinKind::Tail),
736            _ => None,
737        },
738    }
739}
740
741fn body_expr_is_thin_binding_resolved(plan: &ResolvedBodyExprPlan<'_>) -> bool {
742    match plan {
743        ResolvedBodyExprPlan::Leaf(_)
744        | ResolvedBodyExprPlan::Call { .. }
745        | ResolvedBodyExprPlan::ForwardCall(_) => true,
746        ResolvedBodyExprPlan::Expr(expr) => match expr {
747            ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) => true,
748            ResolvedExpr::Ctor(_, _) => true,
749            // Simple arithmetic on idents/literals (e.g. `nextPos = pos + 1`)
750            ResolvedExpr::BinOp(_, l, r) => {
751                is_simple_operand_resolved(&l.node) && is_simple_operand_resolved(&r.node)
752            }
753            // Simple call with ident/literal args (e.g. `reversed = List.reverse(acc)`)
754            ResolvedExpr::Call(_, args) => args.iter().all(|a| is_simple_operand_resolved(&a.node)),
755            _ => false,
756        },
757    }
758}
759
760fn is_simple_operand_resolved(expr: &ResolvedExpr) -> bool {
761    matches!(
762        expr,
763        ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) | ResolvedExpr::Resolved { .. }
764    )
765}
766
767// ---------------------------------------------------------------------------
768// Identity adapters: ResolvedCallee/ResolvedCtor → existing CallPlan /
769// SemanticConstructor shapes. The Rust codegen still consumes the existing
770// enums for its constructor / dispatch emission; these adapters resolve the
771// typed identity through the [`SymbolTable`] so backends don't re-implement
772// canonical-name derivation.
773// ---------------------------------------------------------------------------
774
775use crate::ir::SymbolTable;
776use crate::ir::{CallPlan, SemanticConstructor, WrapperKind as IrWrapperKind};
777
778/// Map a [`ResolvedCallee`] to the existing [`CallPlan`] enum. The
779/// resolver lifts `Result.Ok` / `Option.None` / user variant calls into
780/// `ResolvedExpr::Ctor`, so they never appear here — wrapper / none /
781/// type-constructor variants of `CallPlan` are unreachable from this
782/// adapter.
783pub fn call_plan_from_resolved_callee(
784    callee: &ResolvedCallee,
785    symbol_table: &SymbolTable,
786) -> CallPlan {
787    match callee {
788        ResolvedCallee::Fn(id) => CallPlan::Function(symbol_table.fn_entry(*id).key.canonical()),
789        ResolvedCallee::Builtin(name) => CallPlan::Builtin(name.clone()),
790        ResolvedCallee::Intrinsic(intr) => CallPlan::Builtin(intr.name().to_string()),
791        ResolvedCallee::LocalSlot { .. } | ResolvedCallee::Unresolved { .. } => CallPlan::Dynamic,
792    }
793}
794
795/// Map a [`ResolvedCtor`] to the existing [`SemanticConstructor`] enum.
796pub fn semantic_constructor_from_resolved_ctor(
797    ctor: &ResolvedCtor,
798    symbol_table: &SymbolTable,
799) -> SemanticConstructor {
800    match ctor {
801        ResolvedCtor::Builtin(BuiltinCtor::ResultOk) => {
802            SemanticConstructor::Wrapper(IrWrapperKind::ResultOk)
803        }
804        ResolvedCtor::Builtin(BuiltinCtor::ResultErr) => {
805            SemanticConstructor::Wrapper(IrWrapperKind::ResultErr)
806        }
807        ResolvedCtor::Builtin(BuiltinCtor::OptionSome) => {
808            SemanticConstructor::Wrapper(IrWrapperKind::OptionSome)
809        }
810        ResolvedCtor::Builtin(BuiltinCtor::OptionNone) => SemanticConstructor::NoneValue,
811        ResolvedCtor::User { type_id, name, .. } => SemanticConstructor::TypeConstructor {
812            qualified_type_name: symbol_table.type_entry(*type_id).key.canonical(),
813            variant_name: name.clone(),
814        },
815        ResolvedCtor::Unresolved { name } => SemanticConstructor::Unknown(name.clone()),
816    }
817}