-
-
Notifications
You must be signed in to change notification settings - Fork 94
Expand file tree
/
Copy pathcodegen.rs
More file actions
5743 lines (5572 loc) · 268 KB
/
codegen.rs
File metadata and controls
5743 lines (5572 loc) · 268 KB
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
//! HIR → LLVM IR compilation entry point.
//!
//! Public contract:
//!
//! ```ignore
//! let opts = CompileOptions { target: None, is_entry_module: true };
//! let object_bytes: Vec<u8> = perry_codegen::compile_module(&hir, opts)?;
//! ```
//!
//! The returned bytes are a regular object file produced by `clang -c`.
//! Perry's linking stage in `crates/perry/src/commands/compile.rs`
//! links them against `libperry_runtime.a` and `libperry_stdlib.a`.
//!
//! Currently supported (Phases 1, 2, 2.1, A-strings):
//!
//! - User functions with typed `double` ABI
//! - Recursive and forward calls via `FuncRef`
//! - If/else, for loops, let, return
//! - Binary arithmetic (add/sub/mul/div/mod) and compare
//! - Update (++/--) and LocalSet
//! - `Date.now()` via `js_date_now`
//! - **String literals** via the hoisted `StringPool` (one allocation per
//! literal at module init time, registered as a permanent GC root via
//! `js_gc_register_global_root`; use sites are a single `load`)
//! - `console.log(<expr>)` — uses `js_console_log_number` for static number
//! literals (optimized path) and `js_console_log_dynamic` for everything
//! else (NaN-tag dispatch at runtime)
//!
//! Anything else (objects, arrays, classes, closures, async, imports, …)
//! errors with an actionable "Phase X not yet supported" message.
use std::collections::HashMap;
use anyhow::{anyhow, Context, Result};
use perry_hir::{Function, Module as HirModule};
use crate::expr::FnCtx;
use crate::module::LlModule;
use crate::runtime_decls;
use crate::stmt;
use crate::strings::StringPool;
use crate::types::{LlvmType, DOUBLE, I32, I64, I8, PTR, VOID};
/// Per-application metadata read from `perry.toml` by the CLI and baked into
/// compile-time system APIs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppMetadata {
pub version: String,
pub build_number: i64,
pub bundle_id: String,
}
impl Default for AppMetadata {
fn default() -> Self {
Self {
version: "1.0.0".to_string(),
build_number: 1,
bundle_id: "com.perry.app".to_string(),
}
}
}
/// Options controlling code generation for a single module.
#[derive(Debug, Clone, Default)]
pub struct CompileOptions {
/// Target triple override. `None` uses the host default.
pub target: Option<String>,
/// Whether this module is the program entry point. When true, codegen
/// emits a `main` function that calls `js_gc_init`, the string pool
/// init, every non-entry module's `<prefix>__init`, then the entry
/// module's own top-level statements.
pub is_entry_module: bool,
/// Prefixes of every non-entry module in the program. Only consulted
/// when `is_entry_module = true` — `main` calls `<prefix>__init` for
/// each one in order before running its own init statements. The
/// order matches Perry's existing topological sort (set up by the
/// CLI driver in `crates/perry/src/commands/compile.rs`).
pub non_entry_module_prefixes: Vec<String>,
/// For each imported function name in this module, the prefix of the
/// source module that exports it. Used by `ExternFuncRef` lowering
/// in `lower_call` to generate the correct cross-module call to
/// `perry_fn_<source_prefix>__<funcname>`. Built by the CLI driver
/// from each module's `hir.imports` table.
pub import_function_prefixes: std::collections::HashMap<String, String>,
/// Issue #680: per-namespace member resolution. Keyed by
/// `(namespace_local_name, member_name)` → `source_prefix`. Used by
/// the namespace-member access lowering paths in `expr.rs` and
/// `lower_call.rs` to disambiguate when multiple `import * as X / Y`
/// sources export the same member name. Pre-fix
/// `import_function_prefixes` was a flat name→prefix map and the LAST
/// namespace import to register a name won, so `import * as random;
/// import * as tracer` (both export `make`) made `random.make` resolve
/// to `tracer.make` — Effect's `defaultServices.ts` SIGSEGV'd because
/// it dispatched `tracer.make(Math.random())` instead of
/// `random.make(Math.random())`.
pub namespace_member_prefixes: std::collections::HashMap<(String, String), String>,
/// When true, `compile_module` returns the textual LLVM IR (`.ll`)
/// as bytes instead of invoking `clang -c` to produce an object file.
/// Used by the bitcode-link path (`PERRY_LLVM_BITCODE_LINK=1`).
pub emit_ir_only: bool,
// ── Cross-module import plumbing ──
/// Locals that are namespace imports (`import * as X from "./mod"`).
/// Codegen uses this to know that `X.foo()` should be dispatched as
/// a cross-module call rather than an object method call.
pub namespace_imports: Vec<String>,
/// Imported class definitions from other native modules, keyed by
/// the local alias (or original name when no alias). Each entry
/// carries the class HIR, the module prefix of its origin, and an
/// optional local alias.
pub imported_classes: Vec<ImportedClass>,
/// Imported enum member lists, keyed by the local name under which
/// the enum is visible in this module.
pub imported_enums: Vec<(String, Vec<(String, perry_hir::EnumValue)>)>,
/// Names of imported functions that are async. Codegen needs this to
/// wrap calls in the promise machinery.
pub imported_async_funcs: std::collections::HashSet<String>,
/// Type alias map (name → Type) aggregated from all modules. Codegen
/// uses this to resolve `Named` types in function signatures.
pub type_aliases: std::collections::HashMap<String, perry_types::Type>,
/// Imported function parameter counts, keyed by function name.
pub imported_func_param_counts: std::collections::HashMap<String, usize>,
/// Issue #608 — imported function names whose source-side signature
/// has a trailing `...rest` parameter. Used by the cross-module call
/// site in `lower_call.rs` to pack trailing args into a `js_array_alloc`
/// rest array before the call so the callee's rest binding is a real
/// array, not the raw arg in disguise. Sparse set (only `true` entries).
pub imported_func_has_rest: std::collections::HashSet<String>,
/// Imported function return types, keyed by local function name.
pub imported_func_return_types: std::collections::HashMap<String, perry_types::Type>,
/// Names of imports that are exported VARIABLES (not functions). When an
/// `ExternFuncRef` with one of these names appears as a value (not as a
/// Call callee), the codegen calls the getter function to fetch the value
/// instead of wrapping it as a closure reference. Without this, `import
/// { HONE_VERSION } from './version'` followed by `let v = HONE_VERSION`
/// would create a closure wrapper around the getter, not the actual string.
pub imported_vars: std::collections::HashSet<String>,
// ── Feature plumbing ──
//
// These fields control which runtime libraries and FFI surfaces are
// compiled into the resulting binary. They propagate the CLI's feature
// detection into the codegen so auto-optimize and linker steps work.
//
// NOTE: most of these are informational for the CLI driver's auto-
// optimize rebuild + linker step — `compile_module` itself only
// consults `output_type` (to decide between `main` and a dylib init)
// and `i18n_table` (to materialize the table as rodata). The rest
// are round-tripped through the CompileOptions so the CLI can hand
// them to `build_optimized_libs` / linker flag construction without
// threading separate parameters.
/// Output type. "executable" emits a `main`, "dylib" emits a shared
/// library plugin with no entrypoint.
pub output_type: String,
/// Whether the project needs `libperry_stdlib.a` linked in.
pub needs_stdlib: bool,
/// Whether the project needs `libperry_ui_*.a` linked in.
pub needs_ui: bool,
/// Whether the project needs the Geisterhand inspector linked in.
pub needs_geisterhand: bool,
/// Port the Geisterhand inspector listens on when `needs_geisterhand`.
pub geisterhand_port: u16,
/// Whether the project needs the QuickJS fallback runtime linked in.
pub needs_js_runtime: bool,
/// Cargo feature names enabled for this build, computed by the CLI's
/// `compute_required_features`. Used by the auto-optimize path to
/// decide which optional runtime helpers to compile into
/// `libperry_stdlib.a`.
pub enabled_features: Vec<String>,
/// For the entry module: names of every non-entry native module
/// that needs its `<prefix>__init` called before the entry's own
/// init. Already covered by `non_entry_module_prefixes` for the
/// init sequence, but tracked separately for auto-optimize's
/// feature scan.
pub native_module_init_names: Vec<String>,
/// JavaScript-only modules routed through QuickJS (full specifiers).
pub js_module_specifiers: Vec<String>,
/// Bundled TypeScript extensions — `(absolute_path, module_prefix)`.
pub bundled_extensions: Vec<(String, String)>,
/// Native library FFI from `package.json` — `(library_name,
/// function_names, header_path)` tuples.
pub native_library_functions: Vec<(String, Vec<String>, String)>,
/// i18n translation table snapshot — `(translations, key_count,
/// locale_count, locale_codes, default_locale_idx)`. The
/// `default_locale_idx` is the row index used at compile time to
/// resolve `Expr::I18nString` to the right translation — without
/// it, the lowering would have to either pick locale 0 blindly or
/// fall back to the verbatim key.
/// Tier 4.6 (v0.5.336): wrapped in `Arc` so the per-module clone
/// in the `compile_module` rayon worker is a cheap reference bump
/// instead of duplicating the (potentially large) `Vec<String>` of
/// every translated string. The tuple shape is unchanged for the
/// downstream destructure at `compile_module` line 597.
pub i18n_table: Option<std::sync::Arc<(Vec<String>, usize, usize, Vec<String>, usize)>>,
/// When true, emit LLVM `reassoc contract` per-instruction fast-math
/// flags on every f64 op. Off by default — Perry produces bit-exact
/// output with Node's f64 arithmetic. On (via `--fast-math`,
/// `PERRY_FAST_MATH=1`, or `perry.fastMath: true` in package.json),
/// the optimizer is permitted to reassociate FP chains and fuse
/// multiply-adds, producing observable 1-ULP differences from Node
/// in ~30% of randomly-generated FP programs in exchange for a ~7x
/// speedup on tight `sum += constant` loops (and ~0% on most other
/// FP-heavy code, per benchmarks). See `docs/src/cli/fast-math.md`.
/// Drives `crate::block::FAST_MATH`; included in the object cache
/// key so toggling it invalidates cached `.o` bytes.
pub fast_math: bool,
/// App metadata backing `perry/system` compile-time introspection APIs.
pub app_metadata: AppMetadata,
/// Issue #100: when non-empty, this module is the target of at least
/// one `await import("...")` site somewhere in the program. Codegen
/// emits a `@__perry_ns_<prefix>` static global initialized to
/// undefined, populates it from this list at the end of
/// `__perry_init_<prefix>` (or `main` for the entry module), and
/// registers its address as a GC root. The dispatch site in
/// `Expr::DynamicImport` reads this global and wraps it in
/// `js_promise_resolved`. Empty means no namespace global is emitted.
pub namespace_entries: Vec<NamespaceEntry>,
/// Issue #100: for each `Expr::DynamicImport` site in this module,
/// maps the path-argument string (as it appears in
/// `Expr::DynamicImport::paths`) to the sanitized prefix of the
/// target module. Codegen uses this to load
/// `@__perry_ns_<target_prefix>` for the resolved-promise return
/// value (single-path) or to chain string-compare dispatches
/// (multi-path). Empty if this module performs no dynamic imports.
pub dynamic_import_path_to_prefix: std::collections::HashMap<String, String>,
}
/// Issue #100: one entry in a module's namespace-population list.
/// Codegen iterates this in `__perry_init_<prefix>` to build the
/// `__perry_ns_<prefix>` global. Each variant captures everything
/// needed to emit the value-fetch IR for that key without re-walking
/// the HIR — the driver resolves Var/Function/Class kind and the
/// source-module prefix when it builds the list.
#[derive(Debug, Clone)]
pub struct NamespaceEntry {
/// The key as seen by the consumer of `await import("...")`.
pub name: String,
/// How to fetch the value at populate time.
pub kind: NamespaceEntryKind,
}
/// Issue #100: how to materialise the value for a namespace entry.
#[derive(Debug, Clone)]
pub enum NamespaceEntryKind {
/// Local module-level variable. Codegen loads the value directly
/// from the `@perry_global_<prefix>__<id>` global identified by
/// `global_name`.
LocalVar { global_name: String },
/// Local user function exported as a value. Codegen calls
/// `js_closure_alloc_singleton(@__perry_wrap_<scoped>)`.
LocalFunction { wrap_symbol: String },
/// Local class exported as a value. Codegen emits the
/// INT32-tagged class-id NaN-box that matches `Expr::ClassRef`.
LocalClass { class_id: u32 },
/// Re-exported variable from another module. Codegen calls
/// `perry_fn_<source_prefix>__<source_local>()` as a 0-arg getter
/// returning the f64 value.
ForeignVar {
source_prefix: String,
source_local: String,
},
/// Re-exported function from another module. Codegen declares the
/// target's `perry_fn_*` as extern, emits a per-callsite
/// `__perry_wrap_extern_*` thin wrapper (if not already emitted by
/// the import-wrapper pass), and calls
/// `js_closure_alloc_singleton` against that wrapper.
ForeignFunction {
source_prefix: String,
source_local: String,
param_count: usize,
},
/// `export * as Name from "./sub"` — namespace re-export. The
/// nested value IS the target module's `@__perry_ns_<source_prefix>`
/// global, populated by the target's own `__init`.
NestedNamespace { source_prefix: String },
}
/// A class imported from another native module.
#[derive(Debug, Clone)]
pub struct ImportedClass {
/// The class name as exported from its origin module.
pub name: String,
/// Optional local alias (`import { Foo as Bar }`).
pub local_alias: Option<String>,
/// Symbol prefix of the origin module (for cross-module method calls).
pub source_prefix: String,
/// Number of constructor parameters (needed for dispatch).
pub constructor_param_count: usize,
/// Method names defined on this class.
pub method_names: Vec<String>,
/// Per-method explicit param counts, parallel to `method_names`. Issue #235:
/// codegen uses this to declare cross-module method symbols with the
/// correct arity (was hardcoded "6 as safe upper bound", which made the
/// callee read garbage from uninitialized arg-register slots when the
/// call site passed fewer args than the declaration claimed) AND to pad
/// dispatch-tower call sites with TAG_UNDEFINED so default-param
/// desugaring fires correctly. Empty Vec is the legacy fallback for
/// source modules that haven't been updated to populate it — codegen
/// falls back to the old upper bound when the entry is missing.
pub method_param_counts: Vec<usize>,
/// Issue #672: parallel to `method_names`. `true` means the method's
/// last declared parameter is `...rest`. Without this, cross-module call
/// sites to `c.cmd('a', 'b', 'c')` on `class C { cmd(name, ...args) }`
/// would not pack the trailing `'b', 'c'` into a rest array — only the
/// home module's `method_has_rest` was populated. Symmetric to #484's
/// fix for the freestanding-function path. Empty Vec means "fall through
/// to the old behavior (no rest)".
pub method_has_rest: Vec<bool>,
/// Static field names defined on this class. Used to declare the foreign
/// `@perry_static_<src>__<class>__<field>` global with external linkage
/// so cross-module `[Parent.Symbol.X] = …` reads/writes resolve to the
/// source module's defining global. Without this, `StaticFieldGet`
/// silently produces `0.0` for any imported class. Refs #420.
pub static_field_names: Vec<String>,
/// Static method names defined on this class. Without this, calls like
/// `MyClass.staticMethod(...)` on an imported class are treated as a
/// missing method and fall through to `0.0` — turning every
/// `await Foo.connect(...)` into a no-op that resolves with the number 0.
pub static_method_names: Vec<String>,
/// Getter property names. Without these, cross-module `obj.prop` for a
/// getter property silently falls through to `undefined` because the
/// dispatch site at `expr.rs::PropertyGet` looks up `(class, "__get_prop")`
/// in `method_names`, which previously had no cross-module entry.
pub getter_names: Vec<String>,
/// Setter property names. Symmetric to `getter_names` for `obj.prop = v`.
pub setter_names: Vec<String>,
/// Parent class name, if any.
pub parent_name: Option<String>,
/// Field names in declaration order (for allocation sizing and field index mapping).
pub field_names: Vec<String>,
/// Field types in the same order as `field_names`. Required for
/// `receiver_class_name` to walk through chained `obj.a.b.c` accesses
/// where `a` and `b` are fields whose declared type is itself an
/// imported class. Without this, every field access on an imported
/// class returns `Type::Any` and the dispatch chain breaks at the
/// first hop. Empty (or filled with `Type::Any`) is the legacy fallback
/// when the source side hasn't been updated to populate it yet.
pub field_types: Vec<perry_types::Type>,
/// Class id assigned by the source module. When present, the importing
/// module reuses this id in its `class_ids` map so that `instanceof`
/// on an imported class compares against the same id stamped onto
/// instances by the source module's constructor. `None` falls back
/// to a freshly-assigned id (legacy behavior).
pub source_class_id: Option<u32>,
}
/// Cross-module import context, bundled into a single struct to avoid
/// adding five more individual parameters to every compile_* function.
/// Built once in `compile_module` from `CompileOptions`.
pub(crate) struct CrossModuleCtx {
pub namespace_imports: std::collections::HashSet<String>,
/// Issue #680: per-namespace member resolution. See doc on
/// `CompileOptions::namespace_member_prefixes`.
pub namespace_member_prefixes: std::collections::HashMap<(String, String), String>,
pub imported_async_funcs: std::collections::HashSet<String>,
/// FuncIds of locally-defined async functions in this module. Populated
/// from `hir.functions.is_async`. Used by `is_promise_expr` to refine
/// `let p = asyncFn();` to `Promise(_)` so subsequent `p.then(cb)`
/// chains route through `js_promise_then`.
pub local_async_funcs: std::collections::HashSet<u32>,
pub type_aliases: std::collections::HashMap<String, perry_types::Type>,
pub imported_func_param_counts: std::collections::HashMap<String, usize>,
/// Issue #608 — imported function names whose source-side signature
/// has a trailing `...rest` parameter. Used by the cross-module call
/// site in `lower_call.rs` to pack trailing args into a rest array.
pub imported_func_has_rest: std::collections::HashSet<String>,
pub imported_func_return_types: std::collections::HashMap<String, perry_types::Type>,
/// Per-method explicit param counts, keyed by `(class_name, method_name)`.
/// Built once in `compile_module` from BOTH local `hir.classes` AND
/// `opts.imported_classes`. Used at every method-call dispatch site in
/// `lower_call.rs` to pad missing trailing args with TAG_UNDEFINED so
/// the callee's default-param desugaring fires correctly.
/// Pre-fix the dispatch tower passed only the user-provided args + recv
/// to a function declared with N+1 doubles, leaving any param the caller
/// skipped to be read from an uninitialized arg-register slot. On
/// AArch64 / Win64 those slots typically held a real heap pointer left
/// over from a prior call's return state — dereferencing `options.session`
/// inside the dispatch chain silently hung. See issue #235.
pub method_param_counts: std::collections::HashMap<(String, String), usize>,
/// Per-`(class, method)` rest-parameter flag. Set when the method's
/// final declared param is `...rest` — drives the call-site
/// rest-bundling in `lower_call.rs`'s static / dynamic dispatch
/// arms. Closes #484. Sparse map (only `true` entries stored).
pub method_has_rest: std::collections::HashMap<(String, String), bool>,
/// Per-class `keys_array` global variable names. Each entry maps
/// `class_name → @perry_class_keys_<modprefix>__<sanitized_class>`.
/// Built once in `compile_module` (one entry per class — local
/// definitions + imported stubs). `compile_new` looks up the
/// class here and emits a direct global load + the inline-keys
/// allocator. See `js_object_alloc_class_inline_keys` in
/// `perry-runtime/src/object.rs`.
pub class_keys_globals: std::collections::HashMap<String, String>,
/// Imported class constructor function names. Maps class_name →
/// full constructor symbol (e.g. "Editor" → "hone_editor_...__Editor_constructor").
/// Populated from `opts.imported_classes`.
pub imported_class_ctors: std::collections::HashMap<String, (String, usize)>,
/// Compile-time i18n table for resolving `Expr::I18nString` against
/// the project's default locale. `None` when i18n is not configured.
/// Built from `opts.i18n_table` once at the top of `compile_module`
/// and threaded through every `FnCtx` instantiation as a shared
/// borrow via `cross_module.i18n`.
pub i18n: Option<crate::expr::I18nLowerCtx>,
/// Names of imports that are exported variables (not functions).
pub imported_vars: std::collections::HashSet<String>,
/// Whether perry-stdlib will be linked into the final binary. When
/// false, compile_module_entry skips the `js_stdlib_init_dispatch()`
/// call in main's prologue because only the runtime is linked and
/// the stub symbol isn't pulled in (runtime is built with the
/// `stdlib` feature on when perry-stdlib depends on it, which
/// excludes the cfg-gated stub in `perry-runtime/src/stdlib_stubs.rs`).
pub needs_stdlib: bool,
/// Whether the project needs the Geisterhand inspector linked in.
/// Threaded through from `CompileOptions::needs_geisterhand` so the
/// entry-module init prelude can emit the `perry_geisterhand_start`
/// call site (which also pins the geisterhand server module against
/// `-dead_strip`, keeping `INSPECTOR_HTML` referenced).
pub needs_geisterhand: bool,
/// Port the Geisterhand inspector listens on when `needs_geisterhand`.
pub geisterhand_port: u16,
/// Whether the project pulls in `libperry_jsruntime.a` because some
/// `.ts` module imports from a `.js` file the resolver classified as
/// JS-runtime-loaded (issue #248). Threaded through so the entry-module
/// init prelude can emit the `js_runtime_init()` call before any
/// `js_load_module` / `js_call_function` site fires.
pub needs_js_runtime: bool,
/// Compile-time constant values for module globals. Maps LocalId → f64
/// for variables like `__platform__` whose value is known at compile time.
/// Used by `lower_if` to constant-fold platform checks and skip emitting
/// dead branches (which may reference FFI functions that don't exist on
/// the current target).
pub compile_time_constants: std::collections::HashMap<u32, f64>,
/// App metadata backing compile-time `perry/system` introspection APIs.
pub app_metadata: AppMetadata,
/// Functions with a 3-param clamp pattern: fid → true. Call sites
/// emit `@llvm.smax.i32` + `@llvm.smin.i32` instead of a function call.
pub clamp3_functions: std::collections::HashSet<u32>,
/// Functions with clampU8 pattern (1 param, clamp to [0, 255]).
pub clamp_u8_functions: std::collections::HashSet<u32>,
/// Functions that always return integer (all returns end with `| 0` etc).
pub returns_int_functions: std::collections::HashSet<u32>,
/// (Issue #50) Module-level `const` 2D int arrays folded into flat
/// `[N x i32]` LLVM constants. Maps local_id → info. Populated by
/// scanning `hir.init`; threaded through every FnCtx so the IndexGet
/// lowering can intercept `X[i][j]` / `krow[j]` patterns.
pub flat_const_arrays: std::collections::HashMap<u32, crate::expr::FlatConstInfo>,
/// FFI manifest signatures from `package.json`'s `nativeLibrary.functions`.
/// Maps function name → (param_kinds, return_kind) where each kind is
/// `"i64"`, `"f64"`, `"void"`, `"string"`, or `"ptr"`. Without this map,
/// `lower_call` falls back to a heuristic that puts all numeric args/returns
/// into d-registers (DOUBLE) — incorrect for handle-returning C functions
/// like `hone_editor_create() -> *mut EditorView` whose actual ABI returns
/// the pointer in `x0`, not `d0`. The manifest tells us when to use
/// `i64`/`I64` so the LLVM declaration matches the platform C ABI.
pub ffi_signatures: std::collections::HashMap<String, (Vec<String>, String)>,
/// Per-module mapping: local class/binding name → import source spec.
/// Built once in `compile_module` from `hir.imports`. Used by
/// `lower_builtin_new` to disambiguate ambiguously-named built-in
/// constructors. Without this, `import Client from "better-sqlite3"`
/// (where `Client` is a default-import alias for the sqlite Database
/// class) silently dispatches through the pg `Client` arm and emits
/// an undefined `_js_pg_client_new` reference. With this map, the
/// "Client" arm only fires when the local `Client` was imported from
/// "pg" (named or default). See issue #602.
pub imported_class_sources: std::collections::HashMap<String, String>,
/// Issue #655: map from interface name → HIR Interface definition.
/// Lets `static_type_of` resolve `obj.field` when `obj` is typed
/// against a TS `interface` (not a `class`). The `class_table`
/// only contains real classes, so without this lookup chained
/// access like `m.get(k)!.field.shift()` fell through to generic
/// property dispatch and Array methods returned garbage.
pub interfaces: std::collections::HashMap<String, perry_hir::Interface>,
/// Issue #100: namespace-entry list for this module's
/// `@__perry_ns_<prefix>` populator. Empty unless this module is
/// the target of at least one dynamic `import()` site in the
/// program. Populated at the end of `__perry_init_<prefix>` (or
/// `main` for the entry module).
pub namespace_entries: Vec<NamespaceEntry>,
/// Issue #100: map from each `Expr::DynamicImport` path-arg string
/// to the sanitized prefix of the target module. Read by the
/// dispatch site in `expr.rs::Expr::DynamicImport` to find the
/// `@__perry_ns_<target_prefix>` global to load.
pub dynamic_import_path_to_prefix: std::collections::HashMap<String, String>,
}
/// Compile a Perry HIR module to an object file via LLVM IR.
///
/// CRITICAL (#686): `hir` MUST be `&HirModule` (shared reference), never
/// `&mut`. The caller computes `perry_hir::stable_hash::hash_module(hir)`
/// just before this call to derive the per-module object cache key. If
/// codegen ever mutated the HIR mid-compile, the cached `.o` would no
/// longer correspond to the hashed input and stale entries would be
/// served on subsequent builds. The `&` here is the load-bearing
/// guarantee — do not change to `&mut` without also moving the cache
/// hash to AFTER codegen.
pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>> {
// Set the per-instruction FMF emission mode for this build before
// any LlBlock methods run. All modules in a single program build
// share the same `fast_math` setting, so writing the AtomicBool
// here (potentially redundantly when rayon parallelizes module
// compiles) is safe — every store writes the same value.
crate::block::FAST_MATH.store(opts.fast_math, std::sync::atomic::Ordering::Relaxed);
let triple = opts.target.clone().unwrap_or_else(default_target_triple);
let mut llmod = LlModule::new(&triple);
// Null guard global: a zeroed i32 used as a safe dereference target
// when a NaN-unboxed pointer is null/invalid. Prevents segfaults from
// uninitialized locals or unhandled expressions producing 0.0/TAG_UNDEFINED.
llmod.add_internal_global("perry_null_guard_zero", crate::types::I32, "0");
runtime_decls::declare_phase1(&mut llmod);
// Derive a per-module symbol prefix from the HIR module name:
//
// self.module_symbol_prefix = hir.name.replace(|c: char|
// !c.is_alphanumeric() && c != '_', "_");
//
// Every emitted symbol that could collide across modules
// (user functions, class methods, string pool globals, handle slots,
// module-level globals) gets prefixed with this. The entry module's
// `main` is the only globally-named symbol — non-entry modules emit
// `<prefix>__init` instead.
let module_prefix = sanitize(&hir.name);
// Imports are no longer a hard error — Phase F.1 supports multi-
// module compilation. Cross-module function CALLS via ExternFuncRef
// still land in Phase F.2; for now they'll error at the use site
// with a specific message.
// Phase C.2: classes (and inheritance!) are supported. Perry's HIR
// lowering aggressively pre-resolves both methods and super calls
// into inline statements at the constructor/method body, so the
// LLVM codegen mostly sees a flat object-allocation + field-set
// pattern. We let everything through and let the expression-level
// codegen error at any specific construct it doesn't know how to
// handle.
// Module-wide string literal pool. Owned by the codegen so that
// `compile_function` and `compile_main` can take split borrows of
// (&mut LlFunction, &mut StringPool) without confusing the borrow
// checker — the pool lives outside LlModule. The module prefix
// becomes part of every emitted global so multi-module programs
// don't collide on `.str.0.handle`.
let mut strings = StringPool::with_prefix(module_prefix.clone());
// Class lookup table for `Expr::New`. Indexed by class name —
// the HIR has unique names per module.
let mut class_table: HashMap<String, &perry_hir::Class> =
hir.classes.iter().map(|c| (c.name.clone(), c)).collect();
// Refs #486: also register class-expression self-binding aliases so
// `lookup_new("_X")` and other code paths that consult `class_table` by
// name find the underlying class. See `class_ids` block below for the
// companion id-map registration and the broader rationale.
for c in &hir.classes {
for alias in &c.aliases {
class_table.entry(alias.clone()).or_insert(c);
}
}
// Class id assignment: each user class gets an integer id
// starting at 1 (0 is reserved for anonymous object literals).
// Used by lower_new to tag the object header so virtual
// dispatch and instanceof can read the actual class at runtime.
//
// We use the HIR `ClassId` (assigned by `LoweringContext::fresh_class`)
// rather than a per-module enumerate index, because in multi-module
// compilation the HIR counter is shared across modules (compile.rs
// threads `next_class_id` through `lower_module_with_class_id_and_types`).
// Importing modules look up imported classes via their HIR id (passed
// as `ImportedClass.source_class_id`); using the HIR id here too means
// the source module stamps the same id on `new C()` instances that
// importing modules check against in `e instanceof C`.
let mut class_ids: HashMap<String, u32> =
hir.classes.iter().map(|c| (c.name.clone(), c.id)).collect();
// Refs #486: register class-expression self-binding aliases (e.g. the
// `_X` in `var X = class _X { ... }`) so `new _X()` from inside the class
// body resolves to the same class id as `new X()` would. Without this,
// lower_new("_X") falls into the placeholder path and stamps class_id=0
// on the new instance, breaking method dispatch.
for c in &hir.classes {
for alias in &c.aliases {
class_ids.entry(alias.clone()).or_insert(c.id);
}
}
// Enum lookup table for `Expr::EnumMember`. Each (enum_name,
// member_name) maps to its EnumValue, which the codegen lowers
// to either a numeric or string constant. Built once here.
let mut enum_table: HashMap<(String, String), perry_hir::EnumValue> = hir
.enums
.iter()
.flat_map(|e| {
e.members
.iter()
.map(move |m| ((e.name.clone(), m.name.clone()), m.value.clone()))
})
.collect();
// ── Phase F: merge imported cross-module definitions ──────────
//
// Imported enums: add their members to the enum_table so
// `Expr::EnumMember` can resolve them in this module.
for (enum_name, members) in &opts.imported_enums {
for (member_name, value) in members {
enum_table
.entry((enum_name.clone(), member_name.clone()))
.or_insert_with(|| value.clone());
}
}
// Imported classes: build lightweight stub `Class` objects so the
// codegen dispatch tables (class_table, method_names, class_ids)
// can resolve cross-module class method calls. The actual method
// bodies live in the other module's .o — here we only need the
// metadata for dispatch and the extern LLVM declarations for the
// linker.
let mut imported_class_stubs: Vec<perry_hir::Class> = Vec::new();
// Fallback id range for imported classes whose source_class_id is None
// (legacy callers that didn't populate it). Start above the max local
// HIR id so we don't collide with local class ids.
let next_class_id = hir.classes.iter().map(|c| c.id).max().unwrap_or(0) + 1;
for (idx, ic) in opts.imported_classes.iter().enumerate() {
// Prefer the source module's class id so `instanceof` on an
// imported class matches the id stamped onto real instances
// by the source module's constructor. Fall back to a freshly
// assigned id when the caller didn't pass one.
let class_id = ic
.source_class_id
.unwrap_or_else(|| next_class_id + (idx as u32));
let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name);
// Skip if already defined locally (local definition takes precedence).
if class_table.contains_key(effective_name) {
continue;
}
// Assign a class id for dispatch / instanceof.
//
// Refs #665: `or_insert` (first-writer-wins) instead of `insert`
// (last-writer-wins). When two different classes are both
// default-imported in the same file, both register under
// `effective_name = "default"`. `class_table.entry().or_insert()`
// below already keeps the first stub for that key; the side maps
// must agree, otherwise the method registry builds symbols mixing
// the FIRST writer's methods with the LAST writer's prefix +
// canonical name, producing fnames the linker can't resolve.
class_ids
.entry(effective_name.to_string())
.or_insert(class_id);
// Also register the canonical name if aliased.
if ic.local_alias.is_some() && !class_ids.contains_key(&ic.name) {
class_ids.insert(ic.name.clone(), class_id);
}
// Build a stub Class with the minimum fields the codegen needs.
// Most fields are empty — only name, extends_name, and methods
// are consulted by dispatch.
let stub = perry_hir::Class {
id: 0, // imported — no local ClassId
name: effective_name.to_string(),
type_params: Vec::new(),
extends: None,
extends_name: ic.parent_name.clone(),
native_extends: None,
extends_expr: None,
fields: ic
.field_names
.iter()
.enumerate()
.map(|(i, name)| perry_hir::ClassField {
name: name.clone(),
key_expr: None,
// Use the real declared type when the source-side
// populated `field_types`; fall back to `Any` otherwise.
// Real types let `receiver_class_name`'s `PropertyGet`
// recursion identify chained imported-class field
// dispatch (e.g. `vm.viewport.scroll.scrollTop`).
ty: ic
.field_types
.get(i)
.cloned()
.unwrap_or(perry_types::Type::Any),
init: None,
is_private: false,
is_readonly: false,
decorators: Vec::new(),
})
.collect(),
constructor: None,
methods: ic
.method_names
.iter()
.map(|m| perry_hir::Function {
id: 0,
name: m.clone(),
type_params: Vec::new(),
params: Vec::new(),
return_type: perry_types::Type::Any,
body: Vec::new(),
is_async: false,
is_generator: false,
was_plain_async: false,
was_unrolled: false,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
})
.collect(),
getters: Vec::new(),
setters: Vec::new(),
static_fields: Vec::new(),
static_methods: Vec::new(),
decorators: Vec::new(),
is_exported: false,
aliases: Vec::new(),
};
imported_class_stubs.push(stub);
}
// Issue #309: break inheritance-chain cycles in imported_class_stubs.
// Effect (and other heavily-modular TypeScript packages) declare
// same-named classes across modules (e.g. multiple `class Base extends X`
// inside IIFEs in Data.ts, plus `class Class extends Base` in
// Effectable.ts). When pulled into a single importing module's
// class_table by name, the chains can form a cycle:
// local Base → extends "Class" (imported stub from Effectable)
// imported Class → parent_name "Base" (resolves back to local Base)
// → cycle.
// Every chain-walking site in codegen assumed acyclic inheritance, so
// a single such cycle causes either an OOM (Vec-accumulating walks like
// `apply_field_initializers_recursive`) or a CPU-hang (counter walks
// like `class_field_global_index`). We break the cycle once at this
// central point by detecting it via DFS over the (local ∪ stub) union
// and dropping `extends_name` on the FIRST imported stub that closes
// the cycle. All downstream chain walks then operate on a guaranteed-
// acyclic graph. The fundamental name-collision problem (Data.ts's
// local "Base" being a different class than Effectable.ts's "Base"
// even though they share a name) is left unfixed — that requires
// module-prefixing class names in HIR and is a separate refactor; the
// cycle break here is purely defensive.
{
let local_class_names: std::collections::HashSet<&str> =
hir.classes.iter().map(|c| c.name.as_str()).collect();
let mut stub_idx_by_name: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for (idx, stub) in imported_class_stubs.iter().enumerate() {
stub_idx_by_name.entry(stub.name.clone()).or_insert(idx);
}
// For each stub, walk the chain in the union name space. If the
// walk revisits a name OR exceeds a sane depth cap, drop this
// stub's parent so the cycle dies here.
let mut to_drop: Vec<usize> = Vec::new();
for (idx, stub) in imported_class_stubs.iter().enumerate() {
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
visited.insert(stub.name.clone());
let mut cur = stub.extends_name.clone();
let mut depth: usize = 0;
let mut cycle = false;
while let Some(name) = cur {
depth += 1;
if depth > 64 {
cycle = true;
break;
}
if !visited.insert(name.clone()) {
cycle = true;
break;
}
// Parent resolution: prefer LOCAL class over imported stub
// (matches `class_table.entry().or_insert()` semantics
// below).
cur = if local_class_names.contains(name.as_str()) {
hir.classes
.iter()
.find(|c| c.name == name)
.and_then(|c| c.extends_name.clone())
} else if let Some(&pidx) = stub_idx_by_name.get(&name) {
imported_class_stubs[pidx].extends_name.clone()
} else {
None
};
}
if cycle {
to_drop.push(idx);
}
}
for idx in to_drop {
imported_class_stubs[idx].extends_name = None;
}
}
// Add imported class stubs to the class_table (references into the
// Vec we just built — the Vec lives for the remainder of compile_module).
// Also build a map from class name → source module prefix so method
// dispatch generates the correct cross-module symbol name.
//
// Skip imports that collide by name with a LOCAL class (#431). The
// local class shadows the import in `class_table` (the
// `class_table.entry().or_insert()` loop below preserves the local
// entry), so this map must not point a local-class lookup at an
// import's source prefix — doing so makes `compile_method` mangle
// the LOCAL methods under the IMPORTED module's prefix while the
// dispatch-table builder (line ~3614) still references them under
// the local prefix, leaving `@perry_method_<local>__<C>__<m>`
// undefined at link time. This is the cross-module sibling of
// #336's intra-module collision; #336 disambiguated the
// `@perry_class_keys_*` global, but the method-body prefix needs
// the same fix for cross-module name reuse (Effect's `Class` /
// `Refinement` / `Composite` / `ParseError` /
// `PropertySignatureTransformation` / `DroppingStrategy` cases).
let mut imported_class_prefix: HashMap<String, String> = HashMap::new();
// Issue #568: when `import { Widget as PublicWidget }` (or the
// re-export shape `export { Widget as PublicWidget }` followed by
// `import { PublicWidget }`) renames a cross-module class, the stub
// pushed into `class_table` carries `name = effective_name` (the
// alias). Method-symbol mangling needs the SOURCE-side name (the
// canonical `ic.name`) so the LLVM call resolves to the symbol the
// source module's `.o` actually exports. This side map lets the
// method-registry loop below recover the source name.
let mut imported_class_source_name: HashMap<String, String> = HashMap::new();
for ic in &opts.imported_classes {
let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name);
if hir.classes.iter().any(|c| c.name == *effective_name) {
continue;
}
// Refs #665: first-writer-wins to match `class_table`'s
// `.or_insert()` semantics (see the class-id loop above). When two
// different classes are both default-imported, both register under
// `effective_name = "default"`; using `.insert()` would let the
// LAST writer's source_prefix / canonical name win, while
// `class_table["default"]` keeps the FIRST writer's stub. The
// method-registry builder reads both, and the mismatch produces
// method symbols mangled under the wrong class — the linker can't
// resolve them and the build fails with "undefined value".
imported_class_prefix
.entry(effective_name.to_string())
.or_insert_with(|| ic.source_prefix.clone());
if effective_name != ic.name {
imported_class_source_name
.entry(effective_name.to_string())
.or_insert_with(|| ic.name.clone());
}
}
for stub in &imported_class_stubs {
class_table.entry(stub.name.clone()).or_insert(stub);
}
// Local async function FuncIds — populated below from `hir.functions`
// (the per-function loop further down). Built here so the CrossModuleCtx
// construction is complete before the FnCtx instances reference it.
let mut local_async_funcs: std::collections::HashSet<u32> = std::collections::HashSet::new();
for f in &hir.functions {
// Include both truly-async functions and those transformed from
// async to generator (was_plain_async=true, is_async=false after
// the v0.5.371 async-to-generator pass) — both return Promises
// so is_promise_expr must recognize their call sites.
if f.is_async || f.was_plain_async {
local_async_funcs.insert(f.id);
}
}
// Per-class keys-array globals: each class gets a single internal
// global `@perry_class_keys_<modprefix>__<class>` that holds the
// shared keys_array pointer (built ONCE at module init via
// js_build_class_keys_array). Every `new ClassName()` site then
// emits a direct global load + inline allocator call, bypassing
// the per-call SHAPE_CACHE lookup AND the runtime
// js_object_alloc_class_with_keys function entirely on the hot
// allocation path.
//
// Per-class init data: (global_name, packed_keys_string, total_field_count).
// Used by emit_string_pool to emit the build-call sequence.
let mut class_keys_init_data: Vec<(String, String, u32)> = Vec::new();
let mut class_keys_globals_map: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for c in &hir.classes {
let global_name = format!("perry_class_keys_{}__{}", module_prefix, sanitize(&c.name),);
llmod.add_internal_global(&global_name, I64, "0");
// Build the packed-keys string. Format: each field name
// followed by `\0`. Parent classes contribute their fields
// first (walking from deepest ancestor down) so the slot
// order matches `class_field_global_index`'s assumption.
let mut packed_keys = String::new();
// Skip computed-key fields (`[Symbol.for("k")] = …`): their key is an
// expression evaluated at runtime, not a stable string, so they don't
// get an inline slot. Including their synthetic `__computed_field_*`
// names in the packed keys would surface them as enumerable own
// properties via Object.keys() and inflate the inline-slot count.
// Their values are stored via `apply_field_initializers_recursive`'s
// IndexSet path → js_object_set_field / js_object_set_symbol_property.
let count_keyable = |fields: &[perry_hir::ClassField]| -> u32 {
fields.iter().filter(|f| f.key_expr.is_none()).count() as u32
};
let mut total_field_count = count_keyable(&c.fields);
let mut parent_chain: Vec<String> = Vec::new();
// Resolver that finds a parent's `(fields_vec, next_extends)` either
// in the local HIR or, failing that, in the imported_class_stubs
// built earlier in this fn (which carry `ic.field_names` as full
// ClassField records). Issue #485: without falling back to imports,
// a local subclass that extends an IMPORTED parent ends up with a
// packed_keys / total_field_count that omits the parent's fields,
// so instances get allocated with too-few inline slots and the
// parent's cross-module ctor's `this.field = ...` writes overflow
// the object header — making `f.field` read undefined on the
// importing side even though the parent's ctor "ran".
let lookup_class_chain_link =
|name: &str| -> Option<(Vec<perry_hir::ClassField>, Option<String>)> {
if let Some(parent) = hir.classes.iter().find(|cls| cls.name == name) {
return Some((parent.fields.clone(), parent.extends_name.clone()));
}
if let Some(stub) = imported_class_stubs.iter().find(|cls| cls.name == name) {
return Some((stub.fields.clone(), stub.extends_name.clone()));
}
None
};
let mut p = c.extends_name.clone();
while let Some(parent_name) = p {
if let Some((parent_fields, parent_extends)) = lookup_class_chain_link(&parent_name) {
parent_chain.push(parent_name.clone());
total_field_count += count_keyable(&parent_fields);
p = parent_extends;
} else {
break;
}
}
// Walk from deepest ancestor to direct parent.
for parent_name in parent_chain.iter().rev() {
if let Some((parent_fields, _)) = lookup_class_chain_link(parent_name) {
for f in &parent_fields {
if f.key_expr.is_some() {
continue;
}
packed_keys.push_str(&f.name);
packed_keys.push('\0');
}
}
}
for f in &c.fields {
if f.key_expr.is_some() {
continue;
}
packed_keys.push_str(&f.name);
packed_keys.push('\0');
}
class_keys_globals_map.insert(c.name.clone(), global_name.clone());
// Refs #486: register self-binding aliases (`_X` from `var X = class _X`)
// so the inline-alloc fast path at lower_call.rs:2532 finds the keys
// global when the class is referenced by its inner name. Without this,
// `new _X()` would fall into the slower `js_object_alloc_class_with_keys`
// path that builds packed_keys at the call site — which works but is
// unnecessarily slow.
for alias in &c.aliases {
class_keys_globals_map
.entry(alias.clone())
.or_insert_with(|| global_name.clone());
}
class_keys_init_data.push((global_name, packed_keys, total_field_count));
}
// Same naming convention for IMPORTED class stubs. Pack the field
// names so the importing module allocates the right inline slot count
// and the slot index for each field matches what the source module's
// constructor wrote. Without this, the object is allocated 0 inline
// slots and `this.field = v` in the cross-module constructor writes
// past the object, while reads on the importing side return undefined.
for c in imported_class_stubs.iter() {
if hir.classes.iter().any(|local| local.name == c.name) {
continue;
}
// Skip duplicate imported stubs of the same name. Two namespace
// re-exports of the same class (e.g., `export * as A from "./mod"`
// and `export * as B from "./mod"`) can register the same class
// twice in `imported_class_stubs`. Without this guard, codegen
// would emit `@perry_class_keys_<modprefix>__<name>` twice and
// clang would reject the IR with "redefinition of global". See #336.
if class_keys_globals_map.contains_key(&c.name) {
continue;
}
let global_name = format!("perry_class_keys_{}__{}", module_prefix, sanitize(&c.name),);
llmod.add_internal_global(&global_name, I64, "0");
class_keys_globals_map.insert(c.name.clone(), global_name.clone());
let mut packed_keys = String::new();
let mut total_field_count = c.fields.len() as u32;
// Issue #485: imported subclass stubs also need their parent's
// fields prepended to the packed-keys, so allocations on this
// importing side reserve enough inline slots for parent +
// child. Without this, `new Sub()` in the importing module
// allocates 0 slots when Sub has no own fields and the
// cross-module ctor's `this.parentField = v` writes past the
// object header — exactly the same shape collapse the local-
// class branch above guards against.
let mut parent_chain: Vec<String> = Vec::new();
let mut p = c.extends_name.clone();
while let Some(parent_name) = p {
if let Some(parent) = imported_class_stubs
.iter()
.find(|cls| cls.name == parent_name)
{
parent_chain.push(parent_name.clone());