diff options
author | Alon Zakai <azakai@google.com> | 2024-08-12 12:30:23 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-12 12:30:23 -0700 |
commit | a4f9128f94b540fa04b67610eb501cb32ea203b4 (patch) | |
tree | 8ae772674ca1e90b40e9cde9fe5ad6be5d945415 /test | |
parent | e729e012c50eb118be09f6a8005f0c9090fff3a0 (diff) | |
download | binaryen-a4f9128f94b540fa04b67610eb501cb32ea203b4.tar.gz binaryen-a4f9128f94b540fa04b67610eb501cb32ea203b4.tar.bz2 binaryen-a4f9128f94b540fa04b67610eb501cb32ea203b4.zip |
GlobalTypeOptimization: Reorder fields in order to remove them (#6820)
Before, we only removed fields from the end of a struct. If we had, say
struct Foo {
int x;
int y;
int z;
};
// Add no fields but inherit the parent's.
struct Bar : Foo {};
If y is only used in Bar, but never Foo, then we still kept it around, because
if we removed it from Foo we'd end up with Foo = {x, z}, Bar = {x, y, z} which
is invalid - Bar no longer extends Foo. But we can do this if we first reorder
the two:
struct Foo {
int x;
int z;
int y; // now y is at the end
};
struct Bar : Foo {};
And the optimized form is
struct Foo {
int x;
int z;
};
struct Bar : Foo {
int y; // now y is added in Bar
};
This lets us remove all fields possible in all cases AFAIK.
This situation is not super-common, as most fields are actually used both
up and down the hierarchy (if they are used at all), but testing on some
large real-world codebases, I see 10 fields removed in Java, 45 in Kotlin,
and 31 in Dart testcases.
The NFC change to src/wasm-type-ordering.h was needed for this to
compile.
Diffstat (limited to 'test')
-rw-r--r-- | test/lit/passes/gto-removals.wast | 458 |
1 files changed, 443 insertions, 15 deletions
diff --git a/test/lit/passes/gto-removals.wast b/test/lit/passes/gto-removals.wast index c5e148afa..c2e82aee9 100644 --- a/test/lit/passes/gto-removals.wast +++ b/test/lit/passes/gto-removals.wast @@ -637,26 +637,27 @@ ) ) -;; We can remove fields from the end if they are only used in subtypes, because -;; the subtypes can always add fields at the end (and only at the end). +;; We can remove fields if they are only used in subtypes, because we can +;; reorder the fields in the super and re-add them in the sub, appending on top +;; of the now-shorter super. (module ;; CHECK: (rec - ;; CHECK-NEXT: (type $parent (sub (struct (field i32) (field i64)))) + ;; CHECK-NEXT: (type $parent (sub (struct (field i64)))) (type $parent (sub (struct (field i32) (field i64) (field f32) (field f64)))) - ;; CHECK: (type $child (sub $parent (struct (field i32) (field i64) (field f32) (field f64) (field anyref)))) + ;; CHECK: (type $child (sub $parent (struct (field i64) (field i32) (field f32) (field f64) (field anyref)))) (type $child (sub $parent (struct (field i32) (field i64) (field f32) (field f64) (field anyref)))) ;; CHECK: (type $2 (func (param (ref $parent) (ref $child)))) ;; CHECK: (func $func (type $2) (param $x (ref $parent)) (param $y (ref $child)) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $parent 1 + ;; CHECK-NEXT: (struct.get $parent 0 ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $child 0 + ;; CHECK-NEXT: (struct.get $child 1 ;; CHECK-NEXT: (local.get $y) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -678,9 +679,10 @@ ;; CHECK-NEXT: ) (func $func (param $x (ref $parent)) (param $y (ref $child)) ;; The parent has fields 0, 1, 2, 3 and the child adds 4. - ;; Use fields only 1 in the parent, and all the rest in the child. We can - ;; only remove from the end in the child, which means we can remove 2 and 3 - ;; in the parent, but not 0. + ;; Use only field 1 in the parent, and all the rest in the child. We can + ;; reorder field 1 to the start of the parent (flipping its position with + ;; field 0) and then remove all the fields but the now-first. The child + ;; keeps all fields, but is reordered. (drop (struct.get $parent 1 (local.get $x))) (drop (struct.get $child 0 (local.get $y))) (drop (struct.get $child 2 (local.get $y))) @@ -691,31 +693,31 @@ (module ;; CHECK: (rec - ;; CHECK-NEXT: (type $parent (sub (struct (field i32) (field i64) (field (mut f32))))) + ;; CHECK-NEXT: (type $parent (sub (struct (field i64) (field (mut f32))))) (type $parent (sub (struct (field (mut i32)) (field (mut i64)) (field (mut f32)) (field (mut f64))))) - ;; CHECK: (type $child (sub $parent (struct (field i32) (field i64) (field (mut f32)) (field f64) (field anyref)))) + ;; CHECK: (type $child (sub $parent (struct (field i64) (field (mut f32)) (field i32) (field f64) (field anyref)))) (type $child (sub $parent (struct (field (mut i32)) (field (mut i64)) (field (mut f32)) (field (mut f64)) (field (mut anyref))))) ;; CHECK: (type $2 (func (param (ref $parent) (ref $child)))) ;; CHECK: (func $func (type $2) (param $x (ref $parent)) (param $y (ref $child)) - ;; CHECK-NEXT: (struct.set $parent 2 + ;; CHECK-NEXT: (struct.set $parent 1 ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: (f32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $parent 1 + ;; CHECK-NEXT: (struct.get $parent 0 ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $child 0 + ;; CHECK-NEXT: (struct.get $child 2 ;; CHECK-NEXT: (local.get $y) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $child 2 + ;; CHECK-NEXT: (struct.get $child 1 ;; CHECK-NEXT: (local.get $y) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -743,6 +745,54 @@ ) ) +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $parent (sub (struct (field i64) (field (mut f32))))) + (type $parent (sub (struct (field (mut i32)) (field (mut i64)) (field (mut f32)) (field (mut f64))))) + + ;; CHECK: (type $child (sub $parent (struct (field i64) (field (mut f32)) (field i32) (field anyref)))) + (type $child (sub $parent (struct (field (mut i32)) (field (mut i64)) (field (mut f32)) (field (mut f64)) (field (mut anyref))))) + + ;; CHECK: (type $2 (func (param (ref $parent) (ref $child)))) + + ;; CHECK: (func $func (type $2) (param $x (ref $parent)) (param $y (ref $child)) + ;; CHECK-NEXT: (struct.set $parent 1 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $parent 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $child 2 + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $child 1 + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $child 3 + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func (param $x (ref $parent)) (param $y (ref $child)) + ;; As above, but now we remove fields in the child as well: 3 is not used. + (struct.set $parent 2 (local.get $x) (f32.const 0)) + + (drop (struct.get $parent 1 (local.get $x))) + (drop (struct.get $child 0 (local.get $y))) + (drop (struct.get $child 2 (local.get $y))) + ;; the read of 3 was removed here. + (drop (struct.get $child 4 (local.get $y))) + ) +) + ;; A parent with two children, and there are only reads of the parent. Those ;; reads might be of data of either child, of course (as a refernce to the ;; parent might point to them), so we cannot optimize here. @@ -977,3 +1027,381 @@ ) ) ) + +;; A parent with two children, with fields used in various combinations. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct (field i64) (field v128) (field nullref)))) + (type $A (sub (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + + ;; CHECK: (type $C (sub $A (struct (field i64) (field v128) (field nullref) (field f64) (field anyref)))) + (type $C (sub $A (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + + ;; CHECK: (type $B (sub $A (struct (field i64) (field v128) (field nullref) (field f32) (field anyref)))) + (type $B (sub $A (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + ) + + ;; CHECK: (type $3 (func (param anyref))) + + ;; CHECK: (func $func (type $3) (param $x anyref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 3 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 3 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 4 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 4 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 1 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 1 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 2 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 2 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func (param $x anyref) + ;; Field 0 (i32) is used nowhere. + ;; Field 1 (i64) is used only in $A. + ;; Field 2 (f32) is used only in $B. + ;; Field 3 (f64) is used only in $C. + ;; Field 4 (anyref) is used only in $B and $C. + ;; Field 5 (v128) is used only in $A and $C. + ;; Field 6 (nullref) is used only in $A and $B. + ;; As a result: + ;; * A can keep only fields 1, 5, 6 (i64, v128, nullref). + ;; * B keeps A's fields, and appends 2, 4 (f32, anyref). + ;; * C keeps A's fields, and appends 3, 4 (f64, anyref). + + (drop (struct.get $A 1 (ref.cast (ref $A) (local.get $x)))) + + (drop (struct.get $B 2 (ref.cast (ref $B) (local.get $x)))) + + (drop (struct.get $C 3 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $B 4 (ref.cast (ref $B) (local.get $x)))) + (drop (struct.get $C 4 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $A 5 (ref.cast (ref $A) (local.get $x)))) + (drop (struct.get $C 5 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $A 6 (ref.cast (ref $A) (local.get $x)))) + (drop (struct.get $B 6 (ref.cast (ref $B) (local.get $x)))) + ) +) + +;; As above, but instead of $A having children $B, $C, now they are a chain, +;; $A :> $B :> $C +;; $C must now also include $B's fields (specifically field 2, the f32). +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct (field i64) (field v128) (field nullref)))) + (type $A (sub (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + + ;; CHECK: (type $B (sub $A (struct (field i64) (field v128) (field nullref) (field f32) (field anyref)))) + (type $B (sub $A (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + + ;; CHECK: (type $C (sub $B (struct (field i64) (field v128) (field nullref) (field f32) (field anyref) (field f64)))) + (type $C (sub $B (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + ) + + ;; CHECK: (type $3 (func (param anyref))) + + ;; CHECK: (func $func (type $3) (param $x anyref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 3 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 5 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 4 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 4 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 1 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 1 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 2 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 2 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func (param $x anyref) + ;; Same uses as before. + + (drop (struct.get $A 1 (ref.cast (ref $A) (local.get $x)))) + + (drop (struct.get $B 2 (ref.cast (ref $B) (local.get $x)))) + + (drop (struct.get $C 3 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $B 4 (ref.cast (ref $B) (local.get $x)))) + (drop (struct.get $C 4 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $A 5 (ref.cast (ref $A) (local.get $x)))) + (drop (struct.get $C 5 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $A 6 (ref.cast (ref $A) (local.get $x)))) + (drop (struct.get $B 6 (ref.cast (ref $B) (local.get $x)))) + ) +) + +;; The parent $A is an empty struct, with nothing to remove. See we do not error +;; here. +(module + ;; CHECK: (type $A (sub (struct))) + (type $A (sub (struct))) + + ;; CHECK: (type $B (sub $A (struct))) + (type $B (sub $A (struct))) + + ;; CHECK: (type $2 (func (param (ref $B)))) + + ;; CHECK: (func $func (type $2) (param $x (ref $B)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $func (param $x (ref $B)) + ;; Use $B in a param to keep it alive, and lead us to process it and $A. + ) +) + +;; As above, but now $B has fields to remove. +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (struct))) + + ;; CHECK: (type $B (sub $A (struct))) + (type $B (sub $A (struct (field i32) (field i64)))) + + ;; CHECK: (type $2 (func (param (ref $B)))) + + ;; CHECK: (func $func (type $2) (param $x (ref $B)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $func (param $x (ref $B)) + ) +) + +;; As above, but now $B's fields are used. +(module + ;; CHECK: (type $A (sub (struct))) + (type $A (sub (struct))) + + ;; CHECK: (type $B (sub $A (struct (field i32) (field i64)))) + (type $B (sub $A (struct (field i32) (field i64)))) + + ;; CHECK: (type $2 (func (param (ref $B)))) + + ;; CHECK: (func $func (type $2) (param $x (ref $B)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 1 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func (param $x (ref $B)) + (drop (struct.get $B 0 (local.get $x))) + (drop (struct.get $B 1 (local.get $x))) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct (field i32)))) + (type $A (sub (struct (field i32)))) + ;; CHECK: (type $B (sub $A (struct (field i32)))) + (type $B (sub $A (struct (field i32) (field f64)))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $test (type $2) + ;; CHECK-NEXT: (local $x (ref null $A)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 0 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (local $x (ref null $A)) + ;; We cannot remove anything from $A, but we can from $B. That $A is + ;; unchanged should not confuse us. + (drop + (struct.get $A 0 + (local.get $x) + ) + ) + ;; $B reads field 0, but not its new field 1. + (drop + (struct.get $B 0 + (ref.cast (ref $B) + (local.get $x) + ) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (struct (field i32)))) + ;; CHECK: (type $B (sub $A (struct (field i32) (field f64)))) + (type $B (sub $A (struct (field i32) (field f64)))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $test (type $2) + ;; CHECK-NEXT: (local $x (ref null $B)) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (struct.new $B + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (f64.const 3.14159) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 1 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (local $x (ref null $B)) + ;; We can remove everything from $A, but nothing from $B. That $A changes + ;; entirely, and $B changes not at all, should not cause any errors. + (local.set $x + (struct.new $B + (i32.const 42) + (f64.const 3.14159) + ) + ) + (drop + (struct.get $B 0 + (local.get $x) + ) + ) + (drop + (struct.get $B 1 + (local.get $x) + ) + ) + ) +) |