How to Fix: Incorrect `ref.test` result
The incorrect ref.test result usually means the runtime is evaluating a reference against a type in a way that does not match the actual WebAssembly GC subtype hierarchy, especially when struct types, anyref, and engine-specific feature support interact. In practice, this appears when a value that should fail or pass a type test returns the opposite result because the module, feature flags, or reference casting assumptions do not align with the engine’s implementation.
Understanding the Root Cause
This issue comes from how ref.test works with heap types and runtime values in the WebAssembly type system. In a module like the one described, you define multiple struct types:
(type $appNode
(struct (field $left anyref)
(field $right anyref)))
(type $comb
(struct (field $asciiTag i32)))
Although both are reference-capable GC types, they are still distinct runtime types. A ref.test instruction should only succeed when the tested reference is compatible with the target type under the engine’s subtype rules.
Why the result becomes incorrect typically falls into one of these buckets:
- The engine has partial or buggy support for the current GC proposal behavior.
- The module is relying on a nominal type distinction, but the surrounding code assumes structural compatibility.
- A value stored as
anyrefis later tested as a more specific type without ensuring the original allocation matches that type. - The issue appears in older syntax or proposal variants where
ref.test,br_on_cast, and related GC instructions changed semantics across implementations.
The key technical point is that anyref is not a guarantee of subtype identity. It only means the value is some reference type accepted by that heap category. When you later perform ref.test, the engine must resolve the actual runtime type, not the field declaration type.
If the result is wrong, the problem is often not your high-level expectation but a mismatch between declared reference generality and runtime type identity, or an implementation bug in a specific engine build.
Step-by-Step Solution
The safest fix is to reduce ambiguity, validate the runtime type path, and test against a known engine version with the correct feature set enabled.
1. Reproduce with a minimal module
Strip the module down so only the relevant type declarations, allocation, and ref.test remain:
(module
(type $appNode
(struct (field $left anyref)
(field $right anyref)))
(type $comb
(struct (field $asciiTag i32)))
(func (export "prob") (result i32)
;; Create a $comb and test whether it is an $appNode
(ref.test (ref $appNode)
(struct.new $comb
(i32.const 65)
)
)
)
)
If this returns success when it should fail, you are likely hitting an engine-level bug or proposal-version mismatch.
2. Confirm the engine supports the expected GC behavior
Make sure you are running a build of your runtime, browser, or toolchain that supports the same WebAssembly GC instruction set used by your module. If you are using a command-line runtime, verify the feature flags in its documentation rather than assuming default support.
Use a current toolchain such as one generated through Binaryen or validated with WABT.
3. Avoid over-generalizing with anyref when precise typing is required
If your logic depends on distinguishing node kinds, prefer explicit type boundaries instead of storing everything as anyref and checking later.
(type $appNode
(struct (field $left (ref null $comb))
(field $right (ref null $comb))))
This narrows the allowed values and reduces opportunities for confusing runtime tests. If you truly need heterogenous storage, document every place where a general reference is reinterpreted.
4. Replace fragile type checks with explicit discriminators when appropriate
If your data model represents variants, a manual tag field is often more reliable than depending entirely on runtime casts across engines:
(type $nodeKind
(struct (field $tag i32)))
(type $comb
(sub $nodeKind
(struct (field $tag i32)
(field $asciiTag i32))))
Then branch on the tag value instead of assuming ref.test is the only classifier. This is especially useful when debugging engine inconsistencies.
5. Cross-check with ref.cast or controlled branching
In many cases, validating the same scenario with related instructions helps isolate whether the bug is in the test itself or in how values are constructed:
(func (export "isAppNode") (param $x anyref) (result i32)
(ref.test (ref $appNode)
(local.get $x)
)
)
Then compare behavior across:
- known
$appNodeallocations - known
$comballocations ref.nullvalues
If only one category behaves incorrectly, the bug is easier to isolate and report.
6. Upgrade or pin the runtime version
If the issue came from a specific engine revision, the practical fix may simply be to upgrade to a newer build where GC subtype checks are corrected. If you need stable CI behavior, pin the version explicitly and add regression tests.
7. Add a regression test
(module
(type $appNode
(struct (field $left anyref)
(field $right anyref)))
(type $comb
(struct (field $asciiTag i32)))
(func (export "testCombIsNotAppNode") (result i32)
(ref.test (ref $appNode)
(struct.new $comb (i32.const 65))
)
)
)
Your expected result should be 0 if a $comb is not a subtype of $appNode. If the engine returns 1, that confirms the issue.
Common Edge Cases
- Null references:
ref.testbehavior differs depending on whether the target type is nullable and how nullability is encoded in the instruction form. - Old proposal syntax: Some examples found online use outdated GC or reference-type syntax that may compile differently or map to older semantics.
- Boxing through general refs: Passing values through
anyrefor equivalent broad reference categories can hide the source type and make debugging harder. - Cross-engine differences: A module may behave one way in a browser experimental build and another way in a standalone runtime.
- Assuming structural typing: WebAssembly GC types are generally treated with nominal typing rules, so similar field layouts do not imply cast compatibility.
- Feature flag drift in CI: Local tests may pass with GC enabled while CI runs with different defaults and exposes the bug.
FAQ
Why does ref.test fail even when two structs look compatible?
Because WebAssembly GC uses type identity and subtype rules, not simple field-shape equivalence. Two structs with similar layouts are still different types unless explicitly related.
Should I use anyref for tree nodes with different runtime kinds?
You can, but it makes later runtime type checks more important and more error-prone. If the set of node kinds is known, explicit subtype hierarchies or tag fields are often easier to reason about.
How do I know whether this is my module or an engine bug?
Create a minimal reproduction, validate it with current tooling, and run it in multiple runtimes. If the same minimal module produces inconsistent results across engines, it strongly suggests an implementation issue rather than a modeling error.
In short, fixing an incorrect ref.test result means tightening your type model, verifying the exact GC proposal support in the runtime, and adding a minimal regression test that proves whether a value like $comb is being incorrectly accepted as $appNode. That approach both resolves application-level confusion and gives you a solid artifact for upstream bug reporting.