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