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
1 change: 1 addition & 0 deletions changelog.d/11839-literal-birth-plain-prototype.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **perry-codegen**: an object literal's shape no longer takes another module's class prototype, and the TUI of the natively compiled OpenCode v1.18.30 no longer aborts at startup with "the static ShapeId ... was refused by the shape mint". Anonymous literal births now pass class id 0 to their shape mints, as the startup literal seed does; passing the anonymous class's own id broke whenever that per-module id collided with a declared class elsewhere.
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/codegen/static_shape_ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ pub(crate) struct ClassBirth {
pub wide_live: u32,
/// Its content, when it is nameable.
pub shape: Option<BirthShape>,
/// An anonymous object-literal class (`__AnonShape_*`): its birth names
/// the plain prototype ([`BirthProto::Literal`]), never a class one.
pub literal: bool,
}

/// `(keys global, packed names, field count, raw-f64 mask words, pointer mask
Expand Down Expand Up @@ -387,6 +390,7 @@ pub(crate) fn class_birth(
class_id,
wide_live,
shape,
literal,
}
}

Expand Down
217 changes: 217 additions & 0 deletions crates/perry-codegen/src/codegen/static_shape_ids_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,3 +560,220 @@ fn literal_key_cache_mints_require_a_seed_even_without_a_guard() {
vec![(assigned[&literal], literal)]
);
}

/// A class with plain `this.f = f` fields, as `mint_anon_shape_class`
/// synthesizes for an object literal (or as a declared class would look).
fn field_class(
id: u32,
name: &str,
fields: &[&str],
ctor_id: u32,
param_base: u32,
) -> perry_hir::Class {
use perry_hir::types::Type;
use perry_hir::{ClassField, Expr, Function, Param, Stmt};
let params: Vec<Param> = fields
.iter()
.enumerate()
.map(|(i, f)| Param {
id: param_base + i as u32,
name: (*f).to_string(),
ty: Type::Any,
default: None,
decorators: Vec::new(),
is_rest: false,
arguments_object: None,
})
.collect();
let body = params
.iter()
.map(|p| {
Stmt::Expr(Expr::PropertySet {
object: Box::new(Expr::This),
property: p.name.clone(),
value: Box::new(Expr::LocalGet(p.id)),
})
})
.collect();
perry_hir::Class {
id,
name: name.to_string(),
type_params: Vec::new(),
extends: None,
extends_name: None,
native_extends: None,
extends_expr: None,
heritage_lexically_shadowed: false,
fields: fields
.iter()
.map(|f| ClassField {
name: (*f).to_string(),
key_expr: None,
ty: Type::Any,
init: None,
is_private: false,
is_readonly: false,
decorators: Vec::new(),
})
.collect(),
constructor: Some(Function {
id: ctor_id,
name: "constructor".to_string(),
type_params: Vec::new(),
params,
return_type: Type::Void,
body,
is_async: false,
is_generator: false,
is_strict: true,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
was_plain_async: false,
was_unrolled: false,
}),
methods: Vec::new(),
getters: Vec::new(),
setters: Vec::new(),
static_accessor_names: Vec::new(),
static_accessor_fn_ids: Vec::new(),
static_fields: Vec::new(),
static_methods: Vec::new(),
computed_members: Vec::new(),
decorators: Vec::new(),
is_exported: false,
aliases: Vec::new(),
is_nested: false,
alloc_width_hint: 0,
specialized_from: None,
}
}

/// `const o = { a: 1 }` (an `__AnonShape_*` birth) beside `new Point(2)`.
fn literal_and_declared_module() -> perry_hir::Module {
use perry_hir::types::Type;
use perry_hir::{Expr, Stmt};
let mut hir = perry_hir::Module::new("literal_birth_mint_test");
hir.classes.push(field_class(
1,
"__AnonShape_000000000000a001",
&["a"],
90,
60,
));
hir.classes.push(field_class(2, "Point", &["x"], 91, 70));
for (id, class, v) in [
(50, "__AnonShape_000000000000a001", 1.0),
(51, "Point", 2.0),
] {
hir.init.push(Stmt::Let {
id,
name: format!("v{id}"),
ty: Type::Any,
mutable: false,
init: Some(Expr::New {
class_name: class.to_string(),
args: vec![Expr::Number(v)],
type_args: Vec::new(),
byte_offset: 0,
cap_args_appended: 0,
}),
});
}
hir
}

/// The arguments of every `call ... @callee(...)` in `ir`, as their value text
/// (call instructions only: the module's `declare` line names the callee too).
fn mint_calls(ir: &str, callee: &str) -> Vec<Vec<String>> {
let needle = format!("@{callee}(");
ir.lines()
.filter(|line| line.contains(" call ") && !line.trim_start().starts_with("declare"))
.filter_map(|line| {
let start = line.find(&needle)? + needle.len();
let end = start + line[start..].find(')')?;
Some(
line[start..end]
.split(", ")
.map(|arg| arg.rsplit(' ').next().unwrap_or("").to_string())
.collect(),
)
})
.collect()
}

#[test]
fn a_literal_birth_mints_its_shape_with_the_plain_prototype_on_both_routes() {
// Per-module class ids collide: an `__AnonShape_*` id can also be another
// module's DECLARED class. Passing it to the shape mint let the runtime
// derive that class's prototype for a plain literal, so two modules' equal
// literal contents -- ONE static id -- reached the mint with different
// facts and the second aborted ("the static ShapeId ... was refused by the
// shape mint"; OpenCode's TUI: ajv's and json5's `{ x }` literals). A
// literal birth names the plain prototype, as the startup literal seed
// (`js_shape_seed_plain`) does: class id 0.
let hir = literal_and_declared_module();

// Lazy route (no static id): `(keys, field_count, class_id, rep)`.
let lazy = String::from_utf8(
crate::compile_module(
&hir,
crate::CompileOptions {
emit_ir_only: true,
..Default::default()
},
)
.expect("module compiles"),
)
.expect("UTF-8 IR");
let calls = mint_calls(&lazy, "js_object_shape_id_for_class_keys");
assert_eq!(calls.len(), 2, "one lazy mint per birth class:\n{lazy}");
let class_ids: Vec<&str> = calls.iter().map(|args| args[2].as_str()).collect();
assert!(
class_ids.contains(&"0"),
"the literal's mint must name class id 0: {calls:?}"
);
assert!(
class_ids.iter().any(|cid| *cid != "0"),
"a declared class keeps its own class id (its prototype is a shape fact): {calls:?}"
);

// Static route: the driver assigned the literal's content an id.
let content = BirthShape {
keys: b"a\0".to_vec(),
key_count: 1,
live: 1,
proto: BirthProto::Literal,
typed: None,
rep: 0,
constfn: Vec::new(),
};
let requested = SHAPE_ID_BASE + 7;
let stat = String::from_utf8(
crate::compile_module(
&hir,
crate::CompileOptions {
emit_ir_only: true,
static_shape_ids: vec![(content, requested)],
..Default::default()
},
)
.expect("module compiles"),
)
.expect("UTF-8 IR");
// `(keys, field_count, live, class_id, requested, rep)`
let calls = mint_calls(&stat, "js_object_shape_id_for_class_keys_static");
let literal: Vec<&Vec<String>> = calls
.iter()
.filter(|args| args[4] == requested.to_string())
.collect();
assert_eq!(
literal.len(),
1,
"the driver's id must reach the literal's static mint for this test to mean anything: {calls:?}\n{stat}"
);
assert_eq!(
literal[0][3], "0",
"the literal's static mint must name class id 0, as its seed does: {calls:?}"
);
}
20 changes: 17 additions & 3 deletions crates/perry-codegen/src/codegen/string_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,20 @@ pub(super) fn emit_string_pool(
.unwrap_or(0)
.to_string();
let cid_str = class_id.to_string();
// A literal birth names the plain prototype (`BirthProto::Literal`), so
// its shape mints pass class id 0, exactly as the startup literal seed
// (`js_shape_seed_plain`) does. Passing the anonymous class's own id was
// equivalent only while that id had no vtable class: per-module class
// ids collide, so an `__AnonShape_*` id can also be another module's
// DECLARED class, and the mint then derived that class's prototype --
// different facts under the one static id the driver assigned to this
// content, which the mint refuses with an abort. The keys array still
// carries the real id (`js_build_class_keys_array` above).
let mint_cid_str = if birth.literal {
"0".to_string()
} else {
cid_str.clone()
};
let fc_str = field_count.to_string();
let packed_ref = if packed.is_empty() {
"null".to_string()
Expand Down Expand Up @@ -790,7 +804,7 @@ pub(super) fn emit_string_pool(
(I64, &arr),
(I32, &fc_str),
(I32, &live.to_string()),
(I32, &cid_str),
(I32, &mint_cid_str),
(I32, &requested),
(I64, &rep_str),
],
Expand All @@ -809,7 +823,7 @@ pub(super) fn emit_string_pool(
(I64, &arr),
(I32, &fc_str),
(I32, &birth_live.to_string()),
(I32, &cid_str),
(I32, &mint_cid_str),
(I64, &rep_str),
],
),
Expand All @@ -819,7 +833,7 @@ pub(super) fn emit_string_pool(
&[
(I64, &arr),
(I32, &fc_str),
(I32, &cid_str),
(I32, &mint_cid_str),
(I64, &rep_str),
],
),
Expand Down
Loading