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        ResolvedPattern::Literal(lit) => Some(SemanticDispatchPattern::Literal(
383            dispatch_literal_from_ast(lit),
384        )),
385        ResolvedPattern::EmptyList => Some(SemanticDispatchPattern::EmptyList),
386        ResolvedPattern::Ctor(ctor, bindings) => match ctor {
387            ResolvedCtor::Builtin(BuiltinCtor::OptionNone) if bindings.is_empty() => {
388                Some(SemanticDispatchPattern::NoneValue)
389            }
390            ResolvedCtor::Builtin(BuiltinCtor::ResultOk) if bindings.len() <= 1 => {
391                Some(SemanticDispatchPattern::WrapperTag(WrapperKind::ResultOk))
392            }
393            ResolvedCtor::Builtin(BuiltinCtor::ResultErr) if bindings.len() <= 1 => {
394                Some(SemanticDispatchPattern::WrapperTag(WrapperKind::ResultErr))
395            }
396            ResolvedCtor::Builtin(BuiltinCtor::OptionSome) if bindings.len() <= 1 => {
397                Some(SemanticDispatchPattern::WrapperTag(WrapperKind::OptionSome))
398            }
399            _ => None,
400        },
401        _ => None,
402    }
403}
404
405/// Mirror of [`crate::ir::classify_list_match_shape`] / `_from_patterns`.
406pub fn classify_list_match_shape_resolved(arms: &[ResolvedMatchArm]) -> Option<ListMatchShape> {
407    if arms.len() != 2 {
408        return None;
409    }
410    match (&arms[0].pattern, &arms[1].pattern) {
411        (ResolvedPattern::EmptyList, ResolvedPattern::Cons(_, _)) => Some(ListMatchShape {
412            empty_arm_index: 0,
413            cons_arm_index: 1,
414        }),
415        (ResolvedPattern::Cons(_, _), ResolvedPattern::EmptyList) => Some(ListMatchShape {
416            empty_arm_index: 1,
417            cons_arm_index: 0,
418        }),
419        _ => None,
420    }
421}
422
423/// Mirror of [`crate::ir::classify_bool_match_shape`] / `_from_patterns`.
424pub fn classify_bool_match_shape_resolved(arms: &[ResolvedMatchArm]) -> Option<BoolMatchShape> {
425    if arms.len() != 2 {
426        return None;
427    }
428    use crate::ast::Literal as Lit;
429    match (&arms[0].pattern, &arms[1].pattern) {
430        (ResolvedPattern::Literal(Lit::Bool(true)), ResolvedPattern::Literal(Lit::Bool(false))) => {
431            Some(BoolMatchShape {
432                true_arm_index: 0,
433                false_arm_index: 1,
434            })
435        }
436        (ResolvedPattern::Literal(Lit::Bool(false)), ResolvedPattern::Literal(Lit::Bool(true))) => {
437            Some(BoolMatchShape {
438                true_arm_index: 1,
439                false_arm_index: 0,
440            })
441        }
442        (
443            ResolvedPattern::Literal(Lit::Bool(true)),
444            ResolvedPattern::Wildcard | ResolvedPattern::Ident(_),
445        ) => Some(BoolMatchShape {
446            true_arm_index: 0,
447            false_arm_index: 1,
448        }),
449        _ => None,
450    }
451}
452
453/// Mirror of [`crate::ir::classify_dispatch_table_shape`] /
454/// `_from_patterns`.
455pub fn classify_dispatch_table_shape_resolved(
456    arms: &[ResolvedMatchArm],
457) -> Option<DispatchTableShape> {
458    if arms.len() < 2 {
459        return None;
460    }
461    let has_default = matches!(
462        arms.last().map(|a| &a.pattern),
463        Some(ResolvedPattern::Wildcard | ResolvedPattern::Ident(_))
464    );
465    let dispatchable_end = if has_default {
466        arms.len() - 1
467    } else {
468        arms.len()
469    };
470
471    let mut entries = Vec::new();
472    for (arm_index, arm) in arms[..dispatchable_end].iter().enumerate() {
473        let semantic = classify_dispatch_pattern_resolved(&arm.pattern)?;
474        entries.push(DispatchArmPlan {
475            binding: classify_dispatch_binding_resolved(&arm.pattern, &semantic),
476            pattern: semantic,
477            arm_index,
478        });
479    }
480
481    if entries.len() < 2 {
482        return None;
483    }
484
485    let default_arm = has_default.then(|| {
486        let arm_idx = arms.len() - 1;
487        let binding_name = match &arms[arm_idx].pattern {
488            ResolvedPattern::Ident(name) if name != "_" => Some(name.clone()),
489            _ => None,
490        };
491        DispatchDefaultPlan {
492            arm_index: arm_idx,
493            binding_name,
494        }
495    });
496
497    Some(DispatchTableShape {
498        entries,
499        default_arm,
500    })
501}
502
503/// Mirror of [`crate::ir::classify_match_dispatch_plan`] /
504/// `_from_patterns`.
505pub fn classify_match_dispatch_plan_resolved(
506    arms: &[ResolvedMatchArm],
507) -> Option<MatchDispatchPlan> {
508    if let Some(shape) = classify_bool_match_shape_resolved(arms) {
509        return Some(MatchDispatchPlan::Bool(shape));
510    }
511    if let Some(shape) = classify_list_match_shape_resolved(arms) {
512        return Some(MatchDispatchPlan::List(shape));
513    }
514    classify_dispatch_table_shape_resolved(arms).map(MatchDispatchPlan::Table)
515}
516
517fn classify_dispatch_binding_resolved(
518    pattern: &ResolvedPattern,
519    semantic: &SemanticDispatchPattern,
520) -> DispatchBindingPlan {
521    match (pattern, semantic) {
522        (ResolvedPattern::Ctor(_, bindings), SemanticDispatchPattern::WrapperTag(_))
523            if !bindings.is_empty() && bindings[0] != "_" =>
524        {
525            DispatchBindingPlan::WrapperPayload(bindings[0].clone())
526        }
527        _ => DispatchBindingPlan::None,
528    }
529}
530
531fn dispatch_literal_from_ast(lit: &Literal) -> DispatchLiteral {
532    match lit {
533        Literal::Int(i) => DispatchLiteral::Int(*i),
534        Literal::Float(f) => DispatchLiteral::Float(f.to_string()),
535        Literal::Bool(b) => DispatchLiteral::Bool(*b),
536        Literal::Str(s) => DispatchLiteral::Str(s.clone()),
537        Literal::Unit => DispatchLiteral::Unit,
538    }
539}
540
541/// Reconstruct an `Module.member.sub` dotted path from a chain of
542/// `ResolvedExpr::Ident` / `ResolvedExpr::Attr` nodes. Used by the
543/// VM compiler's leaf-op recognition for static module/builtin
544/// references (`Fibonacci.fib`, `Domain.Tag.Active`, ...) and by
545/// the `Unresolved`-callee fallback path so the existing namespace
546/// dispatch logic can keep operating on dotted names. Returns
547/// `None` for anything whose root isn't an `Ident` (resolved
548/// slots, calls, etc.).
549pub fn resolved_to_dotted(expr: &ResolvedExpr) -> Option<String> {
550    match expr {
551        ResolvedExpr::Ident(name) => Some(name.clone()),
552        ResolvedExpr::Resolved { name, .. } => Some(name.clone()),
553        ResolvedExpr::Attr(obj, field) => {
554            let head = resolved_to_dotted(&obj.node)?;
555            Some(format!("{head}.{field}"))
556        }
557        _ => None,
558    }
559}
560
561/// Map a [`ResolvedCallee`] (call-position) to a forward-call plan
562/// when every supplied arg is a slot-based `Resolved` reference or a
563/// bare `Ident` (the latter for `EmitCtx::is_local_value` style
564/// classification — local idents in the source-shape resolver).
565/// Returns `None` if the callee is dynamic / a wrapper with the
566/// wrong arity / has any non-local argument. `forward_slots` carries
567/// the resolved arg borrows so the emitter can reuse the original
568/// name + last-use stamp without synthesizing empty `Resolved` nodes.
569pub fn classify_forward_call_resolved<'a>(
570    callee: &'a ResolvedCallee,
571    args: &'a [crate::ast::Spanned<ResolvedExpr>],
572) -> Option<ResolvedForwardCallPlan<'a>> {
573    match callee {
574        ResolvedCallee::Unresolved { .. } => return None,
575        ResolvedCallee::LocalSlot { .. } => return None,
576        ResolvedCallee::Intrinsic(_) => return None,
577        _ => {}
578    }
579
580    let forward_slots = args
581        .iter()
582        .map(classify_forward_arg_resolved)
583        .collect::<Option<Vec<_>>>()?;
584
585    Some(ResolvedForwardCallPlan {
586        callee,
587        forward_slots,
588        args,
589    })
590}
591
592fn classify_forward_arg_resolved(expr: &crate::ast::Spanned<ResolvedExpr>) -> Option<ForwardSlot> {
593    match &expr.node {
594        ResolvedExpr::Resolved { slot, .. } => Some(ForwardSlot::Local { slot: *slot }),
595        _ => None,
596    }
597}
598
599// ---------------------------------------------------------------------------
600// Body-shape classifiers — mirror of `crate::ir::body` against `ResolvedExpr`.
601// ---------------------------------------------------------------------------
602//
603// The Rust codegen (#147 phase E PR 8) uses these to drive its body-plan +
604// thin-fn-def emission paths. Same recognition menu as `body.rs`; identity
605// classification of callees / ctors is read off the resolved enums directly.
606
607pub use crate::ir::body::ThinKind;
608
609/// Resolved-form mirror of [`crate::ir::BodyExprPlan`].
610pub enum ResolvedBodyExprPlan<'a> {
611    Expr(&'a ResolvedExpr),
612    Leaf(ResolvedLeafOp<'a>),
613    Call {
614        callee: &'a ResolvedCallee,
615        args: &'a [Spanned<ResolvedExpr>],
616    },
617    ForwardCall(ResolvedForwardCallPlan<'a>),
618}
619
620/// Resolved-form mirror of [`crate::ir::BodyBindingPlan`].
621pub struct ResolvedBodyBindingPlan<'a> {
622    pub name: &'a str,
623    pub expr: ResolvedBodyExprPlan<'a>,
624}
625
626/// Resolved-form mirror of [`crate::ir::BodyPlan`].
627pub enum ResolvedBodyPlan<'a> {
628    SingleExpr(ResolvedBodyExprPlan<'a>),
629    Block {
630        stmts: &'a [ResolvedStmt],
631        bindings: Vec<ResolvedBodyBindingPlan<'a>>,
632        tail: ResolvedBodyExprPlan<'a>,
633    },
634}
635
636/// Resolved-form mirror of [`crate::ir::ThinBodyPlan`]. `params` mirrors
637/// `ResolvedFnDef::params` shape so consumers that look at param types
638/// see the resolved [`crate::ast::Type`] form rather than the source
639/// annotation string.
640pub struct ResolvedThinBodyPlan<'a> {
641    pub params: &'a [(String, crate::ast::Type)],
642    pub body: ResolvedBodyPlan<'a>,
643    pub kind: ThinKind,
644}
645
646pub fn classify_body_expr_plan_resolved<'a>(
647    expr: &'a ResolvedExpr,
648    is_user_type: &impl Fn(&str) -> bool,
649) -> ResolvedBodyExprPlan<'a> {
650    if let Some(leaf) = classify_leaf_op_resolved(expr, is_user_type) {
651        return ResolvedBodyExprPlan::Leaf(leaf);
652    }
653    if let ResolvedExpr::Call(callee, args) = expr {
654        if let Some(plan) = classify_forward_call_resolved(callee, args) {
655            return ResolvedBodyExprPlan::ForwardCall(plan);
656        }
657        // LocalSlot is "dynamic" — fall through to Expr passthrough.
658        // Unresolved keeps the source-shape "call" shape so the thin-fn
659        // classifier still recognises it as a direct call (matches
660        // pre-migration `classify_call_plan` returning `Function(name)`
661        // for any unknown bare ident).
662        if !matches!(callee, ResolvedCallee::LocalSlot { .. }) {
663            return ResolvedBodyExprPlan::Call { callee, args };
664        }
665    }
666    ResolvedBodyExprPlan::Expr(expr)
667}
668
669pub fn classify_body_plan_resolved<'a>(
670    body: &'a ResolvedFnBody,
671    is_user_type: &impl Fn(&str) -> bool,
672) -> Option<ResolvedBodyPlan<'a>> {
673    let stmts = body.stmts();
674    let (tail_stmt, prefix) = stmts.split_last()?;
675
676    let ResolvedStmt::Expr(tail_expr) = tail_stmt else {
677        return None;
678    };
679
680    if prefix.is_empty() {
681        return Some(ResolvedBodyPlan::SingleExpr(
682            classify_body_expr_plan_resolved(&tail_expr.node, is_user_type),
683        ));
684    }
685
686    let mut bindings = Vec::with_capacity(prefix.len());
687    for stmt in prefix {
688        let ResolvedStmt::Binding { name, value, .. } = stmt else {
689            return None;
690        };
691        bindings.push(ResolvedBodyBindingPlan {
692            name: name.as_str(),
693            expr: classify_body_expr_plan_resolved(&value.node, is_user_type),
694        });
695    }
696
697    Some(ResolvedBodyPlan::Block {
698        stmts,
699        bindings,
700        tail: classify_body_expr_plan_resolved(&tail_expr.node, is_user_type),
701    })
702}
703
704pub fn classify_thin_fn_def_resolved<'a>(
705    fd: &'a ResolvedFnDef,
706    is_user_type: &impl Fn(&str) -> bool,
707) -> Option<ResolvedThinBodyPlan<'a>> {
708    let body = classify_body_plan_resolved(&fd.body, is_user_type)?;
709    Some(ResolvedThinBodyPlan {
710        params: &fd.params,
711        kind: classify_thin_kind_resolved(&body, is_user_type)?,
712        body,
713    })
714}
715
716fn classify_thin_kind_resolved(
717    plan: &ResolvedBodyPlan<'_>,
718    is_user_type: &impl Fn(&str) -> bool,
719) -> Option<ThinKind> {
720    match plan {
721        ResolvedBodyPlan::SingleExpr(expr) => classify_thin_expr_kind_resolved(expr, is_user_type),
722        ResolvedBodyPlan::Block { bindings, tail, .. } => {
723            if bindings
724                .iter()
725                .all(|binding| body_expr_is_thin_binding_resolved(&binding.expr))
726            {
727                classify_thin_expr_kind_resolved(tail, is_user_type)
728            } else {
729                None
730            }
731        }
732    }
733}
734
735fn classify_thin_expr_kind_resolved(
736    plan: &ResolvedBodyExprPlan<'_>,
737    _is_user_type: &impl Fn(&str) -> bool,
738) -> Option<ThinKind> {
739    match plan {
740        ResolvedBodyExprPlan::Leaf(_) => Some(ThinKind::Leaf),
741        ResolvedBodyExprPlan::Call { .. } => Some(ThinKind::Direct),
742        ResolvedBodyExprPlan::ForwardCall(_) => Some(ThinKind::Forward),
743        ResolvedBodyExprPlan::Expr(expr) => match expr {
744            ResolvedExpr::Match { arms, .. }
745                if classify_match_dispatch_plan_resolved(arms).is_some() =>
746            {
747                Some(ThinKind::Dispatch)
748            }
749            ResolvedExpr::TailCall { .. } => Some(ThinKind::Tail),
750            _ => None,
751        },
752    }
753}
754
755fn body_expr_is_thin_binding_resolved(plan: &ResolvedBodyExprPlan<'_>) -> bool {
756    match plan {
757        ResolvedBodyExprPlan::Leaf(_)
758        | ResolvedBodyExprPlan::Call { .. }
759        | ResolvedBodyExprPlan::ForwardCall(_) => true,
760        ResolvedBodyExprPlan::Expr(expr) => match expr {
761            ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) => true,
762            ResolvedExpr::Ctor(_, _) => true,
763            // Simple arithmetic on idents/literals (e.g. `nextPos = pos + 1`)
764            ResolvedExpr::BinOp(_, l, r) => {
765                is_simple_operand_resolved(&l.node) && is_simple_operand_resolved(&r.node)
766            }
767            // Simple call with ident/literal args (e.g. `reversed = List.reverse(acc)`)
768            ResolvedExpr::Call(_, args) => args.iter().all(|a| is_simple_operand_resolved(&a.node)),
769            _ => false,
770        },
771    }
772}
773
774fn is_simple_operand_resolved(expr: &ResolvedExpr) -> bool {
775    matches!(
776        expr,
777        ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) | ResolvedExpr::Resolved { .. }
778    )
779}
780
781// ---------------------------------------------------------------------------
782// Identity adapters: ResolvedCallee/ResolvedCtor → existing CallPlan /
783// SemanticConstructor shapes. The Rust codegen still consumes the existing
784// enums for its constructor / dispatch emission; these adapters resolve the
785// typed identity through the [`SymbolTable`] so backends don't re-implement
786// canonical-name derivation.
787// ---------------------------------------------------------------------------
788
789use crate::ir::SymbolTable;
790use crate::ir::{CallPlan, SemanticConstructor, WrapperKind as IrWrapperKind};
791
792/// Map a [`ResolvedCallee`] to the existing [`CallPlan`] enum. The
793/// resolver lifts `Result.Ok` / `Option.None` / user variant calls into
794/// `ResolvedExpr::Ctor`, so they never appear here — wrapper / none /
795/// type-constructor variants of `CallPlan` are unreachable from this
796/// adapter.
797pub fn call_plan_from_resolved_callee(
798    callee: &ResolvedCallee,
799    symbol_table: &SymbolTable,
800) -> CallPlan {
801    match callee {
802        ResolvedCallee::Fn(id) => CallPlan::Function(symbol_table.fn_entry(*id).key.canonical()),
803        ResolvedCallee::Builtin(name) => CallPlan::Builtin(name.clone()),
804        ResolvedCallee::Intrinsic(intr) => CallPlan::Builtin(intr.name().to_string()),
805        ResolvedCallee::LocalSlot { .. } | ResolvedCallee::Unresolved { .. } => CallPlan::Dynamic,
806    }
807}
808
809/// Map a [`ResolvedCtor`] to the existing [`SemanticConstructor`] enum.
810pub fn semantic_constructor_from_resolved_ctor(
811    ctor: &ResolvedCtor,
812    symbol_table: &SymbolTable,
813) -> SemanticConstructor {
814    match ctor {
815        ResolvedCtor::Builtin(BuiltinCtor::ResultOk) => {
816            SemanticConstructor::Wrapper(IrWrapperKind::ResultOk)
817        }
818        ResolvedCtor::Builtin(BuiltinCtor::ResultErr) => {
819            SemanticConstructor::Wrapper(IrWrapperKind::ResultErr)
820        }
821        ResolvedCtor::Builtin(BuiltinCtor::OptionSome) => {
822            SemanticConstructor::Wrapper(IrWrapperKind::OptionSome)
823        }
824        ResolvedCtor::Builtin(BuiltinCtor::OptionNone) => SemanticConstructor::NoneValue,
825        ResolvedCtor::User { type_id, name, .. } => SemanticConstructor::TypeConstructor {
826            qualified_type_name: symbol_table.type_entry(*type_id).key.canonical(),
827            variant_name: name.clone(),
828        },
829        ResolvedCtor::Unresolved { name } => SemanticConstructor::Unknown(name.clone()),
830    }
831}