Skip to content

proposal: use target-specific defer continuation encodings #2276

Description

@cpunion

Motivation

LLGo must preserve two function-local continuations while executing deferred calls:

  • runtime.Defer.Rund resumes after RunDefers.
  • runtime.Defer.Reth resumes panic/rethrow processing, including a panic raised while draining loop defers.

Native code currently represents each continuation as an LLVM blockaddress and resumes it with indirectbr. This is valid LLVM IR: the block address is carried as an opaque pointer and reconstructed for the indirect branch.

WebAssembly is different. Its structured control flow does not expose function-local basic blocks as code pointers and does not provide an arbitrary computed goto. LLVM's WebAssembly SelectionDAG explicitly rejects residual BlockAddress and BRIND operations with "WebAssembly hasn't implemented computed gotos".

LLVM runs IndirectBrExpandPass for WebAssembly and attempts to rewrite blockaddress/indirectbr into integer selectors and a switch. However, that is a late target fallback rather than an LLGo contract. With LLVM 19, the affected LLGo runtime IR deterministically reaches WebAssembly instruction selection with the computed-goto form still present and crashes while compiling runtime.EnsureLocalInitializer. This is a compiler crash before any WebAssembly module is produced, not a crash in the generated program and not a flaky build. Correctness should not depend on a target rescue pass eliminating every continuation value.

LLVM version status

LLVM 22.1.8 does not add native WebAssembly computed-goto support:

  • WebAssemblyISelLowering still marks BlockAddress and BRIND for custom lowering and rejects them as unimplemented computed gotos.
  • WebAssemblyTargetMachine still schedules IndirectBrExpandPass before instruction selection.
  • The relevant IndirectBrExpandPass behavior is materially unchanged from LLVM 19.1.7.

A particular LLVM 19 crash may move or disappear as unrelated optimizer behavior changes, but LLVM 22 retains the same architectural limitation and late-pass dependency. LLGo should therefore keep an explicit WebAssembly continuation encoding rather than version-gate correctness on LLVM.

Relevant upstream sources:

Proposal

Represent a compiler-internal defer continuation as:

deferTarget {
    index uintptr
    block BasicBlock
}

Choose its emitted encoding by target capability:

  • Targets with native computed-goto support store block.Addr() and resume with indirectbr.
  • WebAssembly stores a dense integer selector in the existing pointer-sized slot and resumes with an LLVM switch whose destinations are explicit CFG edges. The invalid default is unreachable.

The selector mappings are local to each dispatch:

  • Rund: selector 0 is the terminal rethrow target; later RunDefers continuations use 1, 2, and so on.
  • Reth: selector 0 is procBlk, selector 1 is terminal rethrow, and 2 onward identify intermediate defer continuations.

Loop-defer draining must store the current Reth continuation before invoking a deferred call so a nested panic resumes the correct drainer continuation.

Why Native keeps blockaddress

This proposal does not convert every target to selectors.

Native LLVM backends can lower blockaddress plus indirectbr directly to a label address and a register-indirect branch. Keeping that representation:

  • preserves the existing Native CFG, ABI behavior, and generated-code strategy;
  • avoids adding selector materialization, pointer/integer conversion, validation/default edges, and a switch;
  • avoids forcing a compare chain or jump table for a problem the target already implements directly;
  • limits the portability workaround to targets that require it, reducing codegen and performance risk.

A selector may occasionally optimize well on a Native target, but that is not sufficient reason to change all existing Native defer dispatch. Such a change should be evaluated separately with target-specific codegen and benchmarks.

The decision is based on target capability, not operating system. WebAssembly is the current selector target; another architecture should opt in only if its backend cannot reliably lower computed gotos.

Runtime ABI

No runtime layout change is required.

runtime.Defer.Rund and runtime.Defer.Reth remain unsafe.Pointer fields with unchanged order, size, and offsets:

  • Native stores an opaque block address.
  • WebAssembly stores a pointer-shaped selector and converts it back to uintptr only at dispatch.

The values are compiler/runtime-internal and are never interpreted as ordinary data pointers.

Alternatives considered

Rely on LLVM's late IndirectBrExpandPass

Rejected as the correctness boundary. The pass is useful as a backend fallback, but LLVM 19 failures show that LLGo cannot assume every emitted defer continuation will be rewritten before WebAssembly instruction selection. LLVM 22 retains the same fallback architecture.

Use selectors on every target

Rejected for this change. It would alter working Native IR and may add dispatch instructions or code size without solving a Native correctness problem.

Change runtime.Defer fields to integers

Rejected because it would create a target-dependent runtime layout or require an ABI migration. Keeping pointer-sized slots supports both encodings.

Convert only Rund

Rejected because Reth also carries a continuation. Panic and loop-defer-drain paths would retain the same unsupported WebAssembly computed-goto form.

Validation

The implementation must cover:

  • Native amd64 and arm64 IR retaining blockaddress and indirectbr, with no selector switch;
  • direct GOOS/GOARCH WebAssembly and named -target=wasm IR containing dense switch i32 dispatches and no blockaddress or indirectbr;
  • both Rund and Reth, including no-defer initialization and multiple continuations;
  • panic during loop-defer draining;
  • unchanged runtime.Defer field types and layout;
  • js/wasm and wasip1/wasm build and module validation;
  • O0, optimized, and LTO paths used by the supported WebAssembly pipeline.

Scope

This proposal defines defer continuation encoding only. It does not add general computed-goto support to WebAssembly, change the Native defer ABI, or define a public runtime API.

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