Skip to content

Write init expressions folded so they can be parsed back - #2830

Merged
sbc100 merged 1 commit into
WebAssembly:mainfrom
Nishuuzz:fix/fold-init-exprs
Aug 19, 2026
Merged

Write init expressions folded so they can be parsed back#2830
sbc100 merged 1 commit into
WebAssembly:mainfrom
Nishuuzz:fix/fold-init-exprs

Conversation

@Nishuuzz

Copy link
Copy Markdown
Contributor

wasm2wat produces output it can't read back for modules that use the extended-const proposal.

An init expression with more than one instruction comes out like this:

(global (;1;) (mut i32) (i32.const 44
    i32.const 3
    i32.sub))

which fails to assemble:

error: unexpected token i32.const, expected ).

WriteInitExpr opens a single pair of parentheses around the whole expression list and then writes the instructions unfolded. For the ordinary one-instruction case that gives (i32.const 45) and is fine, but with several instructions the leading ( reads as the start of a folded expression and everything after it is a syntax error. Globals, data offsets and elem offsets are all affected. --fold-exprs doesn't help, because init expressions never went through the folded writer at all.

This writes them with WriteFoldedExprList instead, which emits one folded expression, valid in every position an init expression can appear:

(global (;1;) (mut i32) (i32.sub
      (i32.const 44)
      (i32.const 3)))

Single-instruction init expressions are unaffected — those still print as (i32.const 45), so existing expectations don't move.

How I found it

I ran every .txt under test/ through wat2wasm → wasm2wat → wat2wasm with --enable-all and compared the two binaries. 1102 modules round-tripped byte-identically and three failed to re-assemble, all for this reason: test/dump/extended-const.txt, test/dump/invalid-data-segment-offset.txt and test/parse/module/bad-global-invalid-expr.txt. After the change all 1105 round-trip and the binaries match.

test/dump/extended-const.txt already covers this module, but it's an objdump test, so nothing ever fed the disassembly back to the assembler.

Testing

Added test/roundtrip/extended-const.txt, covering a global, a data offset and an elem offset with extended constant expressions. It fails without the writer change with the parse error above, and passes with it.

run-roundtrip.py had no --enable-extended-const, which is the reason the roundtrip suite couldn't have caught this in the first place, so I added the flag and passed it to both tools alongside the existing ones.

test/roundtrip goes from 92 to 93 passing, and the unit tests are unchanged at 135.

One caveat on my local runs: this is Windows, and a lot of the wider suite can't execute here — Application Control blocks the freshly built tools for several directories, and the wasm2c tests need a cl.exe I don't have. roundtrip, desugar and typecheck run clean, and I checked that no expected output anywhere in test/ contains the old unparseable shape, but I'd rather flag that than imply I ran everything.

WriteInitExpr wrapped the whole expression list in a single pair of
parentheses and then wrote the instructions unfolded. That is fine for
the usual single-instruction init expression, but an extended constant
expression has several instructions, and the result cannot be parsed:

  (global (;1;) (mut i32) (i32.const 44
    i32.const 3
    i32.sub))

wasm2wat produced this for a module using the extended-const proposal, so
its output no longer assembled. Globals, and data and elem offsets, were
all affected, and --fold-exprs made no difference because init
expressions never went through the folded writer.

Write them with WriteFoldedExprList instead, which emits a single folded
expression that is valid in all of those positions:

  (global (;1;) (mut i32) (i32.sub
      (i32.const 44)
      (i32.const 3)))

Single-instruction init expressions are unchanged.

run-roundtrip.py did not accept --enable-extended-const, which is why
this was never covered; add the flag along with a roundtrip test.

@sbc100 sbc100 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.

LGTM, but is there no way to use the flat (non-folded) form with extended const expressions? i.e. do we need to force one form over the other here or can we support both?

@Nishuuzz

Copy link
Copy Markdown
Contributor Author

Yes, flat is possible, but not uniformly, and that's what pushed me towards folding.

Globals take a bare instruction sequence, so (global (mut i32) i32.const 44 i32.const 3 i32.sub) parses fine today. Data and elem offsets don't: (data i32.const 1 i32.const 2 i32.add "x") is rejected, and the flat form only works with the explicit keyword, (data (offset i32.const 1 i32.const 2 i32.add) "x"). Element items are the same, needing (item ...).

So keeping the flat form means teaching the writer to emit (offset ...) and (item ...), which it never does today. Emitting them unconditionally changes every single-instruction offset in the expected output; emitting them only when the expression has more than one instruction is a size-dependent special case. Either is doable, and once the wrappers exist, hanging the choice off --fold-exprs would be easy. I went with always folding because it's valid in all four positions with no new syntax, and it leaves output alone everywhere else: of the 1106 modules in test/ that round-trip, the only disassembly this changes is the three that previously couldn't be re-assembled.

Happy to do it the other way if you'd rather the writer respect --fold-exprs here instead of always folding.

@sbc100

sbc100 commented Aug 19, 2026

Copy link
Copy Markdown
Member

This approach seems like a good improvement as is.

@sbc100
sbc100 merged commit 3ace4bd into WebAssembly:main Aug 19, 2026
17 checks passed
sbc100 pushed a commit that referenced this pull request Aug 21, 2026
`wasm2wat --generate-names` produces output it can't read back when a
module has an export with an empty name.

```wat
(module (func $f) (export "" (func $f)))
```

Disassembled with `--generate-names`:

```wat
(func $ (type $t0))
(export "" (func $))
```

and assembling that back gives `error: empty identifier.`

Generated names are derived from import and export names, built as `"$"`
followed by the name, so an empty export name leaves just the sigil.
This skips an empty name instead, and the item keeps whatever name the
rest of the pass gives it — another of its export names if it has one,
otherwise the index-based name it would have had anyway.

Empty export names are legal and do turn up:
`test/wasm2c/export-names.txt` already has one, which is where I hit
this.

Imports go through the same helper but can't reach it, because the name
there is always `module_name + "." + field_name` and so is never empty.
I put the check in the shared helper anyway rather than in the export
path, since it's guarding a property of the generated identifier rather
than anything specific to exports.

### How I found it

Same round-trip sweep as #2830, extended to the writer variants rather
than just the default one: for every module in `test/`, disassemble with
`--fold-exprs`, `--inline-exports`, `--inline-imports`,
`--generate-names` and `--no-debug-names` in turn, then assemble the
result again. `--generate-names` was 1105 of 1106, and this was the one
failure. It is 1106 now.

### Testing

`test/roundtrip/generate-empty-export-name.txt` covers a func whose only
export name is empty, alongside one with a usable export name so both
paths are visible in the output. It fails without the change with the
`empty identifier` error above.

Note it deliberately does not pass `--debug-names`: with a name section
present the original names survive and the generator never runs, so the
test wouldn't exercise this at all.

`roundtrip` goes from 93 to 94, and `desugar`, `typecheck`, `parse` and
the unit tests are unchanged.

### Unrelated, while I was in there

The same sweep found `--fold-exprs` output failing to re-assemble for
code metadata annotations and for branch hints:

```
test/dump/code-metadata.txt          error: unexpected token "metadata.code.test", expected an instr.
test/parse/branch-hints.txt          error: unexpected token (, expected ).
```

That looks like a separate problem in how annotations are placed in
folded output, so I've left it out of this PR. Happy to open an issue
for it if it isn't already known.
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