Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4769702
allow non-nullable ref types + init exprs in tables
pufferfish101007 Apr 19, 2025
1855e6c
add support for nullable tables & init expressions in parser + interp…
pufferfish101007 Feb 28, 2026
6a09536
properly initialise table init when parsing WAT
pufferfish101007 Feb 28, 2026
0cdd750
only parse table init if it exists
pufferfish101007 Mar 1, 2026
7ef1502
consider that nullable tables might have an init expr
pufferfish101007 Mar 1, 2026
bbd1d4e
add missing argument to addTable in C api tests
pufferfish101007 Mar 1, 2026
41fc398
allow non-nullable types in element segments
pufferfish101007 Mar 1, 2026
a1f5781
unignore some spec tests that now pass
pufferfish101007 Mar 1, 2026
dc47c17
revert testsuite submodule update
pufferfish101007 Mar 2, 2026
8024e79
format code
pufferfish101007 Mar 2, 2026
9264c1e
check if table init exists before traversing it
pufferfish101007 Mar 3, 2026
92ee421
check for GC if there is a table init, rather than vice verse
pufferfish101007 Mar 3, 2026
6b053c0
add changelog entry for non-nullable tables / table init exprs
pufferfish101007 Mar 3, 2026
8bffe07
Update CHANGELOG.md
pufferfish101007 Mar 3, 2026
a33eae8
introduce named constants for table encoding with init expr
pufferfish101007 Mar 3, 2026
9f2f70d
Revert "introduce named constants for table encoding with init expr"
pufferfish101007 Mar 4, 2026
fdab5ff
use named constants for table initializer encoding
pufferfish101007 Mar 4, 2026
c12800b
add C API kitchen sink test for table init expr
pufferfish101007 Mar 4, 2026
4ab3fb9
Add JS kitchen sink test
pufferfish101007 Mar 4, 2026
1009d7b
format code
pufferfish101007 Mar 4, 2026
5dda1dd
Update JS kitchen sink test to use correct API
pufferfish101007 Mar 4, 2026
4bafe31
update reason for ignoring global.wast spec test
pufferfish101007 Mar 5, 2026
4c149b4
Use `getType()` rather than getting LEB and then decoding
pufferfish101007 Mar 5, 2026
eeea874
remove `table->hasInit()` in preference of directly checking `init`
pufferfish101007 Mar 5, 2026
6c7dd7d
walk table init expr in `walkModuleCode`
pufferfish101007 Mar 5, 2026
d98caed
fix typo... globals -> tables
pufferfish101007 Mar 5, 2026
e8ed315
check that table init exprs only reference imported globals
pufferfish101007 Mar 6, 2026
26b850e
format code
pufferfish101007 Mar 6, 2026
a333fd5
Merge branch 'main' of https://github.com/WebAssembly/binaryen into n…
pufferfish101007 Mar 8, 2026
07257fe
add roundtrip and validation tests
pufferfish101007 Mar 8, 2026
2b35b2d
visit table init expr in SubtypingDiscoverer
pufferfish101007 Mar 8, 2026
46c733f
add test that table init exprs are walked in unused module elements pass
pufferfish101007 Mar 8, 2026
d0aa3ab
visit tables in unsubtyping pass; add test for table init unsubtyping
pufferfish101007 Mar 13, 2026
808c978
check that imported tables do not have init expr
pufferfish101007 Mar 16, 2026
6507fa6
add rudimentary support for table init exprs in fuzzing
pufferfish101007 Mar 16, 2026
ab9e004
format
pufferfish101007 Mar 16, 2026
b0f8f3e
RemoveUnusedModuleElements: reference table init expr
pufferfish101007 Mar 17, 2026
4efbaae
Merge branch 'main' of https://github.com/WebAssembly/binaryen into n…
pufferfish101007 Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ v127
and `setValueI64`, previously took a hi/low pair but now take a single value
which can be bigint or a number. Passing two values to these APIs will now
trigger an assertion. (#7984)
- Add support for non-nullable table types and initialization expressions for
tables. This comes with a breaking change to C API: `BinaryenAddTable` takes
an additional `BinaryenExpressionRef` parameter to provide an initialization
expression. This may be set to NULL for tables without an initializer. In JS
this parameter is optional and so is not breaking. (#8405)

v126
----
Expand Down
7 changes: 1 addition & 6 deletions scripts/test/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,29 +419,24 @@ def get_tests(test_dir, extensions=[], recursive=False):
'data.wast', # Fail to parse data segment offset abbreviation
'elem.wast', # Requires modeling empty declarative segments
'func.wast', # Duplicate parameter names not properly rejected
'global.wast', # Fail to parse table
'if.wast', # Requires more precise unreachable validation
'imports.wast', # Requires fixing handling of mutation to imported globals
'proposals/threads/imports.wast', # Missing memory type validation on instantiation
'linking.wast', # Missing function type validation on instantiation
'proposals/threads/memory.wast', # Missing memory type validation on instantiation
'annotations.wast', # String annotations IDs should be allowed
'instance.wast', # Requires support for table default elements
'table64.wast', # Requires validations for table size
'tag.wast', # Non-empty tag results allowed by stack switching
'local_init.wast', # Requires local validation to respect unnamed blocks
'ref_func.wast', # Requires rejecting undeclared functions references
'ref_is_null.wast', # Requires support for non-nullable reference types in tables
'return_call_indirect.wast', # Requires more precise unreachable validation
'select.wast', # Missing validation of type annotation on select
'table.wast', # Requires support for table default elements
'unreached-invalid.wast', # Requires more precise unreachable validation
'array.wast', # Requires support for table default elements
'array.wast', # Failure to parse element segment item abbreviation
'br_if.wast', # Requires more precise branch validation
'br_on_cast.wast', # Requires host references to not be externalized i31refs
'br_on_cast_fail.wast', # Requires host references to not be externalized i31refs
'extern.wast', # Requires ref.host wast constants
'i31.wast', # Requires support for table default elements
'ref_cast.wast', # Requires host references to not be externalized i31refs
'ref_test.wast', # Requires host references to not be externalized i31refs
'struct.wast', # Fails to roundtrip unnamed types e.g. `(ref 0)`
Expand Down
4 changes: 3 additions & 1 deletion src/binaryen-c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5336,8 +5336,10 @@ BinaryenTableRef BinaryenAddTable(BinaryenModuleRef module,
const char* name,
BinaryenIndex initial,
BinaryenIndex maximum,
BinaryenType tableType) {
BinaryenType tableType,
BinaryenExpressionRef init) {
auto table = Builder::makeTable(name, Type(tableType), initial, maximum);
table->init = init;
table->hasExplicitName = true;
return ((Module*)module)->addTable(std::move(table));
}
Expand Down
3 changes: 2 additions & 1 deletion src/binaryen-c.h
Original file line number Diff line number Diff line change
Expand Up @@ -2932,7 +2932,8 @@ BINARYEN_API BinaryenTableRef BinaryenAddTable(BinaryenModuleRef module,
const char* table,
BinaryenIndex initial,
BinaryenIndex maximum,
BinaryenType tableType);
BinaryenType tableType,
BinaryenExpressionRef init);
BINARYEN_API void BinaryenRemoveTable(BinaryenModuleRef module,
const char* table);
BINARYEN_API BinaryenIndex BinaryenGetNumTables(BinaryenModuleRef module);
Expand Down
5 changes: 5 additions & 0 deletions src/ir/subtype-exprs.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ struct SubtypingDiscoverer : public OverriddenVisitor<SubType> {
self()->noteSubtype(global->init, global->type);
}
}
void visitTable(Table* table) {
if (table->init) {
self()->noteSubtype(table->init, table->type);
}
}
void visitElementSegment(ElementSegment* seg) {
if (seg->offset) {
self()->noteSubtype(seg->type,
Expand Down
4 changes: 2 additions & 2 deletions src/js/binaryen.js-post.js
Original file line number Diff line number Diff line change
Expand Up @@ -2613,8 +2613,8 @@ function wrapModule(module, self = {}) {
self['getGlobal'] = function(name) {
return preserveStack(() => Module['_BinaryenGetGlobal'](module, strToStack(name)));
};
self['addTable'] = function(table, initial, maximum, type = Module['_BinaryenTypeFuncref']()) {
return preserveStack(() => Module['_BinaryenAddTable'](module, strToStack(table), initial, maximum, type));
self['addTable'] = function(table, initial, maximum, type = Module['_BinaryenTypeFuncref'](), init = null) {
return preserveStack(() => Module['_BinaryenAddTable'](module, strToStack(table), initial, maximum, type, init));
}
self['getTable'] = function(name) {
return preserveStack(() => Module['_BinaryenGetTable'](module, strToStack(name)));
Expand Down
1 change: 1 addition & 0 deletions src/parser/context-decls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Result<> ParseDeclsCtx::addTable(Name name,
const std::vector<Name>& exports,
ImportNames* import,
TableType type,
std::optional<ExprT>,
Index pos) {
CHECK_ERR(checkImport(pos, import));
auto t = addTableDecl(pos, name, import, type);
Expand Down
12 changes: 12 additions & 0 deletions src/parser/context-defs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ Result<> ParseDefsCtx::addGlobal(Name,
return Ok{};
}

Result<> ParseDefsCtx::addTable(Name,
const std::vector<Name>&,
ImportNames*,
TableTypeT,
std::optional<ExprT> init,
Index) {
if (init) {
wasm.tables[index]->init = *init;
}
return Ok{};
}

Result<> ParseDefsCtx::addImplicitElems(Type,
std::vector<Expression*>&& elems) {
auto& e = wasm.elementSegments[implicitElemIndices.at(index)];
Expand Down
26 changes: 18 additions & 8 deletions src/parser/contexts.h
Original file line number Diff line number Diff line change
Expand Up @@ -1120,8 +1120,12 @@ struct ParseDeclsCtx : NullTypeParserCtx, NullInstrParserCtx {
Name name,
ImportNames* importNames,
TableType limits);
Result<>
addTable(Name, const std::vector<Name>&, ImportNames*, TableType, Index);
Result<> addTable(Name,
const std::vector<Name>&,
ImportNames*,
TableType,
std::optional<ExprT>,
Index);

// TODO: Record index of implicit elem for use when parsing types and instrs.
Result<> addImplicitElems(TypeT, ElemListT&& elems);
Expand Down Expand Up @@ -1512,8 +1516,12 @@ struct ParseModuleTypesCtx : TypeParserCtx<ParseModuleTypesCtx>,
return Ok{};
}

Result<> addTable(
Name, const std::vector<Name>&, ImportNames*, Type ttype, Index pos) {
Result<> addTable(Name,
const std::vector<Name>&,
ImportNames*,
Type ttype,
std::optional<ExprT> init,
Index pos) {
auto& t = wasm.tables[index];
if (!ttype.isRef()) {
return in.err(pos, "expected reference type");
Expand Down Expand Up @@ -1877,10 +1885,12 @@ struct ParseDefsCtx : TypeParserCtx<ParseDefsCtx>, AnnotationParserCtx {
return Ok{};
}

Result<>
addTable(Name, const std::vector<Name>&, ImportNames*, TableTypeT, Index) {
return Ok{};
}
Result<> addTable(Name,
const std::vector<Name>&,
ImportNames*,
TableTypeT,
std::optional<ExprT>,
Index);

Result<>
addMemory(Name, const std::vector<Name>&, ImportNames*, TableTypeT, Index) {
Expand Down
18 changes: 15 additions & 3 deletions src/parser/parsers.h
Original file line number Diff line number Diff line change
Expand Up @@ -3411,7 +3411,8 @@ template<typename Ctx> MaybeResult<> import_(Ctx& ctx) {
auto name = ctx.in.takeID();
auto type = tabletype(ctx);
CHECK_ERR(type);
CHECK_ERR(ctx.addTable(name ? *name : Name{}, {}, &names, *type, pos));
CHECK_ERR(ctx.addTable(
name ? *name : Name{}, {}, &names, *type, std::nullopt, pos));
} else if (ctx.in.takeSExprStart("memory"sv)) {
auto name = ctx.in.takeID();
auto type = memtype(ctx);
Expand Down Expand Up @@ -3504,7 +3505,11 @@ template<typename Ctx> MaybeResult<> func(Ctx& ctx) {
}

// table ::= '(' 'table' id? ('(' 'export' name ')')*
// '(' 'import' mod:name nm:name ')'? index_type? tabletype ')'
// index_type? tabletype expr?
// ')'
// | '(' 'table' id? ('(' 'export' name ')')*
// '(' 'import' mod:name nm:name ')' index_type? tabletype
// ')'
// | '(' 'table' id? ('(' 'export' name ')')* index_type?
// reftype '(' 'elem' (elemexpr* | funcidx*) ')' ')'
template<typename Ctx> MaybeResult<> table(Ctx& ctx) {
Expand Down Expand Up @@ -3537,6 +3542,7 @@ template<typename Ctx> MaybeResult<> table(Ctx& ctx) {

std::optional<typename Ctx::TableTypeT> ttype;
std::optional<typename Ctx::ElemListT> elems;
std::optional<typename Ctx::ExprT> init;
if (type) {
// We should have inline elements.
if (!ctx.in.takeSExprStart("elem"sv)) {
Expand Down Expand Up @@ -3571,13 +3577,19 @@ template<typename Ctx> MaybeResult<> table(Ctx& ctx) {
auto tabtype = tabletypeContinued(ctx, addressType);
CHECK_ERR(tabtype);
ttype = *tabtype;
if (ctx.in.peekLParen() && !import) {
// imported tables cannot have initialization expression
auto e = expr(ctx);
CHECK_ERR(e);
init = *e;
}
}

if (!ctx.in.takeRParen()) {
return ctx.in.err("expected end of table declaration");
}

CHECK_ERR(ctx.addTable(name, *exports, import.getPtr(), *ttype, pos));
CHECK_ERR(ctx.addTable(name, *exports, import.getPtr(), *ttype, init, pos));

if (elems) {
CHECK_ERR(ctx.addImplicitElems(*type, std::move(*elems)));
Expand Down
7 changes: 6 additions & 1 deletion src/passes/Print.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3409,7 +3409,12 @@ void PrintSExpression::printTableHeader(Table* curr) {
o << ' ' << curr->max;
}
o << ' ';
printType(curr->type) << ')';
printType(curr->type);
if (curr->init) {
o << ' ';
visit(curr->init);
}
o << ')';
}

void PrintSExpression::visitTable(Table* curr) {
Expand Down
14 changes: 13 additions & 1 deletion src/passes/RemoveUnusedModuleElements.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -538,14 +538,19 @@ struct Analyzer {
}
});
break;
case ModuleElementKind::Table:
case ModuleElementKind::Table: {
ModuleUtils::iterTableSegments(
*module, value, [&](ElementSegment* segment) {
if (!segment->data.empty()) {
use({ModuleElementKind::ElementSegment, segment->name});
}
});
auto* table = module->getTable(value);
if (table->init) {
use(table->init);
}
break;
}
case ModuleElementKind::DataSegment: {
auto* segment = module->getDataSegment(value);
if (segment->offset) {
Expand Down Expand Up @@ -759,6 +764,13 @@ struct Analyzer {
for (auto* item : segment->data) {
addReferences(item);
}
} else if (kind == ModuleElementKind::Table) {
auto* table = module->getTable(value);
if (table->init) {
// TODO: might be possible to remove the init expression if the type is
// nullable
addReferences(table->init);
}
}
}
};
Expand Down
3 changes: 3 additions & 0 deletions src/passes/Unsubtyping.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,9 @@ struct Unsubtyping : Pass, Noter<Unsubtyping> {
for (auto& global : wasm.globals) {
collector.visitGlobal(global.get());
}
for (auto& table : wasm.tables) {
collector.visitTable(table.get());
}
for (auto& segment : wasm.elementSegments) {
collector.visitElementSegment(segment.get());
}
Expand Down
30 changes: 29 additions & 1 deletion src/tools/fuzzing/fuzzing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -619,12 +619,17 @@ void TranslateToFuzzReader::setupTables() {
if (wasm.features.hasMemory64() && oneIn(2)) {
addressType = Type::i64;
}
Expression* init = nullptr;
if (wasm.features.hasGC() && oneIn(2)) {
init = builder.makeConstantExpression(Literal::makeNull(HeapType::func));
}
auto tablePtr =
builder.makeTable(Names::getValidTableName(wasm, "fuzzing_table"),
funcref,
initial,
max,
addressType);
addressType,
init);
tablePtr->hasExplicitName = true;
table = wasm.addTable(std::move(tablePtr));
}
Expand Down Expand Up @@ -928,6 +933,22 @@ void TranslateToFuzzReader::finalizeTable() {
table->initial = std::max(table->initial, maxOffset);
});

if (table->init) {
bool hasNonImported = false;
for ([[maybe_unused]] auto* get : FindAll<GlobalGet>(table->init).list) {
if (!wasm.getGlobal(get->name)->imported()) {
hasNonImported = true;
break;
}
}
if (hasNonImported) {
// table initializers can't reference module-defined globals.
// TODO: use makeConst if it can be made to work without crashing?
table->type = table->type.with(Nullable);
table->init = builder.makeRefNull(table->type.getHeapType());
}
}

// The code above raises table->initial to a size large enough to accomodate
// all of its segments, with the intention of avoiding a trap during
// startup. However a single segment of (say) size 4GB would have a table of
Expand All @@ -949,6 +970,13 @@ void TranslateToFuzzReader::finalizeTable() {
if (!preserveImportsAndExports) {
// Avoid an imported table (which the fuzz harness would need to handle).
table->module = table->base = Name();
if (table->type.isNonNullable()) {
// imported tables can have nullable types without an initializer,
// but this is not the case for module-defined tables, so make it
// nullable.
// TODO: use makeConst to provide an initializer of the correct type?
table->type = table->type.with(Nullable);
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/wasm-binary.h
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,9 @@ constexpr uint32_t ExactImport = 1 << 5;
constexpr uint32_t HasMemoryOrderMask = 1 << 5;
constexpr uint32_t HasMemoryIndexMask = 1 << 6;

constexpr uint8_t HasTableInitializer = 0x40;
constexpr uint8_t TableReservedByte = 0x00;

enum EncodedType {
// value types
i32 = -0x1, // 0x7f
Expand Down
4 changes: 3 additions & 1 deletion src/wasm-builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@ class Builder {
Nullable),
Address initial = 0,
Address max = Table::kMaxSize,
Type addressType = Type::i32) {
Type addressType = Type::i32,
Expression* init = nullptr) {
auto table = std::make_unique<Table>();
table->name = name;
table->type = type;
table->addressType = addressType;
table->initial = initial;
table->max = max;
table->init = init;
return table;
}

Expand Down
15 changes: 10 additions & 5 deletions src/wasm-interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -3559,12 +3559,17 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
// parsing/validation checked this already.
assert(inserted && "Unexpected repeated table name");
} else {
assert(table->type.isNullable() &&
"We only support nullable tables today");

auto null = Literal::makeNull(table->type.getHeapType());
Literal initVal;
if (table->init) {
initVal =
ExpressionRunner<SubType>::visit(table->init).getSingleValue();
} else {
assert(table->type.isNullable() &&
"Non-nullable table must have an init expressions");
initVal = Literal::makeNull(table->type.getHeapType());
}
auto& runtimeTable =
definedTables.emplace_back(createTable(null, *table));
definedTables.emplace_back(createTable(initVal, *table));
[[maybe_unused]] auto [_, inserted] =
allTables.try_emplace(table->name, runtimeTable.get());
assert(inserted && "Unexpected repeated table name");
Expand Down
Loading