Skip to content
Closed
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
4 changes: 4 additions & 0 deletions changelog.d/10889-computed-field-key-captures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed computed instance-field keys in nested and CommonJS-wrapped classes being
rewritten to unbound constructor capture parameters. Symbol-keyed fields now
retain the PropertyKey resolved at class definition time, allowing undici's
pool state to initialize under the symbols used by its request path.
8 changes: 4 additions & 4 deletions crates/perry-hir/src/ir/decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,10 @@ pub struct ClassComputedMember {
#[derive(Debug, Clone)]
pub struct ClassField {
pub name: String,
/// When `Some`, this field's key is the lowered expression evaluated at
/// construction time (e.g. `[Symbol.for("k")]` or `[Parent.Symbol.X]`).
/// `name` is then a synthetic placeholder used only for HIR identity —
/// runtime property writes go through `IndexSet` with this expression.
/// When `Some`, this field's key is the lowered expression evaluated once
/// during ClassDefinitionEvaluation (e.g. `[Symbol.for("k")]` or
/// `[Parent.Symbol.X]`). `name` identifies the hidden class slot holding
/// the resolved PropertyKey; construction reuses that stored key.
pub key_expr: Option<Expr>,
pub ty: Type,
pub init: Option<Expr>,
Expand Down
39 changes: 39 additions & 0 deletions crates/perry-hir/src/lower/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,45 @@ fn make_ctx() -> LoweringContext {
LoweringContext::new("test.ts")
}

#[test]
fn a_computed_instance_field_key_is_not_a_constructor_capture() {
let source = r#"
function make() {
const items = Symbol("items");
const payload = { ok: true };
class Base {
[items] = [];
getPayload() { return payload; }
}
return Base;
}
"#;
let module =
perry_parser::parse_typescript(source, "computed-field-key.ts").expect("source parses");
let hir = super::lower_module(&module, "computed-field-key", "computed-field-key.ts")
.expect("source lowers");
let class = hir
.classes
.iter()
.find(|class| class.name == "Base")
.expect("nested class is lowered");

assert_eq!(
class
.fields
.iter()
.filter(|field| field.name.starts_with("__perry_cap_"))
.count(),
1,
"the method value is captured, but the definition-time key is not"
);
assert!(class.fields[0].key_expr.is_some());
assert!(
class.constructor.is_some(),
"the unrelated method capture keeps a synthesized constructor"
);
}

mod instanceof_rhs;
mod literal_shape;

Expand Down
26 changes: 9 additions & 17 deletions crates/perry-hir/src/lower_decl/class_captures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ pub fn synthesize_class_captures(
// Without this, `LocalGet(outer_id)` inside a field's init expression
// would read a non-existent local in the ctor's scope when
// `apply_field_initializers_recursive` lowers the initializer.
// Collect refs from both the init expr and the computed key_expr.
// Collect refs from initializer expressions. Computed field keys are
// deliberately excluded: ClassDefinitionEvaluation resolves them in the
// enclosing scope and stores the resulting PropertyKey in a hidden class
// slot. They do not execute in the constructor and therefore must not be
// rewritten to constructor-local capture parameters. Doing so leaves the
// definition-site StaticFieldSet reading an unbound synthetic parameter;
// undici's PoolBase then installs `[kClients] = []` under the wrong key.
for field in fields.iter() {
if let Some(init) = &field.init {
let mut refs = Vec::new();
Expand All @@ -80,16 +86,6 @@ pub fn synthesize_class_captures(
}
}
}
if let Some(key) = &field.key_expr {
let mut refs = Vec::new();
let mut visited = std::collections::HashSet::new();
crate::analysis::collect_local_refs_expr(key, &mut refs, &mut visited);
for id in refs {
if outer_scope_ids.contains(&id) && !module_level_ids.contains(&id) {
union_captures.insert(id);
}
}
}
}
// Inherited captures: if this class extends a parent that registered
// captures, the parent's instance methods read from
Expand Down Expand Up @@ -583,9 +579,8 @@ pub fn synthesize_class_captures(
}
*constructor = Some(ctor);

// Issue #740: rewrite field initializers and computed-key
// expressions using the same `ctor_id_map`. Field initializers
// are lowered inside the constructor body by
// Issue #740: rewrite field initializers using the same `ctor_id_map`.
// Field initializers are lowered inside the constructor body by
// `apply_field_initializers_recursive`, so `LocalGet(outer_id)`
// inside a field's init must be rewritten to read the fresh
// ctor-local param that holds the captured value (synthesized
Expand All @@ -595,9 +590,6 @@ pub fn synthesize_class_captures(
if let Some(init) = field.init.as_mut() {
crate::analysis::remap_local_ids_in_expr(init, &ctor_id_map);
}
if let Some(key) = field.key_expr.as_mut() {
crate::analysis::remap_local_ids_in_expr(key, &ctor_id_map);
}
}

// 4. Register so `Expr::New { class_name }` appends
Expand Down
Loading