Skip to content

Non-nullable locals, option 1a #4824

Closed
@kripken

Description

@kripken

Now that "1a" is the final implementation of non-nullable locals in the spec, we should implement it. That approach does have the downside of extra work for tools like Binaryen, though, so it's not trivial to do. The problem is that a set allows gets only until the end of the current block, which means many natural code transforms "break" 1a, such as:

(local.set $x ..)
(block
  (local.get $x)
)
(local.get $x)

=>

(block
  (local.tee $x ..) ;; set+get => tee here
)
(local.get $x)
(local.set $x ..)
(local.get $x)

=>

(block
  (local.set $x ..)
  ..new code.. ;; new code added here, wrapped in a block
)
(local.get $x)

However we handle this, we need to add fixup logic for 1a, as well as specific workarounds in relevant passes (we don't want to add a local that will end up fixed up later anyhow, if that is worse, which some passes know). Aside from that, some possible approaches:

No 1a validation in Binaryen IR

In this approach binaryen IR allows more things than normal wasm, in particular, non-nullable locals can be used arbitrarily and not just in the 1a form. To emit valid wasm binaries:

  • Fix up 1a during binary writing. That is, when we find a local that does not validate, make it nullable and add ref.as_non_null on its gets (to keep the types of gets identical).
    • Extra ref.as_non_null may be removed, but this is too late to run the pass that does so.
  • Fix up 1a as part of the pipeline, probably pretty late, and with the opt pass to remove ref.as_non_null after it.
    • Just running an opt pass or two may not validate - the user must remember to run the fixup pass.
  • Do both of the above: fix up late in the pass pipeline, and also in binary writing. The binary writing fixups would hopefully never happen if the normal pass pipeline is run.

A downside is that we never validate that we are emitting a valid wasm (again, since we don't validate 1a). Also, intermediate states during the pass pipeline - before we get to the fixups - may not validate as 1a, which can be annoying during debugging.

We'd also need to decide what to do in the text format. Perhaps we wouldn't do fixups there, which would keep it 1:1 with Binaryen IR for testing.

1a validation in Binaryen IR

Here our validator adds 1a checks. Each pass must therefore emit valid 1a. Some options for that:

  • Fix up after each pass we run, automatically, in the pass runner.
  • Passes that might break validation must run a fixup.
    • This adds a few lines to most of our passes.

The extra work is something like 7% slower compilation overall. It's not a huge cost since it's basically 1a validation, which is simple and fast, and the data is in cache already since we just worked on it. Still, it's a noticeable slowdown. (Just on GC builds, though: when GC is off we can avoid any extra work.)

A downside here is that forcing 1a limits what we can do in intermediate steps. In theory allowing more there might help out even if we end up undoing it later (the other effects might remain). On J2Wasm and Dart at least this seems to be negligible, though it does happen.

Thoughts?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions