Skip to content

Commit c0bcdd6

Browse files
Support (module definition ...) and (module instance ...) in WAST spec tests (#8058)
* Add support for `(module definition ...)` and `(module instance ...)` in WAST spec tests * [Syntax reference](https://github.com/WebAssembly/spec/blob/main/interpreter/README.md#scripts) * These statements are skipped by the WAST splitter, since wasm-opt can't understand them. In the future, we need some extra work to handle explicit module instantiations correctly here. * This fixes the memory.wast spec test. The remaining 3 that currently depend on this syntax still break for other reasons (mentioned in shared.py) * Remove extra unneeded call to run_spec_test
1 parent 854e01b commit c0bcdd6

File tree

7 files changed

+172
-69
lines changed

7 files changed

+172
-69
lines changed

check.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,6 @@ def check_expected(actual, expected):
221221

222222
check_expected(actual, expected)
223223

224-
run_spec_test(wast)
225-
226224
# check binary format. here we can verify execution of the final
227225
# result, no need for an output verification
228226
actual = ''

scripts/test/shared.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -443,13 +443,14 @@ def get_tests(test_dir, extensions=[], recursive=False):
443443
'imports.wast', # Missing validation of missing function on instantiation
444444
'proposals/threads/imports.wast', # Missing memory type validation on instantiation
445445
'linking.wast', # Missing function type validation on instantiation
446-
'memory.wast', # Requires wast `module definition` support
447446
'proposals/threads/memory.wast', # Missing memory type validation on instantiation
448447
'memory64-imports.wast', # Missing validation on instantiation
449448
'annotations.wast', # String annotations IDs should be allowed
450449
'id.wast', # Empty IDs should be disallowed
451-
'instance.wast', # Requires wast `module definition` support
452-
'table64.wast', # Requires wast `module definition` support
450+
# Requires correct handling of tag imports from different instances of the same module,
451+
# ref.null wast constants, and splitting for module instances
452+
'instance.wast',
453+
'table64.wast', # Requires validations for table size
453454
'table_grow.wast', # Incorrect table linking semantics in interpreter
454455
'tag.wast', # Non-empty tag results allowed by stack switching
455456
'try_table.wast', # Requires try_table interpretation
@@ -472,7 +473,7 @@ def get_tests(test_dir, extensions=[], recursive=False):
472473
'type-rec.wast', # Missing function type validation on instantiation
473474
'type-subtyping.wast', # ShellExternalInterface::callTable does not handle subtyping
474475
'call_indirect.wast', # Bug with 64-bit inline element segment parsing
475-
'memory64.wast', # Requires wast `module definition` support
476+
'memory64.wast', # Requires validations for memory size
476477
'imports0.wast', # Missing memory type validation on instantiation
477478
'imports2.wast', # Missing memory type validation on instantiation
478479
'imports3.wast', # Missing memory type validation on instantiation

scripts/test/support.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,14 @@ def untar(tarfile, outdir):
9090

9191
QUOTED = re.compile(r'\(module\s*(\$\S*)?\s+(quote|binary)')
9292

93+
MODULE_DEFINITION_OR_INSTANCE = re.compile(r'(?m)\(module\s+(instance|definition)')
94+
9395

9496
def split_wast(wastFile):
97+
'''
98+
Returns a list of pairs of module definitions and assertions.
99+
Module invalidity tests, as well as (module definition ...) and (module instance ...) are skipped.
100+
'''
95101
# if it's a binary, leave it as is, we can't split it
96102
wast = None
97103
if not wastFile.endswith('.wasm'):
@@ -128,7 +134,7 @@ def to_end(j):
128134
return j
129135

130136
i = 0
131-
ignoring_quoted = False
137+
ignoring_assertions = False
132138
while i >= 0:
133139
start = wast.find('(', i)
134140
if start >= 0 and wast[start + 1] == ';':
@@ -146,17 +152,17 @@ def to_end(j):
146152
break
147153
i = to_end(start + 1)
148154
chunk = wast[start:i]
149-
if QUOTED.match(chunk):
155+
if QUOTED.match(chunk) or MODULE_DEFINITION_OR_INSTANCE.match(chunk):
150156
# There may be assertions after this quoted module, but we aren't
151157
# returning the module, so we need to skip the assertions as well.
152-
ignoring_quoted = True
158+
ignoring_assertions = True
153159
continue
154160
if chunk.startswith('(module'):
155-
ignoring_quoted = False
161+
ignoring_assertions = False
156162
ret += [(chunk, [])]
157163
elif chunk.startswith('(assert_invalid'):
158164
continue
159-
elif chunk.startswith(('(assert', '(invoke', '(register')) and not ignoring_quoted:
165+
elif chunk.startswith(('(assert', '(invoke', '(register')) and not ignoring_assertions:
160166
# ret may be empty if there are some asserts before the first
161167
# module. in that case these are asserts *without* a module, which
162168
# are valid (they may check something that doesn't refer to a module
@@ -190,14 +196,13 @@ def run_command(cmd, expected_status=0, stderr=None,
190196
out, err = proc.communicate()
191197
code = proc.returncode
192198
if expected_status is not None and code != expected_status:
193-
raise Exception(('run_command failed (%s)' % code, out + str(err or '')))
199+
raise Exception(f"run_command `{' '.join(cmd)}` failed ({code}) {err or ''}")
194200
if expected_err is not None:
195201
if err_ignore is not None:
196202
err = "\n".join([line for line in err.split('\n') if err_ignore not in line])
197203
err_correct = expected_err in err if err_contains else expected_err == err
198204
if not err_correct:
199-
raise Exception(('run_command unexpected stderr',
200-
"expected '%s', actual '%s'" % (expected_err, err)))
205+
raise Exception(f"run_command unexpected stderr. Expected '{expected_err}', actual '{err}'")
201206
return out
202207

203208

src/parser/wast-parser.cpp

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,21 @@ Result<Action> action(Lexer& in) {
8888
return in.err("expected action");
8989
}
9090

91-
// (module id? binary string*)
92-
// (module id? quote string*)
93-
// (module ...)
91+
// (module id binary string*)
92+
// (module id quote string*)
93+
// (module definition id? ...)
94+
// (module definition id? binary ...)
9495
Result<WASTModule> wastModule(Lexer& in, bool maybeInvalid = false) {
9596
Lexer reset = in;
9697
if (!in.takeSExprStart("module"sv)) {
9798
return in.err("expected module");
9899
}
99-
// TODO: use ID?
100-
[[maybe_unused]] auto id = in.takeID();
100+
101+
bool isDefinition = in.takeKeyword("definition"sv);
102+
std::optional<Name> id = in.takeID();
103+
104+
Lexer moduleBody = in;
105+
101106
QuotedModuleType type;
102107
if (in.takeKeyword("quote"sv)) {
103108
type = QuotedModuleType::Text;
@@ -118,15 +123,24 @@ Result<WASTModule> wastModule(Lexer& in, bool maybeInvalid = false) {
118123
}
119124
}
120125
std::string mod(reset.next().substr(0, in.getPos() - reset.getPos()));
121-
return QuotedModule{QuotedModuleType::Text, mod};
126+
return WASTModule{isDefinition, QuotedModule{QuotedModuleType::Text, mod}};
122127
} else {
123-
// This is a normal inline module that should be parseable. Reset to the
124-
// start and parse it normally.
125-
in = std::move(reset);
128+
// In this case the module is mostly valid WAT, unless it is a module
129+
// definition in which case it will begin with (module definition ...)
130+
in = std::move(moduleBody);
131+
126132
auto wasm = std::make_shared<Module>();
133+
if (id) {
134+
wasm->name = *id;
135+
}
136+
127137
wasm->features = FeatureSet::All;
128-
CHECK_ERR(parseModule(*wasm, in));
129-
return wasm;
138+
CHECK_ERR(parseModuleBody(*wasm, in));
139+
if (!in.takeRParen()) {
140+
return in.err("expected end of module");
141+
}
142+
143+
return WASTModule{isDefinition, wasm};
130144
}
131145

132146
// We have a quote or binary module. Collect its contents.
@@ -139,7 +153,7 @@ Result<WASTModule> wastModule(Lexer& in, bool maybeInvalid = false) {
139153
return in.err("expected end of module");
140154
}
141155

142-
return QuotedModule{type, ss.str()};
156+
return WASTModule{isDefinition, QuotedModule{type, ss.str()}};
143157
}
144158

145159
Result<NaNKind> nan(Lexer& in) {
@@ -447,17 +461,42 @@ MaybeResult<Register> register_(Lexer& in) {
447461
return in.err("expected name");
448462
}
449463

450-
// TODO: Do we need to use this optional id?
451-
in.takeID();
464+
auto instanceName = in.takeID();
452465

453466
if (!in.takeRParen()) {
454-
// TODO: handle optional module id.
455467
return in.err("expected end of register command");
456468
}
457-
return Register{*name};
469+
470+
return Register{*name, instanceName};
458471
}
459472

460-
// module | register | action | assertion
473+
// (module instance instance_name? module_name?)
474+
MaybeResult<ModuleInstantiation> instantiation(Lexer& in) {
475+
Lexer reset = in;
476+
if (!in.takeSExprStart("module"sv)) {
477+
std::optional<std::string_view> actual = in.peekKeyword();
478+
return in.err((std::stringstream() << "expected `module` keyword but got "
479+
<< actual.value_or("<not a keyword>"))
480+
.str());
481+
}
482+
483+
if (!in.takeKeyword("instance"sv)) {
484+
// This is not a module instance and probably a module instead.
485+
in = reset;
486+
return {};
487+
}
488+
489+
auto instanceId = in.takeID();
490+
auto moduleId = in.takeID();
491+
492+
if (!in.takeRParen()) {
493+
return in.err("expected end of module instantiation");
494+
}
495+
496+
return ModuleInstantiation{moduleId, instanceId};
497+
}
498+
499+
// instantiate | module | register | action | assertion
461500
Result<WASTCommand> command(Lexer& in) {
462501
if (auto cmd = register_(in)) {
463502
CHECK_ERR(cmd);
@@ -471,9 +510,14 @@ Result<WASTCommand> command(Lexer& in) {
471510
CHECK_ERR(cmd);
472511
return *cmd;
473512
}
474-
auto mod = wastModule(in);
475-
CHECK_ERR(mod);
476-
return *mod;
513+
if (auto cmd = instantiation(in)) {
514+
CHECK_ERR(cmd);
515+
return *cmd;
516+
}
517+
518+
auto module = wastModule(in);
519+
CHECK_ERR(module);
520+
return *module;
477521
}
478522

479523
#pragma GCC diagnostic push
@@ -494,7 +538,8 @@ Result<WASTScript> wast(Lexer& in) {
494538
// No, that wasn't the problem. Return the original error.
495539
return Err{err->msg};
496540
}
497-
cmds.push_back({WASTModule{std::move(wasm)}, line});
541+
cmds.push_back(
542+
{WASTModule{/*isDefinition=*/false, std::move(wasm)}, line});
498543
return cmds;
499544
}
500545
CHECK_ERR(cmd);

src/parser/wat-parser.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ struct QuotedModule {
100100
std::string module;
101101
};
102102

103-
using WASTModule = std::variant<QuotedModule, std::shared_ptr<Module>>;
103+
struct WASTModule {
104+
bool isDefinition = false;
105+
std::variant<QuotedModule, std::shared_ptr<Module>> module;
106+
};
104107

105108
enum class ModuleAssertionType { Trap, Malformed, Invalid, Unlinkable };
106109

@@ -112,10 +115,21 @@ struct AssertModule {
112115
using Assertion = std::variant<AssertReturn, AssertAction, AssertModule>;
113116

114117
struct Register {
118+
// TODO: Rename this to distinguish it from instanceName.
115119
Name name;
120+
std::optional<Name> instanceName = std::nullopt;
121+
};
122+
123+
struct ModuleInstantiation {
124+
// If not specified, instantiate the most recent module definition.
125+
std::optional<Name> moduleName;
126+
127+
// If not specified, use the moduleName
128+
std::optional<Name> instanceName;
116129
};
117130

118-
using WASTCommand = std::variant<WASTModule, Register, Action, Assertion>;
131+
using WASTCommand =
132+
std::variant<WASTModule, Register, Action, Assertion, ModuleInstantiation>;
119133

120134
struct ScriptEntry {
121135
WASTCommand cmd;

0 commit comments

Comments
 (0)