Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/crates/fuzzing/src/generators/table_ops.rs
1693 views
1
//! Generating series of `table.get` and `table.set` operations.
2
use mutatis::mutators as m;
3
use mutatis::{Candidates, Context, DefaultMutate, Generate, Mutate, Result as MutResult};
4
use serde::{Deserialize, Serialize};
5
use smallvec::SmallVec;
6
use std::ops::RangeInclusive;
7
use wasm_encoder::{
8
CodeSection, ConstExpr, EntityType, ExportKind, ExportSection, Function, FunctionSection,
9
GlobalSection, ImportSection, Instruction, Module, RefType, TableSection, TableType,
10
TypeSection, ValType,
11
};
12
13
const NUM_PARAMS_RANGE: RangeInclusive<u32> = 0..=10;
14
const NUM_GLOBALS_RANGE: RangeInclusive<u32> = 0..=10;
15
const TABLE_SIZE_RANGE: RangeInclusive<u32> = 0..=100;
16
const NUM_REC_GROUPS_RANGE: RangeInclusive<u32> = 0..=10;
17
const MAX_OPS: usize = 100;
18
19
/// RecGroup ID struct definition.
20
#[derive(Debug, Clone, Eq, PartialOrd, PartialEq, Ord, Hash, Default, Serialize, Deserialize)]
21
pub struct RecGroupId(u32);
22
23
/// Struct types definition.
24
#[derive(Debug, Default, Serialize, Deserialize)]
25
pub struct Types {
26
rec_groups: std::collections::BTreeSet<RecGroupId>,
27
}
28
29
impl Types {
30
/// Create a fresh `Types` allocator with no recursive groups defined yet.
31
pub fn new() -> Self {
32
Self {
33
rec_groups: Default::default(),
34
}
35
}
36
37
/// Insert a rec-group id. Returns true if newly inserted, false if it already existed.
38
pub fn insert_rec_group(&mut self, id: RecGroupId) -> bool {
39
self.rec_groups.insert(id)
40
}
41
42
/// Iterate over all allocated recursive groups.
43
pub fn groups(&self) -> impl Iterator<Item = &RecGroupId> {
44
self.rec_groups.iter()
45
}
46
}
47
48
/// Limits controlling the structure of a generated Wasm module.
49
#[derive(Debug, Default, Serialize, Deserialize)]
50
pub struct TableOpsLimits {
51
pub(crate) num_params: u32,
52
pub(crate) num_globals: u32,
53
pub(crate) table_size: u32,
54
pub(crate) num_rec_groups: u32,
55
}
56
57
impl TableOpsLimits {
58
fn fixup(&mut self) {
59
// NB: Exhaustively match so that we remember to fixup any other new
60
// limits we add in the future.
61
let Self {
62
num_params,
63
num_globals,
64
table_size,
65
num_rec_groups,
66
} = self;
67
68
let clamp = |limit: &mut u32, range: RangeInclusive<u32>| {
69
*limit = (*limit).clamp(*range.start(), *range.end())
70
};
71
clamp(table_size, TABLE_SIZE_RANGE);
72
clamp(num_params, NUM_PARAMS_RANGE);
73
clamp(num_globals, NUM_GLOBALS_RANGE);
74
clamp(num_rec_groups, NUM_REC_GROUPS_RANGE);
75
}
76
}
77
78
/// A description of a Wasm module that makes a series of `externref` table
79
/// operations.
80
#[derive(Debug, Default, Serialize, Deserialize)]
81
pub struct TableOps {
82
pub(crate) limits: TableOpsLimits,
83
pub(crate) ops: Vec<TableOp>,
84
pub(crate) types: Types,
85
}
86
87
impl TableOps {
88
/// Serialize this module into a Wasm binary.
89
///
90
/// The module requires several function imports. See this function's
91
/// implementation for their exact types.
92
///
93
/// The single export of the module is a function "run" that takes
94
/// `self.num_params` parameters of type `externref`.
95
///
96
/// The "run" function does not terminate; you should run it with limited
97
/// fuel. It also is not guaranteed to avoid traps: it may access
98
/// out-of-bounds of the table.
99
pub fn to_wasm_binary(&mut self) -> Vec<u8> {
100
self.fixup();
101
102
let mut module = Module::new();
103
104
// Encode the types for all functions that we are using.
105
let mut types = TypeSection::new();
106
107
// 0: "gc"
108
types.ty().function(
109
vec![],
110
// Return a bunch of stuff from `gc` so that we exercise GCing when
111
// there is return pointer space allocated on the stack. This is
112
// especially important because the x64 backend currently
113
// dynamically adjusts the stack pointer for each call that uses
114
// return pointers rather than statically allocating space in the
115
// stack frame.
116
vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],
117
);
118
119
// 1: "run"
120
let mut params: Vec<ValType> = Vec::with_capacity(self.limits.num_params as usize);
121
for _i in 0..self.limits.num_params {
122
params.push(ValType::EXTERNREF);
123
}
124
let results = vec![];
125
types.ty().function(params, results);
126
127
// 2: `take_refs`
128
types.ty().function(
129
vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],
130
vec![],
131
);
132
133
// 3: `make_refs`
134
types.ty().function(
135
vec![],
136
vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],
137
);
138
139
// Import the GC function.
140
let mut imports = ImportSection::new();
141
imports.import("", "gc", EntityType::Function(0));
142
imports.import("", "take_refs", EntityType::Function(2));
143
imports.import("", "make_refs", EntityType::Function(3));
144
145
// Define our table.
146
let mut tables = TableSection::new();
147
tables.table(TableType {
148
element_type: RefType::EXTERNREF,
149
minimum: u64::from(self.limits.table_size),
150
maximum: None,
151
table64: false,
152
shared: false,
153
});
154
155
// Define our globals.
156
let mut globals = GlobalSection::new();
157
for _ in 0..self.limits.num_globals {
158
globals.global(
159
wasm_encoder::GlobalType {
160
val_type: wasm_encoder::ValType::EXTERNREF,
161
mutable: true,
162
shared: false,
163
},
164
&ConstExpr::ref_null(wasm_encoder::HeapType::EXTERN),
165
);
166
}
167
168
// Define the "run" function export.
169
let mut functions = FunctionSection::new();
170
functions.function(1);
171
172
let mut exports = ExportSection::new();
173
exports.export("run", ExportKind::Func, 3);
174
175
// Give ourselves one scratch local that we can use in various `TableOp`
176
// implementations.
177
let mut func = Function::new(vec![(1, ValType::EXTERNREF)]);
178
179
func.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty));
180
for op in &self.ops {
181
op.insert(&mut func, self.limits.num_params);
182
}
183
func.instruction(&Instruction::Br(0));
184
func.instruction(&Instruction::End);
185
func.instruction(&Instruction::End);
186
187
// Emit one empty (rec ...) per declared group.
188
for _ in self.types.groups() {
189
types.ty().rec(Vec::<wasm_encoder::SubType>::new());
190
}
191
192
let mut code = CodeSection::new();
193
code.function(&func);
194
195
module
196
.section(&types)
197
.section(&imports)
198
.section(&functions)
199
.section(&tables)
200
.section(&globals)
201
.section(&exports)
202
.section(&code);
203
204
module.finish()
205
}
206
207
/// Computes the abstract stack depth after executing all operations
208
pub fn abstract_stack_depth(&self, index: usize) -> usize {
209
debug_assert!(index <= self.ops.len());
210
let mut stack: usize = 0;
211
for op in self.ops.iter().take(index) {
212
let pop = op.operands_len();
213
let push = op.results_len();
214
stack = stack.saturating_sub(pop);
215
stack += push;
216
}
217
stack
218
}
219
220
/// Fixes this test case such that it becomes valid.
221
///
222
/// This is necessary because a random mutation (e.g. removing an op in the
223
/// middle of our sequence) might have made it so that subsequent ops won't
224
/// have their expected operand types on the Wasm stack
225
/// anymore. Furthermore, because we serialize and deserialize test cases,
226
/// and libFuzzer will occasionally mutate those serialized bytes directly,
227
/// rather than use one of our custom mutations, we have no guarantee that
228
/// pre-mutation test cases are even valid! Therefore, we always call this
229
/// method before translating this "AST"-style representation into a raw
230
/// Wasm binary.
231
fn fixup(&mut self) {
232
self.limits.fixup();
233
234
let mut new_ops = Vec::with_capacity(self.ops.len());
235
let mut stack = 0;
236
237
for mut op in self.ops.iter().copied() {
238
op.fixup(&self.limits);
239
240
let mut temp = SmallVec::<[_; 4]>::new();
241
242
while stack < op.operands_len() {
243
temp.push(TableOp::Null());
244
stack += 1;
245
}
246
247
temp.push(op);
248
stack = stack - op.operands_len() + op.results_len();
249
250
new_ops.extend(temp);
251
}
252
253
// Insert drops to balance the final stack state
254
for _ in 0..stack {
255
new_ops.push(TableOp::Drop());
256
}
257
258
self.ops = new_ops;
259
}
260
261
/// Attempts to remove the last opcode from the sequence.
262
///
263
/// Returns `true` if an opcode was successfully removed, or `false` if the list was already empty.
264
pub fn pop(&mut self) -> bool {
265
self.ops.pop().is_some()
266
}
267
}
268
269
/// A mutator for the table ops
270
#[derive(Debug)]
271
pub struct TableOpsMutator;
272
273
impl Mutate<TableOps> for TableOpsMutator {
274
fn mutate(&mut self, c: &mut Candidates<'_>, ops: &mut TableOps) -> mutatis::Result<()> {
275
if !c.shrink() {
276
c.mutation(|ctx| {
277
if let Some(idx) = ctx.rng().gen_index(ops.ops.len() + 1) {
278
let stack = ops.abstract_stack_depth(idx);
279
let (op, _new_stack_size) = TableOp::generate(ctx, &ops, stack)?;
280
ops.ops.insert(idx, op);
281
}
282
Ok(())
283
})?;
284
}
285
if !ops.ops.is_empty() {
286
c.mutation(|ctx| {
287
let idx = ctx
288
.rng()
289
.gen_index(ops.ops.len())
290
.expect("ops is not empty");
291
ops.ops.remove(idx);
292
Ok(())
293
})?;
294
}
295
296
Ok(())
297
}
298
}
299
300
impl DefaultMutate for TableOps {
301
type DefaultMutate = TableOpsMutator;
302
}
303
304
impl Default for TableOpsMutator {
305
fn default() -> Self {
306
TableOpsMutator
307
}
308
}
309
310
impl<'a> arbitrary::Arbitrary<'a> for TableOps {
311
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
312
let mut session = mutatis::Session::new().seed(u.arbitrary()?);
313
session
314
.generate()
315
.map_err(|_| arbitrary::Error::IncorrectFormat)
316
}
317
}
318
319
impl Generate<TableOps> for TableOpsMutator {
320
fn generate(&mut self, ctx: &mut Context) -> MutResult<TableOps> {
321
let num_params = m::range(NUM_PARAMS_RANGE).generate(ctx)?;
322
let num_globals = m::range(NUM_GLOBALS_RANGE).generate(ctx)?;
323
let table_size = m::range(TABLE_SIZE_RANGE).generate(ctx)?;
324
325
let num_rec_groups = m::range(NUM_REC_GROUPS_RANGE).generate(ctx)?;
326
327
let mut ops = TableOps {
328
limits: TableOpsLimits {
329
num_params,
330
num_globals,
331
table_size,
332
num_rec_groups,
333
},
334
ops: vec![
335
TableOp::Null(),
336
TableOp::Drop(),
337
TableOp::Gc(),
338
TableOp::LocalSet(0),
339
TableOp::LocalGet(0),
340
TableOp::GlobalSet(0),
341
TableOp::GlobalGet(0),
342
],
343
types: Types::new(),
344
};
345
346
for i in 0..ops.limits.num_rec_groups {
347
ops.types.insert_rec_group(RecGroupId(i));
348
}
349
350
let mut stack: usize = 0;
351
while ops.ops.len() < MAX_OPS {
352
let (op, new_stack_len) = TableOp::generate(ctx, &ops, stack)?;
353
ops.ops.push(op);
354
stack = new_stack_len;
355
}
356
357
// Drop any leftover refs on the stack.
358
for _ in 0..stack {
359
ops.ops.push(TableOp::Drop());
360
}
361
362
Ok(ops)
363
}
364
}
365
366
macro_rules! define_table_ops {
367
(
368
$(
369
$op:ident $( ( $($limit_var:ident : $limit:expr => $ty:ty),* ) )? : $params:expr => $results:expr ,
370
)*
371
) => {
372
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
373
pub(crate) enum TableOp {
374
$(
375
$op ( $( $($ty),* )? ),
376
)*
377
}
378
#[cfg(test)]
379
const OP_NAMES: &'static[&'static str] = &[
380
$(
381
stringify!($op),
382
)*
383
];
384
385
impl TableOp {
386
#[cfg(test)]
387
fn name(&self) -> &'static str {
388
match self {
389
$(
390
Self::$op (..) => stringify!($op),
391
)*
392
}
393
}
394
395
pub fn operands_len(&self) -> usize {
396
match self {
397
$(
398
Self::$op (..) => $params,
399
)*
400
}
401
}
402
403
pub fn results_len(&self) -> usize {
404
match self {
405
$(
406
Self::$op (..) => $results,
407
)*
408
}
409
}
410
}
411
412
$(
413
#[allow(non_snake_case, reason = "macro-generated code")]
414
fn $op(
415
_ctx: &mut mutatis::Context,
416
_limits: &TableOpsLimits,
417
stack: usize,
418
) -> mutatis::Result<(TableOp, usize)> {
419
#[allow(unused_comparisons, reason = "macro-generated code")]
420
{
421
debug_assert!(stack >= $params);
422
}
423
424
let op = TableOp::$op(
425
$($({
426
let limit_fn = $limit as fn(&TableOpsLimits) -> $ty;
427
let limit = (limit_fn)(_limits);
428
debug_assert!(limit > 0);
429
m::range(0..=limit - 1).generate(_ctx)?
430
})*)?
431
);
432
let new_stack = stack - $params + $results;
433
Ok((op, new_stack))
434
}
435
)*
436
437
impl TableOp {
438
fn fixup(&mut self, limits: &TableOpsLimits) {
439
match self {
440
$(
441
Self::$op( $( $( $limit_var ),* )? ) => {
442
$( $(
443
let limit_fn = $limit as fn(&TableOpsLimits) -> $ty;
444
let limit = (limit_fn)(limits);
445
debug_assert!(limit > 0);
446
*$limit_var = *$limit_var % limit;
447
)* )?
448
}
449
)*
450
}
451
}
452
453
fn generate(
454
ctx: &mut mutatis::Context,
455
ops: &TableOps,
456
stack: usize,
457
) -> mutatis::Result<(TableOp, usize)> {
458
let mut valid_choices: Vec<
459
fn(&mut Context, &TableOpsLimits, usize) -> mutatis::Result<(TableOp, usize)>
460
> = vec![];
461
$(
462
#[allow(unused_comparisons, reason = "macro-generated code")]
463
if stack >= $params $($(
464
&& {
465
let limit_fn = $limit as fn(&TableOpsLimits) -> $ty;
466
let limit = (limit_fn)(&ops.limits);
467
limit > 0
468
}
469
)*)? {
470
valid_choices.push($op);
471
}
472
)*
473
474
let f = *ctx.rng()
475
.choose(&valid_choices)
476
.expect("should always have a valid op choice");
477
478
(f)(ctx, &ops.limits, stack)
479
}
480
}
481
};
482
}
483
484
define_table_ops! {
485
Gc : 0 => 3,
486
487
MakeRefs : 0 => 3,
488
TakeRefs : 3 => 0,
489
490
// Add one to make sure that out of bounds table accesses are possible, but still rare.
491
TableGet(elem_index: |ops| ops.table_size + 1 => u32) : 0 => 1,
492
TableSet(elem_index: |ops| ops.table_size + 1 => u32) : 1 => 0,
493
494
GlobalGet(global_index: |ops| ops.num_globals => u32) : 0 => 1,
495
GlobalSet(global_index: |ops| ops.num_globals => u32) : 1 => 0,
496
497
LocalGet(local_index: |ops| ops.num_params => u32) : 0 => 1,
498
LocalSet(local_index: |ops| ops.num_params => u32) : 1 => 0,
499
500
Drop : 1 => 0,
501
502
Null : 0 => 1,
503
}
504
505
impl TableOp {
506
fn insert(self, func: &mut Function, scratch_local: u32) {
507
let gc_func_idx = 0;
508
let take_refs_func_idx = 1;
509
let make_refs_func_idx = 2;
510
511
match self {
512
Self::Gc() => {
513
func.instruction(&Instruction::Call(gc_func_idx));
514
}
515
Self::MakeRefs() => {
516
func.instruction(&Instruction::Call(make_refs_func_idx));
517
}
518
Self::TakeRefs() => {
519
func.instruction(&Instruction::Call(take_refs_func_idx));
520
}
521
Self::TableGet(x) => {
522
func.instruction(&Instruction::I32Const(x.cast_signed()));
523
func.instruction(&Instruction::TableGet(0));
524
}
525
Self::TableSet(x) => {
526
func.instruction(&Instruction::LocalSet(scratch_local));
527
func.instruction(&Instruction::I32Const(x.cast_signed()));
528
func.instruction(&Instruction::LocalGet(scratch_local));
529
func.instruction(&Instruction::TableSet(0));
530
}
531
Self::GlobalGet(x) => {
532
func.instruction(&Instruction::GlobalGet(x));
533
}
534
Self::GlobalSet(x) => {
535
func.instruction(&Instruction::GlobalSet(x));
536
}
537
Self::LocalGet(x) => {
538
func.instruction(&Instruction::LocalGet(x));
539
}
540
Self::LocalSet(x) => {
541
func.instruction(&Instruction::LocalSet(x));
542
}
543
Self::Drop() => {
544
func.instruction(&Instruction::Drop);
545
}
546
Self::Null() => {
547
func.instruction(&Instruction::RefNull(wasm_encoder::HeapType::EXTERN));
548
}
549
}
550
}
551
}
552
553
#[cfg(test)]
554
mod tests {
555
use super::*;
556
557
/// Creates empty TableOps
558
fn empty_test_ops() -> TableOps {
559
let mut t = TableOps {
560
limits: TableOpsLimits {
561
num_params: 5,
562
num_globals: 5,
563
table_size: 5,
564
num_rec_groups: 5,
565
},
566
ops: vec![],
567
types: Types::new(),
568
};
569
for i in 0..t.limits.num_rec_groups {
570
t.types.insert_rec_group(RecGroupId(i));
571
}
572
t
573
}
574
575
/// Creates TableOps with all default opcodes
576
fn test_ops(num_params: u32, num_globals: u32, table_size: u32) -> TableOps {
577
let mut t = TableOps {
578
limits: TableOpsLimits {
579
num_params,
580
num_globals,
581
table_size,
582
num_rec_groups: 3,
583
},
584
ops: vec![
585
TableOp::Null(),
586
TableOp::Drop(),
587
TableOp::Gc(),
588
TableOp::LocalSet(0),
589
TableOp::LocalGet(0),
590
TableOp::GlobalSet(0),
591
TableOp::GlobalGet(0),
592
TableOp::Null(),
593
TableOp::Drop(),
594
TableOp::Gc(),
595
TableOp::LocalSet(0),
596
TableOp::LocalGet(0),
597
TableOp::GlobalSet(0),
598
TableOp::GlobalGet(0),
599
TableOp::Null(),
600
TableOp::Drop(),
601
],
602
types: Types::new(),
603
};
604
for i in 0..t.limits.num_rec_groups {
605
t.types.insert_rec_group(RecGroupId(i));
606
}
607
t
608
}
609
610
#[test]
611
fn mutate_table_ops_with_default_mutator() -> mutatis::Result<()> {
612
let _ = env_logger::try_init();
613
let mut res = test_ops(5, 5, 5);
614
615
let mut session = mutatis::Session::new();
616
617
for _ in 0..1024 {
618
session.mutate(&mut res)?;
619
let wasm = res.to_wasm_binary();
620
621
let feats = wasmparser::WasmFeatures::default();
622
feats.reference_types();
623
feats.gc();
624
let mut validator = wasmparser::Validator::new_with_features(feats);
625
626
let wat = wasmprinter::print_bytes(&wasm).expect("[-] Failed .print_bytes(&wasm).");
627
let result = validator.validate_all(&wasm);
628
log::debug!("{wat}");
629
assert!(
630
result.is_ok(),
631
"\n[-] Invalid wat: {}\n\t\t==== Failed Wat ====\n{}",
632
result.err().expect("[-] Failed .err() in assert macro."),
633
wat
634
);
635
}
636
Ok(())
637
}
638
639
#[test]
640
fn every_op_generated() -> mutatis::Result<()> {
641
let _ = env_logger::try_init();
642
let mut unseen_ops: std::collections::HashSet<_> = OP_NAMES.iter().copied().collect();
643
644
let mut res = empty_test_ops();
645
let mut session = mutatis::Session::new();
646
647
'outer: for _ in 0..=1024 {
648
session.mutate(&mut res)?;
649
for op in &res.ops {
650
unseen_ops.remove(op.name());
651
if unseen_ops.is_empty() {
652
break 'outer;
653
}
654
}
655
}
656
657
assert!(unseen_ops.is_empty(), "Failed to generate {unseen_ops:?}");
658
Ok(())
659
}
660
661
#[test]
662
fn test_wat_string() -> mutatis::Result<()> {
663
let _ = env_logger::try_init();
664
665
let mut table_ops = test_ops(2, 2, 5);
666
667
let wasm = table_ops.to_wasm_binary();
668
669
let actual_wat = wasmprinter::print_bytes(&wasm).expect("Failed to convert to WAT");
670
let actual_wat = actual_wat.trim();
671
672
let expected_wat = r#"
673
(module
674
(type (;0;) (func (result externref externref externref)))
675
(type (;1;) (func (param externref externref)))
676
(type (;2;) (func (param externref externref externref)))
677
(type (;3;) (func (result externref externref externref)))
678
(rec)
679
(rec)
680
(rec)
681
(import "" "gc" (func (;0;) (type 0)))
682
(import "" "take_refs" (func (;1;) (type 2)))
683
(import "" "make_refs" (func (;2;) (type 3)))
684
(table (;0;) 5 externref)
685
(global (;0;) (mut externref) ref.null extern)
686
(global (;1;) (mut externref) ref.null extern)
687
(export "run" (func 3))
688
(func (;3;) (type 1) (param externref externref)
689
(local externref)
690
loop ;; label = @1
691
ref.null extern
692
drop
693
call 0
694
local.set 0
695
local.get 0
696
global.set 0
697
global.get 0
698
ref.null extern
699
drop
700
call 0
701
local.set 0
702
local.get 0
703
global.set 0
704
global.get 0
705
ref.null extern
706
drop
707
drop
708
drop
709
drop
710
drop
711
drop
712
drop
713
br 0 (;@1;)
714
end
715
)
716
)
717
"#;
718
let expected_wat = expected_wat.trim();
719
720
eprintln!("=== actual ===\n{actual_wat}");
721
eprintln!("=== expected ===\n{expected_wat}");
722
assert_eq!(
723
actual_wat, expected_wat,
724
"actual WAT does not match expected"
725
);
726
727
Ok(())
728
}
729
730
#[test]
731
fn emits_empty_rec_groups_and_validates() -> mutatis::Result<()> {
732
let _ = env_logger::try_init();
733
734
let mut ops = TableOps {
735
limits: TableOpsLimits {
736
num_params: 2,
737
num_globals: 1,
738
table_size: 5,
739
num_rec_groups: 2,
740
},
741
ops: vec![TableOp::Null(), TableOp::Drop()],
742
types: Types::new(),
743
};
744
745
for i in 0..ops.limits.num_rec_groups {
746
ops.types.insert_rec_group(RecGroupId(i));
747
}
748
749
let wasm = ops.to_wasm_binary();
750
751
let feats = wasmparser::WasmFeatures::default();
752
feats.reference_types();
753
feats.gc();
754
let mut validator = wasmparser::Validator::new_with_features(feats);
755
assert!(
756
validator.validate_all(&wasm).is_ok(),
757
"GC validation failed"
758
);
759
760
let wat = wasmprinter::print_bytes(&wasm).expect("to WAT");
761
let recs = wat.matches("(rec").count();
762
let structs = wat.matches("(struct)").count();
763
764
assert_eq!(recs, 2, "expected 2 (rec) blocks, got {recs}");
765
// Still keep as zero. Will update in the next PR
766
assert_eq!(structs, 0, "expected no struct types, got {structs}");
767
768
Ok(())
769
}
770
}
771
772