Skip to main content

yulang_runtime/lower/
core_shape.rs

1use std::collections::BTreeMap;
2
3use yulang_typed_ir as typed_ir;
4
5#[derive(Debug, Default, Clone, PartialEq, Eq)]
6pub struct ShapeTable {
7    pub exprs: Vec<ExprShape>,
8    pub applies: Vec<ApplyShape>,
9}
10
11#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
12pub struct CoreShapeProfile {
13    pub exprs: usize,
14    pub applies: usize,
15    pub apply_complete: usize,
16    pub apply_partial: usize,
17    pub apply_missing_evidence: usize,
18    pub apply_missing_context: usize,
19    pub apply_missing_principal: usize,
20    pub apply_with_principal: usize,
21    pub apply_with_substitutions: usize,
22    pub apply_with_substitution_candidates: usize,
23    pub apply_with_principal_elaboration: usize,
24    pub apply_principal_elaboration_complete: usize,
25    pub apply_principal_elaboration_incomplete: usize,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ExprShape {
30    pub path: ExprPath,
31    pub kind: ExprShapeKind,
32    pub value: ValueShape,
33    pub effect: EffectShape,
34    pub status: ShapeStatus,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ApplyShape {
39    pub path: ExprPath,
40    pub owner: Option<typed_ir::Path>,
41    pub target: Option<typed_ir::Path>,
42    pub callee_kind: ApplyHeadKind,
43    pub callee_intrinsic: Option<typed_ir::TypeBounds>,
44    pub callee_contextual: Option<typed_ir::TypeBounds>,
45    pub arg_intrinsic: Option<typed_ir::TypeBounds>,
46    pub arg_contextual: Option<typed_ir::TypeBounds>,
47    pub result_intrinsic: Option<typed_ir::TypeBounds>,
48    pub callee_source_edge: Option<u32>,
49    pub arg_source_edge: Option<u32>,
50    pub principal_callee: Option<typed_ir::Type>,
51    pub substitutions: Vec<typed_ir::TypeSubstitution>,
52    pub substitution_candidates: Vec<typed_ir::PrincipalSubstitutionCandidate>,
53    pub principal_elaboration: Option<typed_ir::PrincipalElaborationPlan>,
54    pub status: ApplyShapeStatus,
55    pub missing_reasons: Vec<ApplyShapeMissingReason>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ExprPath(pub Vec<ExprPathSegment>);
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ExprPathSegment {
63    RootExpr(usize),
64    Binding(typed_ir::Path),
65    LambdaBody,
66    ApplyCallee,
67    ApplyArg,
68    IfCond,
69    IfThen,
70    IfElse,
71    TupleItem(usize),
72    RecordField(typed_ir::Name),
73    RecordSpread,
74    VariantPayload,
75    SelectBase,
76    MatchScrutinee,
77    MatchGuard(usize),
78    MatchArmBody(usize),
79    BlockStmt(usize),
80    BlockTail,
81    PatternDefault(typed_ir::Name),
82    ModuleBody,
83    HandleBody,
84    HandleGuard(usize),
85    HandleArmBody(usize),
86    CoerceInner,
87    BindHereInner,
88    PackInner,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum ApplyHeadKind {
93    Path(typed_ir::Path),
94    Primitive(typed_ir::PrimitiveOp),
95    Other,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum ExprShapeKind {
100    Var,
101    PrimitiveOp,
102    Lit,
103    Lambda,
104    Apply,
105    If,
106    Tuple,
107    Record,
108    Variant,
109    Select,
110    Match,
111    Block,
112    Handle,
113    Coerce,
114    BindHere,
115    Pack,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Default)]
119pub struct ValueShape {
120    pub intrinsic: Option<typed_ir::TypeBounds>,
121    pub contextual: Option<typed_ir::TypeBounds>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Default)]
125pub struct EffectShape {
126    pub intrinsic: Option<typed_ir::TypeBounds>,
127    pub contextual: Option<typed_ir::TypeBounds>,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum ShapeStatus {
132    Complete,
133    Partial,
134    Unknown,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ApplyShapeStatus {
139    Complete,
140    MissingEvidence,
141    MissingContext,
142    MissingPrincipal,
143    Partial,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
147pub enum ApplyShapeMissingReason {
148    NoApplyEvidence,
149    NoExpectedCallee,
150    NoExpectedArg,
151    RoleMethodWithoutPrincipal,
152    EmptyCalleeBounds,
153    EmptyArgBounds,
154    EmptyResultBounds,
155    NoPrincipalCallee,
156    NoSubstitutions,
157    NoSubstitutionCandidates,
158}
159
160pub(super) fn profile_core_program(program: &typed_ir::CoreProgram) -> CoreShapeProfile {
161    let table = collect_core_shape_table(program);
162    if std::env::var_os("YULANG_DEBUG_CORE_SHAPES").is_some() {
163        print_debug_core_shapes(&table);
164    }
165    table.profile()
166}
167
168pub(super) fn collect_core_shape_table(program: &typed_ir::CoreProgram) -> ShapeTable {
169    let mut collector = ShapeCollector::default();
170    for binding in &program.program.bindings {
171        collector.walk_expr(
172            &binding.body,
173            ExprPath(vec![ExprPathSegment::Binding(binding.name.clone())]),
174        );
175    }
176    for (index, expr) in program.program.root_exprs.iter().enumerate() {
177        collector.walk_expr(expr, ExprPath(vec![ExprPathSegment::RootExpr(index)]));
178    }
179    collector.table
180}
181
182impl ShapeTable {
183    pub fn profile(&self) -> CoreShapeProfile {
184        let mut profile = CoreShapeProfile {
185            exprs: self.exprs.len(),
186            applies: self.applies.len(),
187            ..CoreShapeProfile::default()
188        };
189        for apply in &self.applies {
190            match apply.status {
191                ApplyShapeStatus::Complete => profile.apply_complete += 1,
192                ApplyShapeStatus::MissingEvidence => profile.apply_missing_evidence += 1,
193                ApplyShapeStatus::MissingContext => profile.apply_missing_context += 1,
194                ApplyShapeStatus::MissingPrincipal => profile.apply_missing_principal += 1,
195                ApplyShapeStatus::Partial => profile.apply_partial += 1,
196            }
197            if apply.principal_callee.is_some() {
198                profile.apply_with_principal += 1;
199            }
200            if !apply.substitutions.is_empty() {
201                profile.apply_with_substitutions += 1;
202            }
203            if !apply.substitution_candidates.is_empty() {
204                profile.apply_with_substitution_candidates += 1;
205            }
206            if let Some(plan) = &apply.principal_elaboration {
207                profile.apply_with_principal_elaboration += 1;
208                if plan.complete {
209                    profile.apply_principal_elaboration_complete += 1;
210                } else {
211                    profile.apply_principal_elaboration_incomplete += 1;
212                }
213            }
214        }
215        profile
216    }
217}
218
219#[derive(Default)]
220struct ShapeCollector {
221    table: ShapeTable,
222}
223
224impl ShapeCollector {
225    fn walk_expr(&mut self, expr: &typed_ir::Expr, path: ExprPath) {
226        self.table.exprs.push(expr_shape(expr, path.clone()));
227        match expr {
228            typed_ir::Expr::Var(_) | typed_ir::Expr::PrimitiveOp(_) | typed_ir::Expr::Lit(_) => {}
229            typed_ir::Expr::Lambda { body, .. } => {
230                self.walk_expr(body, path.child(ExprPathSegment::LambdaBody));
231            }
232            typed_ir::Expr::Apply {
233                callee,
234                arg,
235                evidence,
236            } => {
237                self.table
238                    .applies
239                    .push(apply_shape(&path, callee, evidence.as_ref()));
240                self.walk_expr(callee, path.child(ExprPathSegment::ApplyCallee));
241                self.walk_expr(arg, path.child(ExprPathSegment::ApplyArg));
242            }
243            typed_ir::Expr::If {
244                cond,
245                then_branch,
246                else_branch,
247                ..
248            } => {
249                self.walk_expr(cond, path.child(ExprPathSegment::IfCond));
250                self.walk_expr(then_branch, path.child(ExprPathSegment::IfThen));
251                self.walk_expr(else_branch, path.child(ExprPathSegment::IfElse));
252            }
253            typed_ir::Expr::Tuple(items) => {
254                for (index, item) in items.iter().enumerate() {
255                    self.walk_expr(item, path.child(ExprPathSegment::TupleItem(index)));
256                }
257            }
258            typed_ir::Expr::Record { fields, spread } => {
259                for field in fields {
260                    self.walk_expr(
261                        &field.value,
262                        path.child(ExprPathSegment::RecordField(field.name.clone())),
263                    );
264                }
265                if let Some(spread) = spread {
266                    self.walk_record_spread(spread, path.child(ExprPathSegment::RecordSpread));
267                }
268            }
269            typed_ir::Expr::Variant { value, .. } => {
270                if let Some(value) = value {
271                    self.walk_expr(value, path.child(ExprPathSegment::VariantPayload));
272                }
273            }
274            typed_ir::Expr::Select { base, .. } => {
275                self.walk_expr(base, path.child(ExprPathSegment::SelectBase));
276            }
277            typed_ir::Expr::Match {
278                scrutinee, arms, ..
279            } => {
280                self.walk_expr(scrutinee, path.child(ExprPathSegment::MatchScrutinee));
281                for (index, arm) in arms.iter().enumerate() {
282                    self.walk_pattern_defaults(&arm.pattern, &path);
283                    if let Some(guard) = &arm.guard {
284                        self.walk_expr(guard, path.child(ExprPathSegment::MatchGuard(index)));
285                    }
286                    self.walk_expr(&arm.body, path.child(ExprPathSegment::MatchArmBody(index)));
287                }
288            }
289            typed_ir::Expr::Block { stmts, tail } => {
290                for (index, stmt) in stmts.iter().enumerate() {
291                    self.walk_stmt(stmt, path.child(ExprPathSegment::BlockStmt(index)));
292                }
293                if let Some(tail) = tail {
294                    self.walk_expr(tail, path.child(ExprPathSegment::BlockTail));
295                }
296            }
297            typed_ir::Expr::Handle { body, arms, .. } => {
298                self.walk_expr(body, path.child(ExprPathSegment::HandleBody));
299                for (index, arm) in arms.iter().enumerate() {
300                    self.walk_pattern_defaults(&arm.payload, &path);
301                    if let Some(guard) = &arm.guard {
302                        self.walk_expr(guard, path.child(ExprPathSegment::HandleGuard(index)));
303                    }
304                    self.walk_expr(&arm.body, path.child(ExprPathSegment::HandleArmBody(index)));
305                }
306            }
307            typed_ir::Expr::Coerce { expr, .. } => {
308                self.walk_expr(expr, path.child(ExprPathSegment::CoerceInner));
309            }
310            typed_ir::Expr::BindHere { expr } => {
311                self.walk_expr(expr, path.child(ExprPathSegment::BindHereInner));
312            }
313            typed_ir::Expr::Pack { expr, .. } => {
314                self.walk_expr(expr, path.child(ExprPathSegment::PackInner));
315            }
316        }
317    }
318
319    fn walk_stmt(&mut self, stmt: &typed_ir::Stmt, path: ExprPath) {
320        match stmt {
321            typed_ir::Stmt::Let { pattern, value } => {
322                self.walk_pattern_defaults(pattern, &path);
323                self.walk_expr(value, path);
324            }
325            typed_ir::Stmt::Expr(expr) => self.walk_expr(expr, path),
326            typed_ir::Stmt::Module { body, .. } => {
327                self.walk_expr(body, path.child(ExprPathSegment::ModuleBody));
328            }
329        }
330    }
331
332    fn walk_record_spread(&mut self, spread: &typed_ir::RecordSpreadExpr, path: ExprPath) {
333        match spread {
334            typed_ir::RecordSpreadExpr::Head(expr) | typed_ir::RecordSpreadExpr::Tail(expr) => {
335                self.walk_expr(expr, path);
336            }
337        }
338    }
339
340    fn walk_pattern_defaults(&mut self, pattern: &typed_ir::Pattern, path: &ExprPath) {
341        match pattern {
342            typed_ir::Pattern::Wildcard
343            | typed_ir::Pattern::Bind(_)
344            | typed_ir::Pattern::Lit(_) => {}
345            typed_ir::Pattern::Tuple(items) => {
346                for item in items {
347                    self.walk_pattern_defaults(item, path);
348                }
349            }
350            typed_ir::Pattern::List {
351                prefix,
352                spread,
353                suffix,
354            } => {
355                for item in prefix {
356                    self.walk_pattern_defaults(item, path);
357                }
358                if let Some(spread) = spread {
359                    self.walk_pattern_defaults(spread, path);
360                }
361                for item in suffix {
362                    self.walk_pattern_defaults(item, path);
363                }
364            }
365            typed_ir::Pattern::Record { fields, .. } => {
366                for field in fields {
367                    self.walk_pattern_defaults(&field.pattern, path);
368                    if let Some(default) = &field.default {
369                        self.walk_expr(
370                            default,
371                            path.child(ExprPathSegment::PatternDefault(field.name.clone())),
372                        );
373                    }
374                }
375            }
376            typed_ir::Pattern::Variant { value, .. } => {
377                if let Some(value) = value {
378                    self.walk_pattern_defaults(value, path);
379                }
380            }
381            typed_ir::Pattern::Or { left, right } => {
382                self.walk_pattern_defaults(left, path);
383                self.walk_pattern_defaults(right, path);
384            }
385            typed_ir::Pattern::As { pattern, .. } => {
386                self.walk_pattern_defaults(pattern, path);
387            }
388        }
389    }
390}
391
392impl ExprPath {
393    fn child(&self, segment: ExprPathSegment) -> Self {
394        let mut path = self.0.clone();
395        path.push(segment);
396        Self(path)
397    }
398}
399
400fn expr_shape(expr: &typed_ir::Expr, path: ExprPath) -> ExprShape {
401    let kind = expr_shape_kind(expr);
402    let value = value_shape(expr);
403    let status = if value.intrinsic.is_some() && value.contextual.is_some() {
404        ShapeStatus::Complete
405    } else if value.intrinsic.is_some() || value.contextual.is_some() {
406        ShapeStatus::Partial
407    } else {
408        ShapeStatus::Unknown
409    };
410    ExprShape {
411        path,
412        kind,
413        value,
414        effect: EffectShape::default(),
415        status,
416    }
417}
418
419fn value_shape(expr: &typed_ir::Expr) -> ValueShape {
420    match expr {
421        typed_ir::Expr::Apply { evidence, .. } => ValueShape {
422            intrinsic: evidence.as_ref().map(|evidence| evidence.result.clone()),
423            contextual: None,
424        },
425        typed_ir::Expr::If { evidence, .. }
426        | typed_ir::Expr::Match { evidence, .. }
427        | typed_ir::Expr::Handle { evidence, .. } => ValueShape {
428            intrinsic: evidence.as_ref().map(|evidence| evidence.result.clone()),
429            contextual: None,
430        },
431        typed_ir::Expr::Coerce { evidence, .. } => ValueShape {
432            intrinsic: evidence.as_ref().map(|evidence| evidence.actual.clone()),
433            contextual: evidence.as_ref().map(|evidence| evidence.expected.clone()),
434        },
435        _ => ValueShape::default(),
436    }
437}
438
439fn apply_shape(
440    path: &ExprPath,
441    callee: &typed_ir::Expr,
442    evidence: Option<&typed_ir::ApplyEvidence>,
443) -> ApplyShape {
444    let status = apply_shape_status(evidence);
445    let missing_reasons = apply_shape_missing_reasons(evidence);
446    ApplyShape {
447        path: path.clone(),
448        owner: path.owner(),
449        target: core_apply_head_target(callee),
450        callee_kind: apply_head_kind(callee),
451        callee_intrinsic: evidence.map(|evidence| evidence.callee.clone()),
452        callee_contextual: evidence.and_then(|evidence| evidence.expected_callee.clone()),
453        arg_intrinsic: evidence.map(|evidence| evidence.arg.clone()),
454        arg_contextual: evidence.and_then(|evidence| evidence.expected_arg.clone()),
455        result_intrinsic: evidence.map(|evidence| evidence.result.clone()),
456        callee_source_edge: evidence.and_then(|evidence| evidence.callee_source_edge),
457        arg_source_edge: evidence.and_then(|evidence| evidence.arg_source_edge),
458        principal_callee: evidence.and_then(|evidence| evidence.principal_callee.clone()),
459        substitutions: evidence
460            .map(|evidence| evidence.substitutions.clone())
461            .unwrap_or_default(),
462        substitution_candidates: evidence
463            .map(|evidence| evidence.substitution_candidates.clone())
464            .unwrap_or_default(),
465        principal_elaboration: evidence.and_then(|evidence| evidence.principal_elaboration.clone()),
466        status,
467        missing_reasons,
468    }
469}
470
471fn apply_shape_status(evidence: Option<&typed_ir::ApplyEvidence>) -> ApplyShapeStatus {
472    let Some(evidence) = evidence else {
473        return ApplyShapeStatus::MissingEvidence;
474    };
475    if evidence.expected_callee.is_none() || evidence.expected_arg.is_none() {
476        return ApplyShapeStatus::MissingContext;
477    }
478    if evidence.role_method && evidence.principal_callee.is_none() {
479        return ApplyShapeStatus::MissingPrincipal;
480    }
481    if !bounds_present(&evidence.callee)
482        || !bounds_present(&evidence.arg)
483        || !bounds_present(&evidence.result)
484    {
485        return ApplyShapeStatus::Partial;
486    }
487    ApplyShapeStatus::Complete
488}
489
490fn bounds_present(bounds: &typed_ir::TypeBounds) -> bool {
491    bounds.lower.is_some() || bounds.upper.is_some()
492}
493
494fn apply_shape_missing_reasons(
495    evidence: Option<&typed_ir::ApplyEvidence>,
496) -> Vec<ApplyShapeMissingReason> {
497    let Some(evidence) = evidence else {
498        return vec![ApplyShapeMissingReason::NoApplyEvidence];
499    };
500    let mut reasons = Vec::new();
501    if evidence.expected_callee.is_none() {
502        reasons.push(ApplyShapeMissingReason::NoExpectedCallee);
503    }
504    if evidence.expected_arg.is_none() {
505        reasons.push(ApplyShapeMissingReason::NoExpectedArg);
506    }
507    if evidence.role_method && evidence.principal_callee.is_none() {
508        reasons.push(ApplyShapeMissingReason::RoleMethodWithoutPrincipal);
509    }
510    if !bounds_present(&evidence.callee) {
511        reasons.push(ApplyShapeMissingReason::EmptyCalleeBounds);
512    }
513    if !bounds_present(&evidence.arg) {
514        reasons.push(ApplyShapeMissingReason::EmptyArgBounds);
515    }
516    if !bounds_present(&evidence.result) {
517        reasons.push(ApplyShapeMissingReason::EmptyResultBounds);
518    }
519    if evidence.role_method && evidence.principal_callee.is_none() {
520        reasons.push(ApplyShapeMissingReason::NoPrincipalCallee);
521    }
522    if evidence.principal_callee.is_some() && evidence.substitutions.is_empty() {
523        reasons.push(ApplyShapeMissingReason::NoSubstitutions);
524    }
525    if evidence.principal_callee.is_some()
526        && evidence.substitutions.is_empty()
527        && evidence.substitution_candidates.is_empty()
528    {
529        reasons.push(ApplyShapeMissingReason::NoSubstitutionCandidates);
530    }
531    reasons
532}
533
534fn expr_shape_kind(expr: &typed_ir::Expr) -> ExprShapeKind {
535    match expr {
536        typed_ir::Expr::Var(_) => ExprShapeKind::Var,
537        typed_ir::Expr::PrimitiveOp(_) => ExprShapeKind::PrimitiveOp,
538        typed_ir::Expr::Lit(_) => ExprShapeKind::Lit,
539        typed_ir::Expr::Lambda { .. } => ExprShapeKind::Lambda,
540        typed_ir::Expr::Apply { .. } => ExprShapeKind::Apply,
541        typed_ir::Expr::If { .. } => ExprShapeKind::If,
542        typed_ir::Expr::Tuple(_) => ExprShapeKind::Tuple,
543        typed_ir::Expr::Record { .. } => ExprShapeKind::Record,
544        typed_ir::Expr::Variant { .. } => ExprShapeKind::Variant,
545        typed_ir::Expr::Select { .. } => ExprShapeKind::Select,
546        typed_ir::Expr::Match { .. } => ExprShapeKind::Match,
547        typed_ir::Expr::Block { .. } => ExprShapeKind::Block,
548        typed_ir::Expr::Handle { .. } => ExprShapeKind::Handle,
549        typed_ir::Expr::Coerce { .. } => ExprShapeKind::Coerce,
550        typed_ir::Expr::BindHere { .. } => ExprShapeKind::BindHere,
551        typed_ir::Expr::Pack { .. } => ExprShapeKind::Pack,
552    }
553}
554
555fn print_debug_core_shapes(table: &ShapeTable) {
556    eprintln!(
557        "core-shapes: exprs={} applies={}",
558        table.exprs.len(),
559        table.applies.len()
560    );
561    print_debug_core_shape_missing_applies(table);
562    print_debug_core_shape_principal_plans(table);
563    if std::env::var_os("YULANG_TRACE_CORE_SHAPES").is_none() {
564        return;
565    }
566    for apply in &table.applies {
567        eprintln!(
568            "  apply {:?}: owner={} target={} head={:?} status={:?} reasons={:?} callee_edge={:?} arg_edge={:?} principal={} substitutions={} candidates={}",
569            apply.path,
570            apply
571                .owner
572                .as_ref()
573                .map(display_path)
574                .unwrap_or_else(|| "<root>".to_string()),
575            apply
576                .target
577                .as_ref()
578                .map(display_path)
579                .unwrap_or_else(|| "<unknown>".to_string()),
580            apply.callee_kind,
581            apply.status,
582            apply.missing_reasons,
583            apply.callee_source_edge,
584            apply.arg_source_edge,
585            apply.principal_callee.is_some(),
586            apply.substitutions.len(),
587            apply.substitution_candidates.len(),
588        );
589    }
590}
591
592fn print_debug_core_shape_principal_plans(table: &ShapeTable) {
593    let mut counts: BTreeMap<String, PrincipalPlanDebugCounts> = BTreeMap::new();
594    for apply in &table.applies {
595        let Some(plan) = &apply.principal_elaboration else {
596            continue;
597        };
598        let target = plan
599            .target
600            .as_ref()
601            .map(display_path)
602            .unwrap_or_else(|| apply_debug_target(apply));
603        let counts = counts.entry(target).or_default();
604        counts.total += 1;
605        if plan.complete {
606            counts.complete += 1;
607        } else {
608            counts.incomplete += 1;
609            for reason in &plan.incomplete_reasons {
610                *counts.reasons.entry(format!("{reason:?}")).or_default() += 1;
611            }
612        }
613    }
614    if counts.is_empty() {
615        return;
616    }
617    eprintln!("core-shape principal plans:");
618    for (target, counts) in counts {
619        eprintln!(
620            "  {target}: total={} complete={} incomplete={}",
621            counts.total, counts.complete, counts.incomplete
622        );
623        for (reason, count) in counts.reasons {
624            eprintln!("    {reason}: {count}");
625        }
626    }
627}
628
629#[derive(Default)]
630struct PrincipalPlanDebugCounts {
631    total: usize,
632    complete: usize,
633    incomplete: usize,
634    reasons: BTreeMap<String, usize>,
635}
636
637fn print_debug_core_shape_missing_applies(table: &ShapeTable) {
638    let mut counts: BTreeMap<String, BTreeMap<ApplyShapeMissingReason, usize>> = BTreeMap::new();
639    for apply in &table.applies {
640        if apply.missing_reasons.is_empty() {
641            continue;
642        }
643        let target = apply_debug_target(apply);
644        let target_counts = counts.entry(target).or_default();
645        for reason in &apply.missing_reasons {
646            *target_counts.entry(*reason).or_default() += 1;
647        }
648    }
649    if counts.is_empty() {
650        return;
651    }
652    eprintln!("core-shape missing applies:");
653    for (target, reasons) in counts {
654        eprintln!("  {target}:");
655        for (reason, count) in reasons {
656            eprintln!("    {reason:?}: {count}");
657        }
658    }
659}
660
661fn apply_debug_target(apply: &ApplyShape) -> String {
662    apply
663        .target
664        .as_ref()
665        .map(display_path)
666        .unwrap_or_else(|| match &apply.callee_kind {
667            ApplyHeadKind::Primitive(op) => format!("{op:?}"),
668            ApplyHeadKind::Path(path) => display_path(path),
669            ApplyHeadKind::Other => "<unknown>".to_string(),
670        })
671}
672
673fn apply_head_kind(expr: &typed_ir::Expr) -> ApplyHeadKind {
674    match expr {
675        typed_ir::Expr::Var(path) => ApplyHeadKind::Path(path.clone()),
676        typed_ir::Expr::PrimitiveOp(op) => ApplyHeadKind::Primitive(*op),
677        typed_ir::Expr::Apply { callee, .. } => apply_head_kind(callee),
678        _ => ApplyHeadKind::Other,
679    }
680}
681
682fn core_apply_head_target(expr: &typed_ir::Expr) -> Option<typed_ir::Path> {
683    match expr {
684        typed_ir::Expr::Var(path) => Some(path.clone()),
685        typed_ir::Expr::Apply { callee, .. } => core_apply_head_target(callee),
686        _ => None,
687    }
688}
689
690impl ExprPath {
691    fn owner(&self) -> Option<typed_ir::Path> {
692        self.0.iter().find_map(|segment| match segment {
693            ExprPathSegment::Binding(path) => Some(path.clone()),
694            _ => None,
695        })
696    }
697}
698
699fn display_path(path: &typed_ir::Path) -> String {
700    path.segments
701        .iter()
702        .map(|segment| segment.0.as_str())
703        .collect::<Vec<_>>()
704        .join("::")
705}
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710
711    #[test]
712    fn apply_with_evidence_fills_apply_shape_slots() {
713        let evidence = typed_ir::ApplyEvidence {
714            callee_source_edge: Some(1),
715            arg_source_edge: Some(2),
716            callee: bounds(named_type("fun")),
717            expected_callee: Some(bounds(named_type("expected_fun"))),
718            arg: bounds(named_type("bool")),
719            expected_arg: Some(bounds(named_type("int"))),
720            result: bounds(named_type("str")),
721            principal_callee: Some(named_type("principal")),
722            substitutions: vec![typed_ir::TypeSubstitution {
723                var: typed_ir::TypeVar("a".to_string()),
724                ty: named_type("int"),
725            }],
726            substitution_candidates: vec![typed_ir::PrincipalSubstitutionCandidate {
727                var: typed_ir::TypeVar("b".to_string()),
728                relation: typed_ir::PrincipalCandidateRelation::Exact,
729                ty: named_type("bool"),
730                source_edge: Some(2),
731                path: vec![typed_ir::PrincipalSlotPathSegment::Arg],
732            }],
733            role_method: false,
734            principal_elaboration: None,
735        };
736        let program = program_with_root(typed_ir::Expr::Apply {
737            callee: Box::new(typed_ir::Expr::Var(path("f"))),
738            arg: Box::new(typed_ir::Expr::Lit(typed_ir::Lit::Int("1".to_string()))),
739            evidence: Some(evidence),
740        });
741
742        let table = collect_core_shape_table(&program);
743
744        assert_eq!(table.applies.len(), 1);
745        let apply = &table.applies[0];
746        assert_eq!(apply.status, ApplyShapeStatus::Complete);
747        assert_eq!(apply.callee_source_edge, Some(1));
748        assert_eq!(apply.arg_source_edge, Some(2));
749        assert!(apply.callee_intrinsic.is_some());
750        assert!(apply.callee_contextual.is_some());
751        assert!(apply.arg_intrinsic.is_some());
752        assert!(apply.arg_contextual.is_some());
753        assert!(apply.result_intrinsic.is_some());
754        assert!(apply.principal_callee.is_some());
755        assert_eq!(apply.substitutions.len(), 1);
756        assert_eq!(apply.substitution_candidates.len(), 1);
757    }
758
759    #[test]
760    fn apply_without_evidence_is_missing_evidence() {
761        let program = program_with_root(typed_ir::Expr::Apply {
762            callee: Box::new(typed_ir::Expr::Var(path("f"))),
763            arg: Box::new(typed_ir::Expr::Lit(typed_ir::Lit::Unit)),
764            evidence: None,
765        });
766
767        let profile = collect_core_shape_table(&program).profile();
768
769        assert_eq!(profile.applies, 1);
770        assert_eq!(profile.apply_missing_evidence, 1);
771    }
772
773    #[test]
774    fn roots_and_bindings_produce_shape_entries_for_nested_exprs() {
775        let binding_path = path("id");
776        let program = typed_ir::CoreProgram {
777            program: typed_ir::PrincipalModule {
778                path: typed_ir::Path::default(),
779                bindings: vec![typed_ir::PrincipalBinding {
780                    name: binding_path.clone(),
781                    scheme: typed_ir::Scheme {
782                        requirements: Vec::new(),
783                        body: named_type("int"),
784                    },
785                    body: typed_ir::Expr::Lambda {
786                        param: typed_ir::Name("x".to_string()),
787                        param_effect_annotation: None,
788                        param_function_allowed_effects: None,
789                        body: Box::new(typed_ir::Expr::Var(binding_path)),
790                    },
791                }],
792                root_exprs: vec![typed_ir::Expr::Tuple(vec![
793                    typed_ir::Expr::Lit(typed_ir::Lit::Int("1".to_string())),
794                    typed_ir::Expr::Lit(typed_ir::Lit::Int("2".to_string())),
795                ])],
796                roots: vec![typed_ir::PrincipalRoot::Expr(0)],
797            },
798            graph: typed_ir::CoreGraphView::default(),
799            evidence: typed_ir::PrincipalEvidence::default(),
800        };
801
802        let table = collect_core_shape_table(&program);
803
804        assert_eq!(table.exprs.len(), 5);
805        assert!(
806            table
807                .exprs
808                .iter()
809                .any(|shape| matches!(shape.path.0.as_slice(), [ExprPathSegment::Binding(_)]))
810        );
811        assert!(table.exprs.iter().any(|shape| matches!(
812            shape.path.0.as_slice(),
813            [ExprPathSegment::RootExpr(0), ExprPathSegment::TupleItem(1)]
814        )));
815    }
816
817    fn program_with_root(expr: typed_ir::Expr) -> typed_ir::CoreProgram {
818        typed_ir::CoreProgram {
819            program: typed_ir::PrincipalModule {
820                path: typed_ir::Path::default(),
821                bindings: Vec::new(),
822                root_exprs: vec![expr],
823                roots: vec![typed_ir::PrincipalRoot::Expr(0)],
824            },
825            graph: typed_ir::CoreGraphView::default(),
826            evidence: typed_ir::PrincipalEvidence::default(),
827        }
828    }
829
830    fn bounds(ty: typed_ir::Type) -> typed_ir::TypeBounds {
831        typed_ir::TypeBounds::exact(ty)
832    }
833
834    fn named_type(name: &str) -> typed_ir::Type {
835        typed_ir::Type::Named {
836            path: path(name),
837            args: Vec::new(),
838        }
839    }
840
841    fn path(name: &str) -> typed_ir::Path {
842        typed_ir::Path::from_name(typed_ir::Name(name.to_string()))
843    }
844}