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}