Skip to content

Add missing Wasm intrinsics for i64, f32, f64 - #558

Open
Derppening wants to merge 273 commits into
hkust-taco:hkmc2from
Derppening:enhance/typed-ir-missing-intrinsics
Open

Add missing Wasm intrinsics for i64, f32, f64#558
Derppening wants to merge 273 commits into
hkust-taco:hkmc2from
Derppening:enhance/typed-ir-missing-intrinsics

Conversation

@Derppening

Copy link
Copy Markdown
Contributor

No description provided.

Derppening added 30 commits June 2, 2026 14:14
When a variable is assigned twice using different-typed values.
Derppening and others added 18 commits August 22, 2026 03:10
`Lowering` and `Elaborator` each named every `wasm.<mod>.<op>` intrinsic one
by one, so adding one meant editing three lists that had to agree: the prelude
declaration, the builtin symbol, and the `specialBuiltinSymbols` entry.

Enumerate the nested modules of `wasm` and their members instead. Every
function such a module declares *is* an instruction intrinsic, and which
instruction each one emits is decided by the backend's own table, so the
prelude is the only place the set needs to be written down.

The path carried by `SpecialBuiltin.WasmIntrinsic` goes with them: it was
never read, since the use site rebuilds the path from the symbol's owner
chain.

A name the prelude declares and the backend does not implement is now
reported at its use site rather than crashing the compiler, which is what the
hand-maintained lists used to rule out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All four Wasm numeric types share one instruction shape: the mnemonic is the
type's own prefix followed by the operation name, the operands are the stack
arguments, and the result is a value of either the type itself (arithmetic) or
`i32` (comparisons and tests). `i32` spelled each of its fifteen instructions
out in full, which would have meant three more copies of the same block for
`i64`, `f32` and `f64`.

Derive the four from a common base instead, leaving only the genuinely
per-family parts written down: the `const` immediate, and the operations one
of the integer and floating-point types has and the other does not.

`i32.eq` already reported an unreachable result when either operand had one;
this now holds of every numeric instruction, which is the accurate answer for
all of them and not just that one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes hkust-taco#548.

The erased type hierarchy has carried all four unboxed primitives for a while,
but only `Int32` had a Wasm counterpart: the other three fell through
`wasmType` to `anyref`, and the two sites that zero a slot with no explicit
initializer - a struct field under `struct.new_default` and a REPL session
global - refused anything but `i32`. Map each primitive to its own Wasm type,
and derive both zero values from that one mapping.

Declare and implement the `wasm.i64`, `wasm.f32` and `wasm.f64` intrinsics
alongside the existing `wasm.i32` ones, so the three types can actually be
exercised. `i64` mirrors `i32`; the float families trade `rem_s`/`eqz` and the
signed comparisons for `div`, `min`/`max` and the unary `abs`/`neg`/`sqrt`.
As on `i32`, a comparison yields an `Int32`, which `wasm.ref.i31` boxes.

Their immediates need more than the 32-bit integer literal `i32.const` takes:
`i64.const` spans the full signed 64-bit range, and the float ones accept a
decimal literal (or an integer one - `1` and `1.0` denote the same float),
narrowed to the immediate's own type so that a literal only an `f64` can hold
is reported as out of an `f32`'s range rather than emitted as an infinity.

An intrinsic's argument is now compiled as an operand only if the intrinsic
actually reads it as one. A decimal literal has no representation of its own
in this backend, so compiling one eagerly reported *that* rather than the
`f64.const` using it perfectly well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all these new verbose diff outputs really warranted?

@LPTK

LPTK commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Codex found and fixed an issue with mutliple return paths merging into different types. But I'm not sure its solution (in inferEntryResultType) is the right one. It feels wrong to do that at the WASM level. Shouldn't it be done at the IR level? WDYT @Derppening?

@Derppening

Copy link
Copy Markdown
Contributor Author

Codex found and fixed an issue with mutliple return paths merging into different types. But I'm not sure its solution (in inferEntryResultType) is the right one. It feels wrong to do that at the WASM level. Shouldn't it be done at the IR level? WDYT @Derppening?

I think an IR-level solution is indeed the better way, and I think it's achievable by adding Block.erasedType as a lazy val and traversing the block to find and lub all return paths. WDYT about this?

@LPTK

LPTK commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Yes, it sounds reasonable, if all the information is indeed already there in each node. But it should be calles returnErasedType, as blocks themselves don't have a type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants