Skip to content

Fix use child typer in the validator 6613 - #9125

Open
ArkadySkv wants to merge 3 commits into
WebAssembly:mainfrom
ArkadySkv:fix-use-ChildTyper-in-the-validator-6613
Open

ArkadySkv wants to merge 3 commits into
WebAssembly:mainfrom
ArkadySkv:fix-use-ChildTyper-in-the-validator-6613

Conversation

@ArkadySkv

@ArkadySkv ArkadySkv commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Problem: FunctionValidator::visitUnary and visitBinary contain
~400 lines of per-opcode child-type checks expressed as
shouldBeEqual / shouldBeEqualOrFirstIsUnreachable calls. The
same constraints are already encoded in src/ir/child-typer.h and
consumed by the IRBuilder. The duplication is error-prone: a missed
case is a soundness hole. visitRefIsNull and visitRefEq have
the same shape at smaller scale.

Fix (Path A): Add a ValidatorTypeChecker subclass of
ChildTyper<ValidatorTypeChecker> in wasm-validator.cpp. Its
note() callback checks each child's type against the emitted
constraint via PrincipalType::matches, skipping unreachable
children. Replace the switches in visitUnary and visitBinary
with single calls. Migrate the per-child checks in
visitRefIsNull and visitRefEq.

Checks that ChildTyper does not express are kept in the
validator:

  • Feature gates: FP16 on visitUnary and visitBinary; GC and
    reference-types on visitRefIsNull and visitRefEq.
  • Structural: the sibling left->type == right->type check and
    the tuple-arity guard on visitBinary.
  • Cross-child: the sharedness equality on visitRefEq.
    ValidatorTypeChecker calls PrincipalType::matches, which
    builds a fresh VarAssignments per child, so the
    VarSharedness variable that ChildTyper::visitRefEq emits
    does not unify across the two note() calls.

ValidatorTypeChecker gains a no-op noteUnknown(), required for
CRTP instantiation.

Verification:

git show HEAD~3:src/wasm/wasm-validator.cpp \
  | awk '/^void FunctionValidator::visitUnary/{p=1} p{print} p && /^}/{exit}' \
  | grep -oE 'case [A-Za-z0-9_]+:' | sort -u > /tmp/val_unary.txt
sed -n '/void visitUnary(/,/^  }/p' src/ir/child-typer.h \
  | grep -oE 'case [A-Za-z0-9_]+:' | sort -u > /tmp/ct_unary.txt
diff /tmp/val_unary.txt /tmp/ct_unary.txt

git show HEAD~2:src/wasm/wasm-validator.cpp \
  | awk '/^void FunctionValidator::visitBinary/{p=1} p{print} p && /^}/{exit}' \
  | grep -oE 'case [A-Za-z0-9_]+:' | sort -u > /tmp/val_binary.txt
sed -n '/void visitBinary(/,/^  }/p' src/ir/child-typer.h \
  | grep -oE 'case [A-Za-z0-9_]+:' | sort -u > /tmp/ct_binary.txt
diff /tmp/val_binary.txt /tmp/ct_binary.txt

# Coverage diffs must both be empty.
python3 check.py lit

Result: both diffs empty; check.py lit passes with 1002 tests.

New regression tests:

  • test/lit/validation/binary-bad-operand.wast
  • test/lit/validation/ref-is-null-bad-operand.wast
  • test/lit/validation/i31-get-bad-operand.wast (locks in the
    pre-existing visitI31Get rejection message; that method is
    not migrated)

Two migrations were attempted and reverted:

  1. visitI31Get. ChildTyper::visitI31Get emits the unshared
    Type(HeapType::i31, Nullable). The validator's pre-existing
    check uses shouldBeSubTypeIgnoringShared, which accepts
    (ref null (shared i31)). Migrating rejected shared i31 and
    broke basic/shared-i31.wast, passes/make-shared-objects.wast,
    and passes/unsubtyping.wast.

  2. visitCallRef. ChildTyper::visitCallRef notes the target as
    Type(getSignature(), Nullable), which drops sharedness.
    Shared function references were rejected, breaking 25 lit
    tests. The IRBuilder enforces call_ref operand types at
    parse time, so the migrated check was redundant for WAT input.

Diagnostic change: per-opcode messages ("i32 op", "expected v128
operand", etc.) become "child type does not match its constraint",
reported at the failing child rather than the parent.

Follow-up: See #NNNN (ChildTyper sharedness) and #NNNN (complete
the remaining migration). The sharedness issue blocks visitI31Get
and visitCallRef; the completion issue covers the ~70 methods
with ChildTyper counterparts that remain unexamined. The SIMD
and tuple families are deliberately not migrated: their switches
are dominated by immediate validation, memory helpers, and result
type checks.

Refs #6613. Does not close it.

@ArkadySkv
ArkadySkv requested a review from a team as a code owner September 20, 2026 12:01
@ArkadySkv
ArkadySkv requested review from tlively and removed request for a team September 20, 2026 12:01

@tlively tlively left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems promising! Can you:

  1. Rewrite the description to remove extraneous detail.
  2. Measure how this change affects the performance of validation.

Comment on lines +2226 to +2228
// ChildTyper constrains each child to the eqref family, but each note()
// call runs with a fresh VarAssignments, so the sharedness variable does
// not unify across left and right. Keep the cross-child check here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably if we had a utility that gave the full principal type of an expression, we wouldn't have this problem any more. Such a utility would also be useful for e.g. the Outlining pass.

(func (result i32)
(i32.add (i32.const 0) (f64.const 0.0))
)
) No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add newlines at the end of the test files.

Phase 2 of WebAssembly#6613. Add a ValidatorTypeChecker subclass of
ChildTyper<ValidatorTypeChecker> whose note callback validates
each child's type against its constraint via
PrincipalType::matches. Replace the manual per-opcode switch in
FunctionValidator::visitUnary (~200 lines) with a single call.

The none guard and the feature check stay, since neither is a
child-type constraint. Unreachable children are skipped to
preserve the existing shouldBeEqualOrFirstIsUnreachable
semantics.

Coverage diff between the pre-patch visitUnary case labels and
ChildTyper::visitUnary case labels is empty, so no opcode is
lost. The diagnostic changes from opcode-specific messages to
"child type does not match its constraint" at the failing child,
exercised by test/lit/validation/unary-bad-operand.wast.

Partially fixes WebAssembly#6613.
Phase 3 Batch 1 of WebAssembly#6613. Extend ValidatorTypeChecker to visitBinary.
The tuple guard and sibling left->type == right->type check stay in the
validator, since ChildTyper does not express them.

Restore the FP16 feature gates that Phase 2 removed from visitUnary and
that this change would have removed from visitBinary. ChildTyper cannot
express the gate because FP16 ops share their operand types with their
non-FP16 counterparts; the gate is applied via requiresFP16(UnaryOp)
and requiresFP16(BinaryOp) before the ChildTyper check, matching the
pre-WebAssembly#6613 ordering.

Coverage diffs for both methods are empty. Exercised by
test/lit/validation/binary-bad-operand.wast, and by the pre-existing
fp16-unary.wast and fp16.wast, which now pass.

Refs WebAssembly#6613.
Phase 3 Batch 2 of WebAssembly#6613. Extend ValidatorTypeChecker to the two ref
visitors whose ChildTyper constraints are polymorphic over the heap
type and sharedness dimensions, and therefore match the validator's
semantic exactly. Feature gates stay in the validator. The cross-child
shareability check in visitRefEq also stays: ValidatorTypeChecker
calls PrincipalType::matches, which builds a fresh VarAssignments per
child, so the sharedness variable that ChildTyper's visitRefEq emits
does not unify across the two note() calls. Unifying it is a Phase 4
change that requires a persistent VarAssignments member.

visitI31Get is not migrated. ChildTyper::visitI31Get constrains the
argument to the unshared Type(HeapType::i31, Nullable), but the
validator's pre-existing check uses shouldBeSubTypeIgnoringShared,
which accepts (ref null (shared i31)) as well. Migrating would reject
shared i31 modules and regress basic/shared-i31.wast,
passes/make-shared-objects.wast, and passes/unsubtyping.wast. The
discrepancy is the same sharedness gap that the unsubtyping work
tracks; resolving it requires a sharedness-polymorphic constraint
facility in ValidatorTypeChecker, which is Phase 4 scope.

Adds two regression tests:
- test/lit/validation/ref-is-null-bad-operand.wast locks in the
  migrated visitRefIsNull rejection path.
- test/lit/validation/i31-get-bad-operand.wast locks in the
  pre-existing visitI31Get rejection path (a function reference is
  not an i31ref). The fixture uses (ref.func $f) rather than
  (ref.null $struct) because the parser types the latter as nullref,
  which is correctly accepted as an i31ref subtype.

visitTupleExtract, visitTupleMake, visitRefI31, visitRefAs, and the
SIMD family are skipped in this batch. Their switches are dominated
by result-type checks, immediate validation, or feature gates, with
at most one migratable child-type check apiece; migration would add
a cross-file dependency without a code reduction.

Refs WebAssembly#6613.
@ArkadySkv
ArkadySkv force-pushed the fix-use-ChildTyper-in-the-validator-6613 branch from adf4589 to d884472 Compare September 21, 2026 08:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants