midenc-codegen-masm 0.10.0

Miden Assembly backend for the Miden compiler
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
#[cfg(test)]
mod tests;

use alloc::{
    collections::{BTreeMap, BTreeSet},
    sync::Arc,
    vec::Vec,
};

use miden_assembly::{PathBuf as LibraryPath, ast::InvocationTarget};
use miden_assembly_syntax::{
    ast::{Attribute, DebugVarLocation},
    parser::WordValue,
};
use miden_core::serde::Deserializable;
use midenc_hir::{
    FunctionIdent, Op, OpExt, SourceSpan, Span, Symbol, TraceTarget, Type, ValueRef,
    diagnostics::IntoDiagnostic,
    dialects::{
        builtin,
        debuginfo::attributes::{Expression, ExpressionOp, FrameBase, SubprogramAttr},
    },
    interner,
    pass::AnalysisManager,
};
use midenc_hir_analysis::analyses::LivenessAnalysis;
use midenc_session::diagnostics::{Report, Spanned, WrapErr};
use smallvec::SmallVec;

use crate::{
    Event, OperandStack,
    artifact::MasmComponent,
    emitter::BlockEmitter,
    linker::{FunctionTableLayout, LinkInfo, Linker},
    masm,
};

/// The generated procedure each module uses to fill the function-table slots whose callees it
/// defines.
///
/// One procedure per module, rather than one for the component, because `procref` on a private
/// procedure is only legal within its defining module — and a callee's visibility is its
/// author's decision, not something initialization gets to widen. A module's procedure also
/// invokes the procedures of the modules nested within it, so the component's `init` only has
/// to reach the top-level ones; that is the shape a single component-global table would want.
const FUNCTION_TABLE_INIT_PROC: &str = "__init_function_table";

/// The private canonical-ABI entry body generated only for executable dispatch after `main` has
/// already initialized the component.
const EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC: &str = "__midenc_entrypoint_without_init";

/// This trait represents a conversion pass from some HIR entity to a Miden Assembly component.
pub trait ToMasmComponent {
    fn to_masm_component(&self, analysis_manager: AnalysisManager)
    -> Result<MasmComponent, Report>;
}

/// Derivation of a MASM component from an HIR world
///
/// A world is not a component, and the difference is what this impl exists to handle: a
/// component's body holds modules, interfaces and functions, while a world's body holds
/// *components* as well. Handing a world's own operation to `MasmComponentBuilder`, which walks a
/// component body, therefore panics the moment it meets the first `builtin.component`.
///
/// So the shape of the world decides how it is lowered:
///
/// - A world holding **no** component is treated as one logical component whose body is the
///   world's, which is what it has always meant here. This is the shape `frontend/masm`'s
///   disassembler produces — it defines modules directly on the world — so it is a live path.
/// - A world holding **one** component is lowered by lowering that component, because a
///   component is what a Miden package is rooted at and carries the identity it is rooted at.
///   Delegating rather than reimplementing is deliberate: the result is then the same
///   [`MasmComponent`] the equivalent standalone `builtin.component` produces, by construction
///   rather than by two implementations agreeing.
/// - A world holding **more than one** component is reported, and that limitation is external to
///   this crate — see `too_many_components`.
///
/// # Top-level items beside the component are normal, and are not an error
///
/// A world is not "a component, optionally". It may hold a component — the current codegen unit —
/// **plus any number of sibling interfaces and modules**, which are either
///
/// - *external dependencies represented in the IR*, which hold declarations only and contribute
///   nothing to the generated Miden Assembly, or
/// - *supporting modules*, which are translated 1:1 to Miden Assembly modules and linked into the
///   final assembly as ad-hoc modules.
///
/// A world holding a single component is only the *happy path*, and only for the Rust frontend,
/// which compiles to one Wasm component and translates it to one HIR component. Other frontends,
/// the MASM one included, legitimately produce several top-level items. **Neither kind of sibling
/// may fail a build.**
///
/// The first kind is recognized by `is_declaration_only` and ignored, silently, because that is
/// exactly what it is worth. The second is translated beside the component, by handing it to the
/// same `MasmComponentBuilder` the component is lowered by — which is what makes it share the
/// component's `LinkInfo` rather than lay a layout of its own over it.
///
/// The one shape left out is a top-level module that **owns memory**, i.e. declares a global
/// variable or a data segment; see `report_siblings_that_own_memory` for the rule and why it is
/// where the line falls.
///
/// Every producer hands this impl a *top-level* world: the world a whole-`builtin.world` `.hir`
/// file parses to, the world `midenc_hir::parse` anchors any other top-level operation at, the
/// world the Wasm frontend builds, and the one `frontend/masm`'s disassembler builds.
impl ToMasmComponent for builtin::World {
    fn to_masm_component(
        &self,
        analysis_manager: AnalysisManager,
    ) -> Result<MasmComponent, Report> {
        let mut components = Vec::new();
        let mut siblings = Vec::new();
        for op in self.body().entry().body().iter() {
            match op.as_operation_ref().try_downcast_op::<builtin::Component>() {
                Ok(component) => components.push(component),
                Err(op) => siblings.push(op),
            }
        }

        match components.len() {
            0 => world_body_to_masm_component(self, analysis_manager),
            1 => {
                let (supporting, owns_memory) = classify_siblings(&siblings);
                // The analysis manager is rooted at the world, and `AnalysisManager::nest`
                // accepts any proper descendant, so the component impl can nest at its own
                // modules — and at these siblings, which are children of the world — exactly as
                // it does when codegen anchors it at the component itself.
                let lowered = component_to_masm_component(
                    &components[0].borrow(),
                    analysis_manager,
                    &supporting,
                )?;
                // Reported after lowering succeeded, so that a build which failed for an
                // unrelated reason is not also told about a limitation it never reached.
                report_siblings_that_own_memory(self, &owns_memory);
                Ok(lowered)
            }
            _ => Err(too_many_components(self, &components)),
        }
    }
}

/// Whether `op`, a top-level item of a world, contributes nothing to the generated Miden Assembly.
///
/// This is how an *external dependency represented in the IR* is told apart from a *supporting
/// module*: the former holds declarations only. There is no flag for it — `Symbol::is_declaration`
/// is defined on functions and global variables but not on the modules and interfaces that hold
/// them, so the question has to be asked of the contents.
///
/// Deliberately conservative: anything unrecognized counts as carrying definitions. Guessing wrong
/// in that direction produces a warning about something that did not need one, while guessing
/// wrong in the other direction silently omits code.
fn is_declaration_only(op: &midenc_hir::OperationRef) -> bool {
    /// A body defines nothing if every item in it is itself only a declaration.
    ///
    /// An empty body is vacuously declaration-only, which is the answer we want: an empty module
    /// would lower to an empty Miden Assembly module.
    fn body_is_all_declarations(region: &midenc_hir::Region) -> bool {
        region.entry().body().iter().all(|item| {
            if let Some(function) = item.downcast_ref::<builtin::Function>() {
                function.is_declaration()
            } else if let Some(gv) = item.downcast_ref::<builtin::GlobalVariable>() {
                gv.is_declaration()
            } else {
                // A `builtin::Segment` initializes memory, and so does anything unrecognized as
                // far as this predicate is willing to assume.
                false
            }
        })
    }

    if let Ok(module) = op.try_downcast_op::<builtin::Module>() {
        let module = module.borrow();
        body_is_all_declarations(&module.body())
    } else if let Ok(interface) = op.try_downcast_op::<builtin::Interface>() {
        let interface = interface.borrow();
        body_is_all_declarations(&interface.body())
    } else if let Ok(function) = op.try_downcast_op::<builtin::Function>() {
        let function = function.borrow();
        function.is_declaration()
    } else {
        false
    }
}

/// Whether `module` **owns memory**, i.e. declares a `builtin::GlobalVariable` or a
/// `builtin::Segment`.
///
/// Those two are exactly the items [`Linker::link`] scans a component's modules for in order to
/// compute its data layout, and that correspondence is the whole reason a module which owns none
/// of them is safe to translate beside a component: it contributes nothing to the layout, so there
/// is nothing of the component's for it to overlay. A module which *does* own one is not safe,
/// because `Linker::link` walks only the direct module children of the component it is given and
/// so cannot see a sibling of the *world* — see `report_siblings_that_own_memory`.
///
/// Deliberately conservative in two places, both so that the failure mode stays *diagnosed* rather
/// than *silently mistranslated*:
///
/// - **Anything unrecognized counts as owning memory.** A module body is only ever legal here if
///   it holds functions, global variables, segments, function tables and nested modules —
///   `MasmModuleBuilder::build` panics on anything else — so an item this does not recognize is
///   one that cannot be translated anyway. Treating it as owning memory is therefore also what
///   makes the answer right *at any depth*: the only container that could hold a global or a
///   segment deeper down is a nested `builtin::Module`, which is not recognized here and so does
///   not get past this. That is deliberately stricter than lowering now requires — a nested module
///   can be lowered, but a top-level sibling holding one still has no parent component to own
///   whatever memory that nesting hides, and this predicate cannot see that far.
/// - **A declared global counts, not just a defined one.** [`Linker::link`] skips declarations
///   when building the layout, so this is strictly stricter than the overlay hazard requires. It
///   is the right side to err on: a declaration whose definition is elsewhere is absent from the
///   component's layout, so lowering a use of it would panic in
///   `GlobalVariableLayout::get_computed_addr` — and it is a global variable declared by a module
///   with no parent component either way, which is the rule being enforced.
fn module_owns_memory(module: &builtin::Module) -> bool {
    module.body().entry().body().iter().any(|item| {
        // The two items a component owns, which are the two `Linker::link` scans for.
        if item.is::<builtin::GlobalVariable>() || item.is::<builtin::Segment>() {
            return true;
        }
        // Plus anything this cannot place, which includes a nested `builtin::Module` — the only
        // item that could hide a global or a segment deeper down — so this arm is the conservative
        // one, not a third case.
        !item.is::<builtin::Function>()
    })
}

/// Split a world's top-level items, beside its component, into the ones to translate and the ones
/// to report.
///
/// Three outcomes, and everything a world can hold falls into one of them:
///
/// - an item holding only declarations is an *external dependency represented in the IR*, and is
///   dropped here — silently, because that is exactly what it is worth. This is asked *first*, so
///   a stub module whose declarations happen to include a global variable is ignored rather than
///   reported: it contributes nothing at all, which is a stronger statement than owning no memory;
/// - a `builtin::Module` owning no memory is a *supporting module*, and is returned to be
///   translated 1:1 beside the component;
/// - everything else is returned to be reported. That includes a module which owns memory, and
///   also every non-module item, which is not an oversight: `MasmComponentBuilder::define_interface`
///   places an interface *under the component's path* when the component has an id, so a world
///   sibling translated through it would be silently relocated into a namespace it does not belong
///   to. Reporting is the conservative answer until that seam is taught the difference.
fn classify_siblings(
    siblings: &[midenc_hir::OperationRef],
) -> (SmallVec<[builtin::ModuleRef; 4]>, SmallVec<[midenc_hir::OperationRef; 4]>) {
    let mut supporting = SmallVec::<[builtin::ModuleRef; 4]>::new();
    let mut owns_memory = SmallVec::<[midenc_hir::OperationRef; 4]>::new();
    for op in siblings.iter().copied() {
        if is_declaration_only(&op) {
            continue;
        }
        match op.try_downcast_op::<builtin::Module>() {
            Ok(module) if !module_owns_memory(&module.borrow()) => supporting.push(module),
            _ => owns_memory.push(op),
        }
    }
    (supporting, owns_memory)
}

/// Warn about top-level items beside a component that own memory — plus, per `classify_siblings`,
/// the ones this crate treats as if they did — and are therefore left out.
///
/// The rule, which is what the message and help exist to teach:
///
/// > Global variables and data segments are owned by a *component*. It is the component that lays
/// > out memory for them, assigns each one its address, and emits the `init` that writes them
/// > there. A module declared at the top level of a world has no parent component, so there is
/// > nothing to own its memory — which makes declaring either of them there meaningless rather
/// > than merely unsupported.
///
/// The mechanism agrees with the rule, which is why the line falls here rather than anywhere else.
/// [`Linker::link`] walks only the direct `builtin::Module` children of the operation it is handed,
/// so the [`LinkInfo`] computed for the component — `link(Some(id), <the component>)` — cannot see
/// a sibling of the *world*. `LinkInfo` is what assigns every global its address and every segment
/// its offset, so a sibling that owned memory and was lowered anyway would have to be given a
/// layout of its own, laid straight over the component's. A sibling that owns none is invisible to
/// that computation in the only sense that matters: it contributes nothing to it, so sharing the
/// component's `LinkInfo` is not an approximation but the exact answer.
///
/// Declaration-only siblings are *not* reported. They are ignored by design, and warning about
/// them would make the normal case noisy.
fn report_siblings_that_own_memory(world: &builtin::World, siblings: &[midenc_hir::OperationRef]) {
    if siblings.is_empty() {
        return;
    }

    let mut diagnostic = world
        .as_operation()
        .context()
        .diagnostics()
        .diagnostic(miden_assembly::diagnostics::Severity::Warning)
        .with_message(
            "a top-level module beside a component cannot own global variables or data segments",
        );
    // The first label has to be the primary one; the builder asserts on that ordering.
    for (index, op) in siblings.iter().enumerate() {
        let op = op.borrow();
        let label = format!("this '{}' is omitted from the generated package", op.name());
        diagnostic = if index == 0 {
            diagnostic.with_primary_label(op.span(), label)
        } else {
            diagnostic.with_secondary_label(op.span(), label)
        };
    }
    diagnostic
        .with_help(
            "global variables and data segments belong to a component: the component is what lays \
             out memory for them and emits the code that initializes it. A module declared at the \
             top level of a world has no parent component to own them, so this build omits the \
             module, and code that calls into it will fail to resolve. A top-level module that \
             declares neither is a supporting module, and is translated 1:1 to a Miden Assembly \
             module and linked into the final assembly as an ad-hoc module. Top-level items that \
             only declare symbols — external dependencies represented in the IR — contribute no \
             Miden Assembly and are ignored by design; they are not reported here. Any other \
             top-level item is reported here as well, rather than translated on a guess.",
        )
        .emit();
}

/// The report for a world declaring more than one component.
///
/// The **one** shape this impl rejects, and the blocker is external to this crate rather than a
/// gap in it: a Miden package's metadata can currently describe a single component, so a build
/// emits one component per package. Two components in a world would have to become two packages.
/// Work on multi-component packages is happening elsewhere; until it lands there is nothing this
/// crate could do with the second component but invent merge semantics, which would be worse than
/// saying so.
///
/// Note what this is *not*: a claim that worlds are single-component by nature. They are not, and
/// sibling interfaces and modules are ordinary — see the docs on `ToMasmComponent for
/// builtin::World`. Only a second *component* stops a build.
///
/// The wording matters as much as the rejection, so the message says what is unimplemented and the
/// help says who is unblocking it, rather than implying the input is wrong.
fn too_many_components(world: &builtin::World, components: &[builtin::ComponentRef]) -> Report {
    // The limitation belongs in the *message*, not only in the help: a `Report` built from a
    // diagnostic renders its message alone under `Display`, which is all a caller that only
    // formats the error ever sees.
    let mut diagnostic = world
        .as_operation()
        .context()
        .diagnostics()
        .diagnostic(miden_assembly::diagnostics::Severity::Error)
        .with_message(format!(
            "lowering a world containing {} components is not yet implemented",
            components.len()
        ))
        .with_primary_label(world.span(), "in this world");
    for component in components {
        let component = component.borrow();
        diagnostic = diagnostic.with_secondary_label(component.span(), "this component");
    }
    diagnostic
        .with_help(
            "this is a known limitation of the compiler rather than a problem with this input: a \
             Miden package's metadata can currently describe only one component, so a build emits \
             one component per package. Support for multiple components in a package is being \
             worked on; until it lands, compile each component separately.",
        )
        .into_report()
}

/// Report a function that reached codegen with no body.
///
/// Unlike [`too_many_components`], this says the **input is invalid**, not that the compiler is
/// incomplete — and it is worth being precise about why, because the three facts below are what a
/// future reader needs and none of them is obvious from the code.
///
/// **Nothing can ever provide the definition.** A body-less function is a declaration: it names a
/// procedure whose implementation is expected to come from somewhere else. Miden Assembly has no
/// such somewhere else at this point — there is no later link step that could supply it — so a
/// declaration surviving into codegen names a procedure that will never exist.
///
/// **Why a surviving declaration is assumed to be referenced.** Dead symbol elimination would
/// strip a declaration nothing refers to. There is no such pass today, but when there is, an
/// unreferenced declaration will not reach here — so a declaration that *does* reach here is one
/// something referred to, which is exactly the case that cannot be satisfied. That is why this is
/// an error rather than an item to skip: skipping it would emit a module whose callers reference a
/// procedure it does not define, and the failure would surface at link time with nothing to point
/// at.
///
/// **Why this is not checked before codegen.** It is an invariant of Miden Assembly, not of the
/// IR. A body-less `builtin::Function` is a perfectly well-formed operation — verification cannot
/// reject it without rejecting every legitimate declaration — so the check can only live where the
/// IR is being turned into something that has to be complete. Moving it into verification will not
/// work; this is the note that saves the next person the attempt.
fn function_without_a_body(function: &builtin::Function) -> Report {
    // The reason belongs in the *message*, not only the help: a `Report` built from a diagnostic
    // renders its message alone under `Display`, which is all a caller that merely formats the
    // error ever sees. See the same note on `too_many_components`.
    function
        .as_operation()
        .context()
        .diagnostics()
        .diagnostic(miden_assembly::diagnostics::Severity::Error)
        .with_message(
            "cannot emit masm for a function with no body: nothing can provide its definition",
        )
        .with_primary_label(function.span(), "this function is declared but never defined")
        .with_help(
            "a declaration names a procedure whose implementation comes from elsewhere, and Miden \
             Assembly has no later step that could supply one. Either give this function a body, \
             or remove it along with whatever refers to it.",
        )
        .into_report()
}

/// Derive a MASM component by treating `world`'s body as a component body.
///
/// The meaning a world has always had here, and correct only when the world declares no
/// component of its own: every definition-carrying module in it belongs to one logical
/// component, which has no identity beyond the namespace those modules sit in.
fn world_body_to_masm_component(
    world: &builtin::World,
    analysis_manager: AnalysisManager,
) -> Result<MasmComponent, Report> {
    // Get the current compiler context
    let context = world.as_operation().context_rc();

    // Run the linker for this component in order to compute its data layout
    let link_info = Linker::default().link(None, world.as_operation()).map_err(Report::msg)?;

    // Get the entrypoint, if specified
    let entrypoint = match context.session().options.entrypoint.as_deref() {
        Some(entry) => {
            let entry_id = entry
                .parse::<FunctionIdent>()
                .map_err(|_| Report::msg(format!("invalid entrypoint identifier: '{entry}'")))?;
            let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
                entry_id.function.span,
                entry_id.function.as_str().into(),
            )));

            let path = LibraryPath::new(entry_id.module.as_str()).into_diagnostic()?;
            let qualified = masm::QualifiedProcedureName::new(path.as_path(), name);
            Some(masm::InvocationTarget::Path(Span::new(
                entry_id.function.span,
                qualified.into_inner(),
            )))
        }
        None => None,
    };
    let executable_entrypoint = classify_marked_canonical_abi_entrypoint(
        world.as_operation_ref(),
        &[],
        &link_info,
        entrypoint.as_ref(),
    )?;
    let executable_entrypoint_without_init = lower_executable_entrypoint_without_init(
        executable_entrypoint,
        &analysis_manager,
        &link_info,
    )?;

    // If we have global variables, data segments, function tables, or a core Wasm start, we will
    // require a component initializer function, as well as a module to hold component-level
    // functions such as init
    let requires_init = link_info.requires_init();
    let toplevel_namespaces = world
        .body()
        .entry()
        .body()
        .iter()
        // Only modules: this function is reached only for a world that declares no component,
        // so a `builtin::Component` arm here would be unreachable.
        .filter_map(|op| {
            if op.is::<builtin::Module>() {
                Some(op.as_operation_ref())
            } else {
                None
            }
        })
        .collect::<Vec<_>>();
    let init = if requires_init {
        let name = masm::ProcedureName::new("init").unwrap();
        let qualified = match toplevel_namespaces.len() {
            1 => {
                let namespace = toplevel_namespaces[0].borrow().symbol_name_if_symbol().unwrap();
                masm::QualifiedProcedureName::new(format!("::{namespace}").as_str(), name)
            }
            _ => masm::QualifiedProcedureName::new("::init", name),
        };
        Some(masm::InvocationTarget::Path(Span::new(
            SourceSpan::default(),
            qualified.into_inner(),
        )))
    } else {
        None
    };

    // Define the initial component modules set
    //
    // The top-level component module is always defined, but may be empty
    let root = match toplevel_namespaces.len() {
        1 => {
            let namespace = toplevel_namespaces[0].borrow().symbol_name_if_symbol().unwrap();
            Arc::from(
                masm::PathBuf::new(&format!("::{namespace}"))
                    .expect("invalid namespace")
                    .into_boxed_path(),
            )
        }
        _ => Arc::<masm::Path>::from(masm::Path::new("::init")),
    };
    let init_module = Arc::new(masm::Module::new(masm::ModuleKind::Library, &root));
    let modules = vec![init_module];

    let rodata = data_segments_to_rodata(&link_info)?;

    let heap_base = link_info.heap_base();
    let stack_pointer = link_info.globals_layout().stack_pointer_offset();
    let mut masm_component = MasmComponent {
        id: None,
        // A world declaring no component is not the compiler's wrapper around one: it has no
        // component boundary at all.
        synthetic_wrapper: false,
        root,
        init,
        entrypoint,
        executable_entrypoint_without_init,
        rodata,
        heap_base,
        stack_pointer,
        modules,
    };
    let builder = MasmComponentBuilder {
        analysis_manager,
        component: &mut masm_component,
        link_info: &link_info,
        source_manager: context.session().source_manager.clone(),
        init_body: Default::default(),
        invoked_from_init: Default::default(),
    };

    // A world declaring no component has no siblings *beside* one: every top-level module in it
    // is part of the one logical component this treats its body as.
    builder.build(world.as_operation(), &[])?;

    Ok(masm_component)
}

/// 1:1 conversion from HIR component to MASM component
impl ToMasmComponent for builtin::Component {
    fn to_masm_component(
        &self,
        analysis_manager: AnalysisManager,
    ) -> Result<MasmComponent, Report> {
        component_to_masm_component(self, analysis_manager, &[])
    }
}

/// Derive a MASM component from `component`, and from `supporting` beside it.
///
/// `supporting` is empty for a standalone `builtin.component`, which has no siblings; it is
/// non-empty only when the component was reached through the world that declares it, and holds
/// that world's supporting modules — see `ToMasmComponent for builtin::World`.
///
/// They are lowered *here*, rather than by a second pass of their own, for one reason: the
/// [`LinkInfo`] computed below is the component's data layout, and a supporting module has to be
/// lowered against it. `classify_siblings` is what makes that exact rather than approximate — a
/// module that owns no memory contributes nothing to the layout, so the component's `LinkInfo` is
/// the same one a link over the whole world would have produced.
fn component_to_masm_component(
    component: &builtin::Component,
    analysis_manager: AnalysisManager,
    supporting: &[builtin::ModuleRef],
) -> Result<MasmComponent, Report> {
    // Get the current compiler context
    let context = component.as_operation().context_rc();

    // Whether this component is one the compiler invented to wrap a bare core module, which it
    // says by carrying a marker the frontend set rather than by its id — see
    // `builtin::Component::SYNTHETIC_WRAPPER_ATTR`.
    let synthetic_wrapper = component.is_synthetic_wrapper();

    // Run the linker for this component in order to compute its data layout
    let id = component.id();
    let link_info = Linker::default()
        .link(Some(id.clone()), component.as_operation())
        .map_err(Report::msg)?;

    // Get the library path of the component
    let component_path = id
        .to_library_path()
        .to_absolute()
        .map_err(|err| {
            Report::msg(format!("unable to canonicalize '{}': {err}", id.to_library_path()))
        })?
        .into_owned();

    // Get the entrypoint, if specified
    let entrypoint = match context.session().options.entrypoint.as_deref() {
        Some(entry) => {
            let entry_id = entry
                .parse::<FunctionIdent>()
                .map_err(|_| Report::msg(format!("invalid entrypoint identifier: '{entry}'")))?;
            let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
                entry_id.function.span,
                entry_id.function.as_str().into(),
            )));

            // Check if we're inside the synthetic "wrapper" component used for pure Rust
            // compilation. Since the user does not know about it, their entrypoint does not
            // include the synthetic component path. We append the user-provided path to the
            // root component path here if needed.
            //
            // TODO(pauls): Narrow this to only be true if the target env is not 'rollup', we
            // cannot currently do so because we do not have sufficient Cargo metadata yet in
            // 'cargo miden build' to detect the target env, and we default it to 'rollup'
            let path = if synthetic_wrapper {
                component_path.join(entry_id.module.as_str())
            } else {
                // We're compiling a Wasm component and the component id is included
                // in the entrypoint.
                LibraryPath::new(entry_id.module.as_str()).into_diagnostic()?
            };
            let qualified = masm::QualifiedProcedureName::new(path.as_path(), name);
            Some(masm::InvocationTarget::Path(Span::new(
                entry_id.function.span,
                qualified.into_inner(),
            )))
        }
        None => None,
    };
    let executable_entrypoint = classify_marked_canonical_abi_entrypoint(
        component.as_operation_ref(),
        supporting,
        &link_info,
        entrypoint.as_ref(),
    )?;
    let executable_entrypoint_without_init = lower_executable_entrypoint_without_init(
        executable_entrypoint,
        &analysis_manager,
        &link_info,
    )?;

    // If we have global variables, data segments, function tables, or a core Wasm start, we will
    // require a component initializer function, as well as a module to hold component-level
    // functions such as init
    let requires_init = link_info.requires_init();
    let init = if requires_init {
        let name = masm::ProcedureName::new("init").unwrap();
        let qualified = masm::QualifiedProcedureName::new(&component_path, name);
        Some(masm::InvocationTarget::Path(Span::new(
            SourceSpan::default(),
            qualified.into_inner(),
        )))
    } else {
        None
    };

    // Define the initial component modules set
    //
    // The top-level component module is always defined, but may be empty
    let root: Arc<miden_assembly_syntax::Path> = component_path.into_boxed_path().into();
    let root_module = Arc::new(masm::Module::new(masm::ModuleKind::Library, &root));
    let modules = vec![root_module];

    let rodata = data_segments_to_rodata(&link_info)?;

    let heap_base = link_info.heap_base();
    let stack_pointer = link_info.globals_layout().stack_pointer_offset();
    let mut masm_component = MasmComponent {
        id: Some(id),
        synthetic_wrapper,
        root,
        init,
        entrypoint,
        executable_entrypoint_without_init,
        rodata,
        heap_base,
        stack_pointer,
        modules,
    };
    let builder = MasmComponentBuilder {
        analysis_manager,
        component: &mut masm_component,
        link_info: &link_info,
        source_manager: context.session().source_manager.clone(),
        init_body: Default::default(),
        invoked_from_init: Default::default(),
    };

    builder.build(component.as_operation(), supporting)?;

    Ok(masm_component)
}

fn data_segments_to_rodata(link_info: &LinkInfo) -> Result<Vec<crate::Rodata>, Report> {
    use midenc_hir::constants::ConstantData;

    use crate::data_segments::{ResolvedDataSegment, merge_data_segments};
    let mut resolved = SmallVec::<[ResolvedDataSegment; 2]>::new();
    for sref in link_info.segment_layout().iter() {
        let s = sref.borrow();
        resolved.push(ResolvedDataSegment {
            offset: *s.get_offset(),
            data: s.initializer().as_slice().to_vec(),
            readonly: *s.get_readonly(),
        });
    }
    Ok(match merge_data_segments(resolved).map_err(Report::msg)? {
        None => alloc::vec::Vec::new(),
        Some(merged) => {
            let data = alloc::sync::Arc::new(ConstantData::from(merged.data));
            let felts = crate::Rodata::bytes_to_elements(data.as_slice());
            let digest = miden_core::crypto::hash::Poseidon2::hash_elements(&felts);
            alloc::vec![crate::Rodata {
                component: link_info.component().cloned().unwrap_or(builtin::ComponentId {
                    namespace: interner::Symbol::intern("root_ns"),
                    name: interner::Symbol::intern("root"),
                    version: midenc_hir::version::Version::new(1, 0, 0)
                }),
                digest,
                start: super::NativePtr::from_ptr(merged.offset),
                data,
            }]
        }
    })
}

/// Identify the selected canonical-ABI wrapper that needs a private no-init executable copy.
///
/// Generated executable `main` must remain the initialization owner so that component startup runs
/// before test-harness memory initialization. The public canonical wrapper must also retain its
/// prologue so that a direct `call` initializes its fresh MASM context. For the exact combination
/// of a component start and a selected same-component canonical wrapper, codegen therefore emits a
/// private no-init copy for `main` alone. Calling convention by itself is deliberately insufficient:
/// every unmarked input retains the existing path, regardless of its Wasm target.
fn classify_marked_canonical_abi_entrypoint(
    component: midenc_hir::OperationRef,
    supporting: &[builtin::ModuleRef],
    link_info: &LinkInfo,
    entrypoint: Option<&masm::InvocationTarget>,
) -> Result<Option<builtin::FunctionRef>, Report> {
    let (Some(_), Some(entrypoint)) = (link_info.component_start(), entrypoint) else {
        return Ok(None);
    };
    let entrypoint_path = entrypoint
        .unwrap_path()
        .to_absolute()
        .map_err(|err| Report::msg(format!("invalid executable entrypoint path: {err}")))?;

    let find_canonical_entrypoint = |root: midenc_hir::OperationRef| {
        let mut canonical_entrypoint = None;
        root.borrow().prewalk_all(|op| {
            let Some(function) = op.downcast_ref::<builtin::Function>() else {
                return;
            };
            if !function.signature().cc.is_wasm_canonical_abi() {
                return;
            }

            let function_target = super::lowering::invocation_target_from_symbol_path(
                &function.path(),
                function.span(),
            );
            if function_target.unwrap_path() == entrypoint_path.as_ref() {
                canonical_entrypoint = Some(function.as_function_ref());
            }
        });
        canonical_entrypoint
    };

    if let Some(function) = find_canonical_entrypoint(component) {
        if function.borrow().as_operation().parent_op() != Some(component) {
            let path = function.borrow().path();
            return Err(Report::msg(format!(
                "unsupported executable entrypoint '{path}': a canonical-ABI entrypoint for a \
                 component with a core Wasm start function must be defined directly in the \
                 selected component"
            )));
        }
        return Ok(Some(function));
    }

    let supporting_entrypoint = supporting
        .iter()
        .find_map(|module| find_canonical_entrypoint(module.borrow().as_operation_ref()));
    if let Some(function) = supporting_entrypoint {
        let path = function.borrow().path();
        return Err(Report::msg(format!(
            "unsupported executable entrypoint '{path}': a canonical-ABI entrypoint cannot be \
             selected for a component with a core Wasm start function because it would execute \
             component initialization twice in the same context"
        )));
    }

    Ok(None)
}

/// Lower the executable-only copy selected by [`classify_marked_canonical_abi_entrypoint`].
fn lower_executable_entrypoint_without_init(
    function: Option<builtin::FunctionRef>,
    analysis_manager: &AnalysisManager,
    link_info: &LinkInfo,
) -> Result<Option<masm::Procedure>, Report> {
    let Some(function) = function else {
        return Ok(None);
    };
    let function = function.borrow();
    let mut builder = MasmFunctionBuilder::new(&function)?;
    builder.name = masm::ProcedureName::new(EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC).unwrap();
    builder.visibility = masm::Visibility::Private;
    builder
        .build(
            &function,
            analysis_manager.nest(function.as_operation_ref()),
            link_info,
            FunctionLoweringMode::ExecutableEntrypointWithoutInit,
        )
        .map(Some)
}

struct MasmComponentBuilder<'a> {
    component: &'a mut MasmComponent,
    analysis_manager: AnalysisManager,
    link_info: &'a LinkInfo,
    source_manager: Arc<dyn midenc_session::SourceManager>,
    init_body: Vec<masm::Op>,
    invoked_from_init: BTreeSet<masm::Invoke>,
}

impl MasmComponentBuilder<'_> {
    /// Convert the component body to Miden Assembly, along with any `supporting` modules that sit
    /// beside the component in the world that declares it.
    pub fn build(
        mut self,
        component: &midenc_hir::Operation,
        supporting: &[builtin::ModuleRef],
    ) -> Result<(), Report> {
        use masm::{Instruction as Inst, InvocationTarget, Op};

        // Validate exactly the operations this builder will emit. In particular, a world may
        // contain declaration-only or memory-owning siblings which codegen deliberately omits;
        // invalid roots in those items must not mask the established omission diagnostic.
        crate::legalization::validate_procedure_roots(component)?;
        for module in supporting {
            crate::legalization::validate_procedure_roots(module.borrow().as_operation())?;
        }

        // If a component-level init is required, emit code to initialize the heap before any other
        // initialization code.
        if self.component.init.is_some() {
            let span = component.span();

            // Heap metadata initialization
            let heap_base = self.component.heap_base;
            self.init_body.push(masm::Op::Inst(Span::new(
                span,
                Inst::Push(masm::Immediate::Value(Span::unknown(heap_base.into()))),
            )));
            let heap_init = {
                let name = masm::ProcedureName::new("heap_init").unwrap();
                let module = masm::LibraryPath::new("::intrinsics::mem").unwrap();
                let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
                InvocationTarget::Path(Span::new(span, qualified.into_inner()))
            };
            self.init_body.push(Op::Inst(Span::new(
                span,
                Inst::EmitImm(Event::FrameStart.as_event_id().as_felt().into()),
            )));
            self.init_body.push(Op::Inst(Span::new(span, Inst::Exec(heap_init))));
            self.init_body.push(Op::Inst(Span::new(
                span,
                Inst::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()),
            )));

            // Data segment initialization
            //
            // Function table initialization is *not* emitted here: it is attached to the modules
            // defining the callees, which do not exist yet. See below.
            self.emit_data_segment_initialization();
        }

        // Translate component body
        let region = component.region(0);
        let block = region.entry();
        for op in block.body() {
            if let Some(module) = op.downcast_ref::<builtin::Module>() {
                self.define_module(module)?;
            } else if let Some(interface) = op.downcast_ref::<builtin::Interface>() {
                self.define_interface(interface)?;
            } else if let Some(function) = op.downcast_ref::<builtin::Function>() {
                self.define_function(function)?;
            } else {
                panic!(
                    "invalid component-level operation: '{}' is not supported in a component body",
                    op.name()
                )
            }
        }

        // Translate the supporting modules beside the component, into the same set of modules.
        //
        // `define_module` roots a module whose path does not begin with the component's at the top
        // level, which is exactly where these belong, so they end up as siblings of the component
        // root rather than children of it — and therefore in `support` when
        // `MasmComponent::source_inputs` splits the set. None of them can contribute to `init`:
        // `MasmModuleBuilder` only ever appends to it for a global variable, and a module that
        // declared one would not be here.
        for module in supporting {
            self.define_module(&module.borrow())?;
        }

        // Finalize the component-level init, if required
        if self.component.init.is_some() {
            // Function tables are initialized from the modules that define their callees, so this
            // has to run after those modules exist — which is why it is here rather than beside
            // the heap and data segment initialization above. Each fragment invokes the fragments
            // of the modules nested within it; `init` only reaches the outermost ones.
            let fragments = self.build_function_table_fragments()?;
            let owners = fragments.keys().cloned().collect::<Vec<_>>();
            let mut child_calls: BTreeMap<masm::PathBuf, Vec<masm::PathBuf>> = BTreeMap::new();
            let mut roots: Vec<masm::PathBuf> = Vec::new();
            for owner in owners.iter() {
                // The nearest fragment-bearing ancestor, if any: a module's fragment is reached
                // from its closest enclosing one, and only the modules with no such enclosing
                // fragment are reached from `init`. `Path::starts_with_exactly` matches
                // component-wise, so `::a::bc` is not an ancestor's descendant merely because
                // the text of `::a::b` is a prefix of it.
                //
                // A root is only reachable if `init`, which lives in the component root, may
                // `exec` into it — so a *private* nested module whose parent defines no table
                // callee is a root `init` cannot reach: MASM visibility lets a module reach its
                // own children and its siblings, not a private grandchild. Nothing produces that
                // shape today (the Wasm frontend puts every core callee in one module, and a
                // private nested module reached from the component root would be unreachable for
                // ordinary calls too), and it fails loudly at assembly time rather than silently
                // leaving the slots zeroed, so it is recorded here rather than worked around. The
                // fix, if it ever arises, is to give the parent an empty fragment to relay
                // through rather than to promote anyone's visibility.
                match owners
                    .iter()
                    .filter(|candidate| {
                        *candidate != owner
                            && owner.as_path().starts_with_exactly(candidate.as_path())
                    })
                    .max_by_key(|candidate| candidate.as_path().components().count())
                {
                    Some(parent) => {
                        child_calls.entry(parent.clone()).or_default().push(owner.clone())
                    }
                    None => roots.push(owner.clone()),
                }
            }

            let span = SourceSpan::default();
            let proc_name = masm::ProcedureName::new(FUNCTION_TABLE_INIT_PROC).unwrap();
            for (
                owner,
                FunctionTableFragment {
                    mut body,
                    mut invoked,
                    a_callee,
                },
            ) in fragments
            {
                for child in child_calls.remove(&owner).unwrap_or_default() {
                    let qualified =
                        masm::QualifiedProcedureName::new(child.as_path(), proc_name.clone());
                    let target =
                        masm::InvocationTarget::Path(Span::new(span, qualified.into_inner()));
                    invoked.insert(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
                    body.push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(target))));
                }

                // A module holding nothing but declarations is skipped by `classify_siblings` and
                // never lowered, but a table entry may still name a function in it: the entry
                // resolves and the function has a signature, so both the `hir.exec_indirect`
                // verifier and legalization accept the IR. There is no module here to attach the
                // slot-filling code to, and no MAST root to fill the slot with, so it is invalid
                // input rather than a compiler bug — and reported like the rest of the invalid
                // input this function rejects.
                let Some(index) = self
                    .component
                    .modules
                    .iter()
                    .position(|module| module.path() == owner.as_path())
                else {
                    return Err(Report::msg(format!(
                        "invalid function table entry: callee '{a_callee}' is defined in module \
                         '{owner}', which was not lowered because it holds only declarations — a \
                         function table cannot name a callee that has no definition to take the \
                         address of"
                    )));
                };
                let module = Arc::get_mut(&mut self.component.modules[index])
                    .expect("expected unique reference");
                let mut procedure = masm::Procedure::new(
                    span,
                    // Public so the parent module's fragment (or `init`) can reach it; this is
                    // the only symbol table initialization contributes to a module's surface,
                    // and it is the compiler's own, never the author's
                    masm::Visibility::Public,
                    proc_name.clone(),
                    0,
                    masm::Block::new(span, body),
                )
                .with_signature(masm::FunctionType::new(
                    midenc_hir::CallConv::Fast,
                    vec![],
                    vec![],
                ));
                procedure.extend_invoked(invoked);
                module
                    .define_procedure(procedure, self.source_manager.clone())
                    .into_diagnostic()
                    .wrap_err("failed to define a function table initializer")?;
            }

            for root in roots {
                let qualified =
                    masm::QualifiedProcedureName::new(root.as_path(), proc_name.clone());
                let target = masm::InvocationTarget::Path(Span::new(span, qualified.into_inner()));
                self.invoked_from_init
                    .insert(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
                self.init_body
                    .push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(target))));
            }

            // The core Wasm start function is the final phase of initialization. It must observe
            // the initialized heap, data, globals, and function tables, and `exec` keeps it in the
            // same MASM context that `init` is preparing.
            if let Some(start) = self.link_info.component_start() {
                let start = start.borrow();
                let target = super::lowering::invocation_target_from_symbol_path(
                    &start.path(),
                    start.span(),
                );
                self.invoked_from_init
                    .insert(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
                self.init_body
                    .push(masm::Op::Inst(Span::new(start.span(), masm::Instruction::Exec(target))));
            }

            let module =
                Arc::get_mut(&mut self.component.modules[0]).expect("expected unique reference");

            let init_name = masm::ProcedureName::new("init").unwrap();
            let init_body = core::mem::take(&mut self.init_body);
            let mut init = masm::Procedure::new(
                Default::default(),
                masm::Visibility::Public,
                init_name,
                0,
                masm::Block::new(component.span(), init_body),
            )
            .with_signature(masm::FunctionType::new(
                midenc_hir::CallConv::Fast,
                vec![],
                vec![],
            ));
            // What `init` invokes is what the assembler's linker builds its call graph from, and
            // until now nothing attached this set to anything — every invocation recorded while
            // building `init`'s body, by the fragment roots just above and by global variable
            // initializers before them, was written and then dropped. The linker resolves an
            // `exec` from the instruction as well, which is why the omission never showed; an
            // accurate call graph is still what the set is for, and `init` was the one procedure
            // in the component reporting none of its callees.
            init.extend_invoked(core::mem::take(&mut self.invoked_from_init));

            module
                .define_procedure(init, self.source_manager.clone())
                .into_diagnostic()
                .wrap_err("failed to define component `init` procedure")?;
        } else {
            assert!(
                self.init_body.is_empty(),
                "the need for an 'init' function was not expected, but code was generated for one"
            );
        }

        Ok(())
    }

    fn define_interface(&mut self, interface: &builtin::Interface) -> Result<(), Report> {
        let interface_path = if let Some(id) = self.component.id.as_ref() {
            let mut path = id.to_library_path();
            path.push(interface.name().as_str());
            path
        } else {
            interface.path().to_library_path()
        };
        let mut masm_module =
            Box::new(masm::Module::new(masm::ModuleKind::Library, interface_path));
        let builder = MasmModuleBuilder {
            module: &mut masm_module,
            analysis_manager: self
                .analysis_manager
                .nest(interface.as_operation().as_operation_ref()),
            link_info: self.link_info,
            source_manager: self.source_manager.clone(),
            init_body: &mut self.init_body,
            invoked_from_init: &mut self.invoked_from_init,
        };
        builder.build_from_interface(interface)?;

        self.component.modules.push(Arc::from(masm_module));

        Ok(())
    }

    fn define_module(&mut self, module: &builtin::Module) -> Result<(), Report> {
        let module_path = module.path().to_library_path();
        let module_path = module_path.to_absolute().unwrap();
        let trace_target = TraceTarget::category("codegen");
        log::debug!(target: &trace_target, "defining module '{module_path}'");
        // The submodule declaration's visibility decides whether the module's public procedures
        // belong to the public surface of the assembled package: the assembler derives that
        // surface from the modules reachable from the root through *public* submodule
        // declarations. Core modules are private in HIR, so their public procedures stay
        // resolvable package-internally (a private submodule is visible to its parent and siblings)
        // without becoming part of the package's interface.
        //
        // Two of the shapes reaching here have no component boundary to speak of, and in both
        // the modules *are* the artifact's interface, so they keep public submodules. A world
        // lowered without a component id is one. The other is the wrapper the compiler invents
        // around a bare core module, which is not a real boundary either: the wrapped module is
        // the artifact's own interface (the entrypoint of an executable, or the exports of a
        // bare library), and the generated executable `main` module lives outside the wrapper's
        // module tree. That the wrapper is the compiler's is something it *says* — the frontend
        // marks it (`builtin::Component::SYNTHETIC_WRAPPER_ATTR`) — rather than something read
        // off its id, which is a name an author may write. An authored component is the
        // complement, and keeps the visibility its author declared.
        let is_artifact_interface = self.component.id.is_none() || self.component.synthetic_wrapper;
        let visibility = if is_artifact_interface {
            masm::Visibility::Public
        } else {
            match *module.get_visibility() {
                midenc_hir::Visibility::Public => masm::Visibility::Public,
                midenc_hir::Visibility::Internal | midenc_hir::Visibility::Private => {
                    masm::Visibility::Private
                }
            }
        };
        let module_index = if let Some(rest) = module_path.strip_prefix(&self.component.root) {
            self.define_module_tree(rest, Some(0), visibility)?
        } else {
            self.define_module_tree(&module_path, None, visibility)?
        };

        let masm_module = Arc::get_mut(&mut self.component.modules[module_index])
            .expect("expected unique reference");
        let builder = MasmModuleBuilder {
            module: masm_module,
            analysis_manager: self.analysis_manager.nest(module.as_operation_ref()),
            link_info: self.link_info,
            source_manager: self.source_manager.clone(),
            init_body: &mut self.init_body,
            invoked_from_init: &mut self.invoked_from_init,
        };
        let nested = builder.build(module)?;
        for nested_module in nested {
            self.define_module(&nested_module.borrow())?;
        }

        Ok(())
    }

    fn define_module_tree(
        &mut self,
        module_path: &masm::Path,
        mut parent: Option<usize>,
        visibility: masm::Visibility,
    ) -> Result<usize, Report> {
        let trace_target = TraceTarget::category("codegen");
        let mut path = masm::PathBuf::with_capacity(256);
        if let Some(parent) = parent {
            path = self.component.modules[parent].path().to_path_buf();
        }
        let mut components = module_path.components().peekable();
        while let Some(component) = components.next() {
            let name = component.unwrap().as_str();
            // Ignore the root component
            if name == "::" {
                continue;
            }
            path.push_component(name);
            if !path.is_absolute() {
                path = path.to_absolute().unwrap().into_owned();
            }
            // Use the input visibility for the last module we crate, for parent modules, we must
            // specify public visibility so that references to this module are valid.
            let visibility = if components.peek().is_none() {
                visibility
            } else {
                masm::Visibility::Public
            };
            let module_path = &path;
            if let Some(parent_index) = parent {
                let parent_module = Arc::get_mut(&mut self.component.modules[parent_index])
                    .expect("expected unique reference");
                if parent_module.submodules().iter().any(|sm| sm.name.as_str() == name) {
                    // Already defined, look up the submodule as the new `parent`
                    parent = Some(
                        self.component
                            .modules
                            .iter()
                            .position(|m| m.path() == module_path.as_path())
                            .expect(
                                "submodule was already defined, but not registered with component",
                            ),
                    );
                } else {
                    // Create the submodule
                    let submodule =
                        Box::new(masm::Module::new(masm::ModuleKind::Library, module_path));
                    let name = masm::Ident::new(submodule.name()).unwrap();
                    log::debug!(target: &trace_target, "declaring submodule '{name}' of '{}'", parent_module.path());
                    parent_module.declare_submodule(name, visibility)?;
                    parent = Some(self.component.modules.len());
                    self.component.modules.push(Arc::from(submodule));
                }
            } else {
                log::debug!(target: &trace_target, "declaring module '{module_path}'");
                let module = Box::new(masm::Module::new(masm::ModuleKind::Library, module_path));
                parent = Some(self.component.modules.len());
                self.component.modules.push(Arc::from(module));
            }
        }

        Ok(parent.unwrap())
    }

    fn define_function(&mut self, function: &builtin::Function) -> Result<(), Report> {
        let builder = MasmFunctionBuilder::new(function)?;
        let procedure = builder.build(
            function,
            self.analysis_manager.nest(function.as_operation_ref()),
            self.link_info,
            FunctionLoweringMode::Normal,
        )?;

        let module =
            Arc::get_mut(&mut self.component.modules[0]).expect("expected unique reference");
        let expected_path_len = if module.path().is_absolute() { 2 } else { 1 };
        assert_eq!(
            module.path().len(),
            expected_path_len,
            "expected top-level namespace module, but one has not been defined (in '{}' of '{}')",
            module.path(),
            function.path()
        );
        module
            .define_procedure(procedure, self.source_manager.clone())
            .into_diagnostic()
            .wrap_err("failed to define MASM procedure")?;

        Ok(())
    }

    /// Emit the sequence of instructions necessary to consume rodata from the advice stack and
    /// populate the global heap with the data segments of this component, verifying that the
    /// commitments match.
    fn emit_data_segment_initialization(&mut self) {
        use masm::{Instruction as Inst, InvocationTarget, Op};

        // Emit data segment initialization code
        //
        // NOTE: This depends on the program being executed with the data for all data segments
        // having been placed in the advice map with the same commitment and encoding used here.
        // The program will fail to execute if this is not set up correctly.
        let span = SourceSpan::default();
        let pipe_preimage_to_memory = {
            let name = masm::ProcedureName::new("pipe_preimage_to_memory").unwrap();
            let module = masm::LibraryPath::new("::miden::core::mem").unwrap();
            let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
            InvocationTarget::Path(Span::new(span, qualified.into_inner()))
        };
        for rodata in self.component.rodata.iter() {
            // Push the commitment hash (`COM`) for this data onto the operand stack

            // WARNING: These two are equivalent, shouldn't this be a no-op?
            let word = rodata.digest.as_elements();
            let word_value = [word[0], word[1], word[2], word[3]];

            self.init_body.push(Op::Inst(Span::new(
                span,
                Inst::Push(masm::Immediate::Value(Span::unknown(WordValue(word_value).into()))),
            )));
            // Move rodata from the advice map, using the commitment as key, to the advice stack
            self.init_body
                .push(Op::Inst(Span::new(span, Inst::SysEvent(masm::SystemEventNode::PushMapVal))));
            // write_ptr
            assert!(rodata.start.is_word_aligned(), "rodata segments must be word-aligned");
            self.init_body.push(Op::Inst(Span::new(
                span,
                Inst::Push(masm::Immediate::Value(Span::unknown(rodata.start.addr.into()))),
            )));
            // num_words
            self.init_body.push(Op::Inst(Span::new(
                span,
                Inst::Push(masm::Immediate::Value(Span::unknown(
                    (rodata.size_in_words() as u32).into(),
                ))),
            )));
            // [num_words, write_ptr, COM, ..] -> [write_ptr']
            self.init_body.push(Op::Inst(Span::new(
                span,
                Inst::EmitImm(Event::FrameStart.as_event_id().as_felt().into()),
            )));
            self.init_body
                .push(Op::Inst(Span::new(span, Inst::Exec(pipe_preimage_to_memory.clone()))));
            self.init_body.push(Op::Inst(Span::new(
                span,
                Inst::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()),
            )));
            // drop write_ptr'
            self.init_body.push(Op::Inst(Span::new(span, Inst::Drop)));
        }
    }

    /// Build the slot-initialization code for every function table in the component, grouped by
    /// the module that *defines the callee* whose MAST root fills each slot.
    ///
    /// Grouping by callee rather than by table is what keeps every `procref` intra-module: a
    /// table in one module may name a callee in another, and it is the `procref` — not the
    /// store — that the assembler resolves against visibility.
    ///
    /// Only the entries [`builtin::FunctionTable::live_entries`] considers live are written: a
    /// later entry at the same index overwrites an earlier one, so the earlier one is dead. That
    /// is a soundness requirement rather than a saving. The `hir.exec_indirect` verifier compares
    /// signatures only for the entry that wins a slot, so a dead entry's callee has never been
    /// checked against any call site's stack contract — and grouping by owning module means store
    /// order no longer follows the entries' textual order, so "the last store wins" would not even
    /// pick the entry the verifier looked at.
    ///
    /// Uninitialized (null) slots are left as the zero word, since VM memory is
    /// zero-initialized; `dynexec` on such a slot fails at runtime.
    ///
    /// Each fragment carries the `procref`s its stores consume, which its procedure declares as
    /// invocations: that set is what the assembler's linker reads to build its call graph. It is
    /// a declaration of the dependency, not what creates it — the linker also resolves an
    /// invocation target from the instruction itself, so an omission here is a call graph missing
    /// an edge rather than an unresolved symbol.
    fn build_function_table_fragments(
        &self,
    ) -> Result<BTreeMap<masm::PathBuf, FunctionTableFragment>, Report> {
        use masm::{Instruction as Inst, Op};

        let span = SourceSpan::default();
        let mut fragments: BTreeMap<masm::PathBuf, FunctionTableFragment> = BTreeMap::new();
        let layout = self.link_info.function_tables();
        for (table_ref, _) in layout.iter() {
            let base_addr = layout
                .element_addr_of(table_ref)
                .expect("link error: missing function table in computed layout");
            let table = table_ref.borrow();
            // Dead entries are skipped, not merely overwritten; see this function's doc comment.
            // A dead entry is therefore also unvalidated, which costs nothing for the bounds check
            // below — an overwritten entry shares its slot index with the entry that overwrote it,
            // so an out-of-bounds slot is still reported — and is if anything the right answer for
            // the tag: a slot explicitly nulled and then reassigned is not an error.
            let live_entries = table.live_entries().map_err(|op_name| {
                Report::msg(format!(
                    "invalid function table entry: '{op_name}' is not supported in a function \
                     table body"
                ))
            })?;
            for (slot, entry) in live_entries {
                let entry = entry.borrow();
                if slot >= *table.get_num_slots() {
                    return Err(Report::msg(format!(
                        "invalid function table entry: slot {slot} is out of bounds for table \
                         '{}' with {} slots",
                        table.get_name().as_str(),
                        *table.get_num_slots()
                    )));
                }
                let type_tag = *entry.get_type_tag();
                if type_tag == 0 {
                    return Err(Report::msg(format!(
                        "invalid function table entry: slot {slot} of table '{}' uses signature \
                         tag 0, which is reserved for null slots",
                        table.get_name().as_str(),
                    )));
                }
                let Some(callee) = entry.resolve_callee() else {
                    return Err(Report::msg(format!(
                        "invalid function table entry: unable to resolve callee '{}'",
                        entry.callee().path()
                    )));
                };
                let callee_path = callee.borrow().path();
                let target =
                    super::lowering::invocation_target_from_symbol_path(&callee_path, span);

                // The fragment belongs to the module defining the callee: `procref` there needs
                // no visibility beyond what the callee already has
                let owner = callee_path.without_leaf().to_library_path();
                let owner = owner.to_absolute().unwrap().into_owned();
                let fragment = fragments.entry(owner).or_insert_with(|| FunctionTableFragment {
                    body: Default::default(),
                    invoked: Default::default(),
                    a_callee: callee_path.to_string(),
                });
                let FunctionTableFragment { body, invoked, .. } = fragment;
                invoked.insert(masm::Invoke::new(masm::InvokeKind::ProcRef, target.clone()));

                // `procref` pushes the callee's MAST root word (`root[0]` on top),
                // `mem_storew_le` writes it to the slot's element address (leaving the word on
                // the stack), and `dropw` cleans up; the slot's signature tag is then stored in
                // the element right after the digest. The base is word-aligned and each slot is
                // exactly two words, so every slot address stays word-aligned as `dynexec`
                // requires.
                let slot_addr = base_addr + slot * FunctionTableLayout::SLOT_SIZE_ELEMENTS;
                let tag_addr = slot_addr + FunctionTableLayout::TYPE_TAG_OFFSET_ELEMENTS;
                body.push(Op::Inst(Span::new(span, Inst::ProcRef(target))));
                body.push(Op::Inst(Span::new(span, Inst::MemStoreWLeImm(slot_addr.into()))));
                body.push(Op::Inst(Span::new(span, Inst::DropW)));
                body.push(Op::Inst(Span::new(
                    span,
                    Inst::Push(masm::Immediate::Value(Span::new(span, type_tag.into()))),
                )));
                body.push(Op::Inst(Span::new(span, Inst::MemStoreImm(tag_addr.into()))));
            }
        }

        Ok(fragments)
    }
}

/// The slot-initialization code one module contributes, for the callees *it* defines.
struct FunctionTableFragment {
    /// The stores that fill those slots.
    body: Vec<masm::Op>,
    /// The `procref`s those stores consume, for the assembler's linker.
    invoked: BTreeSet<masm::Invoke>,
    /// One of the callees that put this fragment here, for diagnostics. Any of them identifies
    /// the module as well as another, and reporting one is more use than reporting the module
    /// path alone.
    a_callee: String,
}

struct MasmModuleBuilder<'a> {
    module: &'a mut masm::Module,
    analysis_manager: AnalysisManager,
    link_info: &'a LinkInfo,
    source_manager: Arc<dyn midenc_session::SourceManager>,
    init_body: &'a mut Vec<masm::Op>,
    invoked_from_init: &'a mut BTreeSet<masm::Invoke>,
}

impl MasmModuleBuilder<'_> {
    /// Lower `module`'s body, returning any modules nested within it.
    ///
    /// A nested module is not lowered here: MASM's module set is flat and keyed by path, and
    /// [`MasmComponentBuilder::define_module`] is what turns a fully-qualified HIR module path
    /// into that set's entry. Returning them lets the component builder recurse without this
    /// builder having to know how modules are rooted.
    pub fn build(mut self, module: &builtin::Module) -> Result<Vec<builtin::ModuleRef>, Report> {
        let mut nested = Vec::new();
        let region = module.body();
        let block = region.entry();
        for op in block.body() {
            if let Some(function) = op.downcast_ref::<builtin::Function>() {
                self.define_function(function)?;
            } else if let Some(gv) = op.downcast_ref::<builtin::GlobalVariable>() {
                self.emit_global_variable_initializer(gv)?;
            } else if let Some(nested_module) = op.downcast_ref::<builtin::Module>() {
                nested.push(nested_module.as_module_ref());
            } else if op.is::<builtin::Segment>() {
                continue;
            } else if op.is::<builtin::FunctionTable>() {
                // Laid out by the linker; slots are filled by the `__init_function_table`
                // procedures `MasmComponentBuilder::build` attaches to the modules defining the
                // callees, from fragments `build_function_table_fragments` produces
                continue;
            } else {
                panic!(
                    "invalid module-level operation: '{}' is not legal in a MASM module body",
                    op.name()
                )
            }
        }

        Ok(nested)
    }

    pub fn build_from_interface(mut self, interface: &builtin::Interface) -> Result<(), Report> {
        let region = interface.body();
        let block = region.entry();
        for op in block.body() {
            if let Some(function) = op.downcast_ref::<builtin::Function>() {
                self.define_function(function)?;
            } else {
                panic!(
                    "invalid interface-level operation: '{}' is not legal in a MASM module body",
                    op.name()
                )
            }
        }

        Ok(())
    }

    fn define_function(&mut self, function: &builtin::Function) -> Result<(), Report> {
        let builder = MasmFunctionBuilder::new(function)?;

        let procedure = builder.build(
            function,
            self.analysis_manager.nest(function.as_operation_ref()),
            self.link_info,
            FunctionLoweringMode::Normal,
        )?;

        self.module
            .define_procedure(procedure, self.source_manager.clone())
            .map_err(|e| Report::msg(e.to_string()))?;

        Ok(())
    }

    fn emit_global_variable_initializer(
        &mut self,
        gv: &builtin::GlobalVariable,
    ) -> Result<(), Report> {
        // We don't emit anything for declarations
        if gv.is_declaration() {
            return Ok(());
        }

        // We compute liveness for global variables independently
        let analysis_manager = self.analysis_manager.nest(gv.as_operation_ref());
        let liveness = analysis_manager.get_analysis::<LivenessAnalysis>()?;

        // Emit the initializer block
        let initializer_region = gv.region(0);
        let initializer_block = initializer_region.entry();

        let mut block_emitter = BlockEmitter {
            liveness: &liveness,
            link_info: self.link_info,
            invoked: self.invoked_from_init,
            target: Default::default(),
            stack: OperandStack::new(gv.as_operation().context_rc()),
            trace_target: TraceTarget::category("codegen")
                .with_relevant_symbol(gv.name().as_symbol()),
        };
        block_emitter.emit_inline(&initializer_block);

        // Sanity checks
        assert_eq!(block_emitter.stack.len(), 1, "expected only global variable value on stack");
        let return_ty = block_emitter.stack.peek().unwrap().ty();
        assert_eq!(
            &return_ty,
            &*gv.get_ty(),
            "expected initializer to return value of same type as declaration"
        );

        // Write the initialized value to the computed storage offset for this global
        let computed_addr = self
            .link_info
            .globals_layout()
            .get_computed_addr(gv.as_global_var_ref())
            .expect("undefined global variable");
        block_emitter.emitter().store_imm(computed_addr, gv.span());

        // Extend the generated init function with the code to initialize this global
        let mut body = core::mem::take(&mut block_emitter.target);
        self.init_body.append(&mut body);

        Ok(())
    }
}

struct MasmFunctionBuilder {
    span: midenc_hir::SourceSpan,
    name: masm::ProcedureName,
    signature: masm::FunctionType,
    visibility: masm::Visibility,
    num_locals: u16,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FunctionLoweringMode {
    /// Emit a procedure with all prologues and public metadata implied by its HIR function.
    Normal,
    /// Emit the selected canonical entry body for generated executable `main`, which has already
    /// initialized the component and must not repeat the public wrapper's `init` prologue.
    ExecutableEntrypointWithoutInit,
}

impl MasmFunctionBuilder {
    /// Prepare to translate `function`, or report why it cannot be translated.
    ///
    /// This is the single point every function reaches, whichever kind of item declares it —
    /// `MasmComponentBuilder::define_function` for a component-level function,
    /// `MasmModuleBuilder::define_function` for one in a module or an interface — which is why the
    /// check below lives here rather than at any one of them.
    pub fn new(function: &builtin::Function) -> Result<Self, Report> {
        use midenc_hir::{Symbol, Visibility};

        if function.is_declaration() {
            return Err(function_without_a_body(function));
        }

        let name = *function.get_name();
        let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
            name.span,
            name.as_ref().into(),
        )));
        let visibility = match function.visibility() {
            Visibility::Public => masm::Visibility::Public,
            // TODO(pauls): Support internal visibility in MASM
            Visibility::Internal => masm::Visibility::Public,
            Visibility::Private => masm::Visibility::Private,
        };
        let locals_required = function.locals().iter().map(|ty| ty.size_in_felts()).sum::<usize>();
        let num_locals = u16::try_from(locals_required).map_err(|_| {
            let context = function.as_operation().context();
            context
                .diagnostics()
                .diagnostic(miden_assembly::diagnostics::Severity::Error)
                .with_message("cannot emit masm for function")
                .with_primary_label(
                    function.span(),
                    "local storage exceeds procedure limit: no more than u16::MAX elements are \
                     supported",
                )
                .into_report()
        })?;

        let signature =
            semantic_debug_signature(function).unwrap_or_else(|| lowered_signature(function));

        Ok(Self {
            span: function.span(),
            name,
            signature,
            visibility,
            num_locals,
        })
    }

    pub fn build(
        self,
        function: &builtin::Function,
        analysis_manager: AnalysisManager,
        link_info: &LinkInfo,
        mode: FunctionLoweringMode,
    ) -> Result<masm::Procedure, Report> {
        use alloc::collections::BTreeSet;

        use midenc_hir_analysis::analyses::LivenessAnalysis;

        let demangled_symbol_name = midenc_hir::demangle::demangle(function.get_name().as_str());
        let trace_target = TraceTarget::category("codegen")
            .with_relevant_symbol(midenc_hir::SymbolName::intern(demangled_symbol_name));

        log::trace!(target: &trace_target, "lowering {}", function.as_operation());

        let liveness = analysis_manager.get_analysis::<LivenessAnalysis>()?;

        let mut invoked = BTreeSet::default();
        let entry = function.entry_block();
        let mut stack = crate::OperandStack::new(function.as_operation().context_rc());
        {
            let entry_block = entry.borrow();
            for arg in entry_block.arguments().iter().rev().copied() {
                stack.push(arg as ValueRef);
            }
        }
        let mut emitter = BlockEmitter {
            liveness: &liveness,
            link_info,
            invoked: &mut invoked,
            target: Default::default(),
            stack,
            trace_target,
        };

        // For component export functions, invoke the `init` procedure first if needed.
        // It loads the data segments, global vars, and function tables into memory.
        if mode == FunctionLoweringMode::Normal
            && function.signature().cc.is_wasm_canonical_abi()
            && link_info.requires_init()
        {
            // Resolve `init` symbolically within the containing module instead of through a
            // fully-qualified component path, which depends on the (user-editable)
            // `[lib].namespace` matching the component's library identity.
            //
            // INVARIANT: this relies on the canonical-ABI export wrappers being emitted into the
            // root component module — the same module where `MasmComponentBuilder` defines
            // `init` (`self.component.modules[0]`); the inner lifted functions in interface and
            // core child modules carry no init prologue. If export wrappers ever move into child
            // modules, this symbol stops resolving and the init target must be threaded in as a
            // qualified path instead. A user-exported method named `init` collides with the
            // generated procedure at definition time ("symbol conflict: found duplicate
            // definitions"), so it cannot silently shadow this target.
            let init = InvocationTarget::Symbol("init".parse().unwrap());
            // Add init call to the emitter's target before emitting the function body; `emit`
            // also registers the invocation so the assembler can resolve the symbolic target.
            emitter.emitter().emit(masm::Instruction::Exec(init), SourceSpan::default());
        }

        let mut body = emitter.emit(&entry.borrow());

        if function.signature().cc.is_wasm_canonical_abi() {
            // Truncate the stack to 16 elements on exit in the component export function
            // since it is expected to be `call`ed so it has a requirement to have
            // no more than 16 elements on the stack when it returns.
            // See https://0xmiden.github.io/miden-vm/user_docs/assembly/execution_contexts.html
            // Since the VM's `drop` instruction not letting stack size go beyond the 16 elements
            // we most likely end up with stack size > 16 elements at the end.
            // See https://github.com/0xPolygonMiden/miden-vm/blob/c4acf49510fda9ba80f20cee1a9fb1727f410f47/processor/src/stack/mod.rs?plain=1#L226-L253
            let truncate_stack = {
                let name = masm::ProcedureName::new("truncate_stack").unwrap();
                let module = masm::LibraryPath::new("::miden::core::sys").unwrap();
                let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
                InvocationTarget::Path(Span::new(SourceSpan::default(), qualified.into_inner()))
            };
            let span = SourceSpan::default();
            invoked.insert(masm::Invoke::new(masm::InvokeKind::Exec, truncate_stack.clone()));
            body.push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(truncate_stack))));
        }
        let Self {
            span,
            name,
            signature,
            visibility,
            num_locals,
        } = self;

        // Align num_locals to WORD_SIZE, matching the assembler's FMP frame sizing.
        // num_locals already counts all HIR locals (including those allocated for params).
        // The assembler rounds up to next_multiple_of(WORD_SIZE) when advancing FMP
        // (see fmp.rs fmp_start_frame_sequence and mem_ops.rs locaddr), so we must use
        // the same alignment for debug var offset computation.
        let aligned_num_locals = num_locals.next_multiple_of(miden_core::WORD_SIZE as u16);

        // Resolve FrameBase global_index → Miden memory address.
        // Use the stack pointer offset from the linker's global layout.
        let stack_pointer_addr = link_info.globals_layout().stack_pointer_offset();

        // Patch DebugVar Local locations to compute FMP offset.
        // During lowering, Local(idx) stores the raw WASM local index.
        // Now convert to FMP offset: idx - aligned_num_locals
        // This matches locaddr.N which computes -(aligned_num_locals - N).
        patch_debug_var_locals_in_block(&mut body, aligned_num_locals, stack_pointer_addr);

        // If a function body after lowering produces a MASM procedure with an empty body aside
        // from debug decorators, then we must emit a `nop` at the end of the block which will
        // act as the anchor for those decorators. Such a procedure is basically useless, as it is
        // just passing through arguments as results - but the assembler currently rejects empty
        // procedures (not counting decorators), so we must handle this edge case.
        if !block_has_real_instructions(&body) {
            body.push(masm::Op::Inst(Span::unknown(masm::Instruction::Nop)));
        }

        let mut procedure = masm::Procedure::new(span, visibility, name, num_locals, body);
        procedure.set_signature(signature);
        if mode == FunctionLoweringMode::Normal {
            for attribute in [
                midenc_dialect_hir::ACCOUNT_PROCEDURE_EXPORT_ATTR,
                midenc_dialect_hir::AUTH_SCRIPT_EXPORT_ATTR,
                midenc_dialect_hir::NOTE_SCRIPT_EXPORT_ATTR,
                midenc_dialect_hir::TRANSACTION_SCRIPT_EXPORT_ATTR,
            ] {
                if function.has_attribute(attribute) {
                    procedure
                        .attributes_mut()
                        .insert(Attribute::Marker(masm::Ident::new(attribute).unwrap()));
                }
            }
        }
        procedure.extend_invoked(invoked);

        Ok(procedure)
    }
}

fn lowered_signature(function: &builtin::Function) -> masm::FunctionType {
    let sig = function.signature();
    let args = sig.params.iter().map(|param| masm::TypeExpr::from(param.ty.clone())).collect();
    let results = sig
        .results
        .iter()
        .map(|result| masm::TypeExpr::from(result.ty.clone()))
        .collect();
    masm::FunctionType::new(sig.cc, args, results)
}

fn semantic_debug_signature(function: &builtin::Function) -> Option<masm::FunctionType> {
    let subprogram = function
        .as_operation()
        .get_attribute("di.subprogram")?
        .try_downcast_attr::<SubprogramAttr>()
        .ok()?;
    let subprogram = subprogram.borrow();
    let Type::Function(ty) = subprogram.ty.as_ref()? else {
        return None;
    };

    let args = ty.params().iter().cloned().map(masm::TypeExpr::from).collect();
    let results = ty.results().iter().cloned().map(masm::TypeExpr::from).collect();
    Some(masm::FunctionType::new(ty.calling_convention(), args, results))
}

/// Returns true if the block contains at least one real (non-decorator) instruction.
///
/// DebugVar instructions are decorator-only and don't produce MAST nodes. If a procedure
/// body contains only DebugVar ops, the assembler will reject it.
fn block_has_real_instructions(block: &masm::Block) -> bool {
    block.iter().any(|op| match op {
        masm::Op::Inst(inst) => !matches!(inst.inner(), masm::Instruction::DebugVar(_)),
        masm::Op::If {
            then_blk, else_blk, ..
        } => block_has_real_instructions(then_blk) || block_has_real_instructions(else_blk),
        masm::Op::While { body, .. } => block_has_real_instructions(body),
        masm::Op::DoWhile {
            body, condition, ..
        } => block_has_real_instructions(body) || block_has_real_instructions(condition),
        masm::Op::Repeat { body, .. } => block_has_real_instructions(body),
    })
}

/// Recursively patch DebugVar locations in a block.
///
/// Converts `Local(idx)` where idx is the raw WASM local index to `Local(offset)` where
/// `offset = idx - aligned_num_locals` (the FMP-relative offset, typically negative). This matches
/// the assembler's `locaddr.N` formula, i.e. `FMP - aligned_num_locals + N`.
///
/// Also resolves Wasm frame bases to the Miden local/global encoding understood by the debugger.
/// Locations that require resolution but cannot be represented are converted to explicit kill
/// markers so the unresolved Wasm location cannot remain active.
fn patch_debug_var_locals_in_block(
    block: &mut masm::Block,
    aligned_num_locals: u16,
    stack_pointer_addr: Option<u32>,
) {
    for op in block.iter_mut() {
        match op {
            masm::Op::Inst(span_inst) => {
                // Use DerefMut to get mutable access to the inner Instruction
                if let masm::Instruction::DebugVar(info) = &mut **span_inst {
                    let location = patch_debug_var_location(
                        info.value_location(),
                        aligned_num_locals,
                        stack_pointer_addr,
                    );
                    info.set_value_location(location);
                }
            }
            masm::Op::If {
                then_blk, else_blk, ..
            } => {
                patch_debug_var_locals_in_block(then_blk, aligned_num_locals, stack_pointer_addr);
                patch_debug_var_locals_in_block(else_blk, aligned_num_locals, stack_pointer_addr);
            }
            masm::Op::While {
                body: while_body, ..
            } => {
                patch_debug_var_locals_in_block(while_body, aligned_num_locals, stack_pointer_addr);
            }
            masm::Op::DoWhile {
                body, condition, ..
            } => {
                patch_debug_var_locals_in_block(body, aligned_num_locals, stack_pointer_addr);
                patch_debug_var_locals_in_block(condition, aligned_num_locals, stack_pointer_addr);
            }
            masm::Op::Repeat {
                body: repeat_body, ..
            } => {
                patch_debug_var_locals_in_block(
                    repeat_body,
                    aligned_num_locals,
                    stack_pointer_addr,
                );
            }
        }
    }
}

fn patch_debug_var_location(
    location: &DebugVarLocation,
    aligned_num_locals: u16,
    stack_pointer_addr: Option<u32>,
) -> DebugVarLocation {
    match location {
        DebugVarLocation::Local(index) => {
            checked_fmp_local_offset(i64::from(*index), aligned_num_locals)
                .map(DebugVarLocation::Local)
                .unwrap_or_else(debug_var_kill_location)
        }
        DebugVarLocation::FrameBase { byte_offset, .. } => {
            if let Some(resolved_addr) = stack_pointer_addr.filter(|addr| *addr < (1 << 31)) {
                DebugVarLocation::FrameBase {
                    global_index: resolved_addr,
                    byte_offset: *byte_offset,
                }
            } else {
                debug_var_kill_location()
            }
        }
        DebugVarLocation::Expression(bytes) => {
            let Ok(expression) = Expression::read_from_bytes_with_budget(bytes, bytes.len()) else {
                return location.clone();
            };
            let [
                ExpressionOp::FrameBase {
                    base: FrameBase::Local(local_index),
                    byte_offset,
                },
            ] = expression.operations.as_slice()
            else {
                return location.clone();
            };
            checked_fmp_local_offset(i64::from(*local_index), aligned_num_locals)
                .map(|local_offset| DebugVarLocation::FrameBase {
                    global_index: encode_frame_base_local_offset(local_offset),
                    byte_offset: *byte_offset,
                })
                .unwrap_or_else(debug_var_kill_location)
        }
        DebugVarLocation::Stack(_) | DebugVarLocation::Memory(_) | DebugVarLocation::Const(_) => {
            location.clone()
        }
    }
}

fn checked_fmp_local_offset(index: i64, aligned_num_locals: u16) -> Option<i16> {
    i16::try_from(index - i64::from(aligned_num_locals)).ok()
}

fn debug_var_kill_location() -> DebugVarLocation {
    DebugVarLocation::Expression(super::DEBUG_VAR_KILL_SENTINEL.to_vec())
}

const FRAME_BASE_LOCAL_MARKER: u32 = 1 << 31;

fn encode_frame_base_local_offset(local_offset: i16) -> u32 {
    FRAME_BASE_LOCAL_MARKER | u32::from(u16::from_le_bytes(local_offset.to_le_bytes()))
}