Skip to main content

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                    &param_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, &param_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                                &registry_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}