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
19 changes: 17 additions & 2 deletions crates/perry-hir/src/lower/shared_mutable_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -873,9 +873,24 @@ 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,
Expr::New { args, .. } => {
Expr::New {
class_name, args, ..
} => {
// An OBJECT LITERAL also lowers to `New` — against a synthetic
// `__AnonShape_*` whose ctor args are the PROPERTY VALUES, not the
// auto-appended capture handles a real class construction carries.
// Skipping a bare `LocalGet(boxed)` there stored the one-element array
// BOX in the field instead of the value it holds, so `{routing: o}`
// produced `routing === [o]`: every other read of `o` is rewritten to
// `o[0]` and works, while the object's field holds the cell — the guard
// `o.locales.length > 1` passes one line before the literal that then
// hands the callee a `routing` whose `.locales` is `undefined`.
//
// A synthetic shape has no captures, so every one of its args is a value
// and must be rewritten.
let is_anon_shape = class_name.starts_with("__AnonShape_");
for a in args.iter_mut() {
if matches!(a, Expr::LocalGet(id) if index_uses.contains(id)) {
if !is_anon_shape && matches!(a, Expr::LocalGet(id) if index_uses.contains(id)) {
continue; // auto-appended capture argument — keep the array handle
}
rewrite_expr(a, shared, index_uses);
Expand Down
77 changes: 77 additions & 0 deletions test-files/test_gap_anon_shape_boxed_capture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// #5951 boxes a mutable local that a class captures AND mutates into a
// one-element array cell, rewriting every value read `o` into `o[0]`. An object
// literal also lowers to `Expr::New` — against a synthetic `__AnonShape_*` whose
// constructor args are the PROPERTY VALUES, not the capture handles a real class
// construction carries. Passing the bare box there stored the CELL in the field:
// `{ routing: o }` yielded `routing === [o]`, so the reader saw
// `routing.locales === undefined` while every other read of `o` was fine.

function factory() {
let o: any;

// a class that captures AND mutates `o` -> `o` becomes a shared cell
class Holder {
snapshot: any;
constructor() {
this.snapshot = o;
}
update(x: any) {
o = x;
}
}

o = { locales: ["en", "de", "es"], defaultLocale: "en" };
const h = new Holder();

return function (req: string) {
// a 5-property object literal -> lowered to `New { __AnonShape_… }`
const lit: any = {
routing: o,
internalTemplateName: undefined,
localizedPathnames: undefined,
request: req,
resolvedLocale: "en",
};

console.log("direct read :", o.locales.length);
console.log("field isArray :", Array.isArray(lit.routing));
console.log("identity kept :", lit.routing === o);
console.log("locales :", lit.routing.locales.join(","));
console.log("other fields :", lit.request, lit.resolvedLocale);

// reading the field back through a destructured parameter (how it is consumed)
const read = function ({ routing: a, request: r }: any) {
return a.locales.join("|") + " / " + r;
};
console.log("via destructure:", read(lit));

// the cell must still be SHARED with the class (the reason the box exists)
h.update({ locales: ["fr"], defaultLocale: "fr" });
console.log("after update :", o.locales.join(","), Array.isArray(o));
const lit2: any = { routing: o, a: 1, b: 2, c: 3, d: 4 };
console.log("relit isArray :", Array.isArray(lit2.routing), lit2.routing.locales.join(","));
return "ok";
};
}

console.log("result :", factory()("REQ"));

// a real class construction must still receive its capture handle by reference:
// writes through the class stay visible to the declaring function.
function sharedCell() {
let count = 0;
class Counter {
bump() {
count++;
}
read() {
return count;
}
}
const c = new Counter();
c.bump();
c.bump();
count += 10;
return c.read() + "," + count;
}
console.log("shared cell :", sharedCell());