Skip to content

feat(#79): wasm, wasm32 and wasm64, BasicCABI, WASI - #361

Merged
octalide merged 8 commits into
devfrom
feat/79
Oct 17, 2026
Merged

octalide merged 8 commits into
devfrom
feat/79

Conversation

@octalide

@octalide octalide commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Closes #79

The webassembly family: wasm32 and wasm64 under the basic C ABI, and wasm32 under WASI preview1. It absorbs #80, #84, #85 and #90.

What changed

  • Thread-local statement. Access.single{condition} in the address model says one thread runs where the build does not meet the condition. The new mirl.legal.threadless pass reads it from the target's own statement and never its name. It makes a thread-local global an ordinary global on a threadless build, and it refuses one by name with Why.threaded where threads run. WASI states single under the atomics extension for every model.
  • Wasm rows (mirl.target.wasm.axes). The capabilities are values in locals, structured control, physical addressing, stack evaluation, function slots and typed values. The data layout is clang's for each width, and the float NaN rule is cited from the core specification's NaN propagation section. wasm64 differs only in its pointer rows. The WASI model is static, with every thread-local model single.
  • BasicCABI (mirl.target.wasm.abi). It has file: nil, operand pieces numbered from the first param and pointer_home: global(__stack_pointer). A scalar of at most 64 bits is one piece and an i128 argument is two i64 pieces. A singleton is a struct or union that, through nested structs, unions and arrays, holds one scalar and is aligned no more than it. It passes as that scalar's own type, as BasicCABI.md states. Anything else goes by reference. A result by reference takes an address param first. place_bits widens a narrow piece in an operand place to the row's word, so an i8 passes as an i32. The new abi.carried gives a piece in an operand place the type of the scalar member it carries whole, so struct{float} is an f32, struct{double} an f64 and struct{struct{i64}} an i64.
  • Back end (mirl.target.wasm.*), which writes one final module of mink's wasm container:
    • signature: function types from the placement. A declaration, a definition and a direct call all read the same type.
    • local: a value the next instruction reads as its one use stays on the stack, with no instruction moved. Every other value goes in a local assigned by live range over the IR's liveness, and values of one type share a local where their ranges do not meet.
    • code: structured emission from the region tree.
      • A two-way branch becomes an if.
      • A switch becomes nested blocks about a br_table when it is dense, or a chain of tests otherwise.
      • A merge's block params become the construct's block results.
      • A loop is a loop inside its merge block, about its latch block.
      • The irreducible dispatch the structurizer builds is emitted like any other selection.
      • Each opcode is selected per value class. A narrow integer is held zero-extended in an i32.
      • ftoi uses the saturating truncations. Atomics are plain accesses where one thread runs.
      • A linear-memory frame is taken from __stack_pointer and given back before each return.
    • module: imports from the environment's import module (wasi_snapshot_preview1, or env with no environment). Each function whose address is taken gets a table slot from 1, filled by an active element segment. Globals are laid out in linear memory from the host's image base as one data segment, with the stack growing down from that base. The memory and every function other images see are exported. masc's stream holds the types, index spaces and labels, and its container states the sections.
    • func.addr and function address constants give a table slot, and call_indirect carries the signature of the call's own pieces. Fold does no address arithmetic, so it already refuses arithmetic on a slot.
    • Constant time: a constant-time function is walked through mirl.ct.check. A walk masc answers Unread compiles with masc's warning (ct.UNGUARANTEED), written to the new Options.warnings. A secret the walk finds where none may be is refused.
  • Family and registry. mirl.target.wasm.family admits the wasm capabilities. Its rows are the register family's, with threadless first. Back.wasm and Options.warnings are new, and mirl emit writes warnings to stderr.
  • DWARF. DW_OP_WASM_location is added for locals and operand-stack entries in the producer's single locations.
  • Docs. doc/ir/target.md covers the wasm family, Access.single and the operand place_bits rule.

Choices

  • The output is a final module and not a relocatable object. masc writes only shortest LEBs and has no wasm fixups, so mirl lays out every index and address itself.
  • Division overflow is stated quiet, since wasm rem_s of MIN by -1 gives 0, so the helpers insert the overflow test.
  • A target with no host (wasm64 today) takes one page of stack below its data, as wasm-ld lays memory out stack first.
  • A target that states no address model states no threads, so its atomics are refused rather than made plain.
  • A switch is a br_table when its cases span fewer than 4 values per case plus 4. Otherwise it is a chain of tests.

Limits

Moved to #342

  • Gate GI2: GI1's function plus one indirect call for wasm32-wasi, passing wasm-validate.
  • wasm-validate on IR text test modules, with memory64 for wasm64.
  • Tests that a single-use value stays on the stack, a block parameter becomes a block result, and an irreducible graph emits through the dispatch.
  • Tests that func.addr gives a slot, call_indirect carries its signature, the fold refuses slot arithmetic, and other targets stay byte-identical.
  • Golden classification tables against clang. No wasm64 path is copied from wasm32, and no i32 address is left in a memory instruction. The rows include the BasicCABI singletons: struct{float} as f32, struct{double} as f64, struct{struct{i64}} as i64, union{float} as f32, struct{float[1]} as f32, struct{char} as i32, struct{__int128} as two i64, an over-aligned struct{float} and struct{float, float} by reference, and a struct{__int128} result by reference.
  • DWARF DW_OP_WASM_location for locals, globals and operands, with code byte-identical with and without debug info.
  • Constant-time code on wasm compiles with the warning, and an Unread walk is not refused.
  • A thread-local global on threadless wasm lowers to an ordinary global, and a wasm build with threads refuses it by name.

Ran locally: mach check . -a '*', mach build . -a cli and mach fmt --check ..

an address model may state a thread-local access single under a
condition: where the build does not meet it one thread runs, and the
threadless legalisation makes every such variable an ordinary global,
read from the target's statement and never its name. where the build
meets it the variable is refused, naming it.
a piece placed as an operand of the target's own call form takes the
width of the convention row's word, so an extending fill widens a
narrow integer to it, as wasm's basic c abi passes an i8 as an i32.
the webassembly rows of every axis: locals, structured control, physical
addressing, function slots, typed values and stack evaluation, clang's
data layout at either width, the basic c abi with every piece an operand
of wasm's call and the stack pointer homed in __stack_pointer, and wasi
preview1's static model whose thread-local accesses are single under the
atomics extension.
Options.warnings is where a warning the compilation gives is written,
none for nowhere. mirl emit writes them to its diagnostics.
the family admits locals, structured control, physical addressing and
stack evaluation, and runs the register family's rows after the
threadless legalisation. its back end writes one final module of mink's
wasm container: functions typed by their placement, imports from the
environment's import module, a table slot for every function whose
address is taken, globals laid out in linear memory as one data segment
below which the stack grows, and each body emitted from the region tree.
a single-use value the next instruction reads stays on the stack and
every other is held in a local assigned by live range. a merge's block
parameters are its construct's block results. a constant-time function
compiles with masc's warning that wasm cannot guarantee its timing.
@octalide
octalide marked this pull request as ready for review October 17, 2026 03:12
BasicCABI.md passes a struct or union that, through nested structs,
unions and arrays, holds one scalar and is aligned no more than it as
that scalar. the classifier now admits unions and a 128-bit member,
refuses an over-aligned aggregate, and a piece in an operand place takes
the type of the scalar member it carries whole (abi.carried), so a
struct of one float is an f32, of one double an f64, and a struct of a
struct of one i64 an i64.
@octalide
octalide enabled auto-merge October 17, 2026 03:19
@octalide
octalide added this pull request to the merge queue Oct 17, 2026
Merged via the queue into dev with commit 3dab165 Oct 17, 2026
2 checks passed
@octalide
octalide deleted the feat/79 branch October 17, 2026 03:50
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.

mirl-J4: wasm, wasm32 and wasm64, BasicCABI, WASI

1 participant