Repository navigation
Expand file tree
/
Copy pathmod.rs
More file actions
3562 lines (3472 loc) · 165 KB
/
Copy pathmod.rs
File metadata and controls
3562 lines (3472 loc) · 165 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::cell::Cell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use perry_hir::Module as HirModule;
use crate::module::LlModule;
use crate::runtime_decls;
use crate::strings::StringPool;
use crate::types::{LlvmType, DOUBLE, I32, I64};
pub(super) struct CompileProgress {
enabled: bool,
started: Instant,
last_checkpoint: Cell<Instant>,
phase: Arc<AtomicU8>,
stop: Arc<AtomicBool>,
worker: Option<std::thread::JoinHandle<()>>,
module: String,
}
impl CompileProgress {
fn new(module: &str, callables: usize) -> Self {
let progress_mode = std::env::var("PERRY_CODEGEN_PROGRESS").unwrap_or_default();
// Avoid three status lines for every tiny module in dependency-heavy
// projects. Long modules get automatic reporting; `=all` is the
// diagnostic override when per-module detail is wanted regardless.
let enabled = progress_mode == "all" || (progress_mode == "1" && callables >= 1_000);
let started = Instant::now();
let phase = Arc::new(AtomicU8::new(0));
let stop = Arc::new(AtomicBool::new(false));
if enabled {
eprintln!("[perry] codegen: lowering {module} ({callables} callables)");
}
let worker = enabled.then(|| {
let phase = Arc::clone(&phase);
let stop = Arc::clone(&stop);
let module = module.to_string();
std::thread::Builder::new()
.name("perry-progress".into())
.spawn(move || {
while !stop.load(Ordering::Relaxed) {
std::thread::park_timeout(Duration::from_secs(30));
if stop.load(Ordering::Relaxed) {
break;
}
let label = match phase.load(Ordering::Relaxed) {
0 => "lowering HIR",
1 => "finalizing generated IR",
2 => "partitioning/freezing/LLVM codegen",
3 => "releasing generated IR",
_ => "LLVM optimization and object emission",
};
eprintln!(
"[perry] codegen: {module}: {label}, elapsed {:.1} min",
started.elapsed().as_secs_f64() / 60.0
);
}
})
.expect("spawn Perry progress reporter")
});
Self {
enabled,
started,
last_checkpoint: Cell::new(started),
phase,
stop,
worker,
module: module.to_string(),
}
}
fn phase(&self, phase: u8, label: &str) {
self.phase.store(phase, Ordering::Relaxed);
if self.enabled {
eprintln!(
"[perry] codegen: {}: {} ({:.1}s elapsed)",
self.module,
label,
self.started.elapsed().as_secs_f64()
);
}
}
/// Report a completed lowering subphase. Large generated bundles can spend
/// minutes before LLVM sees any IR, so a coarse heartbeat is not enough to
/// distinguish useful progress from a stuck compiler. Keeping the lap time
/// here also makes the output directly usable as a lightweight profile.
pub(super) fn checkpoint(&self, label: &str) {
let now = Instant::now();
let previous = self.last_checkpoint.replace(now);
if self.enabled {
eprintln!(
"[perry] codegen: {}: {} in {:.1}s ({:.1}s total)",
self.module,
label,
now.duration_since(previous).as_secs_f64(),
now.duration_since(self.started).as_secs_f64()
);
}
}
pub(super) fn items(&self, label: &str, done: usize, total: usize, started: Instant) {
if !self.enabled || total == 0 {
return;
}
let elapsed = started.elapsed().as_secs_f64();
let eta = if done == 0 {
0.0
} else {
elapsed * (total.saturating_sub(done)) as f64 / done as f64
};
eprintln!(
"[perry] codegen: {}: {} {}/{} ({:.0}%; {:.1}s elapsed; ETA ~{:.1}s)",
self.module,
label,
done,
total,
done as f64 * 100.0 / total as f64,
elapsed,
eta
);
}
}
impl Drop for CompileProgress {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(worker) = self.worker.take() {
worker.thread().unpark();
let _ = worker.join();
}
if self.enabled {
eprintln!(
"[perry] codegen: {}: stage finished in {:.1}s",
self.module,
self.started.elapsed().as_secs_f64()
);
}
}
}
pub(crate) mod arguments;
mod artifacts;
mod boxed_locals;
mod closure;
mod closure_collect;
mod ctor_arity;
#[cfg(test)]
mod emission_order_tests;
mod entry;
pub mod entry_outline;
mod func_registry;
mod function;
#[cfg(test)]
mod hoisted_callback_method_tests;
#[cfg(test)]
mod index_method_clone_tests;
// `pub(crate)` so `crate::linker` can read the inline-hot-small policy
// (`inline_hot_small_enabled` / `inline_hot_small_hint_threshold`).
#[cfg(test)]
mod clone_suffix_tests;
#[cfg(test)]
mod declared_string_add_tests;
pub(crate) mod helpers;
mod method;
mod method_registry;
mod module_globals_emit;
mod native_namespace_exports;
#[cfg(test)]
mod number_exactness_tests;
mod opts;
#[cfg(test)]
mod ordinary_param_guard_tests;
mod param_guard;
mod spec_abi;
#[cfg(test)]
mod spec_preserve_none_tests;
mod spec_return_proof;
#[cfg(test)]
mod spec_self_recursion_tests;
pub(crate) mod static_fields;
mod string_pool;
#[cfg(test)]
mod testing_feature_gate_tests;
#[cfg(test)]
mod trusted_box_callback_tests;
mod typed_abi;
mod typed_abi_opt_report;
#[cfg(test)]
mod unknown_func_tests;
pub(crate) use closure::emit_typed_capture_guard;
pub(crate) use function::arena_threaded_function_body_name;
pub use helpers::resolve_target_triple;
pub(crate) use helpers::{
decide_codegen_units, decide_full_outline_ic, default_target_triple, full_outline_ic_enabled,
module_callable_count, set_full_outline_ic, write_barriers_enabled,
};
pub use opts::{
AppMetadata, CompileOptions, FpContractMode, ImportedClass, NamespaceEntry, NamespaceEntryKind,
};
pub(crate) use opts::{CrossModuleCtx, ImportedCtor};
pub(crate) use param_guard::scalar_descriptor_rep;
pub(crate) use spec_abi::{spec_abi_enabled, spec_function_name, SpecDispatch, SpecFnPlan};
pub(crate) use typed_abi::{
emit_typed_arg_guard, emit_typed_arg_to_raw, generic_closure_body_name,
generic_function_body_name, generic_method_body_name, nonnegative_index_method_name,
typed_arg_is_guard_candidate, typed_f64_closure_name, typed_f64_function_name,
typed_f64_method_name, typed_f64_receiver_method_info, typed_f64_receiver_method_name,
typed_i1_closure_name, typed_i1_function_name, typed_i1_method_name, typed_i32_closure_name,
typed_i32_function_name, typed_i32_method_name, typed_param_reps_match_args,
typed_string_closure_name, typed_string_function_name, typed_string_method_name, TypedParamRep,
TypedReceiverMethodInfo,
};
use artifacts::{emit_module_artifacts, ModuleArtifactsCtx};
use function::{
compile_function, compile_typed_f64_function, compile_typed_i1_function,
compile_typed_i32_function, compile_typed_string_function,
};
use helpers::{
collect_return_class, emit_buffer_alias_metadata, function_body_returns_generator_object,
sanitize,
};
// Collector and boxing-analysis walkers live in dedicated modules. The
// module-wide pre-walk passes that consumed them moved into the
// `boxed_locals` / `closure_collect` / `module_globals_emit` siblings, which
// import them directly; the trunk no longer references them.
pub(super) fn spec_function_length(params: &[perry_hir::Param]) -> usize {
params
.iter()
.take_while(|p| !p.is_rest && p.default.is_none())
.count()
}
fn should_record_typed_clone_rejection(reason: typed_abi::TypedCloneRejectionReason) -> bool {
if std::env::var_os("PERRY_NATIVE_REPS_ALL_TYPED_CLONE_REJECTIONS").is_some() {
return !matches!(reason, typed_abi::TypedCloneRejectionReason::NotClosure);
}
!matches!(
reason,
typed_abi::TypedCloneRejectionReason::NotClosure
| typed_abi::TypedCloneRejectionReason::ReturnTypeNotF64
| typed_abi::TypedCloneRejectionReason::ReturnTypeNotI32
| typed_abi::TypedCloneRejectionReason::ReturnTypeNotI1
| typed_abi::TypedCloneRejectionReason::ReturnTypeNotString
| typed_abi::TypedCloneRejectionReason::NoReceiverField
)
}
fn record_typed_clone_rejection(
records: &mut Vec<crate::native_value::NativeRepRecord>,
source_function: impl Into<String>,
consumer: &'static str,
reason: typed_abi::TypedCloneRejectionReason,
notes: Vec<String>,
) {
if !should_record_typed_clone_rejection(reason) {
return;
}
let source_function = source_function.into();
// `--opt-report` (#6952): surface the specialized-ABI (RFC Phase 2)
// decision, which is the only place params and returns get a
// representation today. The `typed_*_clone_decision` consumers are the
// older per-type clone mechanism and would report the same function up
// to four times, so they stay out of the report and keep going to the
// native-reps artifact only.
if consumer == "spec_abi_entry_decision" && crate::opt_report::enabled() {
let (why, tier, issue) = reason.opt_report_reason();
crate::opt_report::deny_named(
&source_function,
crate::opt_report::RegionKind::Function,
crate::opt_report::Denial {
position: crate::opt_report::Position::Param,
name: "(parameters + return)",
local_id: None,
analysis: crate::opt_report::Analysis::SpecAbi,
rule: reason.as_str(),
reason: why,
tier,
issue,
loop_depth: 0,
detail: None,
byte_offset: None,
},
);
}
records.push(crate::native_value::typed_clone_rejection_record(
source_function,
consumer,
reason.as_str(),
notes,
));
}
pub(crate) fn static_method_registry_key(method_name: &str) -> String {
format!("__perry_static__{}", method_name)
}
/// 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>> {
let progress = CompileProgress::new(&hir.name, module_callable_count(hir));
let triple = opts.target.clone().unwrap_or_else(default_target_triple);
let fp_flags = crate::block::FpFlags::new(opts.fast_math, opts.fp_contract_mode);
// #5334 lever B: decide ONCE, up front, whether this module is large enough
// to full-outline its class-field IC diamonds (read per-site during
// lowering via `full_outline_ic_enabled()`). Thread-local, so it must be set
// afresh for every module — including the `false` case, to clear any prior
// module's decision on this thread.
set_full_outline_ic(decide_full_outline_ic(module_callable_count(hir)));
// #8595: report the module-entry outlining analysis when asked. Pure
// diagnostic — no transform yet (see codegen/entry_outline.rs).
entry_outline::report_entry_outlining(hir);
// FEAT_JSCVT decision is per-target (apple-arm64 only) — same
// set-per-module discipline as the outline gate above.
helpers::set_jscvt_for_target(&triple);
// Native roots are the default lowering wherever the runtime can walk the
// frames, and the shadow stack elsewhere. Same per-module discipline.
helpers::set_native_roots_for_target(&triple);
// `--opt-report` (#6952): mark the closures that are iterating-builtin
// callbacks before any region is lowered, so their denials carry the
// per-element hotness column. No-op when the report is off.
crate::opt_report::scan_module(hir);
if let Some(source) = opts.module_source.as_deref() {
crate::opt_report::register_module_source(&hir.name, source, opts.debug_source_line_offset);
}
// Module-wide fallback attribution scope. Per-region scopes nest inside
// it and restore it on drop, so decisions taken outside any region (the
// specialized-ABI entry decision) still know their module.
let _opt_report_module_scope = crate::opt_report::enter_module(hir);
let mut llmod = LlModule::new_with_fp_flags(&triple, fp_flags);
// 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());
// #5247: install per-module source-location context for the dynamic
// call-dispatch throw path, but only under `--debug-symbols` (which sets
// `opts.debug_locations` + `opts.module_source`). Off by default — no
// source clone, no per-call emission.
if opts.debug_locations {
if let Some(src) = opts.module_source.clone() {
strings.set_debug_location_ctx(Some((hir.name.clone(), src)));
// #5247 (CJS-wrap coordinate skew): `src` is the WRAPPED source for
// a CommonJS module; subtract the wrapper-prefix line count when
// resolving offsets so the rendered line is in original coordinates.
strings.set_debug_source_line_offset(opts.debug_source_line_offset);
}
}
// 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();
// Issue #26 / #321: the source-module prefix of each entry in
// `imported_class_stubs`, kept index-parallel. Effect (and other heavily
// modular packages) export same-named classes from different modules —
// e.g. `class Type` exists in BOTH `SchemaAST.ts` (fields `type,
// annotations`) and `ParseResult.ts` (fields `_tag, ast, actual,
// message`). Both arrive here as separate stubs that collide by name.
// The packed-keys / field-count chain walks below resolve a class's
// parent by name (`.find(|c| c.name == parent)`), which silently picks
// whichever same-named stub appears first in the Vec. That makes
// `PropertySignature ← OptionalType ← Type` inherit ParseResult.Type's
// fields instead of SchemaAST.Type's, polluting the schema AST that
// decode/encode/is later walk. A class's `extends` clause resolves in
// *its own* module's scope, so we disambiguate by preferring the parent
// stub whose source prefix matches the child's.
let mut imported_stub_prefixes: Vec<String> = Vec::new();
// Issue #26 / #321: imported classes that are shadowed by a same-named
// LOCAL class (so they're intentionally kept OUT of `class_table` /
// `class_ids` / `imported_class_stubs` to preserve local dispatch
// precedence) but are still needed to resolve the parent layout of OTHER
// imported classes. Tuple: (name, source_prefix, parent_name, fields).
// Consulted only by `resolve_parent`.
let mut shadowed_parent_stubs: Vec<(
String,
String,
Option<String>,
Vec<perry_hir::ClassField>,
)> = 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) {
// Issue #26 / #321: a locally-shadowed import is still needed for
// *parent resolution* of OTHER imported classes. Effect's
// ParseResult.ts declares its own local `class Type`
// (`{_tag,ast,actual,message}`) AND imports SchemaAST's
// `OptionalType extends Type`, whose real parent is SchemaAST's
// `Type` (`{type,annotations}`). The local `Type` correctly
// shadows `class_table`/`class_ids` for ParseResult's own code,
// but the imported `OptionalType`'s field layout must still
// resolve to SchemaAST's `Type`. Record the shadowed import in a
// side list keyed by source prefix so `resolve_parent`
// can find it WITHOUT polluting the name-keyed dispatch maps.
if !ic.field_names.is_empty() || ic.parent_name.is_some() {
shadowed_parent_stubs.push((
effective_name.to_string(),
ic.source_prefix.clone(),
ic.parent_name.clone(),
ic.field_names
.iter()
.map(|name| perry_hir::ClassField {
name: name.clone(),
key_expr: None,
ty: perry_hir::types::Type::Any,
init: None,
is_private: false,
is_readonly: false,
decorators: Vec::new(),
})
.collect::<Vec<_>>(),
));
}
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);
}
let imported_getters: Vec<perry_hir::Function> = ic
.getter_names
.iter()
.enumerate()
.map(|(index, prop)| perry_hir::Function {
id: 0,
name: format!("get_{}", prop),
type_params: Vec::new(),
params: Vec::new(),
return_type: ic
.getter_return_types
.get(index)
.cloned()
.unwrap_or(perry_hir::types::Type::Any),
body: Vec::new(),
is_async: false,
is_generator: false,
is_strict: true,
was_plain_async: false,
was_unrolled: false,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
})
.collect();
let imported_setters: Vec<perry_hir::Function> = ic
.setter_names
.iter()
.map(|prop| perry_hir::Function {
id: 0,
name: format!("set_{}", prop),
type_params: Vec::new(),
params: Vec::new(),
return_type: perry_hir::types::Type::Any,
body: Vec::new(),
is_async: false,
is_generator: false,
is_strict: true,
was_plain_async: false,
was_unrolled: false,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
})
.collect();
// Build a stub Class with the minimum fields the codegen needs.
// Imported accessor bodies execute from the source module; carrying
// their names here keeps dispatch and field inference conservative.
let stub = perry_hir::Class {
id: 0, // imported — no local ClassId
name: effective_name.to_string(),
// #6812: width hints don't cross module metadata; imported stubs
// fall back to runtime learned sizing.
alloc_width_hint: 0,
// #7575: monomorphization is per-module, so an imported stub never
// stands in for a specialization — its defining module registers
// the origin edge itself.
specialized_from: None,
type_params: Vec::new(),
extends: None,
extends_name: ic.parent_name.clone(),
native_extends: None,
extends_expr: None,
heritage_lexically_shadowed: false,
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_hir::types::Type::Any),
init: None,
is_private: false,
is_readonly: false,
decorators: Vec::new(),
})
.collect(),
constructor: None,
methods: ic
.method_names
.iter()
.enumerate()
.map(|(index, m)| perry_hir::Function {
id: 0,
name: m.clone(),
type_params: Vec::new(),
params: Vec::new(),
return_type: ic
.method_return_types
.get(index)
.cloned()
.unwrap_or(perry_hir::types::Type::Any),
body: Vec::new(),
is_async: false,
is_generator: false,
is_strict: true,
was_plain_async: false,
was_unrolled: false,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
})
.collect(),
getters: ic
.getter_names
.iter()
.cloned()
.zip(imported_getters)
.collect(),
setters: ic
.setter_names
.iter()
.cloned()
.zip(imported_setters)
.collect(),
static_accessor_names: Vec::new(),
static_accessor_fn_ids: Vec::new(),
static_fields: Vec::new(),
static_methods: ic
.static_method_names
.iter()
.enumerate()
.map(|(index, name)| {
// The body/default expressions remain in the producer,
// but imported static dispatch still needs the declared
// width and trailing rest/`arguments` shape to forward
// arguments with the producer's ABI. Synthetic params are
// sufficient because no imported stub body is compiled.
let declared = ic
.static_method_param_counts
.get(index)
.copied()
.unwrap_or(0);
let has_rest = ic
.static_method_has_rest
.get(index)
.copied()
.unwrap_or(false);
let has_user_rest = ic
.static_method_has_user_rest
.get(index)
.copied()
.unwrap_or(false);
let last_is_synthetic_arguments = ic
.static_method_has_synthetic_arguments
.get(index)
.copied()
.unwrap_or(false);
let params = (0..declared)
.map(|param_index| {
let is_last = param_index + 1 == declared;
let is_user_rest = has_user_rest
&& if last_is_synthetic_arguments {
param_index + 2 == declared
} else {
is_last
};
let is_synthetic_rest = is_last
&& last_is_synthetic_arguments
&& has_rest
&& !has_user_rest;
let is_legacy_rest = is_last
&& !last_is_synthetic_arguments
&& has_rest
&& !has_user_rest;
perry_hir::Param {
id: param_index as perry_hir::types::LocalId,
name: format!("arg{param_index}"),
ty: perry_hir::types::Type::Any,
default: None,
decorators: Vec::new(),
is_rest: is_user_rest || is_synthetic_rest || is_legacy_rest,
arguments_object: (is_last && last_is_synthetic_arguments).then(
|| perry_hir::ArgumentsObjectMeta {
strict: true,
simple_parameters: false,
mapped_parameter_ids: Vec::new(),
restricted_callee: true,
},
),
}
})
.collect();
perry_hir::Function {
id: 0,
name: name.clone(),
type_params: Vec::new(),
params,
return_type: ic
.static_method_return_types
.get(index)
.cloned()
.unwrap_or(perry_hir::types::Type::Any),
body: Vec::new(),
is_async: false,
is_generator: false,
is_strict: true,
was_plain_async: false,
was_unrolled: false,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
}
})
.collect(),
computed_members: Vec::new(),
decorators: Vec::new(),
is_exported: false,
aliases: Vec::new(),
is_nested: false,
};
imported_class_stubs.push(stub);
imported_stub_prefixes.push(ic.source_prefix.clone());
}
// 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();
let mut local_generator_funcs: std::collections::HashSet<u32> =
std::collections::HashSet::new();
let mut funcs_reading_dynamic_this: 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);
}
if function_body_returns_generator_object(&f.body) {
local_generator_funcs.insert(f.id);
}
if perry_hir::analysis::body_reads_dynamic_this(&f.body) {
funcs_reading_dynamic_this.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, raw_f64_mask_words,
// pointer_mask_words).
// Used by emit_string_pool to emit the build-call sequence.
let mut class_keys_init_data: Vec<(String, String, u32, Vec<u64>, Vec<u64>)> = Vec::new();
let mut class_keys_globals_map: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
// Issue #26 / #321: the authoritative total inline-field count for each
// class, as computed by the source-prefix-disambiguated chain walk that
// builds the packed-keys global below. The `new ClassName()` site
// (`lower_new`) recomputes a field count by walking `ctx.classes`
// (a name-keyed map that can only hold ONE same-named parent stub),
// which mis-sizes the allocation and stamps a wrong `field_count` in
// the object header when same-named parents collide (effect's `Type`).
// `lower_new` consults this map first so the allocated slot count and the
// header `field_count` match the keys array length the global holds.