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
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