Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/PENDING-recur2-class-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Generic class specializations now allocate class IDs from one sequence above all declared classes and object-literal shapes in the program. The old per-module maximum plus 1,000 could reuse another module's literal ID, making a class instance's shape claim the default `Object.prototype` edge. This preserves the shape-based prototype fast path while restoring the declared class prototype and inherited methods on descriptor clones, including Effect 4 Schema initialization.

Adds a multi-module gap regression with 1,100 distinct literal shapes and a HIR unit test covering specialization IDs and origin metadata across modules. The regression first became observable in `993bcb2e9704bf9596cc1710c8373060b4372cab` (PR #12024); its parent `8ec553a709` matches Node.
2 changes: 1 addition & 1 deletion crates/perry-hir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pub use lower::{
lower_module_with_class_id, lower_module_with_class_id_and_types,
lower_module_with_class_id_types_and_seed, lower_module_with_class_id_types_seed_and_entry,
};
pub use monomorph::monomorphize_module;
pub use monomorph::{monomorphize_module, monomorphize_modules};
pub use native_profile::exported_native_pod_abi;
pub use patched_builtins::{
clear_patched_builtins, patched_prototype_methods, scan_module as scan_patched_builtins,
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/monomorph/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ impl MonomorphizationContext {
func_work_queue: VecDeque::new(),
class_work_queue: VecDeque::new(),
next_func_id: max_func_id + 1000, // Leave room for original IDs
next_class_id: max_class_id + 1000,
next_class_id: max_class_id + 1,
processed_funcs: HashSet::new(),
processed_classes: HashSet::new(),
}
Expand Down
30 changes: 27 additions & 3 deletions crates/perry-hir/src/monomorph/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,35 @@ use super::*;
/// Main monomorphization pass
/// Processes the module and generates specialized versions of generic functions/classes
pub fn monomorphize_module(module: &mut Module) {
let mut ctx = MonomorphizationContext::new(module);
monomorphize_modules(std::iter::once(module));
}

/// Specialize a program's modules using one class-ID allocation sequence.
/// Lowering has already assigned IDs to declared classes and literal shapes;
/// every specialization must be above that whole program's range, not just
/// above the classes in its own module.
pub fn monomorphize_modules<'a>(modules: impl IntoIterator<Item = &'a mut Module>) {
let modules: Vec<_> = modules.into_iter().collect();
let mut next_class_id = modules
.iter()
.flat_map(|module| &module.classes)
.map(|class| class.id)
.max()
.unwrap_or(0)
+ 1;
for module in modules {
let mut ctx = MonomorphizationContext::new(module);
ctx.next_class_id = next_class_id;
monomorphize_with_context(module, &mut ctx);
next_class_id = ctx.next_class_id;
}
}

fn monomorphize_with_context(module: &mut Module, ctx: &mut MonomorphizationContext) {
let idx = ModuleIndex::new(module);

// First pass: collect all generic instantiations from the code
collect_instantiations(module, &mut ctx, &idx);
collect_instantiations(module, ctx, &idx);

// Process work queues until empty
let mut new_functions = Vec::new();
Expand Down Expand Up @@ -116,7 +140,7 @@ pub fn monomorphize_module(module: &mut Module) {
}

// Update call sites to use specialized versions
update_call_sites(module, &ctx);
update_call_sites(module, ctx);

// Fill in default arguments for constructor calls
fill_default_arguments(module);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/monomorph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ mod tests;

pub use constraints::ConstraintError;
pub use context::MonomorphizationContext;
pub use driver::monomorphize_module;
pub use driver::{monomorphize_module, monomorphize_modules};
pub use specialize::{specialize_class, specialize_function};
pub use substitute_type::substitute_type;

Expand Down
109 changes: 76 additions & 33 deletions crates/perry-hir/src/monomorph/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -948,39 +948,6 @@ fn assert_specialized_call(expr: &Expr, module: &Module, expected_name: &str) {
/// red for days (#5960). This runs on every PR.
#[test]
fn a_specialized_class_reports_the_generics_display_name() {
fn generic_class(id: u32, name: &str) -> Class {
Class {
id,
name: name.to_string(),
type_params: vec![TypeParam {
name: "T".to_string(),
constraint: None,
default: None,
}],
extends: None,
extends_name: None,
native_extends: None,
extends_expr: None,
heritage_lexically_shadowed: false,
fields: vec![],
constructor: None,
methods: vec![],
getters: vec![],
setters: vec![],
static_accessor_names: vec![],
static_accessor_fn_ids: vec![],
static_fields: vec![],
static_methods: vec![],
computed_members: vec![],
decorators: vec![],
is_exported: false,
aliases: vec![],
is_nested: false,
alloc_width_hint: 0,
specialized_from: None,
}
}

let mut module = Module::new("test");
module.classes.push(generic_class(1, "Gen"));
module.init.push(Stmt::Expr(Expr::New {
Expand Down Expand Up @@ -1233,3 +1200,79 @@ fn fill_defaults_pads_before_appended_class_captures() {
"the trailing capture args must be the captured values, not padding: {args:?}"
);
}

fn generic_class(id: u32, name: &str) -> Class {
Class {
id,
name: name.to_string(),
type_params: vec![TypeParam {
name: "T".to_string(),
constraint: None,
default: None,
}],
extends: None,
extends_name: None,
native_extends: None,
extends_expr: None,
heritage_lexically_shadowed: false,
fields: vec![],
constructor: None,
methods: vec![],
getters: vec![],
setters: vec![],
static_accessor_names: vec![],
static_accessor_fn_ids: vec![],
static_fields: vec![],
static_methods: vec![],
computed_members: vec![],
decorators: vec![],
is_exported: false,
aliases: vec![],
is_nested: false,
alloc_width_hint: 0,
specialized_from: None,
}
}

#[test]
fn specializations_do_not_reuse_other_modules_class_or_literal_ids() {
let mut first = Module::new("first");
first.classes.push(generic_class(1, "First"));
let mut second = Module::new("second");
second.classes.push(generic_class(2, "Second"));
let mut literal = generic_class(1001, "Literal");
literal.type_params.clear();
second.classes.push(literal);
let mut third = Module::new("third");
let mut last_literal = generic_class(1100, "LastLiteral");
last_literal.type_params.clear();
third.classes.push(last_literal);

for (module, name) in [(&mut first, "First"), (&mut second, "Second")] {
for ty in [Type::Number, Type::String] {
module.init.push(Stmt::Expr(Expr::New {
class_name: name.to_string(),
args: vec![],
type_args: vec![ty],
byte_offset: 0,
cap_args_appended: 0,
}));
}
}
monomorphize_modules([&mut first, &mut second, &mut third]);

let mut ids = HashSet::new();
let mut specializations = 0;
for module in [&first, &second, &third] {
for class in &module.classes {
assert!(ids.insert(class.id), "duplicate class ID {}", class.id);
if let Some(origin) = &class.specialized_from {
specializations += 1;
assert!(class.id > 1100, "specialization overlaps lowered IDs");
assert_eq!(module.class_display_names.get(&class.id), Some(origin));
}
}
}
assert_eq!(specializations, 4);
assert_eq!(ids.len(), 8);
}
15 changes: 4 additions & 11 deletions crates/perry/src/commands/compile/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,16 +1073,9 @@ pub(super) fn run_native_instance_fixups(ctx: &mut CompilationContext) {
}
}

// Cross-module fix → local-fix re-run → monomorphize (parallel,
// fused per-module). Tier 4.2: pre-fix this was three separate
// `par_iter_mut().for_each(...)` passes. The local-fix re-run
// depends on `fix_cross_module_native_instances` having
// populated cross-module type info on this module, and
// monomorphize depends on the post-local-fix module shape — but
// both dependencies are intra-module, so running all three in
// one rayon job per module is safe and saves two scheduler
// round-trips. The cross-module step is gated on at least one
// export existing (skip the call entirely otherwise).
// Cross-module and local native-instance fixes remain module-local and
// parallel. Specialization below must share a class-ID allocator across
// the entire program, including every module's object-literal shapes.
let has_native_exports =
!exported_instances.is_empty() || !exported_func_return_instances.is_empty();
ctx.native_modules
Expand All @@ -1101,8 +1094,8 @@ pub(super) fn run_native_instance_fixups(ctx: &mut CompilationContext) {
// is false this is effectively a no-op since nothing changed
// since the first local-fix in Pass A above.
perry_hir::fix_local_native_instances(hir_module);
perry_hir::monomorphize_module(hir_module);
});
perry_hir::monomorphize_modules(ctx.native_modules.iter_mut().map(|(_, module)| module));
}

/// --- HarmonyOS Phase 2: harvest perry/ui App({body: ...}) into ArkUI ---
Expand Down
Loading
Loading