Skip to content

Commit fd788c4

Browse files
hyperpolymathclaude
andcommitted
feat(codegen): real body lowering for field writers + width-exact scalar accesses (climb Step 2)
Lowers a single-statement field writer region.set $p .field, <value>; -> [local.get p, <push value>, <store>{offset}] to a real typed store + access-site (value = a matching-typed scalar param, a bool, or a numeric literal; compound expressions fall back to the stub). Extends the Step-1 reader to every scalar width and makes BOTH directions width-exact. Op set: add I64/F64 load+store and the sub-width integer accesses I32Load8{U,S}/I32Load16{U,S}/I32Store8/I32Store16, so a 1- or 2-byte field uses a 1- or 2-byte access (sign-extending i8/i16, zero-extending u8/u16/bool) — never a 4-byte i32 access that would touch the neighbour's bytes. Hardening from an adversarial review of the diff (9 confirmed findings; the 5 in this change's own code are fixed here): * narrow fields lower to store8/store16 + load8/load16, not i32.store/i32.load (was corrupting/over-reading adjacent packed fields — validated clean, so the verifier could not catch it); * a region-handle param is no longer accepted as a stored scalar value (pointer-into-scalar type confusion) -> falls back to the stub; * an out-of-width integer literal no longer silently wraps (4294967296 as i32), and an f32-overflowing float literal no longer becomes f32.const inf -> stub; * a synthesised memory (module declares none) is now sized to cover the accessed region, so a real offset cannot point past linear memory and trap. Verification: * tests/execute_lowering.rs - NEW Tier-1 differential EXECUTION gate: runs the lowered module in-process (wasmi, pure-Rust, no system engine) and asserts set->get round-trips every scalar width (incl. narrow sign/zero-extension) and that narrow stores do not clobber neighbours. Proven non-vacuous: it fails if the narrow-width bug is reintroduced. Turns 'the op widths argue correctness' into 'the engine demonstrates it'. * corpus.rs - writer + per-defect regression tests; 6/6 example corpus stays green; every emitted module still passes wasmparser::Validator + L2/L7/L10. The 3 pre-existing parser panics the review also surfaced (div-by-zero / integer overflow in the array-size evaluator + compute_region_byte_size, violating the never-panic contract) are out of scope here -> separate hardening PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c369d0d commit fd788c4

6 files changed

Lines changed: 868 additions & 30 deletions

File tree

Cargo.lock

Lines changed: 136 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/typed-wasm-codegen/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ typed-wasm-verify = { path = "../typed-wasm-verify", features = ["unstable-l2"]
3131
# Round-trip test validates the emitted module is well-formed wasm before
3232
# handing it to the verifier.
3333
wasmparser = "=0.252.0"
34+
# In-process wasm engine for the Tier-1 differential execution gate
35+
# (tests/execute_lowering.rs): proves lowered store/load bodies COMPUTE the
36+
# right memory semantics at runtime, not just that they validate. Pure-Rust so
37+
# it runs in CI without a system wasmtime (unlike tests/execute.rs).
38+
wasmi = "1.1.0"

crates/typed-wasm-codegen/src/lib.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,46 @@ pub enum Op {
191191
I32Store {
192192
offset: u64,
193193
},
194+
/// Sub-width integer accesses for 1- and 2-byte fields. The value on the
195+
/// wasm stack is i32; load8/load16 sign- (`S`) or zero- (`U`) extend, and
196+
/// store8/store16 write only the low byte(s) — so a narrow field touches
197+
/// exactly its own bytes, never the neighbour's.
198+
I32Load8U {
199+
offset: u64,
200+
},
201+
I32Load8S {
202+
offset: u64,
203+
},
204+
I32Load16U {
205+
offset: u64,
206+
},
207+
I32Load16S {
208+
offset: u64,
209+
},
210+
I32Store8 {
211+
offset: u64,
212+
},
213+
I32Store16 {
214+
offset: u64,
215+
},
216+
I64Load {
217+
offset: u64,
218+
},
219+
I64Store {
220+
offset: u64,
221+
},
194222
F32Load {
195223
offset: u64,
196224
},
197225
F32Store {
198226
offset: u64,
199227
},
228+
F64Load {
229+
offset: u64,
230+
},
231+
F64Store {
232+
offset: u64,
233+
},
200234
/// Call the function at the given global index (imports occupy the
201235
/// low indices). Used by the multi-module caller to invoke an import.
202236
Call(u32),
@@ -338,8 +372,18 @@ fn op_to_instruction(op: Op) -> Instruction<'static> {
338372
Op::F64Const(c) => Instruction::F64Const(c.into()),
339373
Op::I32Load { offset } => Instruction::I32Load(memarg(offset, 2)),
340374
Op::I32Store { offset } => Instruction::I32Store(memarg(offset, 2)),
375+
Op::I32Load8U { offset } => Instruction::I32Load8U(memarg(offset, 0)),
376+
Op::I32Load8S { offset } => Instruction::I32Load8S(memarg(offset, 0)),
377+
Op::I32Load16U { offset } => Instruction::I32Load16U(memarg(offset, 1)),
378+
Op::I32Load16S { offset } => Instruction::I32Load16S(memarg(offset, 1)),
379+
Op::I32Store8 { offset } => Instruction::I32Store8(memarg(offset, 0)),
380+
Op::I32Store16 { offset } => Instruction::I32Store16(memarg(offset, 1)),
381+
Op::I64Load { offset } => Instruction::I64Load(memarg(offset, 3)),
382+
Op::I64Store { offset } => Instruction::I64Store(memarg(offset, 3)),
341383
Op::F32Load { offset } => Instruction::F32Load(memarg(offset, 2)),
342384
Op::F32Store { offset } => Instruction::F32Store(memarg(offset, 2)),
385+
Op::F64Load { offset } => Instruction::F64Load(memarg(offset, 3)),
386+
Op::F64Store { offset } => Instruction::F64Store(memarg(offset, 3)),
343387
Op::Call(i) => Instruction::Call(i),
344388
Op::Drop => Instruction::Drop,
345389
}

0 commit comments

Comments
 (0)