shape_vm/compiler/compiler_impl_reference_model.rs
1use super::*;
2
3impl BytecodeCompiler {
4 pub(super) fn infer_reference_params_from_types(
5 program: &Program,
6 inferred_types: &HashMap<String, Type>,
7 ) -> HashMap<String, Vec<bool>> {
8 let funcs = Self::collect_program_functions(program);
9 let mut inferred = HashMap::new();
10
11 // v0.3 WS-7: the inferred pass-by-reference optimization is
12 // DISABLED. It is unsound on the JIT/MIR pipeline.
13 //
14 // Background. This pass used to flag every UNANNOTATED heap-typed
15 // parameter as an implicit `ByRefShared` reference parameter
16 // (`type_is_heap_like` → `inferred_flags[idx] = true`). The bytecode
17 // VM honors that consistently: the call site emits a borrow
18 // (`compile_implicit_reference_arg`, `helpers.rs`) and the callee
19 // reads the borrowed cell via `DerefLoad`. Both ends agree.
20 //
21 // The MIR/JIT pipeline does NOT. MIR-lowering only emits
22 // `Rvalue::Borrow` for an EXPLICIT `&expr` argument
23 // (`mir/lowering/expr.rs` `Expr::Reference` arm); an inferred-ref
24 // argument is a plain identifier, lowered as `Operand::Copy`. Yet
25 // MIR-lowering still marks the callee parameter as a reference
26 // (`param_reference_kinds[i] = Some(BorrowKind::Shared)`, driven by
27 // the `effective_def.params[i].is_reference = true` write-back in
28 // `compiler/functions.rs`). The JIT then auto-derefs that parameter
29 // slot (`ref_param_slots` in `shape-jit`, the W5c-2-α
30 // jit-ref-param-chain-stamp), treating the slot as a cell address.
31 // Caller passes the heap pointer BY VALUE; callee dereferences it
32 // as a cell. For an `Array<int>` parameter (`fn get(xs, i) {
33 // xs[i] }`) the JIT v2 typed-array fast path then reads
34 // `[arr_ptr + 8]` off a raw `TypedArrayHeader` mistaken for a cell
35 // — SIGSEGV even on a valid in-bounds access once `get` is
36 // tier-compiled.
37 //
38 // The optimization only ever applied to heap-shared types
39 // (`type_is_heap_like` gate). Those values are already `Arc`-backed;
40 // passing the `Arc` pointer by value shares the SAME heap object —
41 // `ByRefShared` adds a cell indirection that buys nothing and is the
42 // sole source of the VM/JIT divergence. An ANNOTATED `Array<int>`
43 // parameter is passed `ByValue` today and is sound in both tiers
44 // (verified) — that is the correct, uniform convention. Mutation
45 // through such a parameter (`fn f(xs) { xs.push(1) }`) is likewise
46 // visible to the caller under `ByValue` because the heap object is
47 // shared. Disabling the inference makes the VM and JIT use one
48 // convention (by-value `Arc`-pointer pass) and removes the
49 // indirection entirely — no cell, no auto-deref, no divergence.
50 //
51 // EXPLICIT reference parameters (`&x` / `&mut x` in source) are
52 // unaffected: they are `param.is_reference` from the parser, their
53 // call sites carry an explicit `&` that MIR-lowering DOES lower to
54 // `Rvalue::Borrow`, so caller and callee remain consistent.
55 for (name, func) in funcs {
56 // Every parameter flagged `false` — no inferred reference
57 // parameters. `inferred_types` is intentionally unused now;
58 // it remains a parameter for call-site signature stability.
59 let _ = inferred_types;
60 inferred.insert(name, vec![false; func.params.len()]);
61 }
62
63 inferred
64 }
65
66 pub(super) fn analyze_statement_for_ref_mutation(
67 stmt: &shape_ast::ast::Statement,
68 caller_name: &str,
69 param_index_by_name: &HashMap<String, usize>,
70 caller_ref_params: &[bool],
71 callee_ref_params: &HashMap<String, Vec<bool>>,
72 direct_mutates: &mut [bool],
73 edges: &mut Vec<(String, usize, String, usize)>,
74 ) {
75 use shape_ast::ast::{ForInit, Statement};
76
77 match stmt {
78 Statement::Return(Some(expr), _) | Statement::Expression(expr, _) => {
79 Self::analyze_expr_for_ref_mutation(
80 expr,
81 caller_name,
82 param_index_by_name,
83 caller_ref_params,
84 callee_ref_params,
85 direct_mutates,
86 edges,
87 );
88 }
89 Statement::VariableDecl(decl, _) => {
90 if let Some(value) = &decl.value {
91 Self::analyze_expr_for_ref_mutation(
92 value,
93 caller_name,
94 param_index_by_name,
95 caller_ref_params,
96 callee_ref_params,
97 direct_mutates,
98 edges,
99 );
100 }
101 }
102 Statement::Assignment(assign, _) => {
103 if let Some(name) = assign.pattern.as_identifier()
104 && let Some(&idx) = param_index_by_name.get(name)
105 && caller_ref_params.get(idx).copied().unwrap_or(false)
106 {
107 direct_mutates[idx] = true;
108 }
109 Self::analyze_expr_for_ref_mutation(
110 &assign.value,
111 caller_name,
112 param_index_by_name,
113 caller_ref_params,
114 callee_ref_params,
115 direct_mutates,
116 edges,
117 );
118 }
119 Statement::If(if_stmt, _) => {
120 Self::analyze_expr_for_ref_mutation(
121 &if_stmt.condition,
122 caller_name,
123 param_index_by_name,
124 caller_ref_params,
125 callee_ref_params,
126 direct_mutates,
127 edges,
128 );
129 for stmt in &if_stmt.then_body {
130 Self::analyze_statement_for_ref_mutation(
131 stmt,
132 caller_name,
133 param_index_by_name,
134 caller_ref_params,
135 callee_ref_params,
136 direct_mutates,
137 edges,
138 );
139 }
140 if let Some(else_body) = &if_stmt.else_body {
141 for stmt in else_body {
142 Self::analyze_statement_for_ref_mutation(
143 stmt,
144 caller_name,
145 param_index_by_name,
146 caller_ref_params,
147 callee_ref_params,
148 direct_mutates,
149 edges,
150 );
151 }
152 }
153 }
154 Statement::While(while_loop, _) => {
155 Self::analyze_expr_for_ref_mutation(
156 &while_loop.condition,
157 caller_name,
158 param_index_by_name,
159 caller_ref_params,
160 callee_ref_params,
161 direct_mutates,
162 edges,
163 );
164 for stmt in &while_loop.body {
165 Self::analyze_statement_for_ref_mutation(
166 stmt,
167 caller_name,
168 param_index_by_name,
169 caller_ref_params,
170 callee_ref_params,
171 direct_mutates,
172 edges,
173 );
174 }
175 }
176 Statement::For(for_loop, _) => {
177 match &for_loop.init {
178 ForInit::ForIn { iter, .. } => {
179 Self::analyze_expr_for_ref_mutation(
180 iter,
181 caller_name,
182 param_index_by_name,
183 caller_ref_params,
184 callee_ref_params,
185 direct_mutates,
186 edges,
187 );
188 }
189 ForInit::ForC {
190 init,
191 condition,
192 update,
193 } => {
194 Self::analyze_statement_for_ref_mutation(
195 init,
196 caller_name,
197 param_index_by_name,
198 caller_ref_params,
199 callee_ref_params,
200 direct_mutates,
201 edges,
202 );
203 Self::analyze_expr_for_ref_mutation(
204 condition,
205 caller_name,
206 param_index_by_name,
207 caller_ref_params,
208 callee_ref_params,
209 direct_mutates,
210 edges,
211 );
212 Self::analyze_expr_for_ref_mutation(
213 update,
214 caller_name,
215 param_index_by_name,
216 caller_ref_params,
217 callee_ref_params,
218 direct_mutates,
219 edges,
220 );
221 }
222 }
223 for stmt in &for_loop.body {
224 Self::analyze_statement_for_ref_mutation(
225 stmt,
226 caller_name,
227 param_index_by_name,
228 caller_ref_params,
229 callee_ref_params,
230 direct_mutates,
231 edges,
232 );
233 }
234 }
235 Statement::Extend(ext, _) => {
236 for method in &ext.methods {
237 for stmt in &method.body {
238 Self::analyze_statement_for_ref_mutation(
239 stmt,
240 caller_name,
241 param_index_by_name,
242 caller_ref_params,
243 callee_ref_params,
244 direct_mutates,
245 edges,
246 );
247 }
248 }
249 }
250 Statement::SetReturnExpr { expression, .. } => {
251 Self::analyze_expr_for_ref_mutation(
252 expression,
253 caller_name,
254 param_index_by_name,
255 caller_ref_params,
256 callee_ref_params,
257 direct_mutates,
258 edges,
259 );
260 }
261 Statement::ReplaceBodyExpr { expression, .. } => {
262 Self::analyze_expr_for_ref_mutation(
263 expression,
264 caller_name,
265 param_index_by_name,
266 caller_ref_params,
267 callee_ref_params,
268 direct_mutates,
269 edges,
270 );
271 }
272 Statement::ReplaceModuleExpr { expression, .. } => {
273 Self::analyze_expr_for_ref_mutation(
274 expression,
275 caller_name,
276 param_index_by_name,
277 caller_ref_params,
278 callee_ref_params,
279 direct_mutates,
280 edges,
281 );
282 }
283 Statement::ReplaceBody { body, .. } => {
284 for stmt in body {
285 Self::analyze_statement_for_ref_mutation(
286 stmt,
287 caller_name,
288 param_index_by_name,
289 caller_ref_params,
290 callee_ref_params,
291 direct_mutates,
292 edges,
293 );
294 }
295 }
296 Statement::SetParamValue { expression, .. } => {
297 Self::analyze_expr_for_ref_mutation(
298 expression,
299 caller_name,
300 param_index_by_name,
301 caller_ref_params,
302 callee_ref_params,
303 direct_mutates,
304 edges,
305 );
306 }
307 Statement::Break(_)
308 | Statement::Continue(_)
309 | Statement::Return(None, _)
310 | Statement::RemoveTarget(_)
311 | Statement::SetParamType { .. }
312 | Statement::SetReturnType { .. } => {}
313 }
314 }
315
316 pub(super) fn ref_param_index_from_arg(
317 arg: &shape_ast::ast::Expr,
318 param_index_by_name: &HashMap<String, usize>,
319 caller_ref_params: &[bool],
320 ) -> Option<usize> {
321 match arg {
322 shape_ast::ast::Expr::Reference { expr: inner, .. } => match inner.as_ref() {
323 shape_ast::ast::Expr::Identifier(name, _) => param_index_by_name
324 .get(name)
325 .copied()
326 .filter(|idx| caller_ref_params.get(*idx).copied().unwrap_or(false)),
327 _ => None,
328 },
329 shape_ast::ast::Expr::Identifier(name, _) => param_index_by_name
330 .get(name)
331 .copied()
332 .filter(|idx| caller_ref_params.get(*idx).copied().unwrap_or(false)),
333 _ => None,
334 }
335 }
336}
337
338impl BytecodeCompiler {
339 pub(super) fn analyze_expr_for_ref_mutation(
340 expr: &shape_ast::ast::Expr,
341 caller_name: &str,
342 param_index_by_name: &HashMap<String, usize>,
343 caller_ref_params: &[bool],
344 callee_ref_params: &HashMap<String, Vec<bool>>,
345 direct_mutates: &mut [bool],
346 edges: &mut Vec<(String, usize, String, usize)>,
347 ) {
348 use shape_ast::ast::Expr;
349 macro_rules! visit_expr {
350 ($e:expr) => {
351 Self::analyze_expr_for_ref_mutation(
352 $e,
353 caller_name,
354 param_index_by_name,
355 caller_ref_params,
356 callee_ref_params,
357 direct_mutates,
358 edges,
359 )
360 };
361 }
362 macro_rules! visit_stmt {
363 ($s:expr) => {
364 Self::analyze_statement_for_ref_mutation(
365 $s,
366 caller_name,
367 param_index_by_name,
368 caller_ref_params,
369 callee_ref_params,
370 direct_mutates,
371 edges,
372 )
373 };
374 }
375
376 match expr {
377 Expr::Assign(assign, _) => {
378 match assign.target.as_ref() {
379 Expr::Identifier(name, _) => {
380 if let Some(&idx) = param_index_by_name.get(name)
381 && caller_ref_params.get(idx).copied().unwrap_or(false)
382 {
383 direct_mutates[idx] = true;
384 }
385 }
386 Expr::IndexAccess { object, .. } | Expr::PropertyAccess { object, .. } => {
387 if let Expr::Identifier(name, _) = object.as_ref()
388 && let Some(&idx) = param_index_by_name.get(name)
389 && caller_ref_params.get(idx).copied().unwrap_or(false)
390 {
391 direct_mutates[idx] = true;
392 }
393 }
394 _ => {}
395 }
396 visit_expr!(&assign.value);
397 }
398 Expr::FunctionCall {
399 name,
400 args,
401 named_args,
402 ..
403 } => {
404 if let Some(callee_params) = callee_ref_params.get(name) {
405 for (arg_idx, arg) in args.iter().enumerate() {
406 if !callee_params.get(arg_idx).copied().unwrap_or(false) {
407 continue;
408 }
409 if let Some(caller_param_idx) = Self::ref_param_index_from_arg(
410 arg,
411 param_index_by_name,
412 caller_ref_params,
413 ) {
414 edges.push((
415 caller_name.to_string(),
416 caller_param_idx,
417 name.clone(),
418 arg_idx,
419 ));
420 }
421 }
422 }
423 // For callees not in the known function set (builtins, intrinsics,
424 // imported functions), assume they do NOT mutate reference parameters.
425 // Being too conservative here causes false B0004 errors when passing
426 // non-identifier expressions (like object literals) to functions whose
427 // parameters are inferred as references.
428
429 for arg in args {
430 visit_expr!(arg);
431 }
432
433 for (_, arg) in named_args {
434 if let Some(idx) =
435 Self::ref_param_index_from_arg(arg, param_index_by_name, caller_ref_params)
436 {
437 direct_mutates[idx] = true;
438 }
439 visit_expr!(arg);
440 }
441 }
442 Expr::QualifiedFunctionCall {
443 namespace,
444 function,
445 args,
446 named_args,
447 ..
448 } => {
449 let scoped_name = format!("{}::{}", namespace, function);
450 if let Some(callee_params) = callee_ref_params.get(&scoped_name) {
451 for (arg_idx, arg) in args.iter().enumerate() {
452 if !callee_params.get(arg_idx).copied().unwrap_or(false) {
453 continue;
454 }
455 if let Some(caller_param_idx) = Self::ref_param_index_from_arg(
456 arg,
457 param_index_by_name,
458 caller_ref_params,
459 ) {
460 edges.push((
461 caller_name.to_string(),
462 caller_param_idx,
463 scoped_name.clone(),
464 arg_idx,
465 ));
466 }
467 }
468 }
469
470 for arg in args {
471 visit_expr!(arg);
472 }
473
474 for (_, arg) in named_args {
475 if let Some(idx) =
476 Self::ref_param_index_from_arg(arg, param_index_by_name, caller_ref_params)
477 {
478 direct_mutates[idx] = true;
479 }
480 visit_expr!(arg);
481 }
482 }
483 Expr::MethodCall {
484 receiver,
485 args,
486 named_args,
487 ..
488 } => {
489 visit_expr!(receiver);
490 for arg in args {
491 visit_expr!(arg);
492 }
493 for (_, arg) in named_args {
494 visit_expr!(arg);
495 }
496 }
497 Expr::UnaryOp { operand, .. }
498 | Expr::Spread(operand, _)
499 | Expr::TryOperator(operand, _)
500 | Expr::Await(operand, _)
501 | Expr::TimeframeContext { expr: operand, .. }
502 | Expr::UsingImpl { expr: operand, .. }
503 | Expr::Reference { expr: operand, .. } => {
504 visit_expr!(operand);
505 }
506 Expr::BinaryOp { left, right, .. } | Expr::FuzzyComparison { left, right, .. } => {
507 visit_expr!(left);
508 visit_expr!(right);
509 }
510 Expr::PropertyAccess { object, .. } => {
511 visit_expr!(object);
512 }
513 Expr::IndexAccess {
514 object,
515 index,
516 end_index,
517 ..
518 } => {
519 visit_expr!(object);
520 visit_expr!(index);
521 if let Some(end) = end_index {
522 visit_expr!(end);
523 }
524 }
525 Expr::Conditional {
526 condition,
527 then_expr,
528 else_expr,
529 ..
530 } => {
531 visit_expr!(condition);
532 visit_expr!(then_expr);
533 if let Some(else_expr) = else_expr {
534 visit_expr!(else_expr);
535 }
536 }
537 Expr::Array(items, _) => {
538 for item in items {
539 visit_expr!(item);
540 }
541 }
542 Expr::TableRows(rows, _) => {
543 for row in rows {
544 for elem in row {
545 visit_expr!(elem);
546 }
547 }
548 }
549 Expr::Object(entries, _) => {
550 for entry in entries {
551 match entry {
552 shape_ast::ast::ObjectEntry::Field { value, .. } => {
553 visit_expr!(value);
554 }
555 shape_ast::ast::ObjectEntry::Spread(spread) => {
556 visit_expr!(spread);
557 }
558 }
559 }
560 }
561 Expr::ListComprehension(comp, _) => {
562 visit_expr!(&comp.element);
563 for clause in &comp.clauses {
564 visit_expr!(&clause.iterable);
565 if let Some(filter) = &clause.filter {
566 visit_expr!(filter);
567 }
568 }
569 }
570 Expr::Block(block, _) => {
571 for item in &block.items {
572 match item {
573 shape_ast::ast::BlockItem::VariableDecl(decl) => {
574 if let Some(value) = &decl.value {
575 visit_expr!(value);
576 }
577 }
578 shape_ast::ast::BlockItem::Assignment(assign) => {
579 if let Some(name) = assign.pattern.as_identifier()
580 && let Some(&idx) = param_index_by_name.get(name)
581 && caller_ref_params.get(idx).copied().unwrap_or(false)
582 {
583 direct_mutates[idx] = true;
584 }
585 visit_expr!(&assign.value);
586 }
587 shape_ast::ast::BlockItem::Statement(stmt) => {
588 visit_stmt!(stmt);
589 }
590 shape_ast::ast::BlockItem::Expression(expr) => {
591 visit_expr!(expr);
592 }
593 }
594 }
595 }
596 Expr::FunctionExpr { body, .. } => {
597 for stmt in body {
598 visit_stmt!(stmt);
599 }
600 }
601 Expr::If(if_expr, _) => {
602 visit_expr!(&if_expr.condition);
603 visit_expr!(&if_expr.then_branch);
604 if let Some(else_branch) = &if_expr.else_branch {
605 visit_expr!(else_branch);
606 }
607 }
608 Expr::While(while_expr, _) => {
609 visit_expr!(&while_expr.condition);
610 visit_expr!(&while_expr.body);
611 }
612 Expr::For(for_expr, _) => {
613 visit_expr!(&for_expr.iterable);
614 visit_expr!(&for_expr.body);
615 }
616 Expr::Loop(loop_expr, _) => {
617 visit_expr!(&loop_expr.body);
618 }
619 Expr::Let(let_expr, _) => {
620 if let Some(value) = &let_expr.value {
621 visit_expr!(value);
622 }
623 visit_expr!(&let_expr.body);
624 }
625 Expr::Match(match_expr, _) => {
626 visit_expr!(&match_expr.scrutinee);
627 for arm in &match_expr.arms {
628 if let Some(guard) = &arm.guard {
629 visit_expr!(guard);
630 }
631 visit_expr!(&arm.body);
632 }
633 }
634 Expr::Join(join_expr, _) => {
635 for branch in &join_expr.branches {
636 visit_expr!(&branch.expr);
637 }
638 }
639 Expr::Annotated { target, .. } => {
640 visit_expr!(target);
641 }
642 Expr::AsyncLet(async_let, _) => {
643 visit_expr!(&async_let.expr);
644 }
645 Expr::AsyncScope(inner, _) => {
646 visit_expr!(inner);
647 }
648 Expr::Comptime(stmts, _) => {
649 for stmt in stmts {
650 visit_stmt!(stmt);
651 }
652 }
653 Expr::ComptimeFor(cf, _) => {
654 visit_expr!(&cf.iterable);
655 for stmt in &cf.body {
656 visit_stmt!(stmt);
657 }
658 }
659 Expr::SimulationCall { params, .. } => {
660 for (_, value) in params {
661 visit_expr!(value);
662 }
663 }
664 Expr::WindowExpr(window_expr, _) => {
665 match &window_expr.function {
666 shape_ast::ast::WindowFunction::Lag { expr, default, .. }
667 | shape_ast::ast::WindowFunction::Lead { expr, default, .. } => {
668 visit_expr!(expr);
669 if let Some(default) = default {
670 visit_expr!(default);
671 }
672 }
673 shape_ast::ast::WindowFunction::FirstValue(expr)
674 | shape_ast::ast::WindowFunction::LastValue(expr)
675 | shape_ast::ast::WindowFunction::NthValue(expr, _)
676 | shape_ast::ast::WindowFunction::Sum(expr)
677 | shape_ast::ast::WindowFunction::Avg(expr)
678 | shape_ast::ast::WindowFunction::Min(expr)
679 | shape_ast::ast::WindowFunction::Max(expr) => {
680 visit_expr!(expr);
681 }
682 shape_ast::ast::WindowFunction::Count(expr) => {
683 if let Some(expr) = expr {
684 visit_expr!(expr);
685 }
686 }
687 shape_ast::ast::WindowFunction::RowNumber
688 | shape_ast::ast::WindowFunction::Rank
689 | shape_ast::ast::WindowFunction::DenseRank
690 | shape_ast::ast::WindowFunction::Ntile(_) => {}
691 }
692
693 for partition_expr in &window_expr.over.partition_by {
694 visit_expr!(partition_expr);
695 }
696 if let Some(order_by) = &window_expr.over.order_by {
697 for (order_expr, _) in &order_by.columns {
698 visit_expr!(order_expr);
699 }
700 }
701 }
702 Expr::FromQuery(fq, _) => {
703 visit_expr!(&fq.source);
704 for clause in &fq.clauses {
705 match clause {
706 shape_ast::ast::QueryClause::Where(expr) => {
707 visit_expr!(expr);
708 }
709 shape_ast::ast::QueryClause::OrderBy(items) => {
710 for item in items {
711 visit_expr!(&item.key);
712 }
713 }
714 shape_ast::ast::QueryClause::GroupBy { element, key, .. } => {
715 visit_expr!(element);
716 visit_expr!(key);
717 }
718 shape_ast::ast::QueryClause::Let { value, .. } => {
719 visit_expr!(value);
720 }
721 shape_ast::ast::QueryClause::Join {
722 source,
723 left_key,
724 right_key,
725 ..
726 } => {
727 visit_expr!(source);
728 visit_expr!(left_key);
729 visit_expr!(right_key);
730 }
731 }
732 }
733 visit_expr!(&fq.select);
734 }
735 Expr::StructLiteral { fields, .. } => {
736 for (_, value) in fields {
737 visit_expr!(value);
738 }
739 }
740 Expr::EnumConstructor { payload, .. } => match payload {
741 shape_ast::ast::EnumConstructorPayload::Unit => {}
742 shape_ast::ast::EnumConstructorPayload::Tuple(values) => {
743 for value in values {
744 visit_expr!(value);
745 }
746 }
747 shape_ast::ast::EnumConstructorPayload::Struct(fields) => {
748 for (_, value) in fields {
749 visit_expr!(value);
750 }
751 }
752 },
753 Expr::TypeAssertion {
754 expr,
755 meta_param_overrides,
756 ..
757 } => {
758 visit_expr!(expr);
759 if let Some(overrides) = meta_param_overrides {
760 for value in overrides.values() {
761 visit_expr!(value);
762 }
763 }
764 }
765 Expr::InstanceOf { expr, .. } => {
766 visit_expr!(expr);
767 }
768 Expr::Range { start, end, .. } => {
769 if let Some(start) = start {
770 visit_expr!(start);
771 }
772 if let Some(end) = end {
773 visit_expr!(end);
774 }
775 }
776 Expr::DataRelativeAccess { reference, .. } => {
777 visit_expr!(reference);
778 }
779 Expr::Break(Some(expr), _) | Expr::Return(Some(expr), _) => {
780 visit_expr!(expr);
781 }
782 Expr::Literal(..)
783 | Expr::Identifier(..)
784 | Expr::DataRef(..)
785 | Expr::DataDateTimeRef(..)
786 | Expr::TimeRef(..)
787 | Expr::DateTime(..)
788 | Expr::PatternRef(..)
789 | Expr::Unit(..)
790 | Expr::Duration(..)
791 | Expr::Continue(..)
792 | Expr::Break(None, _)
793 | Expr::Return(None, _) => {}
794 }
795 }
796}
797
798impl BytecodeCompiler {
799 pub(super) fn infer_reference_model(
800 program: &Program,
801 ) -> (
802 HashMap<String, Vec<bool>>,
803 HashMap<String, Vec<bool>>,
804 HashMap<String, Vec<Option<String>>>,
805 HashMap<String, String>,
806 HashMap<String, Vec<Option<shape_value::v2::ConcreteType>>>,
807 HashMap<String, Vec<Option<Vec<(String, shape_runtime::type_schema::FieldType)>>>>,
808 HashMap<String, Vec<(String, shape_runtime::type_schema::FieldType)>>,
809 ) {
810 let funcs = Self::collect_program_functions(program);
811 let mut inference = shape_runtime::type_system::inference::TypeInferenceEngine::new();
812 let (types, _) = inference.infer_program_best_effort(program);
813 let inferred_ref_params = Self::infer_reference_params_from_types(program, &types);
814 let inferred_param_type_hints = Self::infer_param_type_hints_from_types(program, &types);
815 let inferred_return_type_hints = Self::infer_return_type_hints_from_types(program, &types);
816 // v0.3 WS-7: project the inference engine's per-parameter `Type`
817 // for UNANNOTATED params into a `ConcreteType`. This is the JIT's
818 // proof source for the v2 typed-array fast path on unannotated
819 // array params.
820 let inferred_param_concrete_types =
821 Self::infer_param_concrete_types_from_types(program, &types);
822 // WS-9b: project anonymous-object param types into per-field
823 // `FieldType` lists so `compile_function_body` can register an
824 // inline schema and resolve `param.field` for unannotated
825 // object-literal-shaped parameters.
826 let inferred_param_object_fields =
827 Self::infer_param_object_fields_from_types(program, &types);
828 // WS-9c: project anonymous-object inferred RETURN types so
829 // `compile_expr_function_call` can register an inline schema and
830 // resolve `f(...).field` for unannotated object-literal factories.
831 let inferred_return_object_fields =
832 Self::infer_return_object_fields_from_types(program, &types);
833
834 let mut effective_ref_params: HashMap<String, Vec<bool>> = HashMap::new();
835 for (name, func) in &funcs {
836 let inferred = inferred_ref_params.get(name).cloned().unwrap_or_default();
837 let mut refs = vec![false; func.params.len()];
838 for (idx, param) in func.params.iter().enumerate() {
839 refs[idx] = param.is_reference || inferred.get(idx).copied().unwrap_or(false);
840 }
841 effective_ref_params.insert(name.clone(), refs);
842 }
843
844 let mut direct_mutates: HashMap<String, Vec<bool>> = HashMap::new();
845 let mut edges: Vec<(String, usize, String, usize)> = Vec::new();
846
847 for (name, func) in &funcs {
848 let caller_refs = effective_ref_params
849 .get(name)
850 .cloned()
851 .unwrap_or_else(|| vec![false; func.params.len()]);
852 let mut direct = vec![false; func.params.len()];
853 let mut param_index_by_name: HashMap<String, usize> = HashMap::new();
854 for (idx, param) in func.params.iter().enumerate() {
855 for param_name in param.get_identifiers() {
856 param_index_by_name.insert(param_name, idx);
857 }
858 }
859 for stmt in &func.body {
860 Self::analyze_statement_for_ref_mutation(
861 stmt,
862 name,
863 ¶m_index_by_name,
864 &caller_refs,
865 &effective_ref_params,
866 &mut direct,
867 &mut edges,
868 );
869 }
870 direct_mutates.insert(name.clone(), direct);
871 }
872
873 let mut result = direct_mutates;
874 let mut changed = true;
875 while changed {
876 changed = false;
877 for (caller, caller_idx, callee, callee_idx) in &edges {
878 let callee_mutates = result
879 .get(callee)
880 .and_then(|flags| flags.get(*callee_idx))
881 .copied()
882 .unwrap_or(false);
883 if !callee_mutates {
884 continue;
885 }
886 if let Some(caller_flags) = result.get_mut(caller)
887 && let Some(flag) = caller_flags.get_mut(*caller_idx)
888 && !*flag
889 {
890 *flag = true;
891 changed = true;
892 }
893 }
894 }
895
896 (
897 inferred_ref_params,
898 result,
899 inferred_param_type_hints,
900 inferred_return_type_hints,
901 inferred_param_concrete_types,
902 inferred_param_object_fields,
903 inferred_return_object_fields,
904 )
905 }
906
907 /// WS-9b: project the program-wide type-inference engine's per-parameter
908 /// `Type` into a `Vec<(field_name, FieldType)>` for UNANNOTATED params
909 /// whose resolved type is an anonymous structural object.
910 ///
911 /// Mirrors `infer_param_concrete_types_from_types` — same
912 /// `Type::Function`-keyed lookup, same annotated-param / non-simple-name
913 /// skip. Only `Type::Concrete(TypeAnnotation::Object(_))` params produce
914 /// `Some`; named structs (which resolve through the schema registry via
915 /// their hint name) and every non-object param keep `None`. A field
916 /// whose annotation projects to `FieldType::Any` is still recorded —
917 /// `Any` is the honest "field exists, kind not narrowed" marker, not a
918 /// fabricated primitive.
919 pub(super) fn infer_param_object_fields_from_types(
920 program: &Program,
921 inferred_types: &HashMap<String, Type>,
922 ) -> HashMap<String, Vec<Option<Vec<(String, shape_runtime::type_schema::FieldType)>>>> {
923 use shape_ast::ast::TypeAnnotation;
924 let funcs = Self::collect_program_functions(program);
925 let mut out = HashMap::new();
926
927 for (name, func) in funcs {
928 let mut param_fields: Vec<
929 Option<Vec<(String, shape_runtime::type_schema::FieldType)>>,
930 > = vec![None; func.params.len()];
931 let Some(Type::Function { params, .. }) = inferred_types.get(&name) else {
932 out.insert(name, param_fields);
933 continue;
934 };
935
936 for (idx, param) in func.params.iter().enumerate() {
937 if param.type_annotation.is_some() || param.simple_name().is_none() {
938 continue;
939 }
940 let Some(inferred_param_ty) = params.get(idx) else {
941 continue;
942 };
943 if let Type::Concrete(TypeAnnotation::Object(obj_fields)) = inferred_param_ty {
944 let fields: Vec<(String, shape_runtime::type_schema::FieldType)> = obj_fields
945 .iter()
946 .map(|f| {
947 (
948 f.name.clone(),
949 Self::type_annotation_to_field_type(&f.type_annotation),
950 )
951 })
952 .collect();
953 if !fields.is_empty() {
954 param_fields[idx] = Some(fields);
955 }
956 }
957 }
958
959 out.insert(name, param_fields);
960 }
961
962 out
963 }
964
965 /// WS-9c: project each function's inferred RETURN type into a
966 /// `Vec<(field_name, FieldType)>` when that return type is an anonymous
967 /// structural object.
968 ///
969 /// Mirrors `infer_param_object_fields_from_types` for the return
970 /// position. The motivating shape is an anonymous-object factory:
971 /// `fn aabb(lo, hi) { {min: lo, max: hi} }`. The program-wide inference
972 /// pass resolves the return type to `Object({min: int, max: int})` once
973 /// callsite propagation binds the parameters; this projection hands the
974 /// bytecode compiler the per-field types so it can register an anonymous
975 /// schema for the return value and resolve `aabb(...).field` /
976 /// `let a = aabb(...); a.field` — exactly the resolution a named struct
977 /// return type already gets. A return type that is not an anonymous
978 /// object (a primitive, a named struct, an array, or still-unresolved)
979 /// keeps `None`.
980 pub(super) fn infer_return_object_fields_from_types(
981 program: &Program,
982 inferred_types: &HashMap<String, Type>,
983 ) -> HashMap<String, Vec<(String, shape_runtime::type_schema::FieldType)>> {
984 use shape_ast::ast::TypeAnnotation;
985 let funcs = Self::collect_program_functions(program);
986 let mut out = HashMap::new();
987
988 for (name, func) in funcs {
989 // A function with an explicit return-type annotation already
990 // resolves through the annotation path in
991 // `compile_expr_function_call`; only unannotated functions need
992 // the inferred-return projection.
993 if func.return_type.is_some() {
994 continue;
995 }
996 let Some(Type::Function { returns, .. }) = inferred_types.get(&name) else {
997 continue;
998 };
999 if let Type::Concrete(TypeAnnotation::Object(obj_fields)) = returns.as_ref() {
1000 let fields: Vec<(String, shape_runtime::type_schema::FieldType)> = obj_fields
1001 .iter()
1002 .map(|f| {
1003 (
1004 f.name.clone(),
1005 Self::type_annotation_to_field_type(&f.type_annotation),
1006 )
1007 })
1008 .collect();
1009 if !fields.is_empty() {
1010 out.insert(name, fields);
1011 }
1012 }
1013 }
1014 out
1015 }
1016
1017 /// WS-9c: register an inline anonymous schema for every unannotated
1018 /// function whose inferred return type is an anonymous object, recording
1019 /// the schema id under the function name in `function_return_schema_ids`
1020 /// and the precise per-field types as schema field contracts.
1021 ///
1022 /// The schema is Any-uniform (mirroring
1023 /// `extract_object_schema_id_from_annotation` so the layout matches the
1024 /// pre-existing inline-object shape); the precise field types live in the
1025 /// parallel field-contract side table consulted by `infer_expr_type`.
1026 fn register_inferred_return_object_schemas(&mut self) {
1027 use shape_runtime::type_schema::FieldType;
1028 let return_fields = self.inferred_return_object_fields.clone();
1029 for (fn_name, fields) in return_fields {
1030 if fields.is_empty() {
1031 continue;
1032 }
1033 let typed_fields: Vec<(&str, FieldType)> = fields
1034 .iter()
1035 .map(|(name, _)| (name.as_str(), FieldType::Any))
1036 .collect();
1037 let schema_id = self
1038 .type_tracker
1039 .register_inline_object_schema_typed(&typed_fields);
1040 let mut contracts = std::collections::HashMap::with_capacity(fields.len());
1041 for (name, field_ty) in &fields {
1042 if let Some(ann) =
1043 crate::compiler::expressions::function_calls::field_type_contract_annotation(
1044 field_ty,
1045 )
1046 {
1047 contracts.insert(name.clone(), ann);
1048 }
1049 }
1050 if !contracts.is_empty() {
1051 self.type_tracker
1052 .register_object_field_contracts(schema_id, contracts);
1053 }
1054 self.function_return_schema_ids
1055 .insert(fn_name, schema_id);
1056 }
1057 }
1058
1059 /// v0.3 WS-7: project the program-wide type-inference engine's
1060 /// per-parameter `Type` into a `ConcreteType` for UNANNOTATED params.
1061 ///
1062 /// The JIT's v2 typed-array fast path is gated on
1063 /// `function_local_concrete_types[fn][param_slot]` carrying a precise
1064 /// `ConcreteType::Array(elem)`. For an annotated param that stamp comes
1065 /// from the annotation; for an UNANNOTATED param (`fn get(xs, i) {
1066 /// xs[i] }`) there is no annotation to read, so without this projection
1067 /// the slot stays `ConcreteType::Void`. The JIT then mis-takes the v2
1068 /// `TypedArray<T>` pointer (data@+8/len@+16) for a NaN-boxed v1 array
1069 /// (data@+0/len@+8 after an 8-byte header) and the inline index load
1070 /// reads garbage / SIGSEGVs even on a valid in-bounds access.
1071 ///
1072 /// Mirrors `infer_param_type_hints_from_types` exactly — same
1073 /// `Type::Function`-keyed lookup, same annotated-param skip — but
1074 /// projects to `ConcreteType` (the JIT's proof carrier) instead of a
1075 /// display string. Annotated params keep `None`; their `ConcreteType`
1076 /// is stamped from the annotation in the per-fn seeding pass.
1077 pub(super) fn infer_param_concrete_types_from_types(
1078 program: &Program,
1079 inferred_types: &HashMap<String, Type>,
1080 ) -> HashMap<String, Vec<Option<shape_value::v2::ConcreteType>>> {
1081 let funcs = Self::collect_program_functions(program);
1082 let mut out = HashMap::new();
1083
1084 for (name, func) in funcs {
1085 let mut param_cts: Vec<Option<shape_value::v2::ConcreteType>> =
1086 vec![None; func.params.len()];
1087 let Some(Type::Function { params, .. }) = inferred_types.get(&name) else {
1088 out.insert(name, param_cts);
1089 continue;
1090 };
1091
1092 for (idx, param) in func.params.iter().enumerate() {
1093 // Annotated params are stamped from the annotation directly
1094 // in the `function_local_concrete_types` per-fn seeding pass;
1095 // a destructuring param has no single slot ConcreteType.
1096 if param.type_annotation.is_some() || param.simple_name().is_none() {
1097 continue;
1098 }
1099 let Some(inferred_param_ty) = params.get(idx) else {
1100 continue;
1101 };
1102 // `Type::to_annotation()` reconstructs the `TypeAnnotation`
1103 // for resolved concrete / generic types and yields `None`
1104 // for unresolved type variables — exactly the gate we want
1105 // (no fabricated kind, no Bool-default). The existing
1106 // `concrete_type_from_annotation` then projects
1107 // `Array<int>` → `ConcreteType::Array(I64)`.
1108 let Some(ann) = inferred_param_ty.to_annotation() else {
1109 continue;
1110 };
1111 param_cts[idx] =
1112 crate::compiler::v2_map_emission::concrete_type_from_annotation(&ann);
1113 }
1114
1115 out.insert(name, param_cts);
1116 }
1117
1118 out
1119 }
1120
1121 pub(crate) fn inferred_type_to_hint_name(ty: &Type) -> Option<String> {
1122 match ty {
1123 Type::Concrete(annotation) => Some(annotation.to_type_string()),
1124 Type::Generic { base, args } => {
1125 let base_name = Self::inferred_type_to_hint_name(base)?;
1126 if args.is_empty() {
1127 return Some(base_name);
1128 }
1129 let mut arg_names = Vec::with_capacity(args.len());
1130 for arg in args {
1131 arg_names.push(Self::inferred_type_to_hint_name(arg)?);
1132 }
1133 Some(format!("{}<{}>", base_name, arg_names.join(", ")))
1134 }
1135 Type::Variable(_) | Type::Constrained { .. } | Type::Function { .. } => None,
1136 }
1137 }
1138
1139 pub(super) fn infer_param_type_hints_from_types(
1140 program: &Program,
1141 inferred_types: &HashMap<String, Type>,
1142 ) -> HashMap<String, Vec<Option<String>>> {
1143 let funcs = Self::collect_program_functions(program);
1144 let mut hints = HashMap::new();
1145
1146 for (name, func) in funcs {
1147 let mut param_hints = vec![None; func.params.len()];
1148 let Some(Type::Function { params, .. }) = inferred_types.get(&name) else {
1149 hints.insert(name, param_hints);
1150 continue;
1151 };
1152
1153 for (idx, param) in func.params.iter().enumerate() {
1154 if param.type_annotation.is_some() || param.simple_name().is_none() {
1155 continue;
1156 }
1157 if let Some(inferred_param_ty) = params.get(idx) {
1158 param_hints[idx] = Self::inferred_type_to_hint_name(inferred_param_ty);
1159 }
1160 }
1161
1162 hints.insert(name, param_hints);
1163 }
1164
1165 hints
1166 }
1167
1168 /// Phase 3e: extract a hint name for each function's inferred return
1169 /// type. Used to populate `type_tracker.function_return_types` so call
1170 /// expressions can recover numeric types (and string/bool primitives
1171 /// via `set_function_return_type`) when the source has no explicit
1172 /// return-type annotation.
1173 pub(super) fn infer_return_type_hints_from_types(
1174 program: &Program,
1175 inferred_types: &HashMap<String, Type>,
1176 ) -> HashMap<String, String> {
1177 let funcs = Self::collect_program_functions(program);
1178 let mut hints = HashMap::new();
1179 for (name, _) in funcs {
1180 let Some(Type::Function { returns, .. }) = inferred_types.get(&name) else {
1181 continue;
1182 };
1183 if let Some(rt_name) = Self::inferred_type_to_hint_name(returns) {
1184 hints.insert(name, rt_name);
1185 }
1186 }
1187 hints
1188 }
1189
1190 pub(crate) fn resolve_compiled_annotation_name(
1191 &self,
1192 annotation: &shape_ast::ast::Annotation,
1193 ) -> Option<String> {
1194 self.resolve_compiled_annotation_name_str(&annotation.name)
1195 }
1196
1197 pub(crate) fn resolve_compiled_annotation_name_str(&self, name: &str) -> Option<String> {
1198 if self.program.compiled_annotations.contains_key(name) {
1199 return Some(name.to_string());
1200 }
1201
1202 // W9: handle qualified `@local::name` form by resolving the local
1203 // namespace prefix to its canonical module path, then looking up
1204 // `canonical::name` in compiled_annotations.
1205 if let Some((local_prefix, rest)) = name.split_once("::") {
1206 // First try graph-driven namespace map (canonical for graph compile).
1207 if let Some(canonical) = self.graph_namespace_map.get(local_prefix) {
1208 let qualified = Self::qualify_module_symbol(canonical, rest);
1209 if self.program.compiled_annotations.contains_key(&qualified) {
1210 return Some(qualified);
1211 }
1212 }
1213 // Fall back to module_scope_sources (legacy / non-graph compile).
1214 if let Some(canonical) = self.module_scope_sources.get(local_prefix) {
1215 let qualified = Self::qualify_module_symbol(canonical, rest);
1216 if self.program.compiled_annotations.contains_key(&qualified) {
1217 return Some(qualified);
1218 }
1219 }
1220 return None;
1221 }
1222
1223 for module_path in self.module_scope_stack.iter().rev() {
1224 let scoped = Self::qualify_module_symbol(module_path, name);
1225 if self.program.compiled_annotations.contains_key(&scoped) {
1226 return Some(scoped);
1227 }
1228 }
1229
1230 if let Some(imported) = self.imported_annotations.get(name) {
1231 let hidden_name =
1232 Self::qualify_module_symbol(&imported.hidden_module_name, &imported.original_name);
1233 if self.program.compiled_annotations.contains_key(&hidden_name) {
1234 return Some(hidden_name);
1235 }
1236 }
1237
1238 None
1239 }
1240
1241 pub(crate) fn lookup_compiled_annotation(
1242 &self,
1243 annotation: &shape_ast::ast::Annotation,
1244 ) -> Option<(String, crate::bytecode::CompiledAnnotation)> {
1245 let resolved_name = self.resolve_compiled_annotation_name(annotation)?;
1246 let compiled = self
1247 .program
1248 .compiled_annotations
1249 .get(&resolved_name)?
1250 .clone();
1251 Some((resolved_name, compiled))
1252 }
1253
1254 pub(crate) fn annotation_matches_compiled_name(
1255 &self,
1256 annotation: &shape_ast::ast::Annotation,
1257 compiled_name: &str,
1258 ) -> bool {
1259 self.resolve_compiled_annotation_name(annotation).as_deref() == Some(compiled_name)
1260 }
1261
1262 pub(crate) fn annotation_args_for_compiled_name(
1263 &self,
1264 annotations: &[shape_ast::ast::Annotation],
1265 compiled_name: &str,
1266 ) -> Vec<shape_ast::ast::Expr> {
1267 annotations
1268 .iter()
1269 .find(|annotation| self.annotation_matches_compiled_name(annotation, compiled_name))
1270 .map(|annotation| annotation.args.clone())
1271 .unwrap_or_default()
1272 }
1273
1274 pub(crate) fn is_definition_annotation_target(
1275 target_kind: shape_ast::ast::functions::AnnotationTargetKind,
1276 ) -> bool {
1277 matches!(
1278 target_kind,
1279 shape_ast::ast::functions::AnnotationTargetKind::Function
1280 | shape_ast::ast::functions::AnnotationTargetKind::Type
1281 | shape_ast::ast::functions::AnnotationTargetKind::Module
1282 )
1283 }
1284
1285 /// Validate that an annotation is applicable to the requested target kind.
1286 pub(crate) fn validate_annotation_target_usage(
1287 &self,
1288 ann: &shape_ast::ast::Annotation,
1289 target_kind: shape_ast::ast::functions::AnnotationTargetKind,
1290 fallback_span: shape_ast::ast::Span,
1291 ) -> Result<()> {
1292 let Some((_, compiled)) = self.lookup_compiled_annotation(ann) else {
1293 let span = if ann.span == shape_ast::ast::Span::DUMMY {
1294 fallback_span
1295 } else {
1296 ann.span
1297 };
1298 return Err(ShapeError::SemanticError {
1299 message: format!("Unknown annotation '@{}'", ann.name),
1300 location: Some(self.span_to_source_location(span)),
1301 });
1302 };
1303
1304 let has_definition_lifecycle =
1305 compiled.on_define_handler.is_some() || compiled.metadata_handler.is_some();
1306 if has_definition_lifecycle && !Self::is_definition_annotation_target(target_kind) {
1307 let target_label = format!("{:?}", target_kind).to_lowercase();
1308 let span = if ann.span == shape_ast::ast::Span::DUMMY {
1309 fallback_span
1310 } else {
1311 ann.span
1312 };
1313 return Err(ShapeError::SemanticError {
1314 message: format!(
1315 "Annotation '{}' defines definition-time lifecycle hooks (`on_define`/`metadata`) and cannot be applied to a {}. Allowed targets for these hooks are: function, type, module",
1316 ann.name, target_label
1317 ),
1318 location: Some(self.span_to_source_location(span)),
1319 });
1320 }
1321
1322 if compiled.allowed_targets.is_empty() || compiled.allowed_targets.contains(&target_kind) {
1323 return Ok(());
1324 }
1325
1326 let allowed: Vec<String> = compiled
1327 .allowed_targets
1328 .iter()
1329 .map(|k| format!("{:?}", k).to_lowercase())
1330 .collect();
1331 let target_label = format!("{:?}", target_kind).to_lowercase();
1332
1333 let span = if ann.span == shape_ast::ast::Span::DUMMY {
1334 fallback_span
1335 } else {
1336 ann.span
1337 };
1338
1339 Err(ShapeError::SemanticError {
1340 message: format!(
1341 "Annotation '{}' cannot be applied to a {}. Allowed targets: {}",
1342 ann.name,
1343 target_label,
1344 allowed.join(", ")
1345 ),
1346 location: Some(self.span_to_source_location(span)),
1347 })
1348 }
1349
1350 /// Compile a program to bytecode
1351 pub fn compile(mut self, program: &Program) -> Result<BytecodeProgram> {
1352 // First: desugar the program (converts FromQuery to method chains, etc.)
1353 let mut program = program.clone();
1354 shape_ast::transform::desugar_program(&mut program);
1355 let analysis_program =
1356 shape_ast::transform::augment_program_with_generated_extends(&program);
1357
1358 // Run the shared analyzer and surface diagnostics that are currently
1359 // proven reliable in the compiler execution path.
1360 let mut known_bindings: Vec<String> = self.module_bindings.keys().cloned().collect();
1361 let namespace_bindings = Self::collect_namespace_import_bindings(&analysis_program);
1362 // Inline: collect namespace and annotation import scope sources
1363 for item in &analysis_program.items {
1364 if let shape_ast::ast::Item::Import(import_stmt, _) = item {
1365 if import_stmt.from.is_empty() {
1366 continue;
1367 }
1368 match &import_stmt.items {
1369 shape_ast::ast::ImportItems::Namespace { name, alias } => {
1370 let local_name = alias.clone().unwrap_or_else(|| name.clone());
1371 self.module_scope_sources
1372 .entry(local_name)
1373 .or_insert_with(|| import_stmt.from.clone());
1374 }
1375 shape_ast::ast::ImportItems::Named(specs) => {
1376 // W9: register annotation-import scope source against
1377 // the canonical module path. The synthetic hidden-module
1378 // name is no longer used; use-site annotation resolution
1379 // looks up `canonical_path::name` directly.
1380 if specs.iter().any(|spec| spec.is_annotation) {
1381 self.module_scope_sources
1382 .entry(import_stmt.from.clone())
1383 .or_insert_with(|| import_stmt.from.clone());
1384 }
1385 }
1386 }
1387 }
1388 }
1389 known_bindings.extend(namespace_bindings.iter().cloned());
1390 // R8 W8 Cluster A: imported `pub const` names are valid identifier
1391 // bindings at consumer-side use sites; teach the analyzer about
1392 // them so `unknown-binding` warnings don't blanket the use site
1393 // before the const-inline path replaces the identifier reference.
1394 known_bindings.extend(self.imported_consts.keys().cloned());
1395 self.module_namespace_bindings
1396 .extend(namespace_bindings.into_iter());
1397 for namespace in self.module_namespace_bindings.clone() {
1398 let binding_idx = self.get_or_create_module_binding(&namespace);
1399 self.register_extension_module_schema(&namespace);
1400 let module_schema_name = format!("__mod_{}", namespace);
1401 if self
1402 .type_tracker
1403 .schema_registry()
1404 .get(&module_schema_name)
1405 .is_some()
1406 {
1407 self.set_module_binding_type_info(binding_idx, &module_schema_name);
1408 }
1409 }
1410 known_bindings.sort();
1411 known_bindings.dedup();
1412 let analysis_mode = if matches!(self.type_diagnostic_mode, TypeDiagnosticMode::RecoverAll) {
1413 TypeAnalysisMode::RecoverAll
1414 } else {
1415 TypeAnalysisMode::FailFast
1416 };
1417 if let Err(errors) = analyze_program_with_mode(
1418 &analysis_program,
1419 self.source_text.as_deref(),
1420 None,
1421 Some(&known_bindings),
1422 analysis_mode,
1423 ) {
1424 match self.type_diagnostic_mode {
1425 TypeDiagnosticMode::Strict => {
1426 return Err(Self::type_errors_to_shape(errors));
1427 }
1428 TypeDiagnosticMode::ReliableOnly => {
1429 let strict_errors: Vec<_> = errors
1430 .into_iter()
1431 .filter(|error| Self::should_emit_type_diagnostic(&error.error))
1432 .collect();
1433 if !strict_errors.is_empty() {
1434 return Err(Self::type_errors_to_shape(strict_errors));
1435 }
1436 }
1437 TypeDiagnosticMode::RecoverAll => {
1438 self.errors.extend(
1439 errors
1440 .into_iter()
1441 .map(Self::type_error_with_location_to_shape),
1442 );
1443 }
1444 }
1445 }
1446
1447 let (
1448 inferred_ref_params,
1449 inferred_ref_mutates,
1450 inferred_param_type_hints,
1451 inferred_return_type_hints,
1452 inferred_param_concrete_types,
1453 inferred_param_object_fields,
1454 inferred_return_object_fields,
1455 ) = Self::infer_reference_model(&program);
1456 self.inferred_param_pass_modes =
1457 Self::build_param_pass_mode_map(&program, &inferred_ref_params, &inferred_ref_mutates);
1458 self.inferred_ref_params = inferred_ref_params;
1459 self.inferred_ref_mutates = inferred_ref_mutates;
1460 self.inferred_param_type_hints = inferred_param_type_hints;
1461 self.inferred_param_concrete_types = inferred_param_concrete_types;
1462 self.inferred_param_object_fields = inferred_param_object_fields;
1463 self.inferred_return_object_fields = inferred_return_object_fields;
1464 // WS-9c: eagerly register an inline anonymous schema (+ per-field
1465 // contracts) for every unannotated function whose inferred return
1466 // type is an anonymous object. Registering up-front — before any
1467 // body compiles — makes the return-object schema available both to
1468 // `compile_expr_function_call` (which stamps it on the call's
1469 // `last_expr_schema` so a `let` binding inherits it) and to the
1470 // read-only `infer_expr_type` property-access path (which resolves
1471 // `f(...).field` directly). `register_inline_object_schema_typed` is
1472 // idempotent on the field set, so this never duplicates a schema.
1473 self.register_inferred_return_object_schemas();
1474 // Phase 3e: register inferred return types so function-call
1475 // compilation can recover the numeric type even for sources with
1476 // no explicit `-> T` annotation.
1477 for (fn_name, ret_ty) in &inferred_return_type_hints {
1478 self.type_tracker
1479 .register_function_return_type(fn_name, ret_ty);
1480 }
1481
1482 // Two-phase TypedObject field hoisting:
1483 //
1484 // Phase 1 (here, AST pre-pass): Collect all property assignments (e.g.,
1485 // `a.y = 2`) from the entire program BEFORE any function compilation.
1486 // This populates `hoisted_fields` so that `compile_typed_object_literal`
1487 // can allocate schema slots for future fields at object-creation time.
1488 // Without this pre-pass, the schema would be too small and a later
1489 // `a.y = 2` would require a schema migration at runtime.
1490 //
1491 // Phase 2 (per-function, MIR): During function compilation, MIR field
1492 // analysis (`mir::field_analysis::analyze_fields`) runs flow-sensitive
1493 // definite-initialization and liveness analysis. This detects:
1494 // - `dead_fields`: fields that are written but never read (wasted slots)
1495 // - `conditionally_initialized`: fields only assigned on some paths
1496 //
1497 // After MIR analysis, the compiler can cross-reference
1498 // `mir_field_analyses[func].dead_fields` to prune unused hoisted fields
1499 // from schemas. The dead_fields set uses `(SlotId, FieldIdx)` which must
1500 // be mapped to field names via the schema registry — see the integration
1501 // note in `compile_typed_object_literal`.
1502 {
1503 use shape_runtime::type_system::inference::PropertyAssignmentCollector;
1504 use shape_ast::ast::{Expr, Literal};
1505 use shape_runtime::type_schema::FieldType;
1506 let assignments = PropertyAssignmentCollector::collect(&program);
1507 let grouped = PropertyAssignmentCollector::group_by_variable(&assignments);
1508 // Phase 3e: infer a primitive FieldType for each hoisted field
1509 // when the RHS is a literal whose type is statically known.
1510 // Falls back to FieldType::Any (the prior behavior) for
1511 // non-literal RHS or types we can't map.
1512 let infer_lit = |expr: &Expr| -> Option<FieldType> {
1513 match expr {
1514 Expr::Literal(Literal::Int(_), _) => Some(FieldType::I64),
1515 Expr::Literal(Literal::Number(_), _) => Some(FieldType::F64),
1516 Expr::Literal(Literal::Decimal(_), _) => Some(FieldType::Decimal),
1517 Expr::Literal(Literal::Bool(_), _) => Some(FieldType::Bool),
1518 Expr::Literal(Literal::String(_), _) => Some(FieldType::String),
1519 _ => None,
1520 }
1521 };
1522 for (var_name, var_assignments) in grouped {
1523 let field_names: Vec<String> =
1524 var_assignments.iter().map(|a| a.property.clone()).collect();
1525 let mut type_map: std::collections::HashMap<String, FieldType> =
1526 std::collections::HashMap::new();
1527 for a in &var_assignments {
1528 if let Some(ft) = infer_lit(&a.value_expr) {
1529 type_map.insert(a.property.clone(), ft);
1530 }
1531 }
1532 if !type_map.is_empty() {
1533 self.hoisted_field_types.insert(var_name.clone(), type_map);
1534 }
1535 self.hoisted_fields.insert(var_name, field_names);
1536 }
1537 }
1538
1539 // First pass: collect all function definitions
1540 for item in &program.items {
1541 self.register_item_functions(item)?;
1542 }
1543
1544 // WS-9b: pre-register struct type SCHEMAS (runtime fields only — no
1545 // comptime-handler execution, that stays in the pass-2
1546 // `register_struct_type`). This makes `type` definitions
1547 // order-independent the same way `register_item_functions` makes
1548 // function definitions order-independent: a function declared
1549 // *before* the `type` it takes as a parameter (`fn ov(a, b) { a.lo
1550 // <= b.hi }` ahead of `type Box`) can now resolve `a.lo` against the
1551 // `Box` schema during its body compilation. Without the prepass the
1552 // schema is registered only when the later `type` item compiles, so
1553 // `tracker_schema_id_for_expr` misses it and the property access
1554 // types as `unknown`.
1555 for item in &program.items {
1556 self.predeclare_item_struct_schemas(item);
1557 }
1558
1559 // MIR authority for non-function items: run borrow analysis on top-level
1560 // code before compilation. Errors in cleanly-lowered regions are emitted;
1561 // errors in fallback regions are suppressed (span-granular filtering).
1562 if let Err(e) = self.analyze_non_function_items_with_mir("__main__", &program.items) {
1563 self.errors.push(e);
1564 }
1565
1566 // Start __main__ blob builder for top-level code.
1567 self.current_blob_builder = Some(FunctionBlobBuilder::new(
1568 "__main__".to_string(),
1569 self.program.current_offset(),
1570 self.program.constants.len(),
1571 self.program.strings.len(),
1572 ));
1573
1574 // Push a top-level drop scope so that block expressions and
1575 // statement-level VarDecls can track locals for auto-drop.
1576 self.push_drop_scope();
1577 self.non_function_mir_context_stack
1578 .push("__main__".to_string());
1579
1580 // Register root's imports from the module graph. This emits alias
1581 // copy instructions (e.g. `set = std::core::set`) and MUST happen
1582 // INSIDE the `__main__` blob — emitting before the blob started
1583 // would leave the copies in an unreachable gap, so at runtime the
1584 // alias binding would remain None and `set::contains(...)` would
1585 // read a None callable and raise `InvalidCall`.
1586 if let Some(graph) = self.module_graph.clone() {
1587 let root_id = graph.root_id();
1588 self.register_graph_imports_for_module(root_id, &graph)?;
1589 }
1590
1591 // Second pass: compile all items (collect errors instead of early-returning)
1592 let item_count = program.items.len();
1593 for (idx, item) in program.items.iter().enumerate() {
1594 let is_last = idx == item_count - 1;
1595 let future_names =
1596 self.future_reference_use_names_for_remaining_items(&program.items[idx + 1..]);
1597 self.push_future_reference_use_names(future_names);
1598 let compile_result = self.compile_item_with_context(item, is_last);
1599 self.pop_future_reference_use_names();
1600 if let Err(e) = compile_result {
1601 self.errors.push(e);
1602 }
1603 // E+5.5 Unit C step 2: capture the final expression's return-kind
1604 // signal RIGHT AFTER the last item compiles, before drop-scope
1605 // emission and Halt overwrite `last_expr_*`. The captured value
1606 // is consumed in `populate_program_storage_hints` to populate
1607 // `top_level_frame.return_kind` for the host-boundary
1608 // ValueWord synthesis.
1609 if is_last && self.errors.is_empty() {
1610 // Per ADR-006 §2.7.5.1, `infer_top_level_return_kind` /
1611 // `infer_top_level_return_kind_from_item` carry "kind not
1612 // yet proven" as `Option::None` — `.or_else(...)` falls
1613 // back to the AST-driven path when the state-driven one
1614 // produced no kind.
1615 let kind = self
1616 .infer_top_level_return_kind()
1617 .or_else(|| self.infer_top_level_return_kind_from_item(item));
1618 self.top_level_program_return_kind = kind;
1619 }
1620 self.release_unused_module_reference_borrows_for_remaining_items(
1621 &program.items[idx + 1..],
1622 );
1623 }
1624 self.non_function_mir_context_stack.pop();
1625
1626 // Phase 4b Round 6 WS-1b W16.2-C residual: surface-and-stop any
1627 // top-level bare empty-array accumulator (`let mut out = []`) whose
1628 // element type was never resolved by a downstream `.push(...)`.
1629 if let Err(e) = self.finalize_unresolved_empty_array_accumulators() {
1630 self.errors.push(e);
1631 }
1632
1633 // Return collected errors before emitting Halt
1634 if !self.errors.is_empty() {
1635 if self.errors.len() == 1 {
1636 return Err(self.errors.remove(0));
1637 }
1638 return Err(shape_ast::error::ShapeError::MultiError(self.errors));
1639 }
1640
1641 // Emit drops for top-level locals (from the top-level drop scope)
1642 self.pop_drop_scope()?;
1643
1644 // Emit drops for top-level module bindings that have Drop impls
1645 {
1646 let bindings: Vec<(u16, bool)> = std::mem::take(&mut self.drop_module_bindings);
1647 for (binding_idx, is_async) in bindings.into_iter().rev() {
1648 self.emit_drop_call_for_module_binding(binding_idx, is_async);
1649 }
1650 }
1651
1652 // Add halt instruction at the end
1653 self.emit(Instruction::simple(OpCode::Halt));
1654
1655 // Store module_binding variable names for REPL persistence
1656 // Build a Vec<String> where index matches the module_binding variable index
1657 let mut module_binding_names = vec![String::new(); self.module_bindings.len()];
1658 for (name, &idx) in &self.module_bindings {
1659 module_binding_names[idx as usize] = name.clone();
1660 }
1661 self.program.module_binding_names = module_binding_names;
1662
1663 // Store top-level locals count so executor can advance sp past them
1664 self.program.top_level_locals_count = self.next_local;
1665
1666 // Persist storage hints for JIT width-aware lowering.
1667 self.populate_program_storage_hints();
1668
1669 // Transfer type schema registry for TypedObject field resolution
1670 self.program.type_schema_registry = self.type_tracker.schema_registry().clone();
1671
1672 // Transfer final function definitions after comptime mutation/specialization.
1673 self.program.expanded_function_defs = self.function_defs.clone();
1674
1675 // Transfer monomorphization cache keys for diagnostics/testing.
1676 self.program.monomorphization_keys = self.monomorphization_cache.keys().cloned().collect();
1677
1678 // Cache top-level MIR data for JIT v2 (MirToIR compilation of __main__).
1679 // The MIR and borrow analysis were computed by analyze_non_function_items_with_mir
1680 // above; we combine them with a storage plan here.
1681 {
1682 let mir_opt = self.mir_functions.get("__main__").cloned();
1683 let borrow_opt = self.mir_borrow_analyses.get("__main__").cloned();
1684 if let (Some(mut mir), Some(borrow_analysis)) = (mir_opt, borrow_opt) {
1685 if !self.closure_function_ids.is_empty() {
1686 let mut closure_idx = 0;
1687 let closure_ids = self.closure_function_ids.clone();
1688 let mut has_capture = false;
1689 for block in &mut mir.blocks {
1690 for stmt in &mut block.statements {
1691 let is_placeholder = matches!(
1692 &stmt.kind,
1693 crate::mir::types::StatementKind::Assign(
1694 _,
1695 crate::mir::types::Rvalue::Use(
1696 crate::mir::types::Operand::Constant(
1697 crate::mir::types::MirConstant::ClosurePlaceholder
1698 )
1699 )
1700 )
1701 );
1702 if is_placeholder {
1703 if has_capture {
1704 stmt.kind = crate::mir::types::StatementKind::Nop;
1705 has_capture = false;
1706 } else if closure_idx < closure_ids.len() {
1707 let (ref name, _) = closure_ids[closure_idx];
1708 let slot = match &stmt.kind {
1709 crate::mir::types::StatementKind::Assign(p, _) => {
1710 p.root_local()
1711 }
1712 _ => unreachable!(),
1713 };
1714 stmt.kind = crate::mir::types::StatementKind::Assign(
1715 crate::mir::types::Place::Local(slot),
1716 crate::mir::types::Rvalue::Use(
1717 crate::mir::types::Operand::Constant(
1718 crate::mir::types::MirConstant::Function(
1719 name.clone(),
1720 ),
1721 ),
1722 ),
1723 );
1724 closure_idx += 1;
1725 }
1726 continue;
1727 }
1728 if let crate::mir::types::StatementKind::ClosureCapture {
1729 function_id,
1730 ..
1731 } = &mut stmt.kind
1732 {
1733 if closure_idx < closure_ids.len() {
1734 let (_, idx) = closure_ids[closure_idx];
1735 *function_id = Some(idx);
1736 closure_idx += 1;
1737 has_capture = true;
1738 }
1739 }
1740 }
1741 }
1742 }
1743 use std::collections::{HashMap as StdHashMap, HashSet as StdHashSet};
1744 let planner_input = crate::mir::storage_planning::StoragePlannerInput {
1745 mir: &mir,
1746 analysis: &borrow_analysis,
1747 binding_semantics: &StdHashMap::new(),
1748 closure_captures: &StdHashSet::new(),
1749 mutable_captures: &StdHashSet::new(),
1750 had_fallbacks: true, // conservative: top-level MIR often has fallbacks
1751 callee_summaries: Some(&self.function_borrow_summaries),
1752 };
1753 let storage_plan = crate::mir::storage_planning::plan_storage(&planner_input);
1754
1755 // ADR-006 §2.7.5 stamp-at-compile-time, Phase 3
1756 // cluster-0 Round 16 W17-narrow-follow-up-A: thread
1757 // schema ids on top-level MIR `ObjectStore`
1758 // statements (canonical Smoke 3 site — `let t = X {}`
1759 // is top-level). Same back-patch as the per-function
1760 // path at `compiler/functions.rs` post-closure-id
1761 // patching; reads `mir.local_struct_type_names` +
1762 // `type_tracker.schema_registry()` to align with the
1763 // parallel bytecode-side `OpCode::NewTypedObject`
1764 // operand.
1765 crate::compiler::mir_schema_threading::back_patch_schema_ids(
1766 &mut mir,
1767 &mut self.type_tracker,
1768 );
1769
1770 self.program.top_level_mir =
1771 Some(std::sync::Arc::new(crate::bytecode::MirFunctionData {
1772 mir,
1773 storage_plan,
1774 borrow_analysis,
1775 }));
1776 }
1777 }
1778
1779 // ADR-006 §2.7.5 conduit: stamp per-MIR-slot `ConcreteType` for
1780 // top-level code by walking the cached top-level MIR. The JIT
1781 // MirToIR reads this side-table (`BytecodeProgram.
1782 // top_level_local_concrete_types`) to drive the v2 typed-array
1783 // fast path (avoiding `Rvalue::Aggregate` surface-and-stop) and
1784 // the TypedObject `ObjectStore` short-circuit.
1785 //
1786 // Why MIR-walk rather than bytecode-compiler slot mapping: top-
1787 // level code allocates the user's bindings as module_bindings
1788 // (NOT bytecode locals — `self.next_local` is 0 at top level),
1789 // so the bytecode-compiler's per-local side-tables do not
1790 // carry top-level `let p = Point{...}` slots. The cached top-
1791 // level MIR already encodes the structural type information
1792 // through `StatementKind::{ObjectStore, ArrayStore, EnumStore}`
1793 // — the MIR-level kind-source statements emitted for
1794 // struct/enum/array construction. The walk is purely from the
1795 // proven MIR shape; no runtime decode, no Bool-default fallback.
1796 //
1797 // The result is indexed by MIR `SlotId` (matching MirToIR's
1798 // `concrete_type_for_slot` / `is_v2_typed_array_slot` indexing
1799 // exactly). `ConcreteType::Void` per slot means "no
1800 // information inferred" — a real enum variant per §2.7.5.1, not
1801 // a Bool-default fallback per forbidden #9.
1802 //
1803 // The top-level conduit walk is deferred a few lines down — it
1804 // runs AFTER the per-function return-type side-table is built,
1805 // so the Call-terminator destination stamping in the walk has
1806 // access to callee return types via the resolver. See the
1807 // W12-jit-call-return-kind block below.
1808
1809 // ADR-006 §2.7.5 conduit (W12-jit-call-return-kind close, 2026-05-12):
1810 // Per-user-function declared return ConcreteType, built first so the
1811 // per-function and top-level conduit passes can consume it via the
1812 // callee-return resolver. Returns are classified from the AST
1813 // `FunctionDef.return_type` (preserved via `expanded_function_defs`)
1814 // through `concrete_type_from_annotation` (already used for HashMap
1815 // key/value extraction). When the function has no annotation or the
1816 // annotation doesn't reduce to a known shape, the entry stays
1817 // `ConcreteType::Void` per §2.7.5.1 — NOT a Bool-default fallback.
1818 let mut per_fn_ret: Vec<shape_value::v2::ConcreteType> =
1819 Vec::with_capacity(self.program.functions.len());
1820 for func in &self.program.functions {
1821 let ct = self
1822 .program
1823 .expanded_function_defs
1824 .get(&func.name)
1825 .and_then(|fd| fd.return_type.as_ref())
1826 .and_then(|ann| {
1827 crate::compiler::v2_map_emission::concrete_type_from_annotation(
1828 ann,
1829 )
1830 })
1831 .unwrap_or(shape_value::v2::ConcreteType::Void);
1832 per_fn_ret.push(ct);
1833 }
1834 self.program.function_return_concrete_types = per_fn_ret;
1835
1836 // Build the callee-return resolver: maps `MirConstant::Function(name)`
1837 // to the callee's declared return ConcreteType via the side-table
1838 // just populated. Used by the conduit passes below to stamp
1839 // `TerminatorKind::Call` destination slots. `None` for unknown /
1840 // unannotated / void-returning functions — the destination slot
1841 // stays `Void` (no fabrication).
1842 let name_to_idx: std::collections::HashMap<String, usize> = self
1843 .program
1844 .functions
1845 .iter()
1846 .enumerate()
1847 .map(|(i, f)| (f.name.clone(), i))
1848 .collect();
1849 let returns_vec = self.program.function_return_concrete_types.clone();
1850 let callee_returns = |name: &str| -> Option<shape_value::v2::ConcreteType> {
1851 let idx = *name_to_idx.get(name)?;
1852 let ct = returns_vec.get(idx)?;
1853 if matches!(ct, shape_value::v2::ConcreteType::Void) {
1854 None
1855 } else {
1856 Some(ct.clone())
1857 }
1858 };
1859
1860 // ADR-006 §2.7.5 — Phase 3 cluster-0 Round 13 T1' commit 2:
1861 // method-returns resolver for trait-method dispatch return-kind
1862 // classification. Chains:
1863 // `find_default_trait_impl_for_type_method(type_name, method_name)
1864 // → trait impl function name (e.g. "X::name")
1865 // → function_return_concrete_types[function_index]
1866 // → declared return ConcreteType (e.g. ConcreteType::String)`
1867 //
1868 // Used by the conduit producer to stamp `TerminatorKind::Call`
1869 // destination slots for `MirConstant::Method(_)` arms with a
1870 // receiver slot whose struct type name was recorded in MIR
1871 // (`mir.local_struct_type_names`, T1' gap 1 closure). `None` at
1872 // any link in the chain means "no information" — the destination
1873 // slot stays `Void` per §2.7.5.1 (no fabricated default).
1874 //
1875 // Gap 3 closure (commit 1, `desugar_impl_method` trait
1876 // declaration return-type substitution) ensures
1877 // `function_return_concrete_types["X::name"]` carries the trait's
1878 // declared `ConcreteType::String` even when the impl source
1879 // doesn't repeat the `: string` annotation.
1880 let trait_method_symbols = self.program.trait_method_symbols.clone();
1881 let find_trait_impl_default_suffix =
1882 |type_name: &str, method_name: &str| -> Option<String> {
1883 // Mirror BytecodeProgram::find_default_trait_impl_for_type_method
1884 // semantics (the canonical helper at
1885 // `crates/shape-vm/src/bytecode/program_impl.rs:151`)
1886 // without borrowing `self.program` — the closure must be
1887 // passable by reference to the conduit producer
1888 // alongside `callee_returns`. The "__default__" selector
1889 // string is `DEFAULT_TRAIT_IMPL_SELECTOR` at
1890 // `crates/shape-vm/src/bytecode.rs:15`; inlined here to
1891 // avoid the borrow.
1892 let default_suffix = format!(
1893 "::{}::__default__::{}",
1894 type_name, method_name
1895 );
1896 for (key, func_name) in &trait_method_symbols {
1897 if key.ends_with(&default_suffix) {
1898 return Some(func_name.clone());
1899 }
1900 }
1901 let type_segment = format!("::{}::", type_name);
1902 let suffix = format!("::{}", method_name);
1903 let mut matches: Vec<String> = Vec::new();
1904 for (key, func_name) in &trait_method_symbols {
1905 if key.contains(&type_segment) && key.ends_with(&suffix) {
1906 matches.push(func_name.clone());
1907 }
1908 }
1909 // Multi-trait method-name disambiguation (audit §5):
1910 // when multiple traits declare `method()` for the same
1911 // receiver type, we cannot determine the return
1912 // ConcreteType uniquely from name alone — return None so
1913 // the downstream classifier surfaces unstamped.
1914 if matches.len() == 1 {
1915 Some(matches.pop().unwrap())
1916 } else {
1917 None
1918 }
1919 };
1920 let method_returns =
1921 |type_name: &str, method_name: &str| -> Option<shape_value::v2::ConcreteType> {
1922 let func_name = find_trait_impl_default_suffix(type_name, method_name)?;
1923 let idx = *name_to_idx.get(&func_name)?;
1924 let ct = returns_vec.get(idx)?;
1925 if matches!(ct, shape_value::v2::ConcreteType::Void) {
1926 None
1927 } else {
1928 Some(ct.clone())
1929 }
1930 };
1931
1932 // ADR-006 §2.7.5 V3-S6b conduit consumer: monomorph-method
1933 // resolver. Reads `BytecodeProgram.monomorphized_method_call_sites`
1934 // populated by `try_monomorphize_method_call` /
1935 // `_with_closures` at bytecode-compile time, then chains the
1936 // looked-up specialized FunctionId through `returns_vec` (the
1937 // local clone of `function_return_concrete_types`) to recover the
1938 // callee specialization's declared return type. The closure
1939 // closes over the `current_function` half of the composite key
1940 // — top-level uses `None`; per-fn loop below uses
1941 // `Some(fn_idx)`.
1942 let monomorph_call_sites =
1943 self.program.monomorphized_method_call_sites.clone();
1944 let monomorph_method_returns_top = |span: shape_ast::ast::span::Span|
1945 -> Option<shape_value::v2::ConcreteType>
1946 {
1947 let idx = *monomorph_call_sites.get(&(span, None))?;
1948 let ct = returns_vec.get(idx)?;
1949 if matches!(ct, shape_value::v2::ConcreteType::Void) {
1950 None
1951 } else {
1952 Some(ct.clone())
1953 }
1954 };
1955
1956 // cluster-2-cw-IB-class-b (2026-05-16, supervisor R3 binding-
1957 // ratified): value-call return-ConcreteType resolver. Consumes
1958 // the side-table populated at `compile_expr_function_call`'s
1959 // value-call branch and returns the inferred ConcreteType
1960 // result for closure-bound calls. Top-level conduit closes
1961 // over `None` for the caller half of the composite key — same
1962 // convention as `monomorph_method_returns_top`.
1963 let value_call_sites =
1964 self.program.value_call_return_concrete_types.clone();
1965 let value_call_returns_top = |span: shape_ast::ast::span::Span|
1966 -> Option<shape_value::v2::ConcreteType>
1967 {
1968 let ct = value_call_sites.get(&(span, None))?.clone();
1969 if matches!(ct, shape_value::v2::ConcreteType::Void) {
1970 None
1971 } else {
1972 Some(ct)
1973 }
1974 };
1975
1976 // Re-run top-level conduit with the callee-return resolver so the
1977 // `let r = divide(10, 2)` slot picks up `Result(I64, String)` from
1978 // the Call terminator. (The first run above stamped `Void` for
1979 // Call destinations since no resolver was available.) The
1980 // method-returns resolver is also threaded so `t.name()`-style
1981 // trait-method dispatch destinations pick up the trait's declared
1982 // return ConcreteType. The V3-S6b monomorph-method resolver is
1983 // threaded so `arr.map(...).sum()` chains have the `.map()`
1984 // destination stamped with the specialized callee's return
1985 // ConcreteType.
1986 if let Some(ref mir_data) = self.program.top_level_mir {
1987 let concrete_types =
1988 crate::compiler::helpers::infer_top_level_concrete_types_from_mir_with_resolvers(
1989 &mir_data.mir,
1990 Some(&callee_returns),
1991 Some(&method_returns),
1992 Some(&monomorph_method_returns_top),
1993 Some(&value_call_returns_top),
1994 );
1995 self.program.top_level_local_concrete_types = concrete_types;
1996 }
1997
1998 // ADR-006 §2.7.5 conduit (W12-jit-aggregate-non-array close,
1999 // 2026-05-12): same MIR-walk inference applied per user function.
2000 // The producer (`infer_top_level_concrete_types_from_mir`) is
2001 // generic over any MirFunction — its name is historical from the
2002 // earlier top-level-only landing (Round 3). User-function bodies
2003 // hit the JIT consumer at
2004 // `crates/shape-jit/src/compiler/program.rs::compile_function_with_user_funcs`,
2005 // which currently passes `concrete_types: Vec::new()` and therefore
2006 // surfaces `Rvalue::Aggregate` for every `Ok(v)` / `Err(e)` /
2007 // `Some(x)` / struct-literal construction inside a user function
2008 // body (Smoke 1.5 `divide`, Smoke 2 `first_positive`, 28 stdlib
2009 // helpers verified at audit time).
2010 //
2011 // The callee-return resolver is also threaded here so user-function
2012 // bodies that call other user functions (e.g. `divide` calls a
2013 // helper) propagate the helper's return ConcreteType into their
2014 // own slot, recursing through the conduit.
2015 //
2016 // `ConcreteType::Void` per slot per §2.7.5.1 — NOT a Bool-default
2017 // fallback per forbidden #9. Functions without `mir_data` get an
2018 // empty inner vec; downstream consumers fall back to the legacy
2019 // NaN-boxed path naturally.
2020 let mut per_fn: Vec<Vec<shape_value::v2::ConcreteType>> =
2021 Vec::with_capacity(self.program.functions.len());
2022 for (fn_idx, func) in self.program.functions.iter().enumerate() {
2023 if let Some(ref mir_data) = func.mir_data {
2024 // ADR-006 §2.7.5 V3-S6b conduit consumer: per-fn variant
2025 // of the monomorph-method resolver. Closes over the
2026 // calling function's index for the composite-key lookup
2027 // — must match the value `try_monomorphize_method_call`
2028 // recorded in `self.current_function` at populate time
2029 // (i.e. `Some(fn_idx)` here matches the populator's
2030 // post-monomorphization specialized caller FunctionId).
2031 let current_fn = Some(fn_idx);
2032 let monomorph_method_returns_per_fn = |span: shape_ast::ast::span::Span|
2033 -> Option<shape_value::v2::ConcreteType>
2034 {
2035 let idx = *monomorph_call_sites.get(&(span, current_fn))?;
2036 let ct = returns_vec.get(idx)?;
2037 if matches!(ct, shape_value::v2::ConcreteType::Void) {
2038 None
2039 } else {
2040 Some(ct.clone())
2041 }
2042 };
2043 // cluster-2-cw-IB-class-b: per-fn variant of the value-call
2044 // return-ConcreteType resolver. Same composite-key
2045 // discipline as monomorph_method_returns_per_fn above —
2046 // closes over `Some(fn_idx)` so calls inside user-function
2047 // bodies pick up their own caller-context entries.
2048 let value_call_returns_per_fn = |span: shape_ast::ast::span::Span|
2049 -> Option<shape_value::v2::ConcreteType>
2050 {
2051 let ct = value_call_sites.get(&(span, current_fn))?.clone();
2052 if matches!(ct, shape_value::v2::ConcreteType::Void) {
2053 None
2054 } else {
2055 Some(ct)
2056 }
2057 };
2058 let mut concrete_types =
2059 crate::compiler::helpers::infer_top_level_concrete_types_from_mir_with_resolvers(
2060 &mir_data.mir,
2061 Some(&callee_returns),
2062 Some(&method_returns),
2063 Some(&monomorph_method_returns_per_fn),
2064 Some(&value_call_returns_per_fn),
2065 );
2066 // W15.2-LANG-4 jit-filter-predicate fix (2026-05-18). Seed
2067 // parameter slots from the function definition's parameter
2068 // type annotations. ADR-006 §2.7.5 producer-side
2069 // classification — the parameter's declared type IS the
2070 // proof source for the slot's ConcreteType. Without this
2071 // pass parameter slots stay `ConcreteType::Void`, the JIT
2072 // side's `infer_slot_kinds_with_concrete` projects `None`,
2073 // and `operand_slot_kind_or_carrier` falls back to the
2074 // §2.7.5 carrier `UInt64`. For closure-typed parameters
2075 // (e.g. `Vec.filter::i64`'s `predicate: (int) -> bool`)
2076 // that fallback drives `jit_call_value` into the UInt64
2077 // arm where `is_inline_function` / `is_heap_kind(_,
2078 // HK_CLOSURE)` both fail on the raw-Arc
2079 // `HeapValue::ClosureRaw` callee bits, surfacing the
2080 // §2.7.5 `callee_bits stamped UInt64 but is neither
2081 // inline function nor unified-heap HK_CLOSURE` diagnostic
2082 // and returning TAG_NULL — visible in the wild as
2083 // `samples.filter(|v| v > threshold)` returning the
2084 // unfiltered receiver under JIT (book-truth
2085 // `getting-started/first-query.mdx:41` snippet).
2086 //
2087 // Only seed slots whose current classification is `Void`
2088 // (the §2.7.5.1 "no kind proven" placeholder); the
2089 // MIR-walk inference's classifications dominate when both
2090 // sources are present.
2091 if let Some(def) = self.function_defs.get(&func.name) {
2092 // v0.3 WS-7: inference-resolved per-param `ConcreteType`
2093 // for UNANNOTATED params (projected in
2094 // `infer_param_concrete_types_from_types`). Used as the
2095 // seed source when a param has no annotation to read.
2096 let inferred_param_cts =
2097 self.inferred_param_concrete_types.get(&func.name);
2098 for (i, ¶m_slot) in mir_data.mir.param_slots.iter().enumerate() {
2099 let idx = param_slot.0 as usize;
2100 if idx >= concrete_types.len() {
2101 continue;
2102 }
2103 if !matches!(
2104 concrete_types[idx],
2105 shape_value::v2::ConcreteType::Void
2106 ) {
2107 continue;
2108 }
2109 let Some(param) = def.params.get(i) else {
2110 continue;
2111 };
2112 match param.type_annotation {
2113 Some(ref ann) => {
2114 // Annotated param: the declared type IS the
2115 // proof source for the slot's ConcreteType.
2116 if let Some(ct) =
2117 crate::compiler::v2_map_emission::concrete_type_from_annotation(ann)
2118 {
2119 concrete_types[idx] = ct;
2120 }
2121 }
2122 None => {
2123 // v0.3 WS-7: UNANNOTATED param. The bytecode
2124 // compiler's MIR-walk inference could not
2125 // prove a `ConcreteType` for the slot from
2126 // MIR-observable statements alone (it stayed
2127 // `Void`), but the program-wide
2128 // type-inference engine DID resolve the
2129 // parameter's type — and the VM relies on
2130 // that resolution (strict typing has no
2131 // dynamic fallback). Thread the
2132 // inference-resolved `ConcreteType` so the
2133 // JIT's v2 typed-array / typed-object fast
2134 // paths use the SAME proven type the VM
2135 // uses, instead of mis-classifying a v2
2136 // heap pointer as a NaN-boxed v1 value.
2137 if let Some(ct) = inferred_param_cts
2138 .and_then(|v| v.get(i))
2139 .and_then(|opt| opt.clone())
2140 {
2141 concrete_types[idx] = ct;
2142 }
2143 }
2144 }
2145 }
2146 }
2147 per_fn.push(concrete_types);
2148 } else {
2149 per_fn.push(Vec::new());
2150 }
2151 }
2152 self.program.function_local_concrete_types = per_fn;
2153
2154 // Closure-spec Phase H1: build a `function_id → ClosureLayout` side
2155 // table for the JIT worker. `emit_heap_closure` consumes this to lay
2156 // out captures at their natural-width offsets without going through
2157 // the `jit_make_closure` FFI. Closure spec §14.6 (H6.5) moves this
2158 // ABOVE `build_content_addressed_program` so the layouts propagate
2159 // through the `ContentAddressedProgram` → `LinkedProgram` →
2160 // `BytecodeProgram` path into the VM's producer.
2161 //
2162 // Track A.1C.2: the compiler derives per-capture `CaptureKind`s
2163 // from the source binding form (see `compile_expr_closure`) and
2164 // stores them in `closure_capture_kinds`. For each closure literal
2165 // we rebuild the layout so the `capture_kinds` vector reflects
2166 // those kinds AND the `owned_mutable_capture_mask` /
2167 // `shared_capture_mask` bits are flipped for the corresponding
2168 // capture indices. `op_make_closure` reads those masks to pick
2169 // the per-capture allocation discipline:
2170 // * `CaptureKind::Immutable` — write the capture bits as-is
2171 // at the typed offset.
2172 // * `CaptureKind::OwnedMutable` — `Box::into_raw` a fresh
2173 // `Box<ValueWord>` around the stack value, write the pointer.
2174 // * `CaptureKind::Shared` — the stack value carries the
2175 // raw `*const SharedCell` pointer bits of a previously-
2176 // promoted outer slot. `op_make_closure` does
2177 // `Arc::increment_strong_count` to give the closure its own
2178 // refcount share, then writes the same pointer.
2179 //
2180 // This was gated to "masks stay zero" during A.1C partial so the
2181 // legacy `HeapValue::Closure + SharedCell` fallback could keep
2182 // running while the compiler migration was incomplete. With
2183 // A.1C.2 rerouting the outer-scope var lifecycle onto
2184 // `AllocSharedLocal` / `LoadSharedLocal` / `StoreSharedLocal` /
2185 // `DropSharedLocal` and the closure-body reads/writes onto
2186 // `Load/StoreSharedCapture` and `Load/StoreOwnedMutableCapture`,
2187 // the Raw-path guard can flip bits freely — there is no longer
2188 // any SharedCell-wrapped ValueWord sitting on the stack at
2189 // closure-creation time.
2190 {
2191 use shape_value::v2::closure_layout::{CaptureKind, ClosureLayout};
2192 let total_fns = self.program.functions.len();
2193 let mut layouts: Vec<Option<std::sync::Arc<ClosureLayout>>> = vec![None; total_fns];
2194 // Map function index → per-capture CaptureKind vector.
2195 let kinds_by_fn: std::collections::HashMap<u16, &Vec<CaptureKind>> = self
2196 .closure_capture_kinds
2197 .iter()
2198 .map(|(fid, kinds)| (*fid, kinds))
2199 .collect();
2200 for (fn_idx, type_id) in self.closure_type_ids.iter().copied() {
2201 if let Some(registry_layout) = self.closure_registry.get(type_id) {
2202 if (fn_idx as usize) < total_fns {
2203 // Track A.1C.3: authoritative per-function kinds.
2204 // Both `Shared` AND `OwnedMutable` captures flip
2205 // their corresponding mask bits; `op_make_closure`
2206 // allocates `Box::into_raw(Box::new(initial))` for
2207 // OwnedMutable slots and `Arc::into_raw(Arc::new(
2208 // parking_lot::Mutex<ValueWord>))` / `Arc::increment_
2209 // strong_count` for Shared slots. Module-binding
2210 // `var` captures (migrated in A.1C.3) are also
2211 // Shared and follow the same closure-side
2212 // allocation discipline; the outer-scope promotion
2213 // emits `AllocSharedModuleBinding` (vs.
2214 // `AllocSharedLocal` for locals).
2215 let per_fn_kinds = kinds_by_fn.get(&fn_idx);
2216 let layout_arc = if let Some(kinds) = per_fn_kinds
2217 && kinds.len() == registry_layout.capture_types.len()
2218 {
2219 let rebuilt = ClosureLayout::from_capture_types(
2220 ®istry_layout.capture_types,
2221 kinds,
2222 );
2223 // Preserve the authoritative per-capture
2224 // `capture_kinds` for diagnostics and
2225 // A.1D/E JIT lowering.
2226 let mut rebuilt = rebuilt;
2227 rebuilt.capture_kinds = (*kinds).clone();
2228 std::sync::Arc::new(rebuilt)
2229 } else {
2230 std::sync::Arc::new(registry_layout.clone())
2231 };
2232 layouts[fn_idx as usize] = Some(layout_arc);
2233 }
2234 }
2235 }
2236 self.program.closure_function_layouts = layouts;
2237 }
2238
2239 // Finalize the __main__ blob and build the content-addressed program.
2240 self.build_content_addressed_program();
2241
2242 // Transfer content-addressed program to the bytecode output.
2243 self.program.content_addressed = self.content_addressed_program.take();
2244 if self.program.functions.is_empty() {
2245 self.program.function_blob_hashes.clear();
2246 } else {
2247 if self.function_hashes_by_id.len() < self.program.functions.len() {
2248 self.function_hashes_by_id
2249 .resize(self.program.functions.len(), None);
2250 } else if self.function_hashes_by_id.len() > self.program.functions.len() {
2251 self.function_hashes_by_id
2252 .truncate(self.program.functions.len());
2253 }
2254 self.program.function_blob_hashes = self.function_hashes_by_id.clone();
2255 }
2256
2257 // Transfer source text for error messages
2258 if let Some(source) = self.source_text {
2259 // Set in legacy field for backward compatibility
2260 self.program.debug_info.source_text = source.clone();
2261 // Also set in source map if not already set
2262 if self.program.debug_info.source_map.files.is_empty() {
2263 self.program
2264 .debug_info
2265 .source_map
2266 .add_file("<main>".to_string());
2267 }
2268 if self.program.debug_info.source_map.source_texts.is_empty() {
2269 self.program
2270 .debug_info
2271 .source_map
2272 .set_source_text(0, source);
2273 }
2274 }
2275
2276 // v0.3 Phase 4b Round 5 W17.2-A — post-inference `FieldType::Any`
2277 // boundary verification. Per user 2026-05-18 binding ("after the
2278 // pass, any needs to be gone, if not it is a compile time error")
2279 // + audit §5 / §8 / §9.B.1 / §9.B.3 + user 2026-05-19 R5a 5-
2280 // parallel ratify (transitional whitelist §4.D.1-9 + permanent
2281 // whitelist §4.D.10-15). The verification pass walks the post-
2282 // inference `type_schema_registry` and surfaces E0900 for any
2283 // `FieldType::Any` outside the named-exception classes. ADR-006
2284 // §2.7.5 (producer-side stamp) + §2.7.26 (parallel-`field_kinds`
2285 // carrier for the permanent classes) anchor the discipline.
2286 crate::compiler::post_inference_verify::verify_no_post_inference_any(
2287 &self.program,
2288 )?;
2289
2290 Ok(self.program)
2291 }
2292
2293 /// Compile a program to bytecode with source text for error messages
2294 pub fn compile_with_source(
2295 mut self,
2296 program: &Program,
2297 source: &str,
2298 ) -> Result<BytecodeProgram> {
2299 self.set_source(source);
2300 self.compile(program)
2301 }
2302
2303 /// Compile a program using the module graph for import resolution.
2304 ///
2305 /// This is the graph-driven compilation pipeline. Modules compile in
2306 /// topological order using the graph for cross-module name resolution.
2307 /// No AST inlining occurs — each module's imports are resolved from
2308 /// the graph's `ResolvedImport` entries.
2309 pub fn compile_with_graph(
2310 self,
2311 root_program: &Program,
2312 graph: std::sync::Arc<crate::module_graph::ModuleGraph>,
2313 ) -> Result<BytecodeProgram> {
2314 self.compile_with_graph_and_prelude(root_program, graph, &[])
2315 }
2316
2317 /// Compile with graph and prelude information.
2318 ///
2319 /// All modules (including prelude dependencies) compile uniformly
2320 /// through the normal module path. The `prelude_paths` parameter is
2321 /// retained for API compatibility but no longer used.
2322 pub fn compile_with_graph_and_prelude(
2323 mut self,
2324 root_program: &Program,
2325 graph: std::sync::Arc<crate::module_graph::ModuleGraph>,
2326 _prelude_paths: &[String],
2327 ) -> Result<BytecodeProgram> {
2328 use crate::module_graph::ModuleSourceKind;
2329
2330 self.module_graph = Some(graph.clone());
2331
2332 // Phase 1: Compile dependency modules in topological order.
2333 for &dep_id in graph.topo_order() {
2334 let dep_node = graph.node(dep_id);
2335 match dep_node.source_kind {
2336 ModuleSourceKind::NativeModule => {
2337 self.register_graph_imports_for_module(dep_id, &graph)?;
2338 }
2339 ModuleSourceKind::ShapeSource | ModuleSourceKind::Hybrid => {
2340 self.compile_module_from_graph(dep_id, &graph)?;
2341 }
2342 ModuleSourceKind::CompiledBytecode => {
2343 // Should have been rejected during graph construction.
2344 return Err(shape_ast::error::ShapeError::ModuleError {
2345 message: format!(
2346 "Module '{}' is only available as pre-compiled bytecode",
2347 dep_node.canonical_path
2348 ),
2349 module_path: None,
2350 });
2351 }
2352 }
2353 }
2354
2355 // Phase 2: Compile the root module using the graph for its imports.
2356 // NOTE: root's imports are registered INSIDE `compile()` after the
2357 // `__main__` blob builder starts, so any emitted Load/Store for
2358 // namespace-alias bindings (e.g. `use std::core::set` creates a
2359 // runtime copy from canonical binding `std::core::set` to alias
2360 // binding `set`) lands inside `__main__`. Registering them here —
2361 // before `compile()` opens the `__main__` blob — would leave those
2362 // instructions in an unreachable gap between module bodies and
2363 // `__main__`'s entry point.
2364
2365 // Strip import items from root program (imports already resolved via graph)
2366 let mut stripped_program = root_program.clone();
2367 stripped_program
2368 .items
2369 .retain(|item| !matches!(item, shape_ast::ast::Item::Import(..)));
2370
2371 // Compile the stripped root program using the standard two-pass pipeline
2372 self.compile(&stripped_program)
2373 }
2374
2375 /// Compile a single module from the graph.
2376 ///
2377 /// All modules (including prelude dependencies) compile uniformly:
2378 /// pushes the module scope, qualifies items, registers all symbol kinds,
2379 /// compiles bodies, creates module binding object.
2380 fn compile_module_from_graph(
2381 &mut self,
2382 module_id: crate::module_graph::ModuleId,
2383 graph: &crate::module_graph::ModuleGraph,
2384 ) -> Result<()> {
2385 let node = graph.node(module_id);
2386 let ast = match &node.ast {
2387 Some(ast) => ast.clone(),
2388 None => return Ok(()), // NativeModule / CompiledBytecode
2389 };
2390
2391 let module_path = node.canonical_path.clone();
2392
2393 // All modules compile uniformly through the normal module path.
2394 // Set allow_internal_builtins for stdlib modules.
2395 let prev_allow = self.allow_internal_builtins;
2396 if module_path.starts_with("std::") {
2397 self.allow_internal_builtins = true;
2398 }
2399
2400 self.module_scope_stack.push(module_path.clone());
2401
2402 // 1. Register this module's imports from the graph
2403 self.register_graph_imports_for_module(module_id, graph)?;
2404
2405 // 2. Filter out import statements, qualify remaining items
2406 let mut qualified_items = Vec::new();
2407 for item in &ast.items {
2408 if matches!(item, shape_ast::ast::Item::Import(..)) {
2409 continue;
2410 }
2411 qualified_items.push(self.qualify_module_item(item, &module_path)?);
2412 }
2413
2414 // 3. Phase 1: Register functions in global table with qualified names
2415 for item in &qualified_items {
2416 self.register_missing_module_items(item)?;
2417 }
2418
2419 // 4. Phase 2: Compile function bodies
2420 self.non_function_mir_context_stack
2421 .push(module_path.clone());
2422 let compile_result = (|| -> Result<()> {
2423 for (idx, qualified) in qualified_items.iter().enumerate() {
2424 let future_names = self
2425 .future_reference_use_names_for_remaining_items(&qualified_items[idx + 1..]);
2426 self.push_future_reference_use_names(future_names);
2427 let result = self.compile_item_with_context(qualified, false);
2428 self.pop_future_reference_use_names();
2429 result?;
2430 self.release_unused_module_reference_borrows_for_remaining_items(
2431 &qualified_items[idx + 1..],
2432 );
2433 }
2434 Ok(())
2435 })();
2436 self.non_function_mir_context_stack.pop();
2437 compile_result?;
2438
2439 // 5. Build module object and store in canonical binding
2440 let exports = self.collect_module_runtime_exports(
2441 &ast.items
2442 .iter()
2443 .filter(|i| !matches!(i, shape_ast::ast::Item::Import(..)))
2444 .cloned()
2445 .collect::<Vec<_>>(),
2446 &module_path,
2447 );
2448 let span = shape_ast::ast::Span::default();
2449 let entries: Vec<shape_ast::ast::ObjectEntry> = exports
2450 .into_iter()
2451 .map(|(name, value_ident)| shape_ast::ast::ObjectEntry::Field {
2452 key: name,
2453 value: shape_ast::ast::Expr::Identifier(value_ident, span),
2454 type_annotation: None,
2455 })
2456 .collect();
2457 let module_object = shape_ast::ast::Expr::Object(entries, span);
2458 self.compile_expr(&module_object)?;
2459
2460 let binding_idx = self.get_or_create_module_binding(&module_path);
2461 self.emit(Instruction::new(
2462 OpCode::StoreModuleBinding,
2463 Some(Operand::ModuleBinding(binding_idx)),
2464 ));
2465 self.propagate_initializer_type_to_slot(binding_idx, false, false);
2466
2467 self.module_scope_stack.pop();
2468 self.allow_internal_builtins = prev_allow;
2469 Ok(())
2470 }
2471
2472 /// Compile an imported module's AST to a standalone BytecodeProgram.
2473 ///
2474 /// This takes the Module's AST (Program), compiles all exported functions
2475 /// to bytecode, and returns the compiled program along with a mapping of
2476 /// exported function names to their function indices in the compiled output.
2477 ///
2478 /// The returned `BytecodeProgram` and function name mapping allow the import
2479 /// handler to resolve imported function calls to the correct bytecode indices.
2480 ///
2481 /// Currently handles function exports only. Types and values can be added later.
2482 pub fn compile_module_ast(
2483 module_ast: &Program,
2484 ) -> Result<(BytecodeProgram, HashMap<String, usize>)> {
2485 let mut compiler = BytecodeCompiler::new();
2486 // Stdlib modules need access to __* builtins (intrinsics, into, etc.)
2487 compiler.allow_internal_builtins = true;
2488 let bytecode = compiler.compile(module_ast)?;
2489
2490 // Build name → function index mapping for exported functions
2491 let mut export_map = HashMap::new();
2492 for (idx, func) in bytecode.functions.iter().enumerate() {
2493 export_map.insert(func.name.clone(), idx);
2494 }
2495
2496 Ok((bytecode, export_map))
2497 }
2498}