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/8896-class-capture-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Preserve shared mutable capture cells when refreshing per-evaluation class
expressions, restoring dynamic construction when a fresh class captures an
assigned dependency through its heritage or members.
21 changes: 21 additions & 0 deletions crates/perry-hir/src/lower/shared_mutable_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,27 @@ fn rewrite_expr(expr: &mut Expr, shared: &HashSet<LocalId>, index_uses: &HashSet
// Capture sites snapshot the WHOLE handle (the array). Leave the bare
// `LocalGet(id)` capture args alone; still rewrite non-capture children.
Expr::RegisterClassCaptures { .. } => return,
// Release follow-up: the end-of-body refresh for a fresh class
// object carries the same capture-param-ordered handles as the initial
// `ClassExprFresh` snapshot. A shared-mutable capture is a one-element
// array cell, so refreshing with `id[0]` replaces the cell with its
// current scalar value. The constructor still treats the refreshed
// slot as a cell and reads `[0]`, producing `undefined`. Preserve bare
// shared-cell handles in `captures`, while still rewriting the owner
// expression and any non-capture children normally.
Expr::RefreshClassExprCaptures {
class_value,
captures,
} => {
rewrite_expr(class_value, shared, index_uses);
for capture in captures.iter_mut() {
if matches!(capture, Expr::LocalGet(id) if index_uses.contains(id)) {
continue;
}
rewrite_expr(capture, shared, index_uses);
}
return;
}
// #6497: the per-evaluation fresh-binding path (#6470) carries the
// same capture-param-ordered `LocalGet` args as RegisterClassCaptures
// — they too must snapshot the WHOLE box handle. Rewriting them to
Expand Down
44 changes: 44 additions & 0 deletions crates/perry-hir/src/lower/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,50 @@ fn fresh_class_declaration_collision_keeps_lexical_binding() {
}));
}

/// A fresh class's end-of-body capture refresh must preserve the whole
/// one-element shared-mutable cell, matching the initial `ClassExprFresh`
/// snapshot. Refreshing with `cell[0]` stores the scalar value, while lifted
/// members still read the constructor capture as `capture[0]`.
#[test]
fn fresh_class_refresh_keeps_shared_capture_cell_handle() {
let source = r#"
const exported = (() => {
let dep;
dep = { default: "ok" };
const holder = {};
holder.default = class {
read() { return dep.default; }
};
return holder.default;
})();
"#;
let module = perry_parser::parse_typescript(source, "t.ts").expect("source parses");
let hir = super::lower_module(&module, "t", "t.ts").expect("source lowers");
let compact: String = format!("{:#?}", hir.init)
.chars()
.filter(|ch| !ch.is_whitespace())
.collect();

let mut remainder = compact.as_str();
let mut refreshes = 0usize;
while let Some(offset) = remainder.find("RefreshClassExprCaptures{") {
remainder = &remainder[offset + "RefreshClassExprCaptures{".len()..];
let captures = remainder
.find("captures:[")
.map(|index| &remainder[index + "captures:[".len()..])
.expect("refresh includes a captures vector");
assert!(
captures.starts_with("LocalGet("),
"fresh-class refresh must carry the shared cell handle, not an indexed value: {captures}"
);
refreshes += 1;
}
assert!(
refreshes > 0,
"fixture must emit at least one fresh-class refresh"
);
}

/// Companion (the case the depth rule must NOT break): a module-scope `class e`
/// and a factory-local `let e` holding a different constructor. JS says the
/// nearer local wins, so `new e()` inside the factory must still construct the
Expand Down
Loading