Skip to content

Commit 56c7d5f

Browse files
authored
[gc_fuzz]: Add and complete i31 (#13609)
* Add and complete i31 * Add forgotten import
1 parent 3f44b1c commit 56c7d5f

4 files changed

Lines changed: 294 additions & 3 deletions

File tree

crates/fuzzing/src/generators/gc_ops/ops.rs

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ struct WasmEncodingBases {
2121
typed_first_func_index: u32,
2222
struct_local_idx: u32,
2323
eq_local_idx: u32,
24+
i31_local_idx: u32,
2425
typed_local_base: u32,
2526
struct_global_idx: u32,
2627
eq_global_idx: u32,
28+
i31_global_idx: u32,
2729
typed_global_base: u32,
2830
struct_table_idx: u32,
2931
eq_table_idx: u32,
32+
i31_table_idx: u32,
3033
typed_table_base: u32,
3134
}
3235

@@ -118,6 +121,17 @@ impl GcOps {
118121
vec![],
119122
);
120123

124+
// 6: `take_i31`
125+
//
126+
// Takes a `(ref null i31)` along with the guest's inline `i31.get_s`
127+
// and `i31.get_u` results, so the host can re-derive them and assert
128+
// that its view of the i31 matches the Wasm instructions'.
129+
let take_i31_type_idx = types.len();
130+
types.ty().function(
131+
vec![ValType::Ref(RefType::I31REF), ValType::I32, ValType::I32],
132+
vec![],
133+
);
134+
121135
let struct_type_base: u32 = types.len();
122136

123137
// Build the type-id-to-wasm-index map from the pre-computed
@@ -190,6 +204,7 @@ impl GcOps {
190204
imports.import("", "make_refs", EntityType::Function(3));
191205
imports.import("", "take_struct", EntityType::Function(4));
192206
imports.import("", "take_eq", EntityType::Function(5));
207+
imports.import("", "take_i31", EntityType::Function(take_i31_type_idx));
193208

194209
// For each of our concrete struct types, define a function
195210
// import that takes an argument of that concrete type.
@@ -235,6 +250,15 @@ impl GcOps {
235250
shared: false,
236251
});
237252

253+
let i31_table_idx = tables.len();
254+
tables.table(TableType {
255+
element_type: RefType::I31REF,
256+
minimum: u64::from(self.limits.table_size),
257+
maximum: None,
258+
table64: false,
259+
shared: false,
260+
});
261+
238262
let typed_table_base = tables.len();
239263
for i in 0..struct_count {
240264
let concrete = struct_type_base + i;
@@ -297,6 +321,17 @@ impl GcOps {
297321
}),
298322
);
299323

324+
// Add exactly one (ref.null i31) global.
325+
let i31_global_idx = globals.len();
326+
globals.global(
327+
wasm_encoder::GlobalType {
328+
val_type: ValType::Ref(RefType::I31REF),
329+
mutable: true,
330+
shared: false,
331+
},
332+
&ConstExpr::ref_null(wasm_encoder::HeapType::I31),
333+
);
334+
300335
// Add one typed (ref <type>) global per struct type.
301336
let typed_global_base = globals.len();
302337
for i in 0..struct_count {
@@ -343,7 +378,10 @@ impl GcOps {
343378
let eq_local_idx = struct_local_idx + 1;
344379
local_decls.push((1, ValType::Ref(RefType::EQREF)));
345380

346-
let typed_local_base: u32 = eq_local_idx + 1;
381+
let i31_local_idx = eq_local_idx + 1;
382+
local_decls.push((1, ValType::Ref(RefType::I31REF)));
383+
384+
let typed_local_base: u32 = i31_local_idx + 1;
347385
for i in 0..struct_count {
348386
let concrete = struct_type_base + i;
349387
local_decls.push((
@@ -360,12 +398,15 @@ impl GcOps {
360398
typed_first_func_index,
361399
struct_local_idx,
362400
eq_local_idx,
401+
i31_local_idx,
363402
typed_local_base,
364403
struct_global_idx,
365404
eq_global_idx,
405+
i31_global_idx,
366406
typed_global_base,
367407
struct_table_idx,
368408
eq_table_idx,
409+
i31_table_idx,
369410
typed_table_base,
370411
};
371412

@@ -722,6 +763,79 @@ macro_rules! for_each_gc_op {
722763
type_index = type_index.checked_rem(num_types)?;
723764
})]
724765
StructSet { type_index: u32, field_index: u32 },
766+
767+
#[operands([])]
768+
#[results([I31])]
769+
NullI31,
770+
771+
#[operands([])]
772+
#[results([I31])]
773+
#[fixup(|_limits, _num_types| {
774+
// Any `i32` is a valid operand to `ref.i31` (it wraps to 31
775+
// bits), so no clamping is needed.
776+
})]
777+
RefI31 { value: u32 },
778+
779+
#[operands([Some(I31)])]
780+
#[results([])]
781+
I31LocalSet,
782+
783+
#[operands([])]
784+
#[results([I31])]
785+
I31LocalGet,
786+
787+
#[operands([Some(I31)])]
788+
#[results([])]
789+
I31GlobalSet,
790+
791+
#[operands([])]
792+
#[results([I31])]
793+
I31GlobalGet,
794+
795+
#[operands([Some(I31)])]
796+
#[results([])]
797+
#[fixup(|limits, _num_types| {
798+
// Add one to make sure that out-of-bounds table accesses are
799+
// possible, but still rare.
800+
elem_index = elem_index % (limits.table_size + 1);
801+
})]
802+
I31TableSet { elem_index: u32 },
803+
804+
#[operands([])]
805+
#[results([I31])]
806+
#[fixup(|limits, _num_types| {
807+
// Add one to make sure that out-of-bounds table accesses are
808+
// possible, but still rare.
809+
elem_index = elem_index % (limits.table_size + 1);
810+
})]
811+
I31TableGet { elem_index: u32 },
812+
813+
#[operands([Some(I31)])]
814+
#[results([])]
815+
TakeI31Call,
816+
817+
#[operands([Some(I31)])]
818+
#[results([])]
819+
I31GetS,
820+
821+
#[operands([Some(I31)])]
822+
#[results([])]
823+
I31GetU,
824+
825+
#[operands([Some(Struct(None))])]
826+
#[results([Eq])]
827+
StructRefAsEq,
828+
829+
#[operands([Some(Struct(Some(type_index)))])]
830+
#[results([Eq])]
831+
#[fixup(|_limits, num_types| {
832+
type_index = type_index.checked_rem(num_types)?;
833+
})]
834+
TypedStructRefAsEq { type_index: u32 },
835+
836+
#[operands([Some(I31)])]
837+
#[results([Eq])]
838+
I31RefAsEq,
725839
}
726840
};
727841
}
@@ -913,6 +1027,7 @@ impl GcOp {
9131027
let make_refs_func_idx = 2;
9141028
let take_structref_idx = 3;
9151029
let take_eqref_idx = 4;
1030+
let take_i31_idx = 5;
9161031

9171032
match *self {
9181033
Self::Gc => {
@@ -1229,6 +1344,84 @@ impl GcOp {
12291344
}
12301345
}
12311346
}
1347+
Self::NullI31 => {
1348+
func.instruction(&Instruction::RefNull(wasm_encoder::HeapType::I31));
1349+
}
1350+
Self::RefI31 { value } => {
1351+
func.instruction(&Instruction::I32Const(value.cast_signed()));
1352+
func.instruction(&Instruction::RefI31);
1353+
}
1354+
Self::I31LocalGet => {
1355+
func.instruction(&Instruction::LocalGet(encoding_bases.i31_local_idx));
1356+
}
1357+
Self::I31LocalSet => {
1358+
func.instruction(&Instruction::LocalSet(encoding_bases.i31_local_idx));
1359+
}
1360+
Self::I31GlobalGet => {
1361+
func.instruction(&Instruction::GlobalGet(encoding_bases.i31_global_idx));
1362+
}
1363+
Self::I31GlobalSet => {
1364+
func.instruction(&Instruction::GlobalSet(encoding_bases.i31_global_idx));
1365+
}
1366+
Self::I31TableGet { elem_index } => {
1367+
func.instruction(&Instruction::I32Const(elem_index.cast_signed()));
1368+
func.instruction(&Instruction::TableGet(encoding_bases.i31_table_idx));
1369+
}
1370+
Self::I31TableSet { elem_index } => {
1371+
// Use i31_local_idx (i31ref) to temporarily store the value before table.set.
1372+
func.instruction(&Instruction::LocalSet(encoding_bases.i31_local_idx));
1373+
func.instruction(&Instruction::I32Const(elem_index.cast_signed()));
1374+
func.instruction(&Instruction::LocalGet(encoding_bases.i31_local_idx));
1375+
func.instruction(&Instruction::TableSet(encoding_bases.i31_table_idx));
1376+
}
1377+
Self::StructRefAsEq | Self::TypedStructRefAsEq { .. } | Self::I31RefAsEq => {
1378+
// Upcasting to `eqref` is implicit in Wasm subtyping: both
1379+
// `struct` and `i31` are subtypes of `eq`, so the value already
1380+
// on the stack is a valid `eqref` and no instruction is
1381+
// required. Only the abstract stack type changes (via
1382+
// `result_types`).
1383+
}
1384+
Self::I31GetS | Self::I31GetU => {
1385+
// `i31.get_s`/`i31.get_u` trap on a null reference, so guard
1386+
// against null: save the ref, test it, and only perform the
1387+
// get when non-null. The i32 result is not tracked on the
1388+
// abstract stack, so drop it.
1389+
func.instruction(&Instruction::LocalTee(encoding_bases.i31_local_idx));
1390+
func.instruction(&Instruction::RefIsNull);
1391+
func.instruction(&Instruction::If(wasm_encoder::BlockType::Empty));
1392+
func.instruction(&Instruction::Else);
1393+
func.instruction(&Instruction::LocalGet(encoding_bases.i31_local_idx));
1394+
if matches!(self, Self::I31GetS) {
1395+
func.instruction(&Instruction::I31GetS);
1396+
} else {
1397+
func.instruction(&Instruction::I31GetU);
1398+
}
1399+
func.instruction(&Instruction::Drop);
1400+
func.instruction(&Instruction::End);
1401+
}
1402+
Self::TakeI31Call => {
1403+
// Differential check: pass the i31ref plus the guest's inline
1404+
// `i31.get_s` and `i31.get_u` results to the host, which
1405+
// re-derives them and asserts they match. The get instructions
1406+
// trap on null, so guard against it.
1407+
func.instruction(&Instruction::LocalTee(encoding_bases.i31_local_idx));
1408+
func.instruction(&Instruction::RefIsNull);
1409+
func.instruction(&Instruction::If(wasm_encoder::BlockType::Empty));
1410+
// Null branch: `take_i31(null, 0, 0)`.
1411+
func.instruction(&Instruction::RefNull(wasm_encoder::HeapType::I31));
1412+
func.instruction(&Instruction::I32Const(0));
1413+
func.instruction(&Instruction::I32Const(0));
1414+
func.instruction(&Instruction::Call(take_i31_idx));
1415+
func.instruction(&Instruction::Else);
1416+
// Non-null branch: `take_i31(ref, i31.get_s, i31.get_u)`.
1417+
func.instruction(&Instruction::LocalGet(encoding_bases.i31_local_idx));
1418+
func.instruction(&Instruction::LocalGet(encoding_bases.i31_local_idx));
1419+
func.instruction(&Instruction::I31GetS);
1420+
func.instruction(&Instruction::LocalGet(encoding_bases.i31_local_idx));
1421+
func.instruction(&Instruction::I31GetU);
1422+
func.instruction(&Instruction::Call(take_i31_idx));
1423+
func.instruction(&Instruction::End);
1424+
}
12321425
}
12331426
}
12341427
}

crates/fuzzing/src/generators/gc_ops/tests.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,47 @@ fn every_op_generated() -> mutatis::Result<()> {
234234
Ok(())
235235
}
236236

237+
#[test]
238+
fn i31_and_eq_upcast_ops_validate() -> mutatis::Result<()> {
239+
let _ = env_logger::try_init();
240+
241+
// Exercise every new i31/eqref op end-to-end through encoding + validation.
242+
let mut ops = test_ops(5, 5, 5);
243+
ops.ops = vec![
244+
// Create i31 values and route them through locals/globals/tables.
245+
GcOp::RefI31 { value: 0x4000_0001 },
246+
GcOp::I31LocalSet,
247+
GcOp::I31LocalGet,
248+
GcOp::I31GlobalSet,
249+
GcOp::I31GlobalGet,
250+
GcOp::I31TableSet { elem_index: 0 },
251+
GcOp::I31TableGet { elem_index: 0 },
252+
// Inline i31.get_s / i31.get_u (null-guarded).
253+
GcOp::I31GetS,
254+
GcOp::RefI31 { value: 7 },
255+
GcOp::I31GetU,
256+
// Null i31, then differential host check.
257+
GcOp::NullI31,
258+
GcOp::TakeI31Call,
259+
GcOp::RefI31 { value: 0x7fff_ffff },
260+
GcOp::TakeI31Call,
261+
// Upcast i31 -> eqref and hand it to the eqref-taking host function.
262+
GcOp::RefI31 { value: 1 },
263+
GcOp::I31RefAsEq,
264+
GcOp::TakeEqCall,
265+
// Upcast abstract and concrete struct refs -> eqref.
266+
GcOp::NullStruct,
267+
GcOp::StructRefAsEq,
268+
GcOp::TakeEqCall,
269+
GcOp::StructNew { type_index: 0 },
270+
GcOp::TypedStructRefAsEq { type_index: 0 },
271+
GcOp::TakeEqCall,
272+
];
273+
assert_valid_wasm(&mut ops);
274+
275+
Ok(())
276+
}
277+
237278
#[test]
238279
fn emits_rec_groups_and_validates() -> mutatis::Result<()> {
239280
let _ = env_logger::try_init();

crates/fuzzing/src/generators/gc_ops/types.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ macro_rules! for_each_field_type {
5757
#[storage(wasm_encoder::StorageType::Val(wasm_encoder::ValType::Ref(wasm_encoder::RefType::EQREF)))]
5858
#[default_val(wasm_encoder::Instruction::RefNull(wasm_encoder::HeapType::Abstract { shared: false, ty: wasm_encoder::AbstractHeapType::Eq }))]
5959
EqRef,
60+
61+
#[storage(wasm_encoder::StorageType::Val(wasm_encoder::ValType::Ref(wasm_encoder::RefType::I31REF)))]
62+
#[default_val(wasm_encoder::Instruction::RefNull(wasm_encoder::HeapType::I31))]
63+
I31Ref,
6064
}
6165
};
6266
}
@@ -714,6 +718,8 @@ pub enum StackType {
714718
ExternRef,
715719
/// `eqref`.
716720
Eq,
721+
/// `i31ref`.
722+
I31,
717723
/// `(ref $*)` — optionally with a concrete type index.
718724
Struct(Option<u32>),
719725
}
@@ -759,8 +765,9 @@ impl StackType {
759765
}
760766
},
761767
Some(Self::Eq) => match stack.last() {
762-
// struct <: eq, so a struct on the stack satisfies an eqref requirement.
763-
Some(Self::Eq) | Some(Self::Struct(_)) => {
768+
// struct <: eq and i31 <: eq, so a struct or i31 on the stack
769+
// satisfies an eqref requirement.
770+
Some(Self::Eq) | Some(Self::Struct(_)) | Some(Self::I31) => {
764771
log::trace!("[StackType::fixup] Eq: top ok -> pop");
765772
stack.pop();
766773
}
@@ -773,6 +780,28 @@ impl StackType {
773780
);
774781
}
775782
},
783+
Some(Self::I31) => match stack.last() {
784+
Some(Self::I31) => {
785+
log::trace!("[StackType::fixup] I31: top ok -> pop");
786+
stack.pop();
787+
}
788+
other => {
789+
log::trace!(
790+
"[StackType::fixup] I31: mismatch top={other:?} -> emit RefI31+pop"
791+
);
792+
Self::emit(
793+
GcOp::RefI31 { value: 0 },
794+
stack,
795+
out,
796+
num_types,
797+
&mut result_types,
798+
);
799+
let popped = stack.pop();
800+
log::trace!(
801+
"[StackType::fixup] I31: after emit pop -> {popped:?} stack={stack:?}"
802+
);
803+
}
804+
},
776805
Some(Self::Struct(wanted)) => {
777806
let ok = match (wanted, stack.last()) {
778807
(Some(wanted), Some(Self::Struct(Some(actual)))) => {

0 commit comments

Comments
 (0)