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
15 changes: 15 additions & 0 deletions crates/perry-codegen-wasm/src/emit/closures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,21 @@ pub(super) fn collect_closures_from_expr(
collect_closures_from_expr(index, out);
collect_closures_from_expr(value, out);
}
// #5016: PutValueSet is the lowered form of `obj.prop = v` / `obj[i] = v`.
// Recurse into all operands so a closure used as a value/key/target
// (e.g. `obj.handler = () => {}`) is collected and emitted.
Expr::PutValueSet {
target,
key,
value,
receiver,
..
} => {
collect_closures_from_expr(target, out);
collect_closures_from_expr(key, out);
collect_closures_from_expr(value, out);
collect_closures_from_expr(receiver, out);
}
Expr::NativeMethodCall { args, object, .. } => {
if let Some(o) = object {
collect_closures_from_expr(o, out);
Expand Down
55 changes: 55 additions & 0 deletions crates/perry-codegen-wasm/src/emit/expr/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,61 @@ impl<'a> FuncEmitCtx<'a> {
self.emit_memcall_void(func, "object_set_dynamic", 3);
func.instruction(&Instruction::LocalGet(self.temp_store_local));
}
// #5016: assignment PutValue for property/index references
// (`obj.prop = v` / `obj[i] = v`). The HIR lowers BOTH member-write
// forms to `PutValueSet`; without this arm it fell through to the
// `_ => TAG_UNDEFINED` catch-all and the write was silently dropped
// — module-level array element mutation (the recommended mutable
// state pattern) appeared immutable on the web/wasm target.
//
// Mirror the pre-`PutValueSet` lowering split so behavior matches
// the still-handled `PropertySet`/`IndexSet` arms: a string-literal
// key (`obj.prop = v`) routes through `class_set_field` so class
// getters/setters still fire; any other key (`obj[i] = v`) routes
// through `object_set_dynamic`. Both return the assigned RHS value
// to preserve assignment-expression semantics.
Expr::PutValueSet {
target, key, value, ..
} => {
if let Expr::String(property) = key.as_ref() {
let key_id = self
.emitter
.string_map
.get(property.as_str())
.copied()
.unwrap_or(0);
let key_bits = (STRING_TAG << 48) | (key_id as u64);
self.emit_frame_begin(func, 3);
self.emit_store_arg(func, 0, target);
self.emit_store_const(func, 1, f64::from_bits(key_bits));
self.emit_expr(func, value);
func.instruction(&Instruction::LocalSet(self.temp_store_local));
self.emit_slot_addr(func, 2);
func.instruction(&Instruction::LocalGet(self.temp_store_local));
func.instruction(&Instruction::I64Store(wasm_encoder::MemArg {
offset: 0,
align: 3,
memory_index: 0,
}));
self.emit_memcall_void(func, "class_set_field", 3);
func.instruction(&Instruction::LocalGet(self.temp_store_local));
} else {
self.emit_frame_begin(func, 3);
self.emit_store_arg(func, 0, target);
self.emit_store_arg(func, 1, key);
self.emit_expr(func, value);
func.instruction(&Instruction::LocalSet(self.temp_store_local));
self.emit_slot_addr(func, 2);
func.instruction(&Instruction::LocalGet(self.temp_store_local));
func.instruction(&Instruction::I64Store(wasm_encoder::MemArg {
offset: 0,
align: 3,
memory_index: 0,
}));
self.emit_memcall_void(func, "object_set_dynamic", 3);
func.instruction(&Instruction::LocalGet(self.temp_store_local));
}
}
Expr::IndexUpdate {
object,
index,
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-codegen-wasm/src/emit/js_fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,19 @@ impl WasmModuleEmitter {
obj, idx, val, val
)
}
// #5016: `obj.prop = v` / `obj[i] = v` lower to PutValueSet. In the
// JS fallback both are plain `obj[key] = val`; return the RHS value.
Expr::PutValueSet {
target, key, value, ..
} => {
let obj = self.emit_js_expr(target, locals);
let k = self.emit_js_expr(key, locals);
let val = self.emit_js_expr(value, locals);
format!(
"(toJsValue({})[toJsValue({})] = toJsValue({}), {})",
obj, k, val, val
)
}
Expr::ArrayPush { array_id, value } => {
let arr = locals
.get(array_id)
Expand Down
16 changes: 16 additions & 0 deletions crates/perry-codegen-wasm/src/emit/string_collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,22 @@ impl WasmModuleEmitter {
self.collect_strings_in_expr(index);
self.collect_strings_in_expr(value);
}
// #5016: `obj.prop = v` / `obj[i] = v` lower to PutValueSet. Recurse
// into all operands so the key string (when an `Expr::String`) and
// any string literals in the value/target are interned — the emit
// arm reads the key via `string_map`.
Expr::PutValueSet {
target,
key,
value,
receiver,
..
} => {
self.collect_strings_in_expr(target);
self.collect_strings_in_expr(key);
self.collect_strings_in_expr(value);
self.collect_strings_in_expr(receiver);
}
Expr::Await(e) | Expr::TypeOf(e) | Expr::Void(e) => {
self.collect_strings_in_expr(e);
}
Expand Down
5 changes: 5 additions & 0 deletions tests/wasm/26_module_array_element_write.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fnRead=5
topRead=5
afterTop=99
viaParam=7
objProp=11
36 changes: 36 additions & 0 deletions tests/wasm/26_module_array_element_write.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// #5016 regression guard: writes to module-level array/object members performed
// inside a function (and at top level) must persist on the web/wasm target.
// Before the fix, `A[0] = ...` lowered to a `PutValueSet` HIR node that the WASM
// codegen never handled, so the write fell through to the undefined catch-all and
// reads returned the initializer. Native always worked; this guards the web path.

const A: number[] = [0.0];
function bump(n: number): void {
A[0] = A[0] + n;
} // write a module-level array element inside a function
function readA(): number {
return A[0];
}
const step: number = Date.now() > 0.0 ? 5.0 : 1.0; // runtime value → not const-folded
bump(step);
console.log("fnRead=" + readA().toString());
console.log("topRead=" + A[0].toString());

// Top-level element write must persist too.
A[0] = 99.0;
console.log("afterTop=" + A[0].toString());

// Write through a parameter aliasing the module array.
function setVia(arr: number[], v: number): void {
arr[0] = v;
}
setVia(A, 7.0);
console.log("viaParam=" + A[0].toString());

// Object property write (string key) inside a function.
const o: { x: number } = { x: 1.0 };
function bumpObj(): void {
o.x = o.x + 10.0;
}
bumpObj();
console.log("objProp=" + o.x.toString());