1 use crate::generators::gc_ops::{
2 limits::GcOpsLimits,
3 ops::{GcOp, GcOps, OP_NAMES},
4 types::{RecGroupId, TypeId, Types},
5 };
6 use mutatis;
7 use rand::rngs::StdRng;
8 use rand::{Rng, SeedableRng};
9 use wasmparser;
10 use wasmprinter;
11
12 /// Creates empty GcOps
empty_test_ops() -> GcOps13 fn empty_test_ops() -> GcOps {
14 let mut t = GcOps {
15 limits: GcOpsLimits {
16 num_params: 5,
17 num_globals: 5,
18 table_size: 5,
19 max_rec_groups: 5,
20 max_types: 5,
21 },
22 ops: vec![],
23 types: Types::new(),
24 };
25 for i in 0..t.limits.max_rec_groups {
26 t.types.insert_rec_group(RecGroupId(i));
27 }
28 t
29 }
30
31 /// Creates GcOps with all default opcodes
test_ops(num_params: u32, num_globals: u32, table_size: u32) -> GcOps32 fn test_ops(num_params: u32, num_globals: u32, table_size: u32) -> GcOps {
33 let mut t = GcOps {
34 limits: GcOpsLimits {
35 num_params,
36 num_globals,
37 table_size,
38 max_rec_groups: 7,
39 max_types: 10,
40 },
41 ops: vec![
42 GcOp::NullExtern,
43 GcOp::Drop,
44 GcOp::Gc,
45 GcOp::LocalSet { local_index: 0 },
46 GcOp::LocalGet { local_index: 0 },
47 GcOp::GlobalSet { global_index: 0 },
48 GcOp::GlobalGet { global_index: 0 },
49 GcOp::StructNew { type_index: 0 },
50 ],
51 types: Types::new(),
52 };
53
54 for i in 0..t.limits.max_rec_groups {
55 t.types.insert_rec_group(RecGroupId(i));
56 }
57
58 let mut rng = StdRng::seed_from_u64(0xC0FFEE);
59 if t.limits.max_rec_groups > 0 {
60 for i in 0..t.limits.max_types {
61 let gid = RecGroupId(rng.gen_range(0..t.limits.max_rec_groups));
62 let is_final = false;
63 let supertype = None;
64 t.types
65 .insert_empty_struct(TypeId(i), gid, is_final, supertype);
66 }
67 }
68
69 t
70 }
71
72 #[test]
mutate_gc_ops_with_default_mutator() -> mutatis::Result<()>73 fn mutate_gc_ops_with_default_mutator() -> mutatis::Result<()> {
74 let _ = env_logger::try_init();
75
76 let mut features = wasmparser::WasmFeatures::default();
77 features.insert(wasmparser::WasmFeatures::REFERENCE_TYPES);
78 features.insert(wasmparser::WasmFeatures::FUNCTION_REFERENCES);
79 features.insert(wasmparser::WasmFeatures::GC_TYPES);
80 features.insert(wasmparser::WasmFeatures::GC);
81
82 let mut ops = test_ops(5, 5, 5);
83
84 let mut session = mutatis::Session::new();
85 for _ in 0..2048 {
86 session.mutate(&mut ops)?;
87
88 let wasm = ops.to_wasm_binary();
89 println!("wasm: {}", wasmprinter::print_bytes(&wasm).unwrap());
90 crate::oracles::log_wasm(&wasm);
91
92 let mut validator = wasmparser::Validator::new_with_features(features);
93 if let Err(e) = validator.validate_all(&wasm) {
94 let mut config = wasmprinter::Config::new();
95 config.print_offsets(true);
96 config.print_operand_stack(true);
97 let mut wat = String::new();
98 let wat = match config.print(&wasm, &mut wasmprinter::PrintFmtWrite(&mut wat)) {
99 Ok(()) => wat,
100 Err(e) => format!("<failed to disassemble Wasm binary to WAT: {e}>"),
101 };
102 panic!(
103 "Emitted Wasm binary is not valid!\n\n\
104 === Validation Error ===\n\n\
105 {e}\n\n\
106 === GcOps ===\n\n\
107 {ops:#?}\n\n\
108 === Wat ===\n\n\
109 {wat}"
110 );
111 }
112 }
113 Ok(())
114 }
115
116 #[test]
struct_new_removed_when_no_types() -> mutatis::Result<()>117 fn struct_new_removed_when_no_types() -> mutatis::Result<()> {
118 let _ = env_logger::try_init();
119
120 let mut ops = test_ops(0, 0, 0);
121 ops.limits.max_types = 0;
122 ops.ops = vec![GcOp::StructNew { type_index: 42 }];
123
124 ops.fixup();
125 assert!(
126 ops.ops
127 .iter()
128 .all(|op| !matches!(op, GcOp::StructNew { .. })),
129 "StructNew should be removed when there are no types"
130 );
131 Ok(())
132 }
133
134 #[test]
local_ops_removed_when_no_params() -> mutatis::Result<()>135 fn local_ops_removed_when_no_params() -> mutatis::Result<()> {
136 let _ = env_logger::try_init();
137
138 let mut ops = test_ops(0, 0, 0);
139 ops.limits.num_params = 0;
140 ops.ops = vec![
141 GcOp::LocalGet { local_index: 42 },
142 GcOp::LocalSet { local_index: 99 },
143 ];
144
145 ops.fixup();
146 assert!(
147 ops.ops
148 .iter()
149 .all(|op| !matches!(op, GcOp::LocalGet { .. } | GcOp::LocalSet { .. })),
150 "LocalGet/LocalSet should be removed when there are no params"
151 );
152 Ok(())
153 }
154
155 #[test]
global_ops_removed_when_no_globals() -> mutatis::Result<()>156 fn global_ops_removed_when_no_globals() -> mutatis::Result<()> {
157 let _ = env_logger::try_init();
158
159 let mut ops = test_ops(0, 0, 0);
160 ops.limits.num_globals = 0;
161 ops.ops = vec![
162 GcOp::GlobalGet { global_index: 42 },
163 GcOp::GlobalSet { global_index: 99 },
164 ];
165
166 ops.fixup();
167 assert!(
168 ops.ops
169 .iter()
170 .all(|op| !matches!(op, GcOp::GlobalGet { .. } | GcOp::GlobalSet { .. })),
171 "GlobalGet/GlobalSet should be removed when there are no globals"
172 );
173 Ok(())
174 }
175
176 #[test]
every_op_generated() -> mutatis::Result<()>177 fn every_op_generated() -> mutatis::Result<()> {
178 let _ = env_logger::try_init();
179 let mut unseen_ops: std::collections::HashSet<_> = OP_NAMES.iter().copied().collect();
180
181 let mut res = empty_test_ops();
182 let mut session = mutatis::Session::new().seed(0xC0FFEE);
183
184 'outer: for _ in 0..=1024 {
185 session.mutate(&mut res)?;
186 for op in &res.ops {
187 unseen_ops.remove(op.name());
188 if unseen_ops.is_empty() {
189 break 'outer;
190 }
191 }
192 }
193
194 assert!(unseen_ops.is_empty(), "Failed to generate {unseen_ops:?}");
195 Ok(())
196 }
197
198 #[test]
emits_rec_groups_and_validates() -> mutatis::Result<()>199 fn emits_rec_groups_and_validates() -> mutatis::Result<()> {
200 let _ = env_logger::try_init();
201
202 let mut ops = test_ops(5, 5, 5);
203
204 let wasm = ops.to_wasm_binary();
205
206 let feats = wasmparser::WasmFeatures::default();
207 feats.reference_types();
208 feats.gc();
209 let mut validator = wasmparser::Validator::new_with_features(feats);
210 assert!(
211 validator.validate_all(&wasm).is_ok(),
212 "GC validation failed"
213 );
214
215 let wat = wasmprinter::print_bytes(&wasm).expect("to WAT");
216 let recs = wat.matches("(rec").count();
217 let structs = wat.matches("(struct)").count();
218
219 assert_eq!(
220 recs,
221 ops.types.rec_groups.len(),
222 "one (rec) block per rec group"
223 );
224 assert_eq!(
225 structs,
226 ops.types.type_defs.len(),
227 "one (struct) per struct type in type_defs"
228 );
229
230 Ok(())
231 }
232
233 #[test]
fixup_check_types_and_indexes() -> mutatis::Result<()>234 fn fixup_check_types_and_indexes() -> mutatis::Result<()> {
235 let _ = env_logger::try_init();
236
237 let mut ops = test_ops(5, 5, 5);
238
239 // These `GcOp`s do not have their operands satisfied, and their results are
240 // not the operands of the next op, so `fixup` will need to deal with
241 // that. Additionally, their immediates are out-of-bounds of their
242 // respective index spaces, which `fixup` will also need to address.
243 ops.ops = vec![
244 GcOp::TakeTypedStructCall {
245 type_index: ops.limits.max_types + 7,
246 },
247 GcOp::GlobalSet {
248 global_index: ops.limits.num_globals * 2,
249 },
250 GcOp::StructNew {
251 type_index: ops.limits.max_types + 9,
252 },
253 GcOp::LocalSet {
254 local_index: ops.limits.num_params * 5,
255 },
256 ];
257
258 // Call `fixup` to insert missing types, rewrite the immediates such that
259 // they are within their bounds, insert missing operands, and drop unused
260 // results.
261 ops.fixup();
262
263 // Check that we got the expected `GcOp` sequence after `fixup`:
264 assert_eq!(
265 ops.ops,
266 [
267 // Inserted by `fixup` to satisfy `TakeTypedStructCall`'s operands.
268 GcOp::StructNew { type_index: 7 },
269 // The `type_index` is now valid.
270 GcOp::TakeTypedStructCall { type_index: 7 },
271 // Inserted by `fixup` to satisfy `GlobalSet`'s operands.
272 GcOp::NullExtern,
273 // The `global_index` is now valid.
274 GcOp::GlobalSet { global_index: 0 },
275 // The `type_index` is now valid.
276 GcOp::StructNew { type_index: 9 },
277 // Inserted by `fixup` to satisfy `LocalSet`'s operands.
278 GcOp::NullExtern,
279 // The `local_index` is now valid.
280 GcOp::LocalSet { local_index: 0 },
281 // Inserted by `fixup` to make sure the operand stack is empty at
282 // the end of the block.
283 GcOp::Drop,
284 ]
285 );
286
287 // Verify that we generate a valid Wasm binary after calling `fixup`.
288 let wasm = ops.to_wasm_binary();
289 let wat = wasmprinter::print_bytes(&wasm).unwrap();
290 log::debug!("{wat}");
291 let feats = wasmparser::WasmFeatures::default();
292 feats.reference_types();
293 feats.gc();
294 let mut validator = wasmparser::Validator::new_with_features(feats);
295 assert!(
296 validator.validate_all(&wasm).is_ok(),
297 "GC validation should pass after fixup"
298 );
299
300 Ok(())
301 }
302
303 #[test]
sort_types_by_supertype_orders_supertype_before_subtype_across_rec_groups()304 fn sort_types_by_supertype_orders_supertype_before_subtype_across_rec_groups() {
305 let mut types = Types::new();
306 let ga = RecGroupId(0);
307 let gb = RecGroupId(1);
308 let gc = RecGroupId(2);
309 let gd = RecGroupId(3);
310 types.insert_rec_group(ga);
311 types.insert_rec_group(gb);
312 types.insert_rec_group(gc);
313 types.insert_rec_group(gd);
314
315 let a = TypeId(0);
316 let b = TypeId(1);
317 let c = TypeId(2);
318 let d = TypeId(3);
319
320 // Cross-rec-group chain: C <: A <: B <: D.
321 types.insert_empty_struct(a, ga, false, Some(b)); // A <: B
322 types.insert_empty_struct(b, gb, false, Some(d)); // B <: D
323 types.insert_empty_struct(c, gc, false, Some(a)); // C <: A
324 types.insert_empty_struct(d, gd, false, None); // D
325
326 let mut sorted = Vec::new();
327 types.sort_types_topo(&mut sorted);
328
329 // Rec-group boundaries do not change topological ordering by supertype.
330 assert_eq!(
331 sorted,
332 [TypeId(3), TypeId(1), TypeId(0), TypeId(2)],
333 "topo order: supertype before subtype across rec groups"
334 );
335 }
336
337 #[test]
fixup_preserves_subtyping_within_same_rec_group()338 fn fixup_preserves_subtyping_within_same_rec_group() {
339 let _ = env_logger::try_init();
340
341 let mut types = Types::new();
342 let g = RecGroupId(0);
343 types.insert_rec_group(g);
344
345 let super_ty = TypeId(0);
346 let sub_ty = TypeId(1);
347
348 // Both types are in the same rec group.
349 // The second subtypes the first.
350 types.insert_empty_struct(super_ty, g, false, None);
351 types.insert_empty_struct(sub_ty, g, false, Some(super_ty));
352
353 let limits = GcOpsLimits {
354 num_params: 0,
355 num_globals: 0,
356 table_size: 0,
357 max_rec_groups: 10,
358 max_types: 10,
359 };
360
361 types.fixup(&limits);
362
363 assert_eq!(types.rec_group_of(super_ty), Some(g));
364 assert_eq!(types.rec_group_of(sub_ty), Some(g));
365 assert_eq!(
366 types.type_defs.get(&sub_ty).unwrap().supertype,
367 Some(super_ty)
368 );
369 }
370
371 #[test]
fixup_breaks_one_edge_in_multi_rec_group_type_cycle()372 fn fixup_breaks_one_edge_in_multi_rec_group_type_cycle() {
373 let _ = env_logger::try_init();
374
375 let mut types = Types::new();
376
377 let g_a = RecGroupId(0);
378 let g_bc = RecGroupId(1);
379 let g_d = RecGroupId(2);
380
381 types.insert_rec_group(g_a);
382 types.insert_rec_group(g_bc);
383 types.insert_rec_group(g_d);
384
385 let a = TypeId(0);
386 let b = TypeId(1);
387 let c = TypeId(2);
388 let d = TypeId(3);
389
390 // Rec(a)
391 types.insert_empty_struct(a, g_a, false, Some(d));
392
393 // Rec(b, c)
394 types.insert_empty_struct(b, g_bc, false, None);
395 types.insert_empty_struct(c, g_bc, false, Some(a));
396
397 // Rec(d)
398 types.insert_empty_struct(d, g_d, false, Some(c));
399
400 let limits = GcOpsLimits {
401 num_params: 0,
402 num_globals: 0,
403 table_size: 0,
404 max_rec_groups: 10,
405 max_types: 10,
406 };
407
408 types.fixup(&limits);
409
410 let a_super = types.type_defs.get(&a).unwrap().supertype;
411 let c_super = types.type_defs.get(&c).unwrap().supertype;
412 let d_super = types.type_defs.get(&d).unwrap().supertype;
413
414 let cleared = [a_super, c_super, d_super]
415 .into_iter()
416 .filter(|st| st.is_none())
417 .count();
418
419 assert!(
420 cleared == 1,
421 "fixup should clear exactly one edge to break the cycle"
422 );
423 }
424
425 #[test]
sort_rec_groups_topo_orders_dependencies_first()426 fn sort_rec_groups_topo_orders_dependencies_first() {
427 let mut types = Types::new();
428
429 let g0 = RecGroupId(0);
430 let g1 = RecGroupId(1);
431 let g2 = RecGroupId(2);
432 let g3 = RecGroupId(3);
433
434 types.insert_rec_group(g0);
435 types.insert_rec_group(g1);
436 types.insert_rec_group(g2);
437 types.insert_rec_group(g3);
438
439 let a = TypeId(0);
440 let b = TypeId(1);
441 let c = TypeId(2);
442 let d = TypeId(3);
443 let e = TypeId(4);
444 let f = TypeId(5);
445
446 types.insert_empty_struct(a, g0, false, Some(b)); // g0 -> g1
447 types.insert_empty_struct(b, g1, false, Some(c)); // g1 -> g2
448 types.insert_empty_struct(c, g2, false, Some(d)); // g2 ->g3
449 types.insert_empty_struct(d, g3, false, None);
450 types.insert_empty_struct(e, g0, false, None);
451 types.insert_empty_struct(f, g2, false, None);
452
453 let mut sorted = Vec::new();
454 types.sort_rec_groups_topo(&mut sorted);
455
456 // g3 has no deps, g2 depends on g3, g1 on g2, g0 on g1.
457 assert_eq!(
458 sorted,
459 [RecGroupId(3), RecGroupId(2), RecGroupId(1), RecGroupId(0)],
460 "topo order: depended-on groups before dependent groups"
461 );
462 }
463
464 #[test]
break_rec_group_cycles()465 fn break_rec_group_cycles() {
466 let mut types = Types::new();
467
468 let g0 = RecGroupId(0);
469 let g1 = RecGroupId(1);
470 let g2 = RecGroupId(2);
471 let g3 = RecGroupId(3);
472
473 types.insert_rec_group(g0);
474 types.insert_rec_group(g1);
475 types.insert_rec_group(g2);
476 types.insert_rec_group(g3);
477
478 let a0 = TypeId(0);
479 let a1 = TypeId(1);
480 let b0 = TypeId(2);
481 let b1 = TypeId(3);
482 let c0 = TypeId(4);
483 let c1 = TypeId(5);
484 let c2 = TypeId(6);
485 let d0 = TypeId(7);
486 let d1 = TypeId(8);
487
488 // Before: t
489 //
490 // ----------------------------------
491 // | outer cycle │
492 // v │
493 // +----+ +----+ +----+ │
494 // | g0 |------>| g1 |------>| g2 |---
495 // +----+ +----+ +----+
496 // ^ │
497 // │ inner │
498 // │ cycle v
499 // │ +----+
500 // -----------| g3 |
501 // +----+
502 //
503 // After: back edges dropped, clean chain
504 //
505 // +----+ +----+ +----+ +----+
506 // | g0 |------>| g1 |------>| g2 |------>| g3 |
507 // +----+ +----+ +----+ +----+
508
509 types.insert_empty_struct(a0, g0, false, Some(b0)); // g0 -> g1
510 types.insert_empty_struct(a1, g0, false, None);
511
512 types.insert_empty_struct(b0, g1, false, None);
513 types.insert_empty_struct(b1, g1, false, Some(c0)); // g1 -> g2
514
515 types.insert_empty_struct(c0, g2, false, None);
516 types.insert_empty_struct(c1, g2, false, Some(a1)); // g2 -> g0 (outer back edge)
517 types.insert_empty_struct(c2, g2, false, Some(d0)); // g2 -> g3
518
519 types.insert_empty_struct(d0, g3, false, None);
520 types.insert_empty_struct(d1, g3, false, Some(b0)); // g3 -> g1 (inner back edge)
521
522 // Type graph is acyclic — breaking supertype cycles changes nothing.
523 types.break_supertype_cycles();
524 assert_eq!(types.type_defs.get(&a0).unwrap().supertype, Some(b0));
525 assert_eq!(types.type_defs.get(&b1).unwrap().supertype, Some(c0));
526 assert_eq!(types.type_defs.get(&c1).unwrap().supertype, Some(a1));
527 assert_eq!(types.type_defs.get(&c2).unwrap().supertype, Some(d0));
528 assert_eq!(types.type_defs.get(&d1).unwrap().supertype, Some(b0));
529
530 assert_eq!(types.rec_groups.len(), 4);
531
532 types.break_rec_group_cycles();
533
534 // All four groups preserved.
535 assert_eq!(types.rec_groups.len(), 4);
536 assert!(types.rec_groups.contains_key(&g0));
537 assert!(types.rec_groups.contains_key(&g1));
538 assert!(types.rec_groups.contains_key(&g2));
539 assert!(types.rec_groups.contains_key(&g3));
540
541 // Back edge (g2->g0): c1's supertype cleared.
542 assert_eq!(types.type_defs.get(&c1).unwrap().supertype, None);
543
544 // Back edge (g3->g1): d1's supertype cleared.
545 assert_eq!(types.type_defs.get(&d1).unwrap().supertype, None);
546
547 // All other cross-group supertypes preserved.
548 assert_eq!(types.type_defs.get(&a0).unwrap().supertype, Some(b0));
549 assert_eq!(types.type_defs.get(&b1).unwrap().supertype, Some(c0));
550 assert_eq!(types.type_defs.get(&c2).unwrap().supertype, Some(d0));
551
552 // Result is a clean chain: g0 -> g1 -> g2 -> g3
553 let mut topo = Vec::new();
554 types.sort_rec_groups_topo(&mut topo);
555 assert_eq!(topo.len(), 4);
556 assert_eq!(topo, vec![g3, g2, g1, g0]);
557 }
558