Skip to content

Add support for non-nullable tables and init expressions#8405

Open
pufferfish101007 wants to merge 28 commits intoWebAssembly:mainfrom
pufferfish101007:non-nullable-table
Open

Add support for non-nullable tables and init expressions#8405
pufferfish101007 wants to merge 28 commits intoWebAssembly:mainfrom
pufferfish101007:non-nullable-table

Conversation

@pufferfish101007
Copy link

@pufferfish101007 pufferfish101007 commented Mar 1, 2026

Resolves #5628

Adds support for non-nullable tables with init expressions. I'm not totally sure how I should go about adding new tests, but manual test cases are below. I have also unignored previously ignored spec tests instance.wast, ref_is_null.wast, table.wast, i31.wast, global.wast which now all pass. I have also changed the reason for ignoring array.wast as it does not (and has not in the past, AFAICT) rely on non-nullable table types.

This introduces a breaking change in the C api, as it adds an extra argument to addTable. Is this ok? Should a new method be added instead? (e.g. addTableWithInit?)

Manual tests

click to expand The validation ones might overlap with the spec tests. Unchecked boxes indicate tests that currently fail.
  • Nullable tables should continue to round-trip correctly:
(module
 (type $0 (func (param i32)))
 (table $0 0 funcref)
 (elem $0 (i32.const 0))
 (func $0 (param $0 i32)
 )
)
  • Non-nullable tables with init expr should round-trip correctly:
(module
 (type $0 (func (param i32)))
 (table $0 0 (ref $0) (ref.func $0))
 (func $0 (param $0 i32)
 )
)
  • Non-nullable table without an init expr should fail validation:
(module
 (type $0 (func (param i32)))
 (table $0 0 (ref $0))
 (func $0 (param $0 i32)
 )
)

Correctly fails with [wasm-validator error in module] unexpected false: tables with non-nullable types require an initializer expression, on table

  • Nullable table with init expr should rountrip correctly:
(module
 (type $0 (func (param i32)))
 (table $0 0 funcref (ref.func $0))
 (func $0 (type $0) (param $0 i32)
 )
)
  • Non-nullable table init expr should be validated to be a subtype of the table type
(module
 (type $0 (func (param i32)))
 (table $0 0 (ref $0) (i32.const 0))
 (func $0 (param $0 i32)
 )
)

Correctly fails with [wasm-validator error in module] init expression must be a subtype of the table type, on (i32.const 0)

  • Nullable table init expr (if present) should be validated to be a subtype of the table type
(module
 (type $0 (func (param i32)))
 (table $0 0 funcref (i32.const 0))
 (func $0 (param $0 i32)
 )
)

Correctly fails with [wasm-validator error in module] init expression must be a subtype of the table type, on (i32.const 0)

  • wasm-opt doesn't remove init expr as dead code
(module
 (type $0 (func (param i32)))
 (table $0 0 funcref (ref.func $0))
 (export "table" (table $0))
 (func $0 (type $0) (param $0 i32)
 )
)

TODO:

  • Address review comments
  • Add test that imported globals can be referenced in the init expr

@stevenfontanella
Copy link
Member

I have also unignored previously ignored spec tests instance.wast, ref_is_null.wast, table.wast, i31.wast, which now all pass. I have also changed the reason for ignoring array.wast as it does not (and has not in the past, AFAICT) rely on non-nullable table types.

I don't see these changes in shared.py, can we add them?

@tlively tlively self-requested a review March 2, 2026 18:58
pufferfish101007 and others added 8 commits March 3, 2026 22:40
Uses American spelling and clarify that the breaking change is only in the C API

Co-authored-by: Steven Fontanella <steven.fontanella@gmail.com>
This reverts commit a33eae8.

Reason for revert: storing 0x40 as a uint32_t breaks comparison with the
int32_t
Might not work... pushing so CI can check
Also update expected elem section output due to the module now having
two tables.
@stevenfontanella stevenfontanella self-requested a review March 4, 2026 18:25
@stevenfontanella
Copy link
Member

stevenfontanella commented Mar 4, 2026

Looks good from my side. I'll let @tlively have the final review. Thanks for the contribution!

@pufferfish101007
Copy link
Author

@stevenfontanella thank you for such a thorough review!

Copy link
Member

@tlively tlively left a comment

Choose a reason for hiding this comment

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

Great! A few additional comments:

To test all the text and binary round tripping you mention, please add a new test in test/lit/basic.

Off the top of my head, src/ir/subtype-exprs.h will also need updating, ideally with a new test in test/lit/passes for the unsubtyping pass, which depends on it.

There may be other utilities or passes that need updating, but we won't know until we run the fuzzer with table initializers. There are a couple paths forward for this:

  1. We could defer fuzzer support (and further fixes) until later. If you prefer this, you'll probably have to add new test files to the "unfuzzable" list in scripts/test/fuzzing.py.

  2. We could add fuzzer support for generating table initializers now in src/tools/fuzzing/fuzzing.cpp, and also update any utilities or passes that the fuzzer finds problems with.

Either way, it would be good to run the fuzzer via scripts/fuzz_opt.py for at least 10k iterations without problems before landing this.

Comment on lines +549 to +551
if (table->hasInit()) {
use(table->init);
}
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a test in test/lit/passes that tests this?

Comment on lines +4744 to +4746
if (!table->type.isNullable()) {
info.shouldBeTrue(
table->init != nullptr,
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't look correct for imported tables, which should not have init expressions. Let's fix that and add tests for it.

@pufferfish101007
Copy link
Author

Thanks for the additional comments! I've addressed the easy fixes and probably won't have time to address the others for a week or so (but I'll try to do so as soon as possible to keep momentum going).

I will aim to add fuzzer support now, so that this doesn't come back to bite me later on.

A question:
For lit tests, I notice that each lit test says at the top ;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited., and I'm not entirely clear what this actually means - are there some parts of lit tests that are generated, and some that are written manually? What do I need to write manually? Sorry if this is documented somewhere that I haven't found!

@tlively
Copy link
Member

tlively commented Mar 5, 2026

The way the lit tests work is that you write the module (or modules) and the RUN lines at the top saying how the test should execute. The filecheck command in those run lines compares its input against the expected output in the CHECK comments in the file. (There may be other check prefixes as well.) After you write the RUN lines and the input, the script can generate the expected output in the CHECK lines. You can put multiple input modules in the same wast file if the RUN line uses the foreach tool.

I recommend copying the RUN lines and general structure from other recent tests in the same directory. You can run specific new tests with bin/binaryen-lit -vv path/to/test.wast.

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.

Table init exprs appear to be unimplemented

4 participants