diff --git a/CHANGELOG.md b/CHANGELOG.md index 297bcd91..43333ce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,46 @@ All notable changes to Aver are documented here. Starting with 0.10.0, minor rel ## Unreleased +## 0.17.0 "Purge" — 2026-05-06 + +> _What was never used is no longer in the way._ + +A stdlib sweep before the language gets harder to break. Eight redundant builtins removed, one renamed for symmetry, one rule for what stays: each operation has one obvious spelling. Literals for literals (`{}`, `[]`), operators for primitive composition (`+` on strings, arithmetic), `Target.fromSource` for conversions, interpolation for rendering, named functions only when they add semantics. + +### Removed + +- **`Map.empty()`** — redundant against the `{}` literal. Bidirectional inference propagates `Map` from binding annotations into `{}`, and the discovery walker introduced in 0.16.3 picks up unannotated literals like `m = {a => 3}` from `expr.ty()`. Symmetric with `[]` for List (which never had a `List.empty` builtin). +- **`Int.parse`, `Float.parse`, `Int.rem`** — unreachable builtin aliases. Registered in 4 backend dispatch tables but never wired through typecheck or VM. Lean and dafny treated them as outright aliases for `Int.fromString` / `Float.fromString` / `Int.mod`. +- **`Int.toString`, `Float.toString`** — duplicates of `String.fromInt` / `String.fromFloat`. The convention `Target.fromSource` wins: result type leads (matters for code review and AI-generated code), and it scales (`Int.fromFloat`, `Float.fromInt`, `String.fromBool`, …). Interpolation `"{x}"` remains the primary idiom for rendering values into human-readable strings; `String.fromX` is for explicit data conversion (e.g. `"user:" + String.fromInt(id)`). +- **`Float.toInt`, `Int.toFloat`** — duplicates of `Int.fromFloat` / `Float.fromInt`. Same convention. +- **`String.concat(a, b)`** — literally `a + b`. Zero usage in examples (only in docs); not even dispatched in the wasm-gc backend. The `+` operator is the primary idiom for tail-recursive accumulators (`acc + sep + String.fromInt(head)`) and inline string composition; `String.join(parts, sep)` covers list-join (a different operation). + +### Renamed + +- **`Vector.toList(v)` → `List.fromVector(v)`** — paired with `Vector.fromList(l)` under the same `Target.fromSource` rule. + +### Kept (deliberately) + +- **`+` on strings** — primary idiom for tail-recursive string accumulators and inline composition. Forcing only interpolation `"{a}{b}"` would be awkward in `acc + " " + String.fromInt(head)` patterns. +- **`Bool.and` / `Bool.or` / `Bool.not`** — *intentionally not added* as `&&` / `||` / `!` operators. Aver tracks effects deterministically (record/replay/proof export); short-circuit on `a && f!()` would require deciding whether the effect of `f` may or may not fire depending on `a`, which complicates trace recording and Lean export. Strict-FP eager evaluation matches the rest of the language. Pattern-matching `(x > 0, y > 0)` covers most idiomatic AND/OR cases. Not a "later language decision" — a settled non-feature. +- **`Char.toCode` / `Char.fromCode`, `Byte.toHex` / `Byte.fromHex`, `Option.toResult`, `String.toLower` / `String.toUpper`** — encoding semantics or semantic conversions, not type-to-type. Not duplicates. + +### Migration + +Programs using removed builtins: + +```aver +-- before +m = Map.empty() -- → {} +s = Int.toString(42) -- → String.fromInt(42) or "{42}" +n = Float.toInt(3.7) -- → Int.fromFloat(3.7) +f = Int.toFloat(5) -- → Float.fromInt(5) +xs = Vector.toList(v) -- → List.fromVector(v) +greeting = String.concat("hi, ", name) -- → "hi, " + name +``` + +Examples in `examples/` and `tools/website/` migrated; vera-bench solutions checked (zero usage of any removed builtin, no companion PR needed). All 621 lib tests pass. The full sweep landed in four self-contained commits on the 0.17 branch. + ## 0.16.2 — 2026-05-06 > _Record/replay correctness across all three backends — and a tidier wasm-gc imports tree along the way._ diff --git a/Cargo.lock b/Cargo.lock index 07fe9d91..e47181d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,7 +119,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aver-lang" -version = "0.16.2" +version = "0.17.0" dependencies = [ "aver-memory", "aver-rt", diff --git a/Cargo.toml b/Cargo.toml index 53c6dc96..911fba17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["tools/wasm-runner"] [package] name = "aver-lang" -version = "0.16.2" +version = "0.17.0" edition = "2024" default-run = "aver" description = "VM and transpiler for Aver, a statically-typed language designed for AI-assisted development" diff --git a/aver-lsp/Cargo.toml b/aver-lsp/Cargo.toml index 11f928f8..fdb0335f 100644 --- a/aver-lsp/Cargo.toml +++ b/aver-lsp/Cargo.toml @@ -16,7 +16,7 @@ name = "aver-lsp" path = "src/main.rs" [dependencies] -aver = { path = "..", version = "=0.16.2", package = "aver-lang" } +aver = { path = "..", version = "=0.17.0", package = "aver-lang" } tower-lsp-server = "0.23" tokio = { version = "1", features = ["full"] } serde_json = "1" diff --git a/docs/language.md b/docs/language.md index 380fee17..f8f0049b 100644 --- a/docs/language.md +++ b/docs/language.md @@ -35,7 +35,7 @@ Duplicate binding of the same name in the same scope is a type error. ## Operators -Arithmetic: `+`, `-`, `*`, `/` — operands must match (`Int+Int`, `Float+Float`, `String+String`). No implicit promotion; use `Int.toFloat` / `Float.fromInt` to convert. +Arithmetic: `+`, `-`, `*`, `/` — operands must match (`Int+Int`, `Float+Float`, `String+String`). No implicit promotion; use `Float.fromInt` / `Int.fromFloat` to convert. Comparison: `==`, `!=`, `<`, `>`, `<=`, `>=`. Error propagation: `expr?` — unwraps `Result.Ok`, propagates `Result.Err` as a `RuntimeError`. Independent products: `(a, b)!` — product of independent computations. `(a, b)?!` — same, with Result unwrapping (all must succeed or first error propagates). Elements cannot reference each other; independence is structural. Composes recursively for fan-out parallelism. See [independence.md](independence.md). @@ -245,7 +245,7 @@ Most application code in Aver stays first-order and explicit. Use function param Aver has no dedicated `Set` type. The idiomatic way to express a set is `Map` — a map whose values carry no information. All `Map.*` operations work on sets: ```aver -seen: Map = Map.empty() +seen: Map = {} seen2 = Map.set(seen, "alice", Unit) Map.has(seen2, "alice") // true Map.len(seen2) // 1 diff --git a/docs/services.md b/docs/services.md index 3c8dd502..f2fee056 100644 --- a/docs/services.md +++ b/docs/services.md @@ -44,7 +44,7 @@ Vector is a persistent indexed sequence — use it for grids, buffers, lookup ta | `Vector.set` | `(Vector, Int, T) -> Option>` | O(1) COW update; `None` if out of bounds | | `Vector.len` | `Vector -> Int` | | | `Vector.fromList` | `List -> Vector` | Convert list to vector | -| `Vector.toList` | `Vector -> List` | Convert vector to list | +| `List.fromVector` | `Vector -> List` | Convert vector to list | ### `Result` namespace @@ -75,8 +75,8 @@ Source: `src/types/int.rs` |---|---| | `Int.fromString` | `String -> Result` | | `Int.fromFloat` | `Float -> Int` | -| `Int.toString` | `Int -> String` | -| `Int.toFloat` | `Int -> Float` | +| `String.fromInt` | `Int -> String` | +| `Float.fromInt` | `Int -> Float` | | `Int.abs` | `Int -> Int` | | `Int.min` | `(Int, Int) -> Int` | | `Int.max` | `(Int, Int) -> Int` | @@ -90,7 +90,7 @@ Source: `src/types/float.rs` |---|---| | `Float.fromString` | `String -> Result` | | `Float.fromInt` | `Int -> Float` | -| `Float.toString` | `Float -> String` | +| `String.fromFloat` | `Float -> String` | | `Float.abs` | `Float -> Float` | | `Float.floor` | `Float -> Int` | | `Float.ceil` | `Float -> Int` | @@ -134,7 +134,7 @@ Source: `src/types/map.rs` | Function | Signature | Notes | |---|---|---| -| `Map.empty` | `() -> Map` | | +| `{}` (literal) | — | The empty map; type from context (annotation or expected type). No `Map.empty()` builtin since 0.17 — symmetric with `[]` for List. | | `Map.fromList` | `List<(K, V)> -> Map` | Keys must be hashable (Int, Float, String, Bool) | | `Map.set` | `(Map, K, V) -> Map` | Returns new map with key set | | `Map.get` | `(Map, K) -> Option` | | diff --git a/docs/vm.md b/docs/vm.md index 852beba3..139ad469 100644 --- a/docs/vm.md +++ b/docs/vm.md @@ -117,7 +117,7 @@ The point of `v2` is not only compactness. It is to keep the common Aver shapes - `Bool`, `Unit`, and `None` are pure inline singletons - `Some(true)`, `Ok(Unit)`, `Err(None)` stay inline - `Some(42)`, `Ok(-7)`, `Err(0)` stay inline as long as the int fits the wrapper-inline range -- `[]` and `Map.empty()` are real values under their normal collection tags, not exceptions hidden in `Immediate` +- `[]` and `{}` are real values under their normal collection tags, not exceptions hidden in `Immediate` - strings up to 5 UTF-8 bytes stay inline under `TAG_STRING` - nullary variants such as `Status.Todo` or `Color.Red` travel as `Symbol` handles instead of arena entries diff --git a/examples/data/date.av b/examples/data/date.av index 1cdbe1bd..b38066c8 100644 --- a/examples/data/date.av +++ b/examples/data/date.av @@ -44,8 +44,8 @@ verify parse fn padTwo(n: Int) -> String ? "Pad a single-digit number with a leading zero." match n < 10 - true -> "0{Int.toString(n)}" - false -> Int.toString(n) + true -> "0{String.fromInt(n)}" + false -> String.fromInt(n) verify padTwo padTwo(0) => "00" @@ -54,7 +54,7 @@ verify padTwo fn format(d: Date) -> String ? "Format a Date as YYYY-MM-DD HH:MM:SS." - y = Int.toString(d.year) + y = String.fromInt(d.year) m = padTwo(d.month) da = padTwo(d.day) h = padTwo(d.hour) @@ -82,6 +82,6 @@ fn main() d = parse(now) Console.print("Raw: {now}") Console.print("Parsed: {format(d)}") - Console.print("Year: {Int.toString(d.year)}") - Console.print("Month: {Int.toString(d.month)}") - Console.print("Day: {Int.toString(d.day)}") + Console.print("Year: {String.fromInt(d.year)}") + Console.print("Month: {String.fromInt(d.month)}") + Console.print("Day: {String.fromInt(d.day)}") diff --git a/examples/data/fibonacci.av b/examples/data/fibonacci.av index 8d90a62b..406508cb 100644 --- a/examples/data/fibonacci.av +++ b/examples/data/fibonacci.av @@ -234,7 +234,7 @@ fn goldenApprox(n: Int) -> Result ? "Approximates golden ratio as F(n+1)/F(n). Requires n >= 1." match n < 1 true -> Result.Err("Need n >= 1") - false -> Result.Ok(Int.toFloat(fib(n + 1)) / Int.toFloat(fib(n))) + false -> Result.Ok(Float.fromInt(fib(n + 1)) / Float.fromInt(fib(n))) verify goldenApprox goldenApprox(0) => Result.Err("Need n >= 1") diff --git a/examples/data/json.av b/examples/data/json.av index 12c24795..fa81385e 100644 --- a/examples/data/json.av +++ b/examples/data/json.av @@ -1081,7 +1081,7 @@ verify parseArrayNext fn emptyJsonMap() -> Map ? "JSON helper: emptyJsonMap." - Map.empty() + {} fn parseObject(s: String, pos: Int) -> ParseResult ? "JSON helper: parseObject." diff --git a/examples/data/map.av b/examples/data/map.av index 97cca983..b7d0e2dc 100644 --- a/examples/data/map.av +++ b/examples/data/map.av @@ -6,7 +6,7 @@ module MapExample fn emptyCounts() -> Map ? "Creates an empty word-count map." - Map.empty() + {} fn incCount(counts: Map, word: String) -> Map ? "Increments the counter for one word." @@ -16,16 +16,16 @@ fn incCount(counts: Map, word: String) -> Map Option.None -> Map.set(counts, word, 1) verify incCount - incCount(Map.empty(), "a") => {"a" => 1} + incCount({}, "a") => {"a" => 1} incCount({"a" => 1}, "a") => {"a" => 2} verify incCount law keyPresent - given counts: Map = [Map.empty(), {"a" => 1}, {"x" => 2, "y" => 4}] + given counts: Map = [{}, {"a" => 1}, {"x" => 2, "y" => 4}] given word: String = ["a", "z", "new"] Map.has(incCount(counts, word), word) => true verify incCount law trackedCountStepsByOne - given counts: Map = [Map.empty(), {"a" => 1}, {"a" => 5, "x" => 2}] + given counts: Map = [{}, {"a" => 1}, {"a" => 5, "x" => 2}] given word: String = ["a", "new"] Option.withDefault(Map.get(incCount(counts, word), word), 0) => Option.withDefault(Map.get(counts, word), 0) + 1 @@ -36,7 +36,7 @@ fn countWords(words: List) -> Map [word, ..rest] -> incCount(countWords(rest), word) verify countWords - countWords([]) => Map.empty() + countWords([]) => {} countWords(["a", "b", "a"]) => {"a" => 2, "b" => 1} verify countWords law presenceMatchesContains diff --git a/examples/formal/law_auto.av b/examples/formal/law_auto.av index c99b076a..3bfd2b1b 100644 --- a/examples/formal/law_auto.av +++ b/examples/formal/law_auto.av @@ -84,7 +84,7 @@ fn incCount(counts: Map, word: String) -> Map Option.None -> Map.set(counts, word, 1) verify incCount law keyPresent - given counts: Map = [Map.empty(), {"a" => 1}, {"x" => 2, "y" => 4}] + given counts: Map = [{}, {"a" => 1}, {"x" => 2, "y" => 4}] given word: String = ["a", "z", "new"] Map.has(incCount(counts, word), word) => true diff --git a/examples/games/checkers/main.av b/examples/games/checkers/main.av index d36b5e0d..73794513 100644 --- a/examples/games/checkers/main.av +++ b/examples/games/checkers/main.av @@ -210,11 +210,11 @@ fn renderAiPanel(result: AiResult) -> Unit ! [Terminal.moveTo, Terminal.print, Terminal.resetColor, Terminal.setColor] Terminal.moveTo(22, 1) Terminal.setColor("cyan") - Terminal.print("AI depth={Int.toString(result.depth)} {Int.toString(result.thinkMs)}ms") + Terminal.print("AI depth={String.fromInt(result.depth)} {String.fromInt(result.thinkMs)}ms") Terminal.resetColor() Terminal.moveTo(22, 2) Terminal.setColor("yellow") - Terminal.print("root-parallel x{Int.toString(Ai.rootParallelWidth())} | {Int.toString(List.len(result.candidates))} moves") + Terminal.print("root-parallel x{String.fromInt(Ai.rootParallelWidth())} | {String.fromInt(List.len(result.candidates))} moves") Terminal.resetColor() renderCandidates(result.candidates, result.move, 0) @@ -242,7 +242,7 @@ fn renderChosenCandidate(sm: ScoredMove, i: Int) -> Unit Terminal.setColor("green") Terminal.print("★ ") Terminal.print(moveLabel(sm.move)) - Terminal.print(" {Int.toString(sm.score)}") + Terminal.print(" {String.fromInt(sm.score)}") Terminal.resetColor() fn renderOtherCandidate(sm: ScoredMove, i: Int) -> Unit @@ -250,7 +250,7 @@ fn renderOtherCandidate(sm: ScoredMove, i: Int) -> Unit ! [Terminal.print] Terminal.print(" ") Terminal.print(moveLabel(sm.move)) - Terminal.print(" {Int.toString(sm.score)}") + Terminal.print(" {String.fromInt(sm.score)}") fn isChosenMove(a: List, b: List) -> Bool ? "True if two moves are the same (compare first and last points)" @@ -295,7 +295,7 @@ fn moveLabel(move: List) -> String ? "Human-readable label for a move path" match firstPoint(move) Option.Some(from) -> match lastPoint(move) - Option.Some(to) -> "{colChar(from.x)}{Int.toString(8 - from.y)}→{colChar(to.x)}{Int.toString(8 - to.y)}" + Option.Some(to) -> "{colChar(from.x)}{String.fromInt(8 - from.y)}→{colChar(to.x)}{String.fromInt(8 - to.y)}" Option.None -> "?" Option.None -> "?" @@ -307,7 +307,7 @@ fn renderHelp(state: GameState) -> Unit ! [Terminal.moveTo, Terminal.print, Terminal.resetColor, Terminal.setColor] Terminal.moveTo(0, 14) Terminal.setColor("green") - Terminal.print(" You (black) vs AI (white) · depth {Int.toString(state.searchDepth)} · +/- to change") + Terminal.print(" You (black) vs AI (white) · depth {String.fromInt(state.searchDepth)} · +/- to change") Terminal.resetColor() Terminal.moveTo(0, 15) match state.selected @@ -415,7 +415,7 @@ fn aiPhase(state: GameState) -> Result ] renderFrame(state) Terminal.moveTo(0, 14) - Terminal.print("AI thinking (depth {Int.toString(state.searchDepth)}, root-parallel x{Int.toString(Ai.rootParallelWidth())})...") + Terminal.print("AI thinking (depth {String.fromInt(state.searchDepth)}, root-parallel x{String.fromInt(Ai.rootParallelWidth())})...") Terminal.flush() t0 = Time.unixMs() aiState = handleAiTurn(state) @@ -443,7 +443,7 @@ fn gameOverResult(state: GameState) -> Result Terminal.print("Game over!") Terminal.flush() Time.sleep(2000) - Result.Ok("Game over! Move {Int.toString(state.moveNum)}") + Result.Ok("Game over! Move {String.fromInt(state.moveNum)}") fn gameTurn(state: GameState) -> Result ? "Dispatch to player or AI" diff --git a/examples/games/checkers/render.av b/examples/games/checkers/render.av index 5e3e472f..f0145779 100644 --- a/examples/games/checkers/render.av +++ b/examples/games/checkers/render.av @@ -240,7 +240,7 @@ fn renderRowLabelsNext(r: Int) -> Unit fn rowLabel(r: Int) -> String ? "Row label: row 0 = 8, row 7 = 1" - Int.toString(8 - r) + String.fromInt(8 - r) verify rowLabel rowLabel(0) => "8" @@ -281,7 +281,7 @@ fn renderStatus(currentPlayer: Color, moveNum: Int) -> Unit Terminal.setColor(playerColor(currentPlayer)) Terminal.print("{colorName(currentPlayer)}") Terminal.resetColor() - Terminal.print(" to move | Move #{Int.toString(moveNum)}") + Terminal.print(" to move | Move #{String.fromInt(moveNum)}") fn renderAiTrace(move: List, score: Int, alternatives: Int, depth: Int) -> Unit ? "Show AI decision trace" @@ -290,4 +290,4 @@ fn renderAiTrace(move: List, score: Int, alternatives: Int, depth: Int) - Terminal.setColor("cyan") Terminal.print("AI: ") Terminal.resetColor() - Terminal.print("score={Int.toString(score)} alts={Int.toString(alternatives)} depth={Int.toString(depth)}") + Terminal.print("score={String.fromInt(score)} alts={String.fromInt(alternatives)} depth={String.fromInt(depth)}") diff --git a/examples/games/doom/enemy.av b/examples/games/doom/enemy.av index 670ab64e..e99d3f5e 100644 --- a/examples/games/doom/enemy.av +++ b/examples/games/doom/enemy.av @@ -49,8 +49,8 @@ fn ghostTeleport(enemy: Enemy, seed: Int, levelMap: List) -> Enemy ? "Teleport ghost to a random nearby floor tile" s1 = Rng.nextSeed(seed) s2 = Rng.nextSeed(s1) - dx = Int.toFloat(Rng.seedToRange(s1, 0 - 4, 4)) - dy = Int.toFloat(Rng.seedToRange(s2, 0 - 4, 4)) + dx = Float.fromInt(Rng.seedToRange(s1, 0 - 4, 4)) + dy = Float.fromInt(Rng.seedToRange(s2, 0 - 4, 4)) nx = enemy.x + dx ny = enemy.y + dy match Level.isWall(levelMap, Float.floor(nx), Float.floor(ny)) @@ -251,7 +251,7 @@ fn applyShotVec(enemies: Vector, idx: Int) -> (List, String) ? "Apply 5 damage to enemy at index using indexed vector access" match Vector.get(enemies, idx) Option.Some(e) -> applyDamageToEnemyVec(enemies, idx, e) - Option.None -> (Vector.toList(enemies), "Shot missed!") + Option.None -> (List.fromVector(enemies), "Shot missed!") verify applyShotVec applyShotVec(Vector.fromList([]), 0) => ([], "Shot missed!") @@ -268,7 +268,7 @@ fn applyDamageToEnemyVec(enemies: Vector, idx: Int, e: Enemy) -> (List (removeEnemyVec(enemies, idx, 0, []), "Killed {Types.enemyGlyph(e.kind)}!") - false -> (replaceEnemyVec(enemies, idx, Enemy.update(e, hp = newHp), 0, []), "Hit {Types.enemyGlyph(e.kind)}! ({Int.toString(newHp)} HP)") + false -> (replaceEnemyVec(enemies, idx, Enemy.update(e, hp = newHp), 0, []), "Hit {Types.enemyGlyph(e.kind)}! ({String.fromInt(newHp)} HP)") verify applyDamageToEnemyVec applyDamageToEnemyVec(Vector.fromList([Types.mkEnemy(1.0, 1.0, EnemyKind.Imp, 3)]), 0, Types.mkEnemy(1.0, 1.0, EnemyKind.Imp, 3)) => ([], "Killed !!") diff --git a/examples/games/doom/level.av b/examples/games/doom/level.av index e83b0e4b..7c72e3d1 100644 --- a/examples/games/doom/level.av +++ b/examples/games/doom/level.av @@ -280,7 +280,7 @@ verify roomFromSeed fn roomCenter(room: Room) -> (Float, Float) ? "Center of a room as float coordinates" - (Int.toFloat(room.x + room.w / 2) + 0.5, Int.toFloat(room.y + room.h / 2) + 0.5) + (Float.fromInt(room.x + room.w / 2) + 0.5, Float.fromInt(room.y + room.h / 2) + 0.5) verify roomCenter roomCenter(Room(x = 4, y = 4, w = 6, h = 6)) => (7.5, 7.5) diff --git a/examples/games/doom/main.av b/examples/games/doom/main.av index 089bcf05..836b8aba 100644 --- a/examples/games/doom/main.av +++ b/examples/games/doom/main.av @@ -33,8 +33,8 @@ verify turnSpeed fn spawnInRoom(room: Level.Room, seed: Int) -> Enemy ? "Spawn an enemy at the center of a room" kind = pickKind(seed) - cx = Int.toFloat(room.x + room.w / 2) + 0.5 - cy = Int.toFloat(room.y + room.h / 2) + 0.5 + cx = Float.fromInt(room.x + room.w / 2) + 0.5 + cy = Float.fromInt(room.y + room.h / 2) + 0.5 Types.mkEnemy(cx, cy, kind, Types.enemyMaxHp(kind)) verify spawnInRoom @@ -214,7 +214,7 @@ fn updateTurn(state: GameState) -> GameState msgs = match isOver true -> ["You died!"] false -> match damaged.hp < state.player.hp - true -> List.concat(state.messages, ["Ouch! HP: {Int.toString(damaged.hp)}"]) + true -> List.concat(state.messages, ["Ouch! HP: {String.fromInt(damaged.hp)}"]) false -> state.messages trimmedMsgs = trimMessages(msgs) GameState.update(state, enemies = newEnemies, player = damaged, turnCount = state.turnCount + 1, gameOver = isOver, messages = trimmedMsgs) diff --git a/examples/games/doom/math.av b/examples/games/doom/math.av index 9b89add2..15f90514 100644 --- a/examples/games/doom/math.av +++ b/examples/games/doom/math.av @@ -29,7 +29,7 @@ verify radToDeg fn normalizeAngle(a: Float) -> Float ? "Wrap angle to [0, 2*pi)" twoPi = Float.pi() * 2.0 - rem = a - Int.toFloat(Float.floor(a / twoPi)) * twoPi + rem = a - Float.fromInt(Float.floor(a / twoPi)) * twoPi match rem < 0.0 true -> rem + twoPi false -> rem diff --git a/examples/games/doom/render.av b/examples/games/doom/render.av index 823002fc..1123b5ea 100644 --- a/examples/games/doom/render.av +++ b/examples/games/doom/render.av @@ -134,7 +134,7 @@ fn wallHeight(distance: Float) -> Int ? "Pixel height of the wall strip for a given corrected distance" match distance < 0.1 true -> pixelH() - false -> Math.clampInt(Float.floor(Int.toFloat(pixelH()) / distance), 0, pixelH()) + false -> Math.clampInt(Float.floor(Float.fromInt(pixelH()) / distance), 0, pixelH()) verify wallHeight wallHeight(1.0) => 80 @@ -282,7 +282,7 @@ verify buildZBuffer fn buildZBufferCol(levelMap: List, wallMap: List, px: Float, py: Float, angle: Float, col: Int, acc: List) -> List ? "Cast one ray and append to buffer" - rayAngle = angle - fov() / 2.0 + Int.toFloat(col) * fov() / Int.toFloat(screenW()) + rayAngle = angle - fov() / 2.0 + Float.fromInt(col) * fov() / Float.fromInt(screenW()) hit = castRay(levelMap, wallMap, px, py, rayAngle) corrected = RayHit.update(hit, dist = hit.dist * Float.cos(rayAngle - angle)) buildZBuffer(levelMap, wallMap, px, py, angle, col + 1, List.concat(acc, [corrected])) @@ -375,7 +375,7 @@ fn renderEnemySprite(enemies: Vector, zbuf: Vector, px: Float, py eDist = Math.dist(px, py, e.x, e.y) eAngle = Float.atan2(dy, dx) relAngle = Math.normalizeAngle(eAngle - pAngle + Float.pi()) - Float.pi() - screenCol = Float.floor(Int.toFloat(screenW()) / 2.0 + relAngle * Int.toFloat(screenW()) / fov()) + screenCol = Float.floor(Float.fromInt(screenW()) / 2.0 + relAngle * Float.fromInt(screenW()) / fov()) match isEnemyVisibleVec(screenCol, eDist, zbuf) true -> drawEnemyChar(e, screenCol, eDist, enemies, zbuf, px, py, pAngle, i) false -> renderEnemiesVec(enemies, zbuf, px, py, pAngle, i + 1) @@ -409,8 +409,8 @@ verify isEnemyVisibleVec fn drawEnemyChar(e: Enemy, screenCol: Int, eDist: Float, enemies: Vector, zbuf: Vector, px: Float, py: Float, pAngle: Float, i: Int) -> Unit ? "Draw enemy using solid Unicode block characters" ! [Terminal.flush, Terminal.moveTo, Terminal.print, Terminal.resetColor, Terminal.setColor] - spriteH = Math.clampInt(Float.floor(2.5 / eDist * Int.toFloat(screenH())), 1, 10) - spriteW = Math.clampInt(Float.floor(5.0 / eDist * Int.toFloat(screenW()) / 10.0), 3, 13) + spriteH = Math.clampInt(Float.floor(2.5 / eDist * Float.fromInt(screenH())), 1, 10) + spriteW = Math.clampInt(Float.floor(5.0 / eDist * Float.fromInt(screenW()) / 10.0), 3, 13) topRow = screenH() / 2 - spriteH / 2 leftCol = screenCol - spriteW / 2 drawBlockGrid(e, leftCol, topRow, spriteH, spriteW, 0, 0) @@ -463,9 +463,9 @@ verify fullBlock fn isInsideShape(kind: EnemyKind, col: Int, row: Int, spriteW: Int, spriteH: Int) -> Bool ? "Check if (col, row) is inside the enemy silhouette" - halfW = Int.toFloat(spriteW) / 2.0 - cx = Float.abs(Int.toFloat(col) - halfW + 0.5) / halfW - cy = Int.toFloat(row) / Int.toFloat(spriteH) + halfW = Float.fromInt(spriteW) / 2.0 + cx = Float.abs(Float.fromInt(col) - halfW + 0.5) / halfW + cy = Float.fromInt(row) / Float.fromInt(spriteH) maxW = shapeWidth(kind, cy) cx < maxW @@ -557,13 +557,13 @@ fn renderHud(state: GameState) -> Unit Terminal.print("BRAILLE DOOM") Terminal.setColor("red") Terminal.moveTo(hudX, 3) - Terminal.print("HP: {Int.toString(state.player.hp)}/100") + Terminal.print("HP: {String.fromInt(state.player.hp)}/100") Terminal.setColor("white") Terminal.moveTo(hudX, 4) deg = Float.floor(Math.radToDeg(state.player.angle)) - Terminal.print("angle: {Int.toString(deg)}") + Terminal.print("angle: {String.fromInt(deg)}") Terminal.moveTo(hudX, 5) - Terminal.print("enemies: {Int.toString(List.len(state.enemies))}") + Terminal.print("enemies: {String.fromInt(List.len(state.enemies))}") Terminal.setColor("cyan") Terminal.moveTo(hudX, 7) Terminal.print("W/S forward/back") diff --git a/examples/games/doom/rng.av b/examples/games/doom/rng.av index ca76be06..52312e63 100644 --- a/examples/games/doom/rng.av +++ b/examples/games/doom/rng.av @@ -38,7 +38,7 @@ verify advanceSeed fn randomFloat(s: Int) -> Float ? "Map seed to [0.0, 1.0)" - Int.toFloat(Result.withDefault(Int.mod(Int.abs(s), 10000), 0)) / 10000.0 + Float.fromInt(Result.withDefault(Int.mod(Int.abs(s), 10000), 0)) / 10000.0 verify randomFloat randomFloat(0) => 0.0 diff --git a/examples/games/life.av b/examples/games/life.av index b5488dec..cf25411e 100644 --- a/examples/games/life.av +++ b/examples/games/life.av @@ -301,7 +301,7 @@ fn drawOneRow(grid: Vector, s: Int, w: Int, h: Int, row: Int) -> Unit fn drawHud(st: SimState, mode: String) -> Unit ? "Draw two-line HUD: status on line 0, controls on line 1." ! [Terminal.moveTo, Terminal.print, Terminal.resetColor, Terminal.setColor] - genStr = Int.toString(st.gen) + genStr = String.fromInt(st.gen) Terminal.setColor("green") Terminal.moveTo(0, 0) line1 = match mode == "edit" @@ -310,7 +310,7 @@ fn drawHud(st: SimState, mode: String) -> Unit true -> " PAUSED Gen:{genStr} " false -> match st.delay == 0 true -> " {st.fps.display}fps Gen:{genStr} BENCHMARK " - false -> " {st.fps.display}fps Gen:{genStr} {Int.toString(st.delay)}ms " + false -> " {st.fps.display}fps Gen:{genStr} {String.fromInt(st.delay)}ms " Terminal.print(line1) Terminal.moveTo(0, 1) Terminal.setColor("cyan") @@ -327,7 +327,7 @@ fn formatFps(frames: Int, elapsed: Int) -> String tenths = frames * 10000 / elapsed whole = tenths / 10 frac = Result.withDefault(Int.mod(tenths, 10), 0) - Int.toString(whole) + "." + Int.toString(frac) + String.fromInt(whole) + "." + String.fromInt(frac) verify formatFps formatFps(2, 1000) => "2.0" @@ -452,7 +452,7 @@ fn simAction(k: String, st: SimState) -> Result true -> 0 false -> 1 match k - "q" -> Result.Ok("Stopped at generation {Int.toString(st.gen)}.") + "q" -> Result.Ok("Stopped at generation {String.fromInt(st.gen)}.") " " -> gameLoop(SimState.update(st, paused = newPaused)) "e" -> simToEditor(st) "+" -> gameLoop(SimState.update(st, delay = Int.max(st.delay - 10, 0))) @@ -470,7 +470,7 @@ fn simToEditor(st: SimState) -> Result result = editorLoop(st.grid, st.stride, st.width, st.height, st.width / 2, st.height / 2) match result Result.Ok(newGrid) -> gameLoop(SimState.update(st, grid = newGrid, gen = 0)) - Result.Err(_) -> Result.Ok("Stopped at generation {Int.toString(st.gen)}.") + Result.Err(_) -> Result.Ok("Stopped at generation {String.fromInt(st.gen)}.") // ─── Entry ────────────────────────────────────────────────── diff --git a/examples/games/rogue/main.av b/examples/games/rogue/main.av index 565b39dd..07b7cbed 100644 --- a/examples/games/rogue/main.av +++ b/examples/games/rogue/main.av @@ -354,7 +354,7 @@ fn resolveAttack(state: GameState, target: Entity, idx: Int) -> GameState hit = Combat.applyDamageToEntity(target, dmg) match Combat.isEntityDead(hit) true -> killEntity(state, target, idx, dmg) - false -> alertNearby(addMessage(GameState.update(state, entities = Combat.replaceEntity(state.entities, idx, hit, 0)), "You hit {target.name} for {Int.toString(dmg)}!"), state.player.pos, 12, 0) + false -> alertNearby(addMessage(GameState.update(state, entities = Combat.replaceEntity(state.entities, idx, hit, 0)), "You hit {target.name} for {String.fromInt(dmg)}!"), state.player.pos, 12, 0) fn killEntity(state: GameState, target: Entity, idx: Int, dmg: Int) -> GameState ? "Kill entity — for-loop gets one respawn (off-by-one error)" @@ -373,7 +373,7 @@ fn loopRespawn(state: GameState, target: Entity, idx: Int) -> GameState fn actualKill(state: GameState, target: Entity, idx: Int) -> GameState ? "Actually remove entity and gain exp" newPlayer = Combat.gainExp(state.player, Combat.expForKill(target.kind)) - addMessage(GameState.update(state, entities = Combat.removeAt(state.entities, idx, 0), player = newPlayer), "You defeated {target.name}! (+{Int.toString(Combat.expForKill(target.kind))} exp)") + addMessage(GameState.update(state, entities = Combat.removeAt(state.entities, idx, 0), player = newPlayer), "You defeated {target.name}! (+{String.fromInt(Combat.expForKill(target.kind))} exp)") verify actualKill List.len(actualKill(GameState(gameMap = [], player = Types.makePlayer(0, 0), entities = [Types.makeIfElse(1, 1)], items = [], visible = [], visGrid = [], remembered = [], messages = [], seed = 0, gameOver = false), Types.makeIfElse(1, 1), 0).entities) => 0 @@ -400,9 +400,9 @@ verify pickUpItem fn applyItem(state: GameState, item: Item, idx: Int) -> GameState ? "Apply item effect" match item.kind - ItemKind.PotionOfPurity -> addMessage(GameState.update(state, player = Combat.healPlayer(state.player, item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You drink {item.name}. Purity restored! (+{Int.toString(item.value)} HP)") - ItemKind.ScrollOfPatternMatch -> addMessage(GameState.update(state, player = Player.update(state.player, attack = state.player.attack + item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You read {item.name}. Your pattern matching grows stronger! (+{Int.toString(item.value)} ATK)") - ItemKind.ShieldOfImmutability -> addMessage(GameState.update(state, player = Player.update(state.player, defense = state.player.defense + item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You equip {item.name}. Nothing can change you! (+{Int.toString(item.value)} DEF)") + ItemKind.PotionOfPurity -> addMessage(GameState.update(state, player = Combat.healPlayer(state.player, item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You drink {item.name}. Purity restored! (+{String.fromInt(item.value)} HP)") + ItemKind.ScrollOfPatternMatch -> addMessage(GameState.update(state, player = Player.update(state.player, attack = state.player.attack + item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You read {item.name}. Your pattern matching grows stronger! (+{String.fromInt(item.value)} ATK)") + ItemKind.ShieldOfImmutability -> addMessage(GameState.update(state, player = Player.update(state.player, defense = state.player.defense + item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You equip {item.name}. Nothing can change you! (+{String.fromInt(item.value)} DEF)") verify applyItem applyItem(GameState(gameMap = [], player = Player.update(Types.makePlayer(0, 0), hp = 10), entities = [], items = [Types.makePurity(0, 0)], visible = [], visGrid = [], remembered = [], messages = [], seed = 0, gameOver = false), Types.makePurity(0, 0), 0).player.hp => 18 @@ -533,8 +533,8 @@ fn ifElseAttack(state: GameState, entity: Entity, i: Int) -> GameState match Combat.isPlayerDead(newPlayer) true -> addMessage(GameState.update(state, player = newPlayer, gameOver = true), "{entity.name} branch-predicts your death!") false -> match doubled == 1 - true -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} takes BOTH branches! {Int.toString(totalDmg)} dmg!") - false -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} hits you for {Int.toString(dmg)}.") + true -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} takes BOTH branches! {String.fromInt(totalDmg)} dmg!") + false -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} hits you for {String.fromInt(dmg)}.") fn loopAttack(state: GameState, entity: Entity, i: Int) -> GameState ? "for-loop: normal attack but respawns once when killed (off-by-one)" @@ -542,7 +542,7 @@ fn loopAttack(state: GameState, entity: Entity, i: Int) -> GameState newPlayer = Combat.applyDamageToPlayer(state.player, dmg) match Combat.isPlayerDead(newPlayer) true -> addMessage(GameState.update(state, player = newPlayer, gameOver = true), "{entity.name} iterates you to death!") - false -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} iterates on you for {Int.toString(dmg)}!") + false -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} iterates on you for {String.fromInt(dmg)}!") verify loopAttack loopAttack(GameState(gameMap = [], player = Types.makePlayer(0, 0), entities = [Types.makeLoop(1, 0)], items = [], visible = [], visGrid = [], remembered = [], messages = [], seed = 0, gameOver = false), Types.makeLoop(1, 0), 0).player.hp < 20 => true @@ -551,7 +551,7 @@ fn nullAttack(state: GameState, entity: Entity, i: Int) -> GameState ? "null: steals EXP instead of dealing HP damage (null dereference — wipes memory)" stolen = Int.max(1, state.player.exp / 4) newPlayer = Player.update(state.player, exp = Int.max(0, state.player.exp - stolen)) - addMessage(GameState.update(state, player = newPlayer), "{entity.name} dereferences your memory! -{Int.toString(stolen)} EXP!") + addMessage(GameState.update(state, player = newPlayer), "{entity.name} dereferences your memory! -{String.fromInt(stolen)} EXP!") verify nullAttack nullAttack(GameState(gameMap = [], player = Player.update(Types.makePlayer(0, 0), exp = 40), entities = [Types.makeNull(1, 0)], items = [], visible = [], visGrid = [], remembered = [], messages = [], seed = 0, gameOver = false), Types.makeNull(1, 0), 0).player.exp => 30 @@ -655,7 +655,7 @@ fn descend(state: GameState) -> GameState itms = spawnItems(newSeed, rooms, 0) vis = Fov.computeFov(newMap, Types.pt(px, py), 8) rem = markRemembered(emptyRemembered(0), vis, 0) - addMessage(GameState(gameMap = newMap, player = newPlayer, entities = ents, items = itms, visible = vis, visGrid = buildVisGrid(vis, emptyRemembered(0), 0), remembered = rem, messages = state.messages, seed = Map.nextSeed(newSeed), gameOver = false), "You descend to floor {Int.toString(newFloor)} of Non-Aver!") + addMessage(GameState(gameMap = newMap, player = newPlayer, entities = ents, items = itms, visible = vis, visGrid = buildVisGrid(vis, emptyRemembered(0), 0), remembered = rem, messages = state.messages, seed = Map.nextSeed(newSeed), gameOver = false), "You descend to floor {String.fromInt(newFloor)} of Non-Aver!") verify descend descend(initGame(42)).player.floor => 2 @@ -715,7 +715,7 @@ fn gameLoop(state: GameState) -> Result ] renderFrame(state) match state.gameOver - true -> Result.Ok("Game over on floor {Int.toString(state.player.floor)}. EXP: {Int.toString(state.player.exp)}. The Aver way prevails!") + true -> Result.Ok("Game over on floor {String.fromInt(state.player.floor)}. EXP: {String.fromInt(state.player.exp)}. The Aver way prevails!") false -> waitAndProcess(state) fn waitAndProcess(state: GameState) -> Result diff --git a/examples/games/snake.av b/examples/games/snake.av index e10a086b..c169a9b0 100644 --- a/examples/games/snake.av +++ b/examples/games/snake.av @@ -274,7 +274,7 @@ fn render(state: GameState) -> Unit drawAt(state.food.x, state.food.y, "██") Terminal.resetColor() Terminal.moveTo(0, state.height) - Terminal.print("Score: {Int.toString(state.score)} | Q / ESC to quit") + Terminal.print("Score: {String.fromInt(state.score)} | Q / ESC to quit") Terminal.flush() fn handleKey(state: GameState, k: String) -> GameState @@ -304,7 +304,7 @@ fn tick(state: GameState) -> Result Time.sleep, ] match state.gameOver - true -> Result.Ok("Game over! Score: {Int.toString(state.score)}") + true -> Result.Ok("Game over! Score: {String.fromInt(state.score)}") false -> tickMove(moveSnake(state)) fn tickMove(moved: GameState) -> Result @@ -315,7 +315,7 @@ fn tickMove(moved: GameState) -> Result Time.sleep, ] match checkCollision(moved) - true -> Result.Ok("Game over! Score: {Int.toString(moved.score)}") + true -> Result.Ok("Game over! Score: {String.fromInt(moved.score)}") false -> match didEatFood(moved) true -> gameLoop(growSnake(randomFood(moved))) false -> gameLoop(moved) diff --git a/examples/games/tetris/main.av b/examples/games/tetris/main.av index 41172ebb..db30b14f 100644 --- a/examples/games/tetris/main.av +++ b/examples/games/tetris/main.av @@ -127,11 +127,11 @@ fn renderInfo(state: GameState) -> Unit ? "Draw score, level, lines info" ! [Terminal.moveTo, Terminal.print] Terminal.moveTo(26, 2) - Terminal.print("Score: {Int.toString(state.score)}") + Terminal.print("Score: {String.fromInt(state.score)}") Terminal.moveTo(26, 4) - Terminal.print("Level: {Int.toString(state.level)}") + Terminal.print("Level: {String.fromInt(state.level)}") Terminal.moveTo(26, 6) - Terminal.print("Lines: {Int.toString(state.lines)}") + Terminal.print("Lines: {String.fromInt(state.lines)}") Terminal.moveTo(26, 10) Terminal.print("Controls:") Terminal.moveTo(26, 11) @@ -204,7 +204,7 @@ fn tick(state: GameState) -> Result Time.sleep, ] match state.gameOver - true -> Result.Ok("Game over! Score: {Int.toString(state.score)} Lines: {Int.toString(state.lines)}") + true -> Result.Ok("Game over! Score: {String.fromInt(state.score)} Lines: {String.fromInt(state.lines)}") false -> tickGravity(state) fn gameLoop(state: GameState) -> Result diff --git a/examples/games/wumpus.av b/examples/games/wumpus.av index a1847be4..721c4638 100644 --- a/examples/games/wumpus.av +++ b/examples/games/wumpus.av @@ -48,7 +48,7 @@ verify validRoom fn roomListItem(r: Int, rest: List) -> String ? "Format a single room entry and recurse on remainder." - rs = Int.toString(r) + rs = String.fromInt(r) match rest [] -> rs _ -> "{rs}, {roomList(rest)}" @@ -246,10 +246,10 @@ fn showRoom(game: Game) -> Unit ? "Display current room, tunnels, and hazard warnings." ! [Console.print] Console.print("") - Console.print("You are in room {Int.toString(game.player)}.") + Console.print("You are in room {String.fromInt(game.player)}.") Console.print("Tunnels lead to: {roomList(neighbors(game.player))}") Console.print(warningText(game)) - Console.print("Arrows: {Int.toString(game.arrows)}") + Console.print("Arrows: {String.fromInt(game.arrows)}") fn gameLoop(game: Game) -> Result ? "Main game loop — one turn per recursive call." @@ -311,7 +311,7 @@ fn teleportAndCheck(game: Game) -> Result ? "Bats carry you to a random room; check hazards there." ! [Console.print, Console.readLine, Random.int] dest = randRoom() - Console.print("Super bats carry you to room {Int.toString(dest)}!") + Console.print("Super bats carry you to room {String.fromInt(dest)}!") resolveRoomCheck(checkRoom(Game.update(game, player = dest))) fn retry(game: Game, msg: String) -> Result diff --git a/examples/services/redis.av b/examples/services/redis.av index be070750..948d9fd8 100644 --- a/examples/services/redis.av +++ b/examples/services/redis.av @@ -40,7 +40,7 @@ record RedisConfig fn respBulkLen(s: String) -> String ? "Returns the byte-length of s as a string, used to build RESP bulk-string headers." - Int.toString(String.byteLength(s)) + String.fromInt(String.byteLength(s)) verify respBulkLen respBulkLen("PING") => "4" @@ -77,7 +77,7 @@ fn sendArgs(conn: Tcp.Connection, args: List) -> Result) -> Result ? "Sends a full RESP command: writes the '*N' array header then each arg via sendArgs." ! [Tcp.writeLine] - _ = Tcp.writeLine(conn, "*{Int.toString(List.len(args))}")? + _ = Tcp.writeLine(conn, "*{String.fromInt(List.len(args))}")? sendArgs(conn, args) // Public API diff --git a/src/checker/perf.rs b/src/checker/perf.rs index 95ec8246..e9a17f23 100644 --- a/src/checker/perf.rs +++ b/src/checker/perf.rs @@ -99,7 +99,7 @@ fn expr_to_short_str(expr: &Expr) -> String { } /// Check whether an expression mentions only identifiers from `allowed` and literals. -/// Namespace prefixes (e.g. `Int` in `Int.toFloat(...)`) are treated as constants, +/// Namespace prefixes (e.g. `Int` in `Float.fromInt(...)`) are treated as constants, /// not as variable references that need to be in `allowed`. fn expr_uses_only(expr: &Expr, allowed: &HashSet) -> bool { match expr { @@ -110,7 +110,7 @@ fn expr_uses_only(expr: &Expr, allowed: &HashSet) -> bool { } Expr::FnCall(callee, args) => { let callee_ok = match &callee.node { - // Namespace call like `Int.toFloat(...)` — the namespace is a constant + // Namespace call like `Float.fromInt(...)` — the namespace is a constant Expr::Attr(obj, _) => { if let Expr::Ident(ns) = &obj.node { // Check if this is a known pure namespace prefix @@ -905,15 +905,15 @@ mod tests { #[test] fn b4_loop_invariant_warns() { // fn draw(x: Int, y: Int, width: Int) -> Int - // w = Int.toFloat(width) + // w = Float.fromInt(width) // ... // TailCall("draw", [x + 1, y, width]) // - // `width` is forwarded unchanged → `Int.toFloat(width)` is loop-invariant + // `width` is forwarded unchanged → `Float.fromInt(width)` is loop-invariant let int_to_float_call = Expr::FnCall( Box::new(spanned(Expr::Attr( - Box::new(spanned(ident("Int"))), - "toFloat".to_string(), + Box::new(spanned(ident("Float"))), + "fromInt".to_string(), ))), vec![spanned(ident("width"))], ); @@ -939,7 +939,7 @@ mod tests { assert!( warnings .iter() - .any(|w| w.message.contains("Int.toFloat(width)") + .any(|w| w.message.contains("Float.fromInt(width)") && w.message.contains("doesn't change")), "expected loop-invariant warning, got {:?}", warnings @@ -949,12 +949,12 @@ mod tests { #[test] fn b4_changed_param_no_warning() { // fn draw(x: Int, width: Int) -> Int - // w = Int.toFloat(width) + // w = Float.fromInt(width) // TailCall("draw", [x + 1, width - 1]) ← width is NOT forwarded unchanged let int_to_float_call = Expr::FnCall( Box::new(spanned(Expr::Attr( - Box::new(spanned(ident("Int"))), - "toFloat".to_string(), + Box::new(spanned(ident("Float"))), + "fromInt".to_string(), ))), vec![spanned(ident("width"))], ); @@ -977,7 +977,7 @@ mod tests { let items = vec![TopLevel::FnDef(fd)]; let warnings = collect_perf_warnings(&items); assert!( - !warnings.iter().any(|w| w.message.contains("Int.toFloat")), + !warnings.iter().any(|w| w.message.contains("Float.fromInt")), "expected no warning when width changes, got {:?}", warnings ); @@ -1048,12 +1048,12 @@ mod tests { #[test] fn b4_no_tailcall_no_warning() { // fn f(x: Int, y: Int) -> Int - // w = Int.toFloat(y) + // w = Float.fromInt(y) // w (no TailCall → not a recursive fn in TCO sense) let int_to_float_call = Expr::FnCall( Box::new(spanned(Expr::Attr( - Box::new(spanned(ident("Int"))), - "toFloat".to_string(), + Box::new(spanned(ident("Float"))), + "fromInt".to_string(), ))), vec![spanned(ident("y"))], ); diff --git a/src/codegen/builtins.rs b/src/codegen/builtins.rs index db1db69a..643356e9 100644 --- a/src/codegen/builtins.rs +++ b/src/codegen/builtins.rs @@ -17,13 +17,10 @@ pub(crate) enum Builtin { // --- Int --- IntAbs, - IntToFloat, - IntToString, + IntFromFloat, IntFromString, - IntParse, IntMin, IntMax, - IntRem, IntMod, // --- Float --- @@ -33,10 +30,8 @@ pub(crate) enum Builtin { FloatRound, FloatFloor, FloatCeil, - FloatToInt, - FloatToString, + FloatFromInt, FloatFromString, - FloatParse, FloatPi, FloatMin, FloatMax, @@ -46,7 +41,6 @@ pub(crate) enum Builtin { // --- String --- StringLen, - StringConcat, StringCharAt, StringChars, StringSlice, @@ -99,10 +93,9 @@ pub(crate) enum Builtin { VectorSet, VectorLen, VectorFromList, - VectorToList, + ListFromVector, // --- Map --- - MapEmpty, MapGet, MapSet, MapHas, @@ -132,13 +125,10 @@ pub(crate) fn recognize_builtin(name: &str) -> Option { // Int "Int.abs" => Builtin::IntAbs, - "Int.toFloat" | "Float.fromInt" => Builtin::IntToFloat, - "Int.toString" => Builtin::IntToString, + "Int.fromFloat" => Builtin::IntFromFloat, "Int.fromString" => Builtin::IntFromString, - "Int.parse" => Builtin::IntParse, "Int.min" => Builtin::IntMin, "Int.max" => Builtin::IntMax, - "Int.rem" => Builtin::IntRem, "Int.mod" => Builtin::IntMod, // Float @@ -148,10 +138,8 @@ pub(crate) fn recognize_builtin(name: &str) -> Option { "Float.round" => Builtin::FloatRound, "Float.floor" => Builtin::FloatFloor, "Float.ceil" => Builtin::FloatCeil, - "Float.toInt" => Builtin::FloatToInt, - "Float.toString" => Builtin::FloatToString, + "Float.fromInt" => Builtin::FloatFromInt, "Float.fromString" => Builtin::FloatFromString, - "Float.parse" => Builtin::FloatParse, "Float.pi" => Builtin::FloatPi, "Float.min" => Builtin::FloatMin, "Float.max" => Builtin::FloatMax, @@ -161,7 +149,6 @@ pub(crate) fn recognize_builtin(name: &str) -> Option { // String "String.len" => Builtin::StringLen, - "String.concat" => Builtin::StringConcat, "String.charAt" => Builtin::StringCharAt, "String.chars" => Builtin::StringChars, "String.slice" => Builtin::StringSlice, @@ -214,10 +201,9 @@ pub(crate) fn recognize_builtin(name: &str) -> Option { "Vector.set" => Builtin::VectorSet, "Vector.len" => Builtin::VectorLen, "Vector.fromList" => Builtin::VectorFromList, - "Vector.toList" => Builtin::VectorToList, + "List.fromVector" => Builtin::ListFromVector, // Map - "Map.empty" => Builtin::MapEmpty, "Map.get" => Builtin::MapGet, "Map.set" => Builtin::MapSet, "Map.has" => Builtin::MapHas, diff --git a/src/codegen/dafny/expr.rs b/src/codegen/dafny/expr.rs index 752b434e..c72c64c4 100644 --- a/src/codegen/dafny/expr.rs +++ b/src/codegen/dafny/expr.rs @@ -377,20 +377,21 @@ fn emit_dafny_builtin(b: crate::codegen::builtins::Builtin, a: &[String]) -> Str // Int IntAbs => format!("(if {} >= 0 then {} else -{})", a[0], a[0], a[0]), - IntToFloat => format!("({} as real)", a[0]), - IntToString | StringFromInt => format!("IntToString({})", a[0]), - IntFromString | IntParse => format!("IntFromString({})", a[0]), + IntFromFloat => format!("({} as int)", a[0]), + StringFromInt => format!("IntToString({})", a[0]), + IntFromString => format!("IntFromString({})", a[0]), IntMin => format!("(if {} <= {} then {} else {})", a[0], a[1], a[0], a[1]), IntMax => format!("(if {} >= {} then {} else {})", a[0], a[1], a[0], a[1]), - IntRem | IntMod => format!("Result.Ok(({} % {}))", a[0], a[1]), + IntMod => format!("Result.Ok(({} % {}))", a[0], a[1]), // Float FloatAbs => format!("(if {} >= 0.0 then {} else -{})", a[0], a[0], a[0]), FloatSqrt => format!("FloatSqrt({})", a[0]), FloatPow => format!("FloatPow({}, {})", a[0], a[1]), - FloatRound | FloatFloor | FloatCeil | FloatToInt => format!("FloatToInt({})", a[0]), - FloatToString | StringFromFloat => format!("FloatToString({})", a[0]), - FloatFromString | FloatParse => format!("FloatFromString({})", a[0]), + FloatRound | FloatFloor | FloatCeil => format!("FloatToInt({})", a[0]), + FloatFromInt => format!("({} as real)", a[0]), + StringFromFloat => format!("FloatToString({})", a[0]), + FloatFromString => format!("FloatFromString({})", a[0]), FloatPi => "FloatPi()".to_string(), FloatMin => format!("(if {} <= {} then {} else {})", a[0], a[1], a[0], a[1]), FloatMax => format!("(if {} >= {} then {} else {})", a[0], a[1], a[0], a[1]), @@ -400,7 +401,6 @@ fn emit_dafny_builtin(b: crate::codegen::builtins::Builtin, a: &[String]) -> Str // String StringLen => format!("|{}|", a[0]), - StringConcat => format!("({} + {})", a[0], a[1]), StringCharAt => format!("StringCharAt({}, {})", a[0], a[1]), StringChars => format!("StringChars({})", a[0]), StringSlice => format!("{}[{}..{}]", a[0], a[1], a[2]), @@ -465,10 +465,9 @@ fn emit_dafny_builtin(b: crate::codegen::builtins::Builtin, a: &[String]) -> Str ), VectorLen => format!("|{}|", a[0]), VectorFromList => a[0].clone(), - VectorToList => a[0].clone(), + ListFromVector => a[0].clone(), // Map - MapEmpty => "map[]".to_string(), MapGet => format!("MapGet({}, {})", a[0], a[1]), MapSet => format!("{}[{} := {}]", a[0], a[1], a[2]), MapHas => format!("({} in {})", a[1], a[0]), diff --git a/src/codegen/lean/builtins.rs b/src/codegen/lean/builtins.rs index 251b545a..d5484cdd 100644 --- a/src/codegen/lean/builtins.rs +++ b/src/codegen/lean/builtins.rs @@ -42,24 +42,21 @@ pub fn emit_builtin_call( // ---- Int ---- IntAbs => format!("{}.natAbs", p(&a[0])), - IntToFloat => format!("Float.ofInt {}", p(&a[0])), - IntToString => format!("toString {}", p(&a[0])), + IntFromFloat => format!("AverFloat.toInt {}", p(&a[0])), IntMin => format!("min {} {}", p(&a[0]), p(&a[1])), IntMax => format!("max {} {}", p(&a[0]), p(&a[1])), - IntRem | IntMod => format!("(Except.ok ({} % {}) : Except String Int)", a[0], a[1]), + IntMod => format!("(Except.ok ({} % {}) : Except String Int)", a[0], a[1]), IntFromString => format!("Int.fromString {}", p(&a[0])), - IntParse => format!("Int.fromString {}", p(&a[0])), // ---- Float ---- FloatAbs => format!("Float.abs {}", p(&a[0])), FloatSqrt => format!("Float.sqrt {}", p(&a[0])), - FloatToString => format!("toString {}", p(&a[0])), - FloatFromString | FloatParse => format!("Float.fromString {}", p(&a[0])), + FloatFromString => format!("Float.fromString {}", p(&a[0])), + FloatFromInt => format!("Float.ofInt {}", p(&a[0])), FloatPow => format!("AverFloat.pow {} {}", p(&a[0]), p(&a[1])), FloatRound => format!("AverFloat.round {}", p(&a[0])), FloatFloor => format!("AverFloat.floor {}", p(&a[0])), FloatCeil => format!("AverFloat.ceil {}", p(&a[0])), - FloatToInt => format!("AverFloat.toInt {}", p(&a[0])), FloatPi => "(3.141592653589793 : Float)".to_string(), FloatMin => format!("min {} {}", p(&a[0]), p(&a[1])), FloatMax => format!("max {} {}", p(&a[0]), p(&a[1])), @@ -82,7 +79,6 @@ pub fn emit_builtin_call( // ---- String ---- StringLen => format!("{}.length", p(&a[0])), - StringConcat => format!("({} ++ {})", p(&a[0]), p(&a[1])), StringCharAt => format!("String.charAt {} {}", p(&a[0]), p(&a[1])), StringChars => format!("String.chars {}", p(&a[0])), StringSlice => format!("String.slice {} {} {}", p(&a[0]), p(&a[1]), p(&a[2])), @@ -132,10 +128,9 @@ pub fn emit_builtin_call( ), VectorLen => format!("{}.size", p(&a[0])), VectorFromList => format!("{}.toArray", p(&a[0])), - VectorToList => format!("{}.toList", p(&a[0])), + ListFromVector => format!("{}.toList", p(&a[0])), // ---- Map ---- - MapEmpty => "AverMap.empty".to_string(), MapGet => format!("AverMap.get {} {}", p(&a[0]), p(&a[1])), MapSet => format!("AverMap.set {} {} {}", p(&a[0]), p(&a[1]), p(&a[2])), MapHas => format!("AverMap.has {} {}", p(&a[0]), p(&a[1])), diff --git a/src/codegen/lean/expr.rs b/src/codegen/lean/expr.rs index b683fa9b..4d90f760 100644 --- a/src/codegen/lean/expr.rs +++ b/src/codegen/lean/expr.rs @@ -405,14 +405,8 @@ fn extract_bool_arms(arms: &[MatchArm]) -> Option<(&Spanned, &Spanned String { match stmt { Stmt::Binding(name, type_ann, expr) => { - let mut val = emit_expr(expr, ctx); - // Map binding initialized with Map.empty → set empty - if let Some(ann) = type_ann - && crate::codegen::common::is_set_annotation(ann) - && val == "AverMap.empty" - { - val = "AverSet.empty".to_string(); - } + let val = emit_expr(expr, ctx); + let _ = type_ann; format!("let {} := {}", aver_name_to_lean(name), val) } Stmt::Expr(expr) => emit_expr(expr, ctx), diff --git a/src/codegen/rust/builtins.rs b/src/codegen/rust/builtins.rs index 7c31d5b4..55156ed1 100644 --- a/src/codegen/rust/builtins.rs +++ b/src/codegen/rust/builtins.rs @@ -12,10 +12,6 @@ fn builtin_needs_str_conversion(name: &str) -> bool { name, "Console.readLine" | "Time.now" - | "Int.toString" - | "Float.toString" - | "Int.parse" - | "Float.parse" | "Int.fromString" | "Float.fromString" | "String.slice" @@ -410,17 +406,9 @@ fn emit_builtin_call_inner( let arg = emit_arg(0); Some(format!("{}.abs()", arg)) } - "Int.toFloat" => { + "Int.fromFloat" => { let arg = emit_arg(0); - Some(format!("({} as f64)", arg)) - } - "Int.toString" => { - let arg = emit_arg(0); - Some(format!("{}.to_string()", arg)) - } - "Int.parse" => { - let arg = emit_arg(0); - Some(format!("{}.parse::().map_err(|e| e.to_string())", arg)) + Some(format!("({} as i64)", arg)) } "Int.fromString" => { let arg = emit_arg(0); @@ -436,11 +424,6 @@ fn emit_builtin_call_inner( let b = emit_arg(1); Some(format!("{}.max({})", a, b)) } - "Int.rem" => { - let a = emit_arg(0); - let b = emit_arg(1); - Some(format!("({} % {})", a, b)) - } "Int.mod" => { let a = emit_arg(0); let b = emit_arg(1); @@ -470,18 +453,6 @@ fn emit_builtin_call_inner( let arg = emit_arg(0); Some(format!("{}.parse::().map_err(|e| e.to_string())", arg)) } - "Float.toInt" => { - let arg = emit_arg(0); - Some(format!("({} as i64)", arg)) - } - "Float.toString" => { - let arg = emit_arg(0); - Some(format!("{}.to_string()", arg)) - } - "Float.parse" => { - let arg = emit_arg(0); - Some(format!("{}.parse::().map_err(|e| e.to_string())", arg)) - } "Float.sqrt" => { let arg = emit_arg(0); Some(format!("{}.sqrt()", arg)) @@ -662,7 +633,6 @@ fn emit_builtin_call_inner( )) } // ---- Map ---- - "Map.empty" => Some("HashMap::new()".to_string()), "Map.fromList" => { let list = clone_arg(&args[0], ctx, ectx); Some(format!( @@ -799,7 +769,7 @@ fn emit_builtin_call_inner( let list = emit_arg(0); Some(format!("aver_rt::AverVector::from_vec({}.to_vec())", list)) } - "Vector.toList" => { + "List.fromVector" => { let vec = emit_arg(0); Some(format!("{}.to_list()", vec)) } diff --git a/src/codegen/rust/expr.rs b/src/codegen/rust/expr.rs index 783e1b01..6630c532 100644 --- a/src/codegen/rust/expr.rs +++ b/src/codegen/rust/expr.rs @@ -1074,7 +1074,6 @@ fn expr_is_numeric(expr: &Expr, ectx: &EmitCtx) -> bool { "Int.abs" | "Int.min" | "Int.max" - | "Int.rem" | "Float.abs" | "Float.floor" | "Float.ceil" @@ -1087,7 +1086,6 @@ fn expr_is_numeric(expr: &Expr, ectx: &EmitCtx) -> bool { | "Float.cos" | "Float.atan2" | "Float.fromInt" - | "Int.toFloat" | "List.len" | "Vector.len" | "Map.len" diff --git a/src/codegen/wasm/alloc_policy.rs b/src/codegen/wasm/alloc_policy.rs index d5fdf376..866e31cf 100644 --- a/src/codegen/wasm/alloc_policy.rs +++ b/src/codegen/wasm/alloc_policy.rs @@ -46,7 +46,7 @@ fn is_pure_non_alloc_builtin(name: &str) -> bool { "Int.abs" | "Int.min" | "Int.max" - | "Int.toFloat" + | "Float.fromInt" // Float arithmetic, transcendentals, constants, predicates. | "Float.abs" | "Float.floor" @@ -60,7 +60,6 @@ fn is_pure_non_alloc_builtin(name: &str) -> bool { | "Float.pow" | "Float.atan2" | "Float.pi" - | "Float.fromInt" // Char ↔ code-point. `Char.fromCode` returns Option // (allocates the Some wrapper around a heap String); only the // reverse direction is pure. @@ -96,7 +95,7 @@ mod tests { assert!(!p.builtin_allocates("Float.sin")); assert!(!p.builtin_allocates("Float.sqrt")); assert!(!p.builtin_allocates("Int.abs")); - assert!(!p.builtin_allocates("Int.toFloat")); + assert!(!p.builtin_allocates("Float.fromInt")); } #[test] diff --git a/src/codegen/wasm/expr/builtins.rs b/src/codegen/wasm/expr/builtins.rs index b07fa00a..cd97d811 100644 --- a/src/codegen/wasm/expr/builtins.rs +++ b/src/codegen/wasm/expr/builtins.rs @@ -116,21 +116,17 @@ impl<'a> ExprEmitter<'a> { "Float.fromInt" if args.len() == 1 => { self.instructions.push(Instruction::F64ConvertI64S); } - "Int.toFloat" if args.len() == 1 => { + "Float.fromInt" if args.len() == 1 => { self.instructions.push(Instruction::F64ConvertI64S); } - "Int.toString" if args.len() == 1 => { + "String.fromInt" if args.len() == 1 => { self.instructions .push(Instruction::Call(self.rt.i64_to_str_obj)); } - "Float.toString" if args.len() == 1 => { + "String.fromFloat" if args.len() == 1 => { self.instructions .push(Instruction::Call(self.rt.f64_to_str_obj)); } - "Map.empty" if args.is_empty() => { - // Empty map = empty association list = null ptr - self.instructions.push(Instruction::I32Const(0)); - } "Map.get" if args.len() == 2 => { // args on stack: [map(i32), key(?)] // ABI: rt_map_get(map: i32, key: i64, kind: i32) -> i32 @@ -348,7 +344,7 @@ impl<'a> ExprEmitter<'a> { )); self.instructions.push(Instruction::Call(self.rt.vec_new)); } - "Vector.toList" if args.len() == 1 => { + "List.fromVector" if args.len() == 1 => { self.instructions .push(Instruction::Call(self.rt.vec_to_list)); } @@ -630,7 +626,7 @@ impl<'a> ExprEmitter<'a> { self.instructions .push(Instruction::F64Const(std::f64::consts::PI)); } - "Float.toInt" if args.len() == 1 => { + "Int.fromFloat" if args.len() == 1 => { self.instructions.push(Instruction::I64TruncF64S); } "Float.fromString" if args.len() == 1 => { diff --git a/src/codegen/wasm_gc/body.rs b/src/codegen/wasm_gc/body.rs index 714c4f38..4f2b558f 100644 --- a/src/codegen/wasm_gc/body.rs +++ b/src/codegen/wasm_gc/body.rs @@ -35,6 +35,7 @@ use crate::ir::CallLowerCtx; mod builtins; mod emit; pub(super) mod eq_helpers; +pub(super) mod hash_helpers; mod infer; mod slots; @@ -158,6 +159,7 @@ pub(super) fn emit_fn_body( self_wasm_idx: u32, registry: &TypeRegistry, effect_idx_lookup: &HashMap, + caller_fn_collector: &std::cell::RefCell, ) -> Result, WasmGcError> { let slots = SlotTable::build_for_fn(fd, registry, fn_map)?; let FnBody::Block(stmts) = fd.body.as_ref(); @@ -187,6 +189,7 @@ pub(super) fn emit_fn_body( params: &fd.params, binding_names: &binding_names, effect_idx_lookup, + caller_fn_collector, }; for (i, stmt) in stmts.iter().enumerate() { @@ -247,6 +250,35 @@ pub(super) fn emit_fn_body( Ok(slots.extra_locals(count_value_params(&fd.params))) } +/// Lazy-populated registry of caller_fn names actually emitted by the +/// codegen. Replaces the AST walker `fn_body_emits_effect_call` from +/// 0.16.2 — every site that calls `emit_caller_fn_idx` registers the +/// fn name here on demand, so the wasm output's caller-fn name table +/// contains exactly the fns whose bodies emit a caller_fn slot. Zero +/// false positives, zero rozjazdy walker↔codegen. +#[derive(Default)] +pub(super) struct CallerFnCollector { + /// Aver fn name → idx in the exported caller-fn table (0..N). + pub(super) idx_by_name: HashMap, + /// Insertion order — the host walks `__caller_fn_name(0..N)` and + /// the i-th entry must match `names[i]`. + pub(super) names: Vec, +} + +impl CallerFnCollector { + /// Get-or-insert a name, returning the i32 idx the codegen emits + /// at the call site. New names land at the end of `names`. + pub(super) fn register(&mut self, name: &str) -> u32 { + if let Some(&i) = self.idx_by_name.get(name) { + return i; + } + let i = self.names.len() as u32; + self.names.push(name.to_string()); + self.idx_by_name.insert(name.to_string(), i); + i + } +} + /// Per-fn lowering context — read-only state every emit fn needs. pub(super) struct EmitCtx<'a> { pub(super) fn_map: &'a FnMap, @@ -276,6 +308,12 @@ pub(super) struct EmitCtx<'a> { /// independent product anywhere — discovery only registers the /// three host imports if it sees one. pub(super) effect_idx_lookup: &'a HashMap, + /// Lazy collector for caller_fn names. `emit_caller_fn_idx` + /// registers the current fn name here on each effect call site; + /// the eventual `__caller_fn_name(idx)` body and data segments + /// are emitted from `names` after every fn body has been + /// produced. + pub(super) caller_fn_collector: &'a std::cell::RefCell, } impl<'a> EmitCtx<'a> { diff --git a/src/codegen/wasm_gc/body/builtins.rs b/src/codegen/wasm_gc/body/builtins.rs index 3079b7dd..c97932e6 100644 --- a/src/codegen/wasm_gc/body/builtins.rs +++ b/src/codegen/wasm_gc/body/builtins.rs @@ -60,7 +60,7 @@ pub(super) fn emit_dotted_builtin( for arg in args { emit_expr(func, arg, slots, ctx)?; } - super::emit::emit_caller_fn_global(func, ctx)?; + super::emit::emit_caller_fn_idx(func, ctx)?; func.instruction(&Instruction::Call(wasm_idx)); return Ok(()); } @@ -155,13 +155,6 @@ pub(super) fn emit_dotted_builtin( func.instruction(&Instruction::F64Const(std::f64::consts::PI)); Ok(()) } - // `Int.toFloat` is the same op as `Float.fromInt` — Aver has - // both spellings; map both to the same instruction. - "Int.toFloat" if args.len() == 1 => { - emit_expr(func, &args[0], slots, ctx)?; - func.instruction(&Instruction::F64ConvertI64S); - Ok(()) - } "Int.abs" if args.len() == 1 => { // Branched: if (x < 0) 0 - x else x. Two evaluations of x; // cheap when x is a Resolved local. @@ -295,20 +288,15 @@ pub(super) fn emit_dotted_builtin( // Int.toString / Float.toString — Aver source allows both, // backend points each at the same helper. "String.fromInt" if args.len() == 1 => { - let to_string_idx = - ctx.fn_map - .builtins - .get("Int.toString") - .copied() - .ok_or(WasmGcError::Validation( - "String.fromInt requires Int.toString builtin".into(), - ))?; + let to_string_idx = ctx.fn_map.builtins.get("String.fromInt").copied().ok_or( + WasmGcError::Validation("String.fromInt requires Int.toString builtin".into()), + )?; emit_expr(func, &args[0], slots, ctx)?; func.instruction(&Instruction::Call(to_string_idx)); Ok(()) } "String.fromFloat" if args.len() == 1 => { - let to_string_idx = ctx.fn_map.builtins.get("Float.toString").copied().ok_or( + let to_string_idx = ctx.fn_map.builtins.get("String.fromFloat").copied().ok_or( WasmGcError::Validation("String.fromFloat requires Float.toString builtin".into()), )?; emit_expr(func, &args[0], slots, ctx)?; @@ -387,9 +375,9 @@ pub(super) fn emit_dotted_builtin( emit_option_to_result(func, &args[0], &args[1], slots, ctx) } // Map — dispatch to the per-instantiation helper. The - // canonical comes from inferring the type of the map argument - // (or the surrounding context for Map.empty). - "Map.empty" => emit_map_empty_call(func, args, slots, ctx), + // canonical comes from inferring the type of the map argument. + // Empty map literals (`{}`) flow through `emit_map_literal` + // — there is no `Map.empty()` builtin. "Map.set" | "Map.get" | "Map.len" | "Map.has" | "Map.keys" | "Map.values" | "Map.remove" | "Map.entries" => emit_map_kv_call(func, method, args, slots, ctx), "Map.fromList" if args.len() == 1 => emit_map_from_list_call(func, &args[0], slots, ctx), @@ -415,7 +403,7 @@ pub(super) fn emit_dotted_builtin( "List.zip" if args.len() == 2 => emit_list_zip_call(func, &args[0], &args[1], slots, ctx), // Vector.fromList(list: List) -> Vector "Vector.fromList" if args.len() == 1 => emit_vec_from_list_call(func, &args[0], slots, ctx), - "Vector.toList" if args.len() == 1 => emit_vec_to_list_call(func, &args[0], slots, ctx), + "List.fromVector" if args.len() == 1 => emit_vec_to_list_call(func, &args[0], slots, ctx), // String.split / String.join — singleton (T=String). "String.split" if args.len() == 2 => { let ops = ctx.fn_map.string_split_ops.ok_or(WasmGcError::Validation( @@ -444,11 +432,6 @@ pub(super) fn emit_dotted_builtin( } } -/// `Map.empty()` → `call $map_empty_KV`. With a single registered -/// instantiation the canonical is unambiguous; with several, the type -/// must be deducible from the surrounding context (which today only -/// works when one instantiation exists — generalising would mean -/// threading expected-type through expression emission). /// Inline lowering of `Args.get()` (no args, returns `List`). /// Host imports are `args_len(): i64` and `args_get(i: i64): String`; /// no `args_get_all`. Walks `i = len-1 .. 0` cons-building the list so @@ -492,7 +475,7 @@ pub(super) fn emit_args_get_inline( ))?; // len = args_len() - super::emit::emit_caller_fn_global(func, ctx)?; + super::emit::emit_caller_fn_idx(func, ctx)?; func.instruction(&Instruction::Call(args_len_idx)); func.instruction(&Instruction::LocalSet(len_slot)); // i = len - 1 @@ -515,7 +498,7 @@ pub(super) fn emit_args_get_inline( func.instruction(&Instruction::BrIf(1)); // s = args_get(i) func.instruction(&Instruction::LocalGet(i_slot)); - super::emit::emit_caller_fn_global(func, ctx)?; + super::emit::emit_caller_fn_idx(func, ctx)?; func.instruction(&Instruction::Call(args_get_idx)); func.instruction(&Instruction::LocalSet(s_slot)); // acc = struct.new List { head: s, tail: acc } @@ -537,44 +520,6 @@ pub(super) fn emit_args_get_inline( Ok(()) } -pub(super) fn emit_map_empty_call( - func: &mut Function, - args: &[Spanned], - _slots: &SlotTable, - ctx: &EmitCtx<'_>, -) -> Result<(), WasmGcError> { - if !args.is_empty() { - return Err(WasmGcError::Validation(format!( - "Map.empty expects 0 args, got {}", - args.len() - ))); - } - let canonical = if ctx.registry.map_order.len() == 1 { - ctx.registry.map_order[0].clone() - } else { - // Multi-Map module — disambiguate by checking the enclosing - // fn's return type. Common case: `fn build() -> Map` - // wraps a `Map.set(Map.empty(), ...)` chain; the empty call - // inherits its slot from the declared return type. - let ret_canonical = super::super::types::normalize_compound(ctx.return_type); - if ctx.registry.map_slots(&ret_canonical).is_some() { - ret_canonical - } else { - return Err(WasmGcError::Unimplemented( - "Map.empty across multiple Map instantiations needs context-driven type inference", - )); - } - }; - let helpers = ctx - .fn_map - .map_helpers_lookup(&canonical) - .ok_or(WasmGcError::Validation(format!( - "Map.empty: helpers missing for `{canonical}`" - )))?; - func.instruction(&Instruction::Call(helpers.empty)); - Ok(()) -} - /// Map.set / Map.get / Map.len dispatch — the canonical is recovered /// from the map argument's inferred type, helper indices come from /// `fn_map.map_helpers`. @@ -1162,7 +1107,7 @@ pub(super) fn emit_interpolated_str( "String" => { /* identity */ } "Int" => { let to_string_idx = - ctx.fn_map.builtins.get("Int.toString").copied().ok_or( + ctx.fn_map.builtins.get("String.fromInt").copied().ok_or( WasmGcError::Validation( "interpolation of Int requires Int.toString builtin".into(), ), @@ -1171,7 +1116,7 @@ pub(super) fn emit_interpolated_str( } "Float" => { let to_string_idx = - ctx.fn_map.builtins.get("Float.toString").copied().ok_or( + ctx.fn_map.builtins.get("String.fromFloat").copied().ok_or( WasmGcError::Validation( "interpolation of Float requires Float.toString builtin".into(), ), diff --git a/src/codegen/wasm_gc/body/emit.rs b/src/codegen/wasm_gc/body/emit.rs index 0b5f31bd..f8e4bd98 100644 --- a/src/codegen/wasm_gc/body/emit.rs +++ b/src/codegen/wasm_gc/body/emit.rs @@ -556,25 +556,74 @@ pub(super) fn emit_expr( /// type mismatch. fn sum_or_record_eq_fn(operand: &Spanned, ctx: &EmitCtx<'_>) -> Option { let ty = operand.ty()?; - let crate::types::Type::Named(name) = ty else { - return None; - }; - // Newtypes already lower to their underlying primitive — no helper - // needed (the default i64/f64 eq handles them). - if ctx.registry.newtype_underlying(name).is_some() { - return None; + match ty { + crate::types::Type::Named(name) => { + // Newtypes already lower to their underlying primitive — + // no helper needed (the default i64/f64 eq handles them). + if ctx.registry.newtype_underlying(name).is_some() { + return None; + } + let is_record = ctx.registry.record_fields.contains_key(name); + let is_sum = ctx + .registry + .variants + .values() + .flat_map(|v| v.iter()) + .any(|v| &v.parent == name); + if !is_record && !is_sum { + return None; + } + ctx.fn_map.eq_helpers.get(name).copied() + } + // Generic carriers — `Option`, `Result`, `Tuple<…>` + // have per-instantiation `__eq_` helpers since + // 0.16.3. Lookup by the type's display canonical (whitespace- + // free, matching how the discovery walker registered it). + crate::types::Type::Option(_) + | crate::types::Type::Result(_, _) + | crate::types::Type::Tuple(_) => { + let canonical: String = ty + .display() + .chars() + .filter(|c| !c.is_whitespace()) + .collect(); + ctx.fn_map.eq_helpers.get(&canonical).copied() + } + // List / Vector — list_helpers / vfl_helpers slot the per-T + // eq fn at registration time. Dispatch through there. Both + // ops carry `eq: Option` (None when T isn't equality- + // resolvable; falls through and the surrounding default i64 + // arm fails validation, same as before this PR). + crate::types::Type::List(_) => { + let canonical: String = ty + .display() + .chars() + .filter(|c| !c.is_whitespace()) + .collect(); + ctx.fn_map.list_ops.get(&canonical).and_then(|ops| ops.eq) + } + crate::types::Type::Vector(_) => { + let canonical: String = ty + .display() + .chars() + .filter(|c| !c.is_whitespace()) + .collect(); + ctx.fn_map.vfl_ops.get(&canonical).and_then(|ops| ops.eq) + } + // Map structural eq — `__eq_Map` slot lives in + // MapHelperRegistry but we mirror the fn idx into + // `eq_helpers` so the lookup shape stays uniform with + // record/sum/carrier/list/vec dispatch. + crate::types::Type::Map(_, _) => { + let canonical: String = ty + .display() + .chars() + .filter(|c| !c.is_whitespace()) + .collect(); + ctx.fn_map.eq_helpers.get(&canonical).copied() + } + _ => None, } - let is_record = ctx.registry.record_fields.contains_key(name); - let is_sum = ctx - .registry - .variants - .values() - .flat_map(|v| v.iter()) - .any(|v| &v.parent == name); - if !is_record && !is_sum { - return None; - } - ctx.fn_map.eq_helpers.get(name).copied() } fn nullary_variant_idx(expr: &Spanned, ctx: &EmitCtx<'_>) -> Option { @@ -1322,7 +1371,7 @@ pub(super) fn emit_independent_product_unwrap( /// to match. fn emit_group_call(func: &mut Function, ctx: &EmitCtx<'_>, op: &str) { if let Some(idx) = ctx.effect_idx_lookup.get(op) { - if emit_caller_fn_global(func, ctx).is_err() { + if emit_caller_fn_idx(func, ctx).is_err() { // Should never trip — every fn def has a global allocated // in `TypeRegistry::build`. return; @@ -1339,7 +1388,7 @@ fn emit_group_call(func: &mut Function, ctx: &EmitCtx<'_>, op: &str) { fn emit_branch_marker(func: &mut Function, ctx: &EmitCtx<'_>, branch_idx: u32) { if let Some(idx) = ctx.effect_idx_lookup.get("__record_set_branch") { func.instruction(&Instruction::I64Const(branch_idx as i64)); - if emit_caller_fn_global(func, ctx).is_err() { + if emit_caller_fn_idx(func, ctx).is_err() { return; } func.instruction(&Instruction::Call(*idx)); @@ -2352,27 +2401,22 @@ pub(super) fn emit_string_match( Ok(()) } -/// Push the caller-fn String ref via `global.get`. Each fn def in -/// the program owns one immutable `(ref null $string)` global, -/// `array.new_data`-init from the fn-name passive segment at module -/// instantiation. The hot path is a single `global.get` per effect -/// call — zero alloc, ~3 bytes per call site. -pub(super) fn emit_caller_fn_global( +/// Push the caller-fn idx as an `i32` immediate. Lazy-registers the +/// current fn name with the collector so the post-emit phase knows +/// exactly which fn names to put in the exported caller-fn table. +/// Single source of truth: any code path that wants to label its +/// effect call site with the originating fn just calls this — no +/// AST walker, no rozjazdy walker↔codegen. Hot path: 2-3 bytes +/// (`i32.const `), zero allocation. +pub(super) fn emit_caller_fn_idx( func: &mut Function, ctx: &EmitCtx<'_>, ) -> Result<(), WasmGcError> { let idx = ctx - .registry - .caller_fn_globals - .get(ctx.self_fn_name) - .copied() - .ok_or_else(|| { - WasmGcError::Validation(format!( - "no caller_fn global registered for fn `{}`", - ctx.self_fn_name - )) - })?; - func.instruction(&Instruction::GlobalGet(idx)); + .caller_fn_collector + .borrow_mut() + .register(ctx.self_fn_name); + func.instruction(&Instruction::I32Const(idx as i32)); Ok(()) } diff --git a/src/codegen/wasm_gc/body/eq_helpers.rs b/src/codegen/wasm_gc/body/eq_helpers.rs index 09b28f8a..18044b88 100644 --- a/src/codegen/wasm_gc/body/eq_helpers.rs +++ b/src/codegen/wasm_gc/body/eq_helpers.rs @@ -36,11 +36,19 @@ use super::super::WasmGcError; use super::super::lists::{emit_record_eq_inline, emit_sum_eq_inline}; use super::super::types::TypeRegistry; -/// What kind of nominal type a registered eq helper covers. +/// What kind of type a registered eq helper covers. Records and +/// sums are nominal (registry name = type name). The generic- +/// carrier kinds (Option / Result / Tuple) carry their canonical +/// instantiation string as the registry name (e.g. `"Option"`, +/// `"Result"`, `"Tuple"`) — one helper slot +/// per concrete inner shape. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum EqKind { Record, Sum, + OptionEq, + ResultEq, + TupleEq, } /// Per-module registry of `__eq_` helpers needed by @@ -68,6 +76,186 @@ impl EqHelperRegistry { } } + /// Register `type_name` and recursively register every nominal + /// field type it transitively reaches. Needed because the + /// emitted `__eq_` body dispatches nested record/sum fields + /// by `Call(__eq_)` — those helper slots have to + /// exist even if the user never wrote `field == field` directly. + /// `register` is idempotent on cycle (Tree → Tree.Node → Tree) + /// so the recursion terminates. + pub(crate) fn register_transitive( + &mut self, + type_name: &str, + kind: EqKind, + registry: &TypeRegistry, + ) { + if self.kinds.contains_key(type_name) { + return; + } + // Skip nominal types whose fields contain unhandled shapes + // (List/Map/Vector/Set inside fields — the inline eq emitter + // covers Option/Result/Tuple via per-instantiation helpers + // registered alongside the parent below). Without this guard + // `register_field_type` silently dropped the unsupported + // field and the helper body emit later panicked with "no eq + // dispatch". + let mut seen = std::collections::HashSet::new(); + let resolvable = match kind { + EqKind::Record => { + super::super::lists::record_fields_resolvable(type_name, registry, &mut seen) + } + EqKind::Sum => { + super::super::lists::sum_fields_resolvable(type_name, registry, &mut seen) + } + // Carrier kinds — Option/Result/Tuple have one + // shape regardless of inner; resolvability is checked at + // the record/sum level via `field_type_resolvable`. + EqKind::OptionEq | EqKind::ResultEq | EqKind::TupleEq => true, + }; + if !resolvable { + return; + } + self.register(type_name, kind); + // Walk fields and recurse on nominal types. + match kind { + EqKind::Record => { + if let Some(fields) = registry.record_fields.get(type_name) { + for (_, field_ty) in fields { + self.register_field_type(field_ty.trim(), registry); + } + } + } + EqKind::Sum => { + let variants: Vec<_> = registry + .variants + .values() + .flat_map(|vs| vs.iter()) + .filter(|v| v.parent == type_name) + .cloned() + .collect(); + for v in &variants { + for field_ty in &v.fields { + self.register_field_type(field_ty.trim(), registry); + } + } + } + // Carrier kinds — recurse into inner types so a direct + // top-level register (e.g. discovery seed walker hitting + // `Option` from a `List>`) + // still registers PieceKind. When the carrier is reached + // via `register_field_type` the recursion happens there + // too; this duplicate is idempotent (slots dedup by + // canonical). + EqKind::OptionEq => { + if let Some(inner) = type_name + .strip_prefix("Option<") + .and_then(|s| s.strip_suffix('>')) + { + self.register_field_type(inner.trim(), registry); + } + } + EqKind::ResultEq => { + if let Some((ok, err)) = parse_result_kv(type_name) { + self.register_field_type(ok.trim(), registry); + self.register_field_type(err.trim(), registry); + } + } + EqKind::TupleEq => { + if let Some(elems) = parse_tuple_elems(type_name) { + for e in elems { + self.register_field_type(e.trim(), registry); + } + } + } + } + } + + fn register_field_type(&mut self, field_ty: &str, registry: &TypeRegistry) { + if matches!( + field_ty, + "Int" | "Float" | "Bool" | "String" | "Unit" | "Byte" | "Char" + ) { + return; + } + if registry.record_fields.contains_key(field_ty) { + self.register_transitive(field_ty, EqKind::Record, registry); + return; + } + if registry + .variants + .values() + .flat_map(|v| v.iter()) + .any(|v| v.parent == field_ty) + { + self.register_transitive(field_ty, EqKind::Sum, registry); + return; + } + // Generic carriers — `Option`, `Result`, `Tuple<…>`. + // Each instantiation gets its own helper slot keyed by the + // canonical string. Inner types are walked too so any + // nominal piece (`Option` → register Color). + if let Some(inner) = field_ty + .strip_prefix("Option<") + .and_then(|s| s.strip_suffix('>')) + { + self.register_transitive(field_ty, EqKind::OptionEq, registry); + self.register_field_type(inner.trim(), registry); + } else if field_ty.starts_with("Result<") && field_ty.ends_with('>') { + self.register_transitive(field_ty, EqKind::ResultEq, registry); + if let Some((ok, err)) = parse_result_kv(field_ty) { + self.register_field_type(ok.trim(), registry); + self.register_field_type(err.trim(), registry); + } + } else if field_ty.starts_with("Tuple<") && field_ty.ends_with('>') { + self.register_transitive(field_ty, EqKind::TupleEq, registry); + if let Some(elems) = parse_tuple_elems(field_ty) { + for elem in elems { + self.register_field_type(elem.trim(), registry); + } + } + } else if let Some(inner) = field_ty + .strip_prefix("List<") + .and_then(|s| s.strip_suffix('>')) + { + // List / Vector — element kind needs its own + // helper too. The list_helpers slot itself is owned by + // ListHelperRegistry (separate registry); this walk + // ensures the per-element `__eq_` exists in + // eq_helpers when inner is a carrier or nominal type. + self.register_field_type(inner.trim(), registry); + } else if let Some(inner) = field_ty + .strip_prefix("Vector<") + .and_then(|s| s.strip_suffix('>')) + { + self.register_field_type(inner.trim(), registry); + } else if let Some(inner) = field_ty + .strip_prefix("Map<") + .and_then(|s| s.strip_suffix('>')) + { + // Map — eq+hash slots live in MapHelperRegistry, + // not eq_helpers. This walk only ensures K and V's own + // helpers exist (K may be a record/sum/carrier; V same). + // Structural eq on the map dispatches K and V via Call + // into those helpers. + let bytes = inner.as_bytes(); + let mut depth: i32 = 0; + for (idx, b) in bytes.iter().enumerate() { + match b { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b',' if depth == 0 => { + let k = inner[..idx].trim(); + let v = inner[idx + 1..].trim(); + self.register_field_type(k, registry); + self.register_field_type(v, registry); + return; + } + _ => {} + } + } + } + } + pub(crate) fn iter(&self) -> impl Iterator + '_ { self.order.iter().map(|n| (n.as_str(), self.kinds[n])) } @@ -115,21 +303,99 @@ impl EqHelperRegistry { codes: &mut wasm_encoder::CodeSection, registry: &TypeRegistry, string_eq_fn_idx: Option, + compound_lookup: &HashMap, ) -> Result<(), WasmGcError> { + // Snapshot type_name → fn_idx so the inline emitters can + // dispatch nested record/sum fields by `Call(idx)` instead + // of erroring on `Unimplemented`. Self-recursive fields + // (parent==field) get their own `self_fn_idx` argument so + // recursive sum/record types (Tree.Node holding Tree, …) + // resolve to a recursive call into the same helper. Compound + // lookups (`List`, `Vector`) are merged in so a record + // field of type `List>` can `Call(__eq_List<…>)` + // — list_helpers owns those fn idxs and threads them in via + // the `compound_lookup` arg. + let mut helper_idx_map: HashMap = self + .slots + .iter() + .map(|(n, (fn_idx, _))| (n.clone(), *fn_idx)) + .collect(); + for (canonical, fn_idx) in compound_lookup { + helper_idx_map.insert(canonical.clone(), *fn_idx); + } for name in &self.order { let kind = self.kinds[name]; - let mut f = Function::new(Vec::new()); - // Local 0 = lhs, local 1 = rhs (function params). Reuse the - // inline emitters from `lists.rs` which expect both operands - // already stashed in named slots — no extra prologue needed. + let self_fn_idx = self.slots.get(name).map(|(f, _)| *f); match kind { - EqKind::Sum => emit_sum_eq_inline(&mut f, name, registry, 0, 1, string_eq_fn_idx)?, + EqKind::Sum => { + // Sum's emit_sum_eq_inline does its own + // ref.test/ref.cast cascade per variant — the + // raw eqref params are fine to feed in directly. + let mut f = Function::new(Vec::new()); + emit_sum_eq_inline( + &mut f, + name, + registry, + 0, + 1, + string_eq_fn_idx, + &helper_idx_map, + self_fn_idx, + )?; + f.instruction(&Instruction::End); + codes.function(&f); + } EqKind::Record => { - emit_record_eq_inline(&mut f, name, registry, 0, 1, string_eq_fn_idx)? + // Record's emit_record_eq_inline reads fields via + // `struct.get $record_idx ` straight off the + // local — that needs a typed `(ref null $record)`, + // not the eqref the helper signature carries. So + // declare two typed locals (idxs 2, 3), `ref.cast` + // each param into its slot, then drive the inline + // emitter against the typed locals. + let r_idx = registry + .record_type_idx(name) + .ok_or(WasmGcError::Validation(format!( + "eq helper for record `{name}`: record not registered" + )))?; + let r_ref = wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(r_idx), + }); + let mut f = Function::new(vec![(2, r_ref)]); + let r_heap = wasm_encoder::HeapType::Concrete(r_idx); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(r_heap)); + f.instruction(&Instruction::LocalSet(2)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::RefCastNonNull(r_heap)); + f.instruction(&Instruction::LocalSet(3)); + emit_record_eq_inline( + &mut f, + name, + registry, + 2, + 3, + string_eq_fn_idx, + &helper_idx_map, + self_fn_idx, + )?; + f.instruction(&Instruction::End); + codes.function(&f); + } + EqKind::OptionEq => { + let f = emit_option_eq_body(name, registry, string_eq_fn_idx, &helper_idx_map)?; + codes.function(&f); + } + EqKind::ResultEq => { + let f = emit_result_eq_body(name, registry, string_eq_fn_idx, &helper_idx_map)?; + codes.function(&f); + } + EqKind::TupleEq => { + let f = emit_tuple_eq_body(name, registry, string_eq_fn_idx, &helper_idx_map)?; + codes.function(&f); } } - f.instruction(&Instruction::End); - codes.function(&f); } Ok(()) } @@ -148,6 +414,332 @@ impl EqHelperRegistry { } } +/// `Result` → `Some(("Ok", "Err"))`. Tracks angle/paren +/// depth so `Result, MyError>` splits at the right comma. +fn parse_result_kv(canonical: &str) -> Option<(&str, &str)> { + let inner = canonical + .trim() + .strip_prefix("Result<")? + .strip_suffix('>')?; + let bytes = inner.as_bytes(); + let mut depth: i32 = 0; + for (idx, b) in bytes.iter().enumerate() { + match b { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b',' if depth == 0 => { + return Some((inner[..idx].trim(), inner[idx + 1..].trim())); + } + _ => {} + } + } + None +} + +/// `Tuple` → `Some(vec!["A", "B", "C"])`. Same depth-aware +/// split as `parse_result_kv`. +fn parse_tuple_elems(canonical: &str) -> Option> { + let inner = canonical.trim().strip_prefix("Tuple<")?.strip_suffix('>')?; + let bytes = inner.as_bytes(); + let mut depth: i32 = 0; + let mut start = 0; + let mut out = Vec::new(); + for (idx, b) in bytes.iter().enumerate() { + match b { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b',' if depth == 0 => { + out.push(inner[start..idx].trim()); + start = idx + 1; + } + _ => {} + } + } + out.push(inner[start..].trim()); + Some(out) +} + +/// Emit `(eqref, eqref) -> i32` body for `Option`. Layout: +/// `(struct (mut i32 tag) (mut X value))`, tag=0 None, tag=1 Some. +/// Compare tags first; if differ → 0; if both 0 → 1; if both 1 → +/// inner eq. +fn emit_option_eq_body( + canonical: &str, + registry: &TypeRegistry, + string_eq_fn_idx: Option, + helper_idx_map: &HashMap, +) -> Result { + let opt_idx = registry + .option_type_idx(canonical) + .ok_or(WasmGcError::Validation(format!( + "eq helper for `{canonical}`: option not registered" + )))?; + let inner = TypeRegistry::option_element_type(canonical).ok_or(WasmGcError::Validation( + format!("eq helper for `{canonical}`: can't parse inner"), + ))?; + let opt_ref = wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(opt_idx), + }); + let mut f = Function::new(vec![(2, opt_ref)]); + let opt_heap = wasm_encoder::HeapType::Concrete(opt_idx); + // Cast both eqref params → typed Option ref into locals 2, 3. + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(opt_heap)); + f.instruction(&Instruction::LocalSet(2)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::RefCastNonNull(opt_heap)); + f.instruction(&Instruction::LocalSet(3)); + // if tag(lhs) != tag(rhs) → return 0 + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 0, + }); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 0, + }); + f.instruction(&Instruction::I32Ne); + f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + f.instruction(&Instruction::I32Const(0)); + f.instruction(&Instruction::Return); + f.instruction(&Instruction::End); + // tags equal — if 0 (None), return 1 + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 0, + }); + f.instruction(&Instruction::I32Eqz); + f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + f.instruction(&Instruction::I32Const(1)); + f.instruction(&Instruction::Return); + f.instruction(&Instruction::End); + // Both Some — compare inner via per-type dispatch. + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 1, + }); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 1, + }); + emit_inner_eq_dispatch( + &mut f, + inner.trim(), + registry, + string_eq_fn_idx, + helper_idx_map, + )?; + f.instruction(&Instruction::End); + Ok(f) +} + +/// Emit `(eqref, eqref) -> i32` body for `Result`. Layout: +/// `(struct (mut i32 tag) (mut X ok) (mut Y err))`, tag=0 Err, +/// tag=1 Ok. Compare tags; if differ → 0; both 0 → err eq; +/// both 1 → ok eq. +fn emit_result_eq_body( + canonical: &str, + registry: &TypeRegistry, + string_eq_fn_idx: Option, + helper_idx_map: &HashMap, +) -> Result { + let res_idx = registry + .result_type_idx(canonical) + .ok_or(WasmGcError::Validation(format!( + "eq helper for `{canonical}`: result not registered" + )))?; + let (ok_inner, err_inner) = parse_result_kv(canonical).ok_or(WasmGcError::Validation( + format!("eq helper for `{canonical}`: can't parse inner"), + ))?; + let res_ref = wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(res_idx), + }); + let mut f = Function::new(vec![(2, res_ref)]); + let res_heap = wasm_encoder::HeapType::Concrete(res_idx); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(res_heap)); + f.instruction(&Instruction::LocalSet(2)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::RefCastNonNull(res_heap)); + f.instruction(&Instruction::LocalSet(3)); + // if tag(lhs) != tag(rhs) → 0 + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 0, + }); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 0, + }); + f.instruction(&Instruction::I32Ne); + f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + f.instruction(&Instruction::I32Const(0)); + f.instruction(&Instruction::Return); + f.instruction(&Instruction::End); + // tags equal — branch on tag value: tag=1 (Ok) → field 1 eq; + // tag=0 (Err) → field 2 eq. + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 0, + }); + f.instruction(&Instruction::If(wasm_encoder::BlockType::Result( + wasm_encoder::ValType::I32, + ))); + // Ok arm + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 1, + }); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 1, + }); + emit_inner_eq_dispatch( + &mut f, + ok_inner.trim(), + registry, + string_eq_fn_idx, + helper_idx_map, + )?; + f.instruction(&Instruction::Else); + // Err arm + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 2, + }); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 2, + }); + emit_inner_eq_dispatch( + &mut f, + err_inner.trim(), + registry, + string_eq_fn_idx, + helper_idx_map, + )?; + f.instruction(&Instruction::End); + f.instruction(&Instruction::End); + Ok(f) +} + +/// Emit `(eqref, eqref) -> i32` body for `Tuple`. +/// Layout: `(struct field0 field1 …)`. Per-element eq, AND-fold. +fn emit_tuple_eq_body( + canonical: &str, + registry: &TypeRegistry, + string_eq_fn_idx: Option, + helper_idx_map: &HashMap, +) -> Result { + let tup_idx = registry + .tuple_type_idx(canonical) + .ok_or(WasmGcError::Validation(format!( + "eq helper for `{canonical}`: tuple not registered" + )))?; + let elems = parse_tuple_elems(canonical).ok_or(WasmGcError::Validation(format!( + "eq helper for `{canonical}`: can't parse elements" + )))?; + let tup_ref = wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(tup_idx), + }); + let mut f = Function::new(vec![(2, tup_ref)]); + let tup_heap = wasm_encoder::HeapType::Concrete(tup_idx); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(tup_heap)); + f.instruction(&Instruction::LocalSet(2)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::RefCastNonNull(tup_heap)); + f.instruction(&Instruction::LocalSet(3)); + for (i, elem) in elems.iter().enumerate() { + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: tup_idx, + field_index: i as u32, + }); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::StructGet { + struct_type_index: tup_idx, + field_index: i as u32, + }); + emit_inner_eq_dispatch( + &mut f, + elem.trim(), + registry, + string_eq_fn_idx, + helper_idx_map, + )?; + if i > 0 { + f.instruction(&Instruction::I32And); + } + } + f.instruction(&Instruction::End); + Ok(f) +} + +/// Stack: `[lhs_value, rhs_value]` of `inner` aver type. Push i32 +/// eq verdict. Same shape used by Option's payload, Result's two +/// arms, Tuple's per-element compares. +/// +/// Newtype-optimised records (`Box(val: Int)` → i64) are handled +/// before the helper-map lookup: the actual stack values are the +/// underlying primitive, so dispatch through the primitive arm. +fn emit_inner_eq_dispatch( + f: &mut Function, + inner: &str, + registry: &TypeRegistry, + string_eq_fn_idx: Option, + helper_idx_map: &HashMap, +) -> Result<(), WasmGcError> { + // Resolve newtypes — `Box(n: Int)` reaches here as "Box" but + // its wasm representation is i64. + let resolved: String = if let Some(under) = registry.newtype_underlying(inner) { + under.to_string() + } else { + inner.to_string() + }; + match resolved.as_str() { + "Int" => { + f.instruction(&Instruction::I64Eq); + } + "Bool" => { + f.instruction(&Instruction::I32Eq); + } + "Float" => { + f.instruction(&Instruction::F64Eq); + } + "String" => { + let eq_fn = string_eq_fn_idx.ok_or(WasmGcError::Validation( + "carrier eq with String inner needs __wasmgc_string_eq".into(), + ))?; + f.instruction(&Instruction::Call(eq_fn)); + } + other if helper_idx_map.contains_key(other) => { + f.instruction(&Instruction::Call(helper_idx_map[other])); + } + other => { + return Err(WasmGcError::Validation(format!( + "carrier eq inner type `{other}` has no eq dispatch" + ))); + } + } + Ok(()) +} + fn type_has_string_field( name: &str, kind: EqKind, @@ -169,5 +761,11 @@ fn type_has_string_field( .flat_map(|vs| vs.iter()) .filter(|v| v.parent == name) .any(|v| v.fields.iter().any(|t| t.trim() == "String")), + // Option/Result/Tuple<…> — check whether any inner + // type is `String`. Cheap string match on the canonical + // (e.g. `"Option"` contains `"String"`); accurate + // enough since we just need to force-register + // `__wasmgc_string_eq` when it might be called. + EqKind::OptionEq | EqKind::ResultEq | EqKind::TupleEq => name.contains("String"), } } diff --git a/src/codegen/wasm_gc/body/hash_helpers.rs b/src/codegen/wasm_gc/body/hash_helpers.rs new file mode 100644 index 00000000..735c0d00 --- /dev/null +++ b/src/codegen/wasm_gc/body/hash_helpers.rs @@ -0,0 +1,720 @@ +//! Per-record / per-sum / per-carrier hash helpers — the symmetric +//! counterpart to `eq_helpers.rs` for the hash side of nominal + +//! generic-carrier dispatch. +//! +//! ## Why +//! +//! `Map` keyed by a record (`Map`) wants a +//! deterministic hash so two `Person` values that compare equal +//! collapse to the same bucket. The inline `emit_record_inline_hash` +//! / `emit_sum_inline_hash` in `lists.rs` previously fell back to +//! `drop + i32.const 0` for any non-primitive field — correct +//! (eq still disambiguates the bucket) but degenerate, every value +//! sharing primitive-prefix maps to one bucket and lookup goes +//! O(n). +//! +//! This module sets up `__hash_` helpers per nominal / +//! carrier instantiation. Body emit goes through `Call(__hash_)` +//! for non-primitive field values; the helper itself does the +//! shape-specific DJB2 fold. Symmetric to how `__eq_` works on +//! the equality side. + +use std::collections::HashMap; + +use wasm_encoder::{Function, Instruction}; + +use super::super::WasmGcError; +use super::super::types::TypeRegistry; + +/// What kind of type a registered hash helper covers. Same shapes +/// as `EqKind`, separate enum to keep the two registries +/// independent (a type may want a hash helper without an eq helper +/// — e.g. `Map` registers hash for X without ever forcing +/// `==` on the surface). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum HashKind { + Record, + Sum, + OptionHash, + ResultHash, + TupleHash, +} + +/// Per-module registry of `__hash_` helpers needed for +/// shape-faithful hashing inside list/vec/map helpers + per-record +/// / sum / carrier inline hash bodies. +#[derive(Default)] +pub(crate) struct HashHelperRegistry { + order: Vec, + kinds: HashMap, + /// `type_name -> (wasm_fn_idx, wasm_type_idx)`. + slots: HashMap, +} + +impl HashHelperRegistry { + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn register(&mut self, type_name: &str, kind: HashKind) { + if !self.kinds.contains_key(type_name) { + self.order.push(type_name.to_string()); + self.kinds.insert(type_name.to_string(), kind); + } + } + + /// Walk `type_name`'s fields and register `__hash_` for + /// every nominal / carrier piece reachable. Mirrors + /// `EqHelperRegistry::register_transitive` but for hash; same + /// resolvability gate so we don't end up with a registered + /// helper whose body can't be emitted (a record holding e.g. + /// `List` field — list-as-field hash isn't wired yet). + pub(crate) fn register_transitive( + &mut self, + type_name: &str, + kind: HashKind, + registry: &TypeRegistry, + ) { + if self.kinds.contains_key(type_name) { + return; + } + let mut seen = std::collections::HashSet::new(); + let resolvable = match kind { + HashKind::Record => { + super::super::lists::record_fields_resolvable(type_name, registry, &mut seen) + } + HashKind::Sum => { + super::super::lists::sum_fields_resolvable(type_name, registry, &mut seen) + } + HashKind::OptionHash | HashKind::ResultHash | HashKind::TupleHash => true, + }; + if !resolvable { + return; + } + self.register(type_name, kind); + match kind { + HashKind::Record => { + if let Some(fields) = registry.record_fields.get(type_name) { + for (_, field_ty) in fields { + self.register_field_type(field_ty.trim(), registry); + } + } + } + HashKind::Sum => { + let variants: Vec<_> = registry + .variants + .values() + .flat_map(|vs| vs.iter()) + .filter(|v| v.parent == type_name) + .cloned() + .collect(); + for v in &variants { + for field_ty in &v.fields { + self.register_field_type(field_ty.trim(), registry); + } + } + } + // Mirror eq_helpers — recurse so direct top-level + // registration of a carrier (seed walker etc.) still + // discovers inner types. + HashKind::OptionHash => { + if let Some(inner) = type_name + .strip_prefix("Option<") + .and_then(|s| s.strip_suffix('>')) + { + self.register_field_type(inner.trim(), registry); + } + } + HashKind::ResultHash => { + if let Some((ok, err)) = parse_result_kv(type_name) { + self.register_field_type(ok.trim(), registry); + self.register_field_type(err.trim(), registry); + } + } + HashKind::TupleHash => { + if let Some(elems) = parse_tuple_elems(type_name) { + for e in elems { + self.register_field_type(e.trim(), registry); + } + } + } + } + } + + fn register_field_type(&mut self, field_ty: &str, registry: &TypeRegistry) { + if matches!( + field_ty, + "Int" | "Float" | "Bool" | "String" | "Unit" | "Byte" | "Char" + ) { + return; + } + if registry.record_fields.contains_key(field_ty) { + self.register_transitive(field_ty, HashKind::Record, registry); + return; + } + if registry + .variants + .values() + .flat_map(|v| v.iter()) + .any(|v| v.parent == field_ty) + { + self.register_transitive(field_ty, HashKind::Sum, registry); + return; + } + if let Some(inner) = field_ty + .strip_prefix("Option<") + .and_then(|s| s.strip_suffix('>')) + { + self.register_transitive(field_ty, HashKind::OptionHash, registry); + self.register_field_type(inner.trim(), registry); + } else if field_ty.starts_with("Result<") && field_ty.ends_with('>') { + self.register_transitive(field_ty, HashKind::ResultHash, registry); + if let Some((ok, err)) = parse_result_kv(field_ty) { + self.register_field_type(ok.trim(), registry); + self.register_field_type(err.trim(), registry); + } + } else if field_ty.starts_with("Tuple<") && field_ty.ends_with('>') { + self.register_transitive(field_ty, HashKind::TupleHash, registry); + if let Some(elems) = parse_tuple_elems(field_ty) { + for elem in elems { + self.register_field_type(elem.trim(), registry); + } + } + } else if let Some(inner) = field_ty + .strip_prefix("List<") + .and_then(|s| s.strip_suffix('>')) + { + // Symmetric to eq_helpers — recurse into the element so + // `List>` registers `__hash_Option`. The + // list_helpers slot itself is owned by the separate + // ListHelperRegistry; this walk only covers the carrier + // / nominal hash dispatch the inline body needs. + self.register_field_type(inner.trim(), registry); + } else if let Some(inner) = field_ty + .strip_prefix("Vector<") + .and_then(|s| s.strip_suffix('>')) + { + self.register_field_type(inner.trim(), registry); + } else if let Some(inner) = field_ty + .strip_prefix("Map<") + .and_then(|s| s.strip_suffix('>')) + { + // Mirror eq_helpers — Map hash slot lives in + // MapHelperRegistry; recurse so K and V's hash helpers + // exist for the structural fold. + let bytes = inner.as_bytes(); + let mut depth: i32 = 0; + for (idx, b) in bytes.iter().enumerate() { + match b { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b',' if depth == 0 => { + let k = inner[..idx].trim(); + let v = inner[idx + 1..].trim(); + self.register_field_type(k, registry); + self.register_field_type(v, registry); + return; + } + _ => {} + } + } + } + } + + pub(crate) fn iter(&self) -> impl Iterator + '_ { + self.order.iter().map(|n| (n.as_str(), self.kinds[n])) + } + + pub(crate) fn assign_slots(&mut self, next_fn_idx: &mut u32, next_type_idx: &mut u32) { + for name in &self.order { + self.slots + .insert(name.clone(), (*next_fn_idx, *next_type_idx)); + *next_fn_idx += 1; + *next_type_idx += 1; + } + } + + pub(crate) fn lookup_fn_idx(&self, type_name: &str) -> Option { + self.slots.get(type_name).map(|(f, _)| *f) + } + + pub(crate) fn lookup_type_idx(&self, type_name: &str) -> Option { + self.slots.get(type_name).map(|(_, t)| *t) + } + + /// Emit `(eqref) -> i32` fn type for each registered helper, in + /// the same order as `assign_slots`. + pub(crate) fn emit_helper_types(&self, types: &mut wasm_encoder::TypeSection) { + let eq_ref = wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Eq, + }, + }); + for _ in &self.order { + types.ty().function([eq_ref], [wasm_encoder::ValType::I32]); + } + } + + pub(crate) fn emit_helper_bodies( + &self, + codes: &mut wasm_encoder::CodeSection, + registry: &TypeRegistry, + string_eq_fn_idx: Option, + compound_lookup: &HashMap, + ) -> Result<(), WasmGcError> { + let _ = string_eq_fn_idx; // String fields hash via array.len, no helper needed. + // Same shape as eq_helpers — merge `List` / `Vector` + // hash fn idxs from list_helpers so a record field of a + // compound type can `Call(__hash_)`. + let mut helper_idx_map: HashMap = self + .slots + .iter() + .map(|(n, (fn_idx, _))| (n.clone(), *fn_idx)) + .collect(); + for (canonical, fn_idx) in compound_lookup { + helper_idx_map.insert(canonical.clone(), *fn_idx); + } + for name in &self.order { + let kind = self.kinds[name]; + let self_fn_idx = self.slots.get(name).map(|(f, _)| *f); + match kind { + HashKind::Record => { + let f = emit_record_hash_body(name, registry, &helper_idx_map, self_fn_idx)?; + codes.function(&f); + } + HashKind::Sum => { + let f = emit_sum_hash_body(name, registry, &helper_idx_map, self_fn_idx)?; + codes.function(&f); + } + HashKind::OptionHash => { + let f = emit_option_hash_body(name, registry, &helper_idx_map)?; + codes.function(&f); + } + HashKind::ResultHash => { + let f = emit_result_hash_body(name, registry, &helper_idx_map)?; + codes.function(&f); + } + HashKind::TupleHash => { + let f = emit_tuple_hash_body(name, registry, &helper_idx_map)?; + codes.function(&f); + } + } + } + Ok(()) + } +} + +/// `(eqref) -> i32` body for a record. Cast → typed, DJB2 fold over +/// fields, return. +fn emit_record_hash_body( + name: &str, + registry: &TypeRegistry, + helper_idx_map: &HashMap, + self_fn_idx: Option, +) -> Result { + let r_idx = registry + .record_type_idx(name) + .ok_or(WasmGcError::Validation(format!( + "hash helper for record `{name}`: not registered" + )))?; + let fields = registry + .record_fields + .get(name) + .ok_or(WasmGcError::Validation(format!( + "hash helper for record `{name}`: no fields" + )))?; + let r_ref = wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(r_idx), + }); + // Locals: 1 = typed record ref, 2 = h accumulator. + let mut f = Function::new(vec![(1, r_ref), (1, wasm_encoder::ValType::I32)]); + let r_heap = wasm_encoder::HeapType::Concrete(r_idx); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(r_heap)); + f.instruction(&Instruction::LocalSet(1)); + f.instruction(&Instruction::I32Const(5381)); + f.instruction(&Instruction::LocalSet(2)); + for (i, (_, field_ty)) in fields.iter().enumerate() { + // h = h * 33 + field_hash + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: r_idx, + field_index: i as u32, + }); + emit_inner_hash_dispatch( + &mut f, + field_ty.trim(), + registry, + helper_idx_map, + self_fn_idx.filter(|_| field_ty.trim() == name), + )?; + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(2)); + } + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::End); + Ok(f) +} + +/// `(eqref) -> i32` body for a sum. ref.test cascade per variant; +/// matched arm folds tag idx + variant fields. +fn emit_sum_hash_body( + parent_name: &str, + registry: &TypeRegistry, + helper_idx_map: &HashMap, + self_fn_idx: Option, +) -> Result { + let mut variants: Vec<(String, super::super::types::VariantInfo)> = registry + .variants + .iter() + .flat_map(|(n, vs)| vs.iter().map(move |v| (n.clone(), v.clone()))) + .filter(|(_, v)| v.parent == parent_name) + .collect(); + variants.sort_by(|a, b| a.0.cmp(&b.0)); + if variants.is_empty() { + return Err(WasmGcError::Validation(format!( + "hash helper for sum `{parent_name}`: no variants" + ))); + } + // Locals: 1 = h accumulator. (Per-variant typed cast happens + // inside the if-arm via `RefCastNonNull` → `StructGet`; we read + // each field directly off the casted ref so no scratch slot + // for the variant ref is needed.) + let mut f = Function::new(vec![(1, wasm_encoder::ValType::I32)]); + f.instruction(&Instruction::I32Const(5381)); + f.instruction(&Instruction::LocalSet(1)); + for (_v_name, info) in &variants { + let v_idx = info.type_idx; + let v_heap = wasm_encoder::HeapType::Concrete(v_idx); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefTestNonNull(v_heap)); + f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + // Mix variant tag (its type_idx) into hash. + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::I32Const(v_idx as i32)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(1)); + for (i, field_ty) in info.fields.iter().enumerate() { + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(v_heap)); + f.instruction(&Instruction::StructGet { + struct_type_index: v_idx, + field_index: i as u32, + }); + emit_inner_hash_dispatch( + &mut f, + field_ty.trim(), + registry, + helper_idx_map, + self_fn_idx.filter(|_| field_ty.trim() == parent_name), + )?; + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(1)); + } + f.instruction(&Instruction::End); + } + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::End); + Ok(f) +} + +/// `(eqref) -> i32` body for `Option`. h held in local 2 across +/// the if-arm so the block body can be empty-typed (no stack-shape +/// constraint). DJB2-fold tag, then if Some fold inner hash too. +fn emit_option_hash_body( + canonical: &str, + registry: &TypeRegistry, + helper_idx_map: &HashMap, +) -> Result { + let opt_idx = registry + .option_type_idx(canonical) + .ok_or(WasmGcError::Validation(format!( + "hash helper for `{canonical}`: option not registered" + )))?; + let inner = TypeRegistry::option_element_type(canonical).ok_or(WasmGcError::Validation( + format!("hash helper for `{canonical}`: can't parse inner"), + ))?; + let opt_ref = wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(opt_idx), + }); + // Locals: 1 = typed Option ref, 2 = h. + let mut f = Function::new(vec![(1, opt_ref), (1, wasm_encoder::ValType::I32)]); + let opt_heap = wasm_encoder::HeapType::Concrete(opt_idx); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(opt_heap)); + f.instruction(&Instruction::LocalSet(1)); + f.instruction(&Instruction::I32Const(5381)); + f.instruction(&Instruction::LocalSet(2)); + // h = h * 33 + tag + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 0, + }); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(2)); + // if Some (tag != 0), mix inner hash into h. + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 0, + }); + f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 1, + }); + emit_inner_hash_dispatch(&mut f, inner.trim(), registry, helper_idx_map, None)?; + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(2)); + f.instruction(&Instruction::End); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::End); + Ok(f) +} + +/// `(eqref) -> i32` body for `Result`. h held in local 2. +fn emit_result_hash_body( + canonical: &str, + registry: &TypeRegistry, + helper_idx_map: &HashMap, +) -> Result { + let res_idx = registry + .result_type_idx(canonical) + .ok_or(WasmGcError::Validation(format!( + "hash helper for `{canonical}`: result not registered" + )))?; + let (ok_inner, err_inner) = parse_result_kv(canonical).ok_or(WasmGcError::Validation( + format!("hash helper for `{canonical}`: can't parse inner"), + ))?; + let res_ref = wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(res_idx), + }); + let mut f = Function::new(vec![(1, res_ref), (1, wasm_encoder::ValType::I32)]); + let res_heap = wasm_encoder::HeapType::Concrete(res_idx); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(res_heap)); + f.instruction(&Instruction::LocalSet(1)); + f.instruction(&Instruction::I32Const(5381)); + f.instruction(&Instruction::LocalSet(2)); + // h = h * 33 + tag + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 0, + }); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(2)); + // Branch on tag. + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 0, + }); + f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + // Ok arm: mix field 1 (ok) into h. + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 1, + }); + emit_inner_hash_dispatch(&mut f, ok_inner.trim(), registry, helper_idx_map, None)?; + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(2)); + f.instruction(&Instruction::Else); + // Err arm: mix field 2 (err) into h. + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: res_idx, + field_index: 2, + }); + emit_inner_hash_dispatch(&mut f, err_inner.trim(), registry, helper_idx_map, None)?; + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(2)); + f.instruction(&Instruction::End); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::End); + Ok(f) +} + +/// `(eqref) -> i32` body for `Tuple`. DJB2 fold per-elem. +fn emit_tuple_hash_body( + canonical: &str, + registry: &TypeRegistry, + helper_idx_map: &HashMap, +) -> Result { + let tup_idx = registry + .tuple_type_idx(canonical) + .ok_or(WasmGcError::Validation(format!( + "hash helper for `{canonical}`: tuple not registered" + )))?; + let elems = parse_tuple_elems(canonical).ok_or(WasmGcError::Validation(format!( + "hash helper for `{canonical}`: can't parse elements" + )))?; + let tup_ref = wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(tup_idx), + }); + let mut f = Function::new(vec![(1, tup_ref), (1, wasm_encoder::ValType::I32)]); + let tup_heap = wasm_encoder::HeapType::Concrete(tup_idx); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(tup_heap)); + f.instruction(&Instruction::LocalSet(1)); + f.instruction(&Instruction::I32Const(5381)); + f.instruction(&Instruction::LocalSet(2)); + for (i, elem) in elems.iter().enumerate() { + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: tup_idx, + field_index: i as u32, + }); + emit_inner_hash_dispatch(&mut f, elem.trim(), registry, helper_idx_map, None)?; + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(2)); + } + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::End); + Ok(f) +} + +/// Stack: `[value]` of `inner` aver type. Push `i32` hash. Mirror +/// of `eq_helpers::emit_inner_eq_dispatch` but for hash. +fn emit_inner_hash_dispatch( + f: &mut Function, + inner: &str, + registry: &TypeRegistry, + helper_idx_map: &HashMap, + self_fn_idx: Option, +) -> Result<(), WasmGcError> { + let resolved: String = if let Some(under) = registry.newtype_underlying(inner) { + under.to_string() + } else { + inner.to_string() + }; + match resolved.as_str() { + "Int" => { + f.instruction(&Instruction::I32WrapI64); + } + "Bool" => {} // already i32 + "Float" => { + f.instruction(&Instruction::I64ReinterpretF64); + f.instruction(&Instruction::I32WrapI64); + } + "String" => { + f.instruction(&Instruction::ArrayLen); + } + other if Some(other) == self_fn_idx.and(Some(inner)) => { + // Self-recursive case — caller passed `Some(self_fn_idx)` + // when the field is a recursive ref to the parent. + let idx = self_fn_idx.unwrap(); + f.instruction(&Instruction::Call(idx)); + } + other if helper_idx_map.contains_key(other) => { + f.instruction(&Instruction::Call(helper_idx_map[other])); + } + _other => { + // Last-resort fallback (newtype-erased through to a + // non-eq-able underlying, or a shape we genuinely can't + // resolve). Drop the value, contribute 0. Same + // collision-tolerant degradation the older inline + // emitters used. + f.instruction(&Instruction::Drop); + f.instruction(&Instruction::I32Const(0)); + } + } + Ok(()) +} + +/// `Result` → `Some(("Ok", "Err"))`. Tracks angle/paren +/// depth so `Result, MyError>` splits at the right comma. +fn parse_result_kv(canonical: &str) -> Option<(&str, &str)> { + let inner = canonical + .trim() + .strip_prefix("Result<")? + .strip_suffix('>')?; + let bytes = inner.as_bytes(); + let mut depth: i32 = 0; + for (idx, b) in bytes.iter().enumerate() { + match b { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b',' if depth == 0 => { + return Some((inner[..idx].trim(), inner[idx + 1..].trim())); + } + _ => {} + } + } + None +} + +/// `Tuple` → `Some(vec!["A", "B", "C"])`. +fn parse_tuple_elems(canonical: &str) -> Option> { + let inner = canonical.trim().strip_prefix("Tuple<")?.strip_suffix('>')?; + let bytes = inner.as_bytes(); + let mut depth: i32 = 0; + let mut start = 0; + let mut out = Vec::new(); + for (idx, b) in bytes.iter().enumerate() { + match b { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b',' if depth == 0 => { + out.push(inner[start..idx].trim()); + start = idx + 1; + } + _ => {} + } + } + out.push(inner[start..].trim()); + Some(out) +} diff --git a/src/codegen/wasm_gc/builtins/mod.rs b/src/codegen/wasm_gc/builtins/mod.rs index 76a44203..79f0583b 100644 --- a/src/codegen/wasm_gc/builtins/mod.rs +++ b/src/codegen/wasm_gc/builtins/mod.rs @@ -127,7 +127,7 @@ pub(super) enum BuiltinName { impl BuiltinName { pub(super) fn from_dotted(s: &str) -> Option { match s { - "Int.toString" | "String.fromInt" => Some(Self::IntToString), + "String.fromInt" => Some(Self::IntToString), "String.fromFloat" => Some(Self::FloatToString), "String.len" | "String.length" | "String.byteLength" => Some(Self::StringLength), "String.startsWith" => Some(Self::StringStartsWith), @@ -138,7 +138,6 @@ impl BuiltinName { "String.trim" => Some(Self::StringTrim), "Int.fromString" => Some(Self::IntFromString), "Float.fromString" => Some(Self::FloatFromString), - "Float.toString" => Some(Self::FloatToString), "String.endsWith" => Some(Self::StringEndsWith), "String.fromBool" => Some(Self::StringFromBool), "String.charAt" => Some(Self::StringCharAt), @@ -153,7 +152,7 @@ impl BuiltinName { pub(super) fn canonical(self) -> &'static str { match self { - Self::IntToString => "Int.toString", + Self::IntToString => "String.fromInt", Self::StringLength => "String.len", Self::StringConcatN => "__wasmgc_concat_n", Self::StringStartsWith => "String.startsWith", @@ -164,7 +163,7 @@ impl BuiltinName { Self::StringTrim => "String.trim", Self::IntFromString => "Int.fromString", Self::FloatFromString => "Float.fromString", - Self::FloatToString => "Float.toString", + Self::FloatToString => "String.fromFloat", Self::StringEq => "__wasmgc_string_eq", Self::StringCompare => "__wasmgc_string_compare", Self::StringEndsWith => "String.endsWith", diff --git a/src/codegen/wasm_gc/effects.rs b/src/codegen/wasm_gc/effects.rs index 1bd98a82..cd901cf4 100644 --- a/src/codegen/wasm_gc/effects.rs +++ b/src/codegen/wasm_gc/effects.rs @@ -411,7 +411,13 @@ impl EffectName { /// their interpreter stack frames, wasm-gc has to be told. pub(super) fn params(self, registry: &TypeRegistry) -> Result, WasmGcError> { let mut p = self.params_without_caller(registry)?; - p.push(any_ref_ty()); + // Trailing caller_fn arg is now an `i32` index into the + // exported caller-fn name table (built by the host at + // instantiation via `__caller_fn_count` + `__caller_fn_name`). + // 0.16.2 used `(ref null $string)` per-call — replaced to + // drop N globals + N start-fn allocs and to give the host + // a vector-index lookup instead of a per-call LM round-trip. + p.push(ValType::I32); Ok(p) } diff --git a/src/codegen/wasm_gc/lists.rs b/src/codegen/wasm_gc/lists.rs index 122bcf6d..4a267e14 100644 --- a/src/codegen/wasm_gc/lists.rs +++ b/src/codegen/wasm_gc/lists.rs @@ -162,16 +162,14 @@ impl ListHelperRegistry { } else { None }; - // List eq + hash use a stricter rule than contains — - // their bodies expect stack-shape per-element eq. - // SumEq / RecordEq need scratch locals + cascade emit; - // the bodies don't support those today. The record-key - // field-dispatch path that drives these helpers only - // ever calls them with primitive / String elements. - let needs_list_helpers = matches!( - contains_eq, - Some(ListEqKind::I64 | ListEqKind::F64 | ListEqKind::I32 | ListEqKind::StringEq) - ); + // List eq + hash slots track the same kinds contains + // does — primitives, String, and (since 0.16.3) record + // / sum nominal elements that resolve through the + // per-type `__eq_` helper map. The body emitters + // (`emit_list_eq` / `emit_list_hash`) dispatch nominal + // elements through `Call(__eq_)` / inline record + // /sum hash now. + let needs_list_helpers = contains_eq.is_some(); let list_eq_type = if needs_list_helpers { let t = *next_type_idx; *next_type_idx += 1; @@ -272,13 +270,10 @@ impl ListHelperRegistry { *next_wasm_fn_idx += 1; let to_fn = *next_wasm_fn_idx; *next_wasm_fn_idx += 1; - // Vector eq + hash conditionally — same primitive / - // String constraint as list eq+hash (SumEq / RecordEq - // not supported in the stack-eq body). - let eq_kind_ok = matches!( - list_eq_kind(elem.trim(), registry), - Some(ListEqKind::I64 | ListEqKind::F64 | ListEqKind::I32 | ListEqKind::StringEq) - ); + // Vector eq + hash slots match the list cap — same + // resolvable kinds (primitive / String / nominal record- + // sum since 0.16.3). + let eq_kind_ok = list_eq_kind(elem.trim(), registry).is_some(); let (vec_eq_ty, vec_hash_ty, vec_eq_fn, vec_hash_fn) = if eq_kind_ok { let eq_ty = *next_type_idx; *next_type_idx += 1; @@ -414,10 +409,7 @@ impl ListHelperRegistry { if kind.is_some() { types.ty().function([list_ref, elem_val], [ValType::I32]); } - if matches!( - kind, - Some(ListEqKind::I64 | ListEqKind::F64 | ListEqKind::I32 | ListEqKind::StringEq) - ) { + if kind.is_some() { // eq : (List, List) -> i32 types.ty().function([list_ref, list_ref], [ValType::I32]); // hash : (List) -> i32 @@ -451,10 +443,7 @@ impl ListHelperRegistry { // to_list : (Vector) -> List types.ty().function([vec_ref], [list_ref]); let elem = TypeRegistry::list_element_type(canonical).unwrap(); - if matches!( - list_eq_kind(elem.trim(), registry), - Some(ListEqKind::I64 | ListEqKind::F64 | ListEqKind::I32 | ListEqKind::StringEq) - ) { + if list_eq_kind(elem.trim(), registry).is_some() { // eq : (Vector, Vector) -> i32 types.ty().function([vec_ref, vec_ref], [ValType::I32]); // hash : (Vector) -> i32 @@ -569,6 +558,8 @@ impl ListHelperRegistry { codes: &mut CodeSection, registry: &TypeRegistry, string_eq_fn_idx: Option, + eq_helper_fn_idx: &std::collections::HashMap, + hash_helper_fn_idx: &std::collections::HashMap, ) -> Result<(), WasmGcError> { for canonical in &self.list_order { // Order MUST match `assign_slots` and @@ -589,6 +580,7 @@ impl ListHelperRegistry { registry, kind.clone(), string_eq_fn_idx, + eq_helper_fn_idx, )?); if let (Some(eq_fn), Some(_hash_fn)) = (ops.eq, ops.hash) { codes.function(&emit_list_eq( @@ -597,6 +589,7 @@ impl ListHelperRegistry { kind.clone(), string_eq_fn_idx, eq_fn, + eq_helper_fn_idx, )?); codes.function(&emit_list_hash( canonical, @@ -604,6 +597,7 @@ impl ListHelperRegistry { kind, string_eq_fn_idx, ops.hash.unwrap(), + hash_helper_fn_idx, )?); } } @@ -620,8 +614,15 @@ impl ListHelperRegistry { registry, kind.clone(), string_eq_fn_idx, + eq_helper_fn_idx, + )?); + codes.function(&emit_vec_hash( + canonical, + registry, + kind, + string_eq_fn_idx, + hash_helper_fn_idx, )?); - codes.function(&emit_vec_hash(canonical, registry, kind, string_eq_fn_idx)?); } } for tup_canonical in &self.zip_order { @@ -682,6 +683,13 @@ enum ListEqKind { /// if both head and needle are V_i, compare fields; if only one /// is V_i, return false. Carries the parent type name. SumEq(String), + /// Generic carrier element — `Option`, `Result`, or + /// `Tuple<…>`. List eq dispatches per element via + /// `Call(__eq_)` from `eq_helpers`; signature is + /// `(eqref, eqref) -> i32`, same shape as record/sum eq. Carries + /// the canonical (whitespace-free) so the body emitter can look + /// it up in the helper idx map at emit time. + CarrierEq(String), } fn list_eq_kind(elem: &str, registry: &TypeRegistry) -> Option { @@ -691,26 +699,46 @@ fn list_eq_kind(elem: &str, registry: &TypeRegistry) -> Option { if let Some(under) = registry.newtype_underlying(trimmed) { return list_eq_kind(under, registry); } + // Generic carriers — `Option` / `Result` / `Tuple<…>` + // get a per-instantiation `__eq_` helper from + // eq_helpers. Their list-element dispatch is a thin Call into + // that helper, same shape as record/sum dispatch. Same path + // covers `List` / `Vector` elements (helpers live in + // list_helpers but the calling convention is identical: + // `(eqref, eqref) -> i32` after implicit upcast). + if (trimmed.starts_with("Option<") + || trimmed.starts_with("Result<") + || trimmed.starts_with("Tuple<") + || trimmed.starts_with("List<") + || trimmed.starts_with("Vector<") + || trimmed.starts_with("Map<")) + && trimmed.ends_with('>') + { + let canonical: String = trimmed.chars().filter(|c| !c.is_whitespace()).collect(); + let mut seen = std::collections::HashSet::new(); + if field_type_resolvable(trimmed, registry, &mut seen) { + return Some(ListEqKind::CarrierEq(canonical)); + } + return None; + } match trimmed { "Int" => Some(ListEqKind::I64), "Float" => Some(ListEqKind::F64), "Bool" => Some(ListEqKind::I32), "String" => Some(ListEqKind::StringEq), other => { + // Record / sum element gets a contains/eq/hash slot + // whenever its fields are themselves resolvable: primitives + // or nominal types we've already accepted. Recursive refs + // (Tree.Node holding Tree) and nominal cross-references + // (Item holding Color) both flow through `field_resolvable` + // which recurses with a `seen` set so cycles terminate. + // The inline emitters (emit_record_eq_inline / + // emit_sum_eq_inline) handle these via `eq_helper_fn_idx` + // dispatch + `self_fn_idx` for self-recursion since 0.16.3. + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); if registry.record_type_idx(other).is_some() { - // Record element only gets a `contains` slot when - // every field is comparable inline. Nested records, - // lists, vectors, sums in fields would need a - // recursive eq dispatch the inline emit doesn't - // support — those records still work as Map keys - // (where nested record helpers are force-registered - // and called by fn idx); they just don't get a - // List.contains slot. - let fields = registry.record_fields.get(other)?; - let all_simple = fields - .iter() - .all(|(_, t)| matches!(t.trim(), "Int" | "Float" | "Bool" | "String")); - if all_simple { + if record_fields_resolvable(other, registry, &mut seen) { Some(ListEqKind::RecordEq(other.to_string())) } else { None @@ -721,17 +749,7 @@ fn list_eq_kind(elem: &str, registry: &TypeRegistry) -> Option { .flat_map(|v| v.iter()) .any(|v| v.parent == other) { - let all_simple = registry - .variants - .values() - .flat_map(|vs| vs.iter()) - .filter(|v| v.parent == other) - .all(|v| { - v.fields - .iter() - .all(|t| matches!(t.trim(), "Int" | "Float" | "Bool" | "String")) - }); - if all_simple { + if sum_fields_resolvable(other, registry, &mut seen) { Some(ListEqKind::SumEq(other.to_string())) } else { None @@ -743,6 +761,162 @@ fn list_eq_kind(elem: &str, registry: &TypeRegistry) -> Option { } } +/// True when every field of `record` is something the inline eq +/// emitters can dispatch: primitive, a registered record/sum +/// (recursively resolvable), or self-recursion. Cycles terminate +/// via the `seen` set. +pub(super) fn record_fields_resolvable( + record: &str, + registry: &TypeRegistry, + seen: &mut std::collections::HashSet, +) -> bool { + if !seen.insert(record.to_string()) { + return true; // already visiting — break the cycle + } + let Some(fields) = registry.record_fields.get(record) else { + return false; + }; + fields + .iter() + .all(|(_, t)| field_type_resolvable(t.trim(), registry, seen)) +} + +pub(super) fn sum_fields_resolvable( + parent: &str, + registry: &TypeRegistry, + seen: &mut std::collections::HashSet, +) -> bool { + if !seen.insert(parent.to_string()) { + return true; + } + registry + .variants + .values() + .flat_map(|vs| vs.iter()) + .filter(|v| v.parent == parent) + .all(|v| { + v.fields + .iter() + .all(|t| field_type_resolvable(t.trim(), registry, seen)) + }) +} + +pub(super) fn field_type_resolvable( + field: &str, + registry: &TypeRegistry, + seen: &mut std::collections::HashSet, +) -> bool { + if matches!(field, "Int" | "Float" | "Bool" | "String") { + return true; + } + if registry.record_type_idx(field).is_some() { + return record_fields_resolvable(field, registry, seen); + } + if registry + .variants + .values() + .flat_map(|vs| vs.iter()) + .any(|v| v.parent == field) + { + return sum_fields_resolvable(field, registry, seen); + } + // Generic carriers — `Option`, `Result`, `Tuple<…>` get + // per-instantiation `__eq_` helpers since 0.16.3. + // Resolvable iff every inner type is itself resolvable. + // List/Vector/Map field types still fall through (their dispatch + // from `emit_record_eq_inline` is a separate followup). + if let Some(inner) = field + .strip_prefix("Option<") + .and_then(|s| s.strip_suffix('>')) + { + return field_type_resolvable(inner.trim(), registry, seen); + } + if let Some(inner) = field + .strip_prefix("Result<") + .and_then(|s| s.strip_suffix('>')) + { + let bytes = inner.as_bytes(); + let mut depth: i32 = 0; + for (idx, b) in bytes.iter().enumerate() { + match b { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b',' if depth == 0 => { + let ok = inner[..idx].trim(); + let err = inner[idx + 1..].trim(); + return field_type_resolvable(ok, registry, seen) + && field_type_resolvable(err, registry, seen); + } + _ => {} + } + } + return false; + } + if let Some(inner) = field + .strip_prefix("Tuple<") + .and_then(|s| s.strip_suffix('>')) + { + let bytes = inner.as_bytes(); + let mut depth: i32 = 0; + let mut start = 0; + for (idx, b) in bytes.iter().enumerate() { + match b { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b',' if depth == 0 => { + let elem = inner[start..idx].trim(); + if !field_type_resolvable(elem, registry, seen) { + return false; + } + start = idx + 1; + } + _ => {} + } + } + return field_type_resolvable(inner[start..].trim(), registry, seen); + } + // List / Vector — list/vec helpers slot a per-instantiation + // eq+hash fn in `list_helpers` whenever X is itself + // `list_eq_kind`-able (recursively: primitive, record, sum, or a + // resolvable carrier). Record/sum field dispatch then calls + // `__eq_List` / `__eq_Vector` via the compound lookup + // map threaded into `emit_record_eq_inline`. + if let Some(inner) = field + .strip_prefix("List<") + .and_then(|s| s.strip_suffix('>')) + { + return field_type_resolvable(inner.trim(), registry, seen); + } + if let Some(inner) = field + .strip_prefix("Vector<") + .and_then(|s| s.strip_suffix('>')) + { + return field_type_resolvable(inner.trim(), registry, seen); + } + // Map — `__eq_Map` / `__hash_Map` live in + // MapHelperRegistry::kv. Resolvable iff both K and V are + // themselves resolvable. + if let Some(inner) = field.strip_prefix("Map<").and_then(|s| s.strip_suffix('>')) { + let bytes = inner.as_bytes(); + let mut depth: i32 = 0; + for (idx, b) in bytes.iter().enumerate() { + match b { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b',' if depth == 0 => { + let k = inner[..idx].trim(); + let v = inner[idx + 1..].trim(); + return field_type_resolvable(k, registry, seen) + && field_type_resolvable(v, registry, seen); + } + _ => {} + } + } + return false; + } + false +} + fn list_idx_of(canonical: &str, registry: &TypeRegistry) -> Result { registry .list_type_idx(canonical) @@ -1498,6 +1672,7 @@ fn emit_list_contains( registry: &TypeRegistry, kind: ListEqKind, string_eq_fn_idx: Option, + eq_helper_fn_idx: &std::collections::HashMap, ) -> Result { let list_idx = list_idx_of(canonical, registry)?; let list_ref = ValType::Ref(RefType { @@ -1512,7 +1687,8 @@ fn emit_list_contains( // params: 0=in, 1=needle. local 2 = cur. RecordEq adds two // extra scratch locals (3 = head, 4 = needle copy) since field- // by-field eq needs `struct.get` against both refs multiple - // times. + // times. CarrierEq needs no scratch — the helper takes eqref + // params directly so head + needle are forwarded as-is. let mut locals: Vec<(u32, ValType)> = vec![(1, list_ref)]; if matches!(&kind, ListEqKind::RecordEq(_) | ListEqKind::SumEq(_)) { // Record / sum eq does multiple struct.get reads against @@ -1543,7 +1719,16 @@ fn emit_list_contains( f.instruction(&Instruction::LocalSet(3)); f.instruction(&Instruction::LocalGet(1)); f.instruction(&Instruction::LocalSet(4)); - emit_record_eq_inline(&mut f, record_name, registry, 3, 4, string_eq_fn_idx)?; + emit_record_eq_inline( + &mut f, + record_name, + registry, + 3, + 4, + string_eq_fn_idx, + eq_helper_fn_idx, + None, + )?; } ListEqKind::SumEq(parent_name) => { // Same scratch dance as RecordEq — both ref.test and @@ -1556,7 +1741,34 @@ fn emit_list_contains( f.instruction(&Instruction::LocalSet(3)); f.instruction(&Instruction::LocalGet(1)); f.instruction(&Instruction::LocalSet(4)); - emit_sum_eq_inline(&mut f, parent_name, registry, 3, 4, string_eq_fn_idx)?; + emit_sum_eq_inline( + &mut f, + parent_name, + registry, + 3, + 4, + string_eq_fn_idx, + eq_helper_fn_idx, + None, + )?; + } + ListEqKind::CarrierEq(canonical) => { + // `Call(__eq_)` — both head and needle pushed + // as eqref args (head from struct.get, needle from + // param 1). + let eq_fn = eq_helper_fn_idx.get(canonical).copied().ok_or_else(|| { + WasmGcError::Validation(format!( + "List.contains over carrier `{canonical}`: \ + __eq_{canonical} not registered in eq_helpers" + )) + })?; + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: list_idx, + field_index: 0, + }); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::Call(eq_fn)); } _ => { f.instruction(&Instruction::LocalGet(2)); @@ -1575,12 +1787,12 @@ fn emit_list_contains( ))?; f.instruction(&Instruction::Call(eq_fn)) } - ListEqKind::RecordEq(_) | ListEqKind::SumEq(_) => panic!( - "internal compiler error: List.contains emit reached \ - RecordEq/SumEq path; should be filtered upstream by \ - `list_eq_kind` returning None for record/sum elements. \ - Please file at https://github.com/jasisz/aver/issues" - ), + ListEqKind::RecordEq(_) | ListEqKind::SumEq(_) | ListEqKind::CarrierEq(_) => { + unreachable!( + "filtered by outer match arms — RecordEq/SumEq/CarrierEq \ + handled above before this fallthrough" + ) + } }; } } @@ -1610,6 +1822,7 @@ fn emit_list_contains( /// surface as Unimplemented (same constraint as `emit_eq_record` in /// maps.rs — extending requires nested-record / list / vector eq /// dispatch). +#[allow(clippy::too_many_arguments)] pub(super) fn emit_record_eq_inline( f: &mut Function, record_name: &str, @@ -1617,6 +1830,8 @@ pub(super) fn emit_record_eq_inline( head_local: u32, needle_local: u32, string_eq_fn_idx: Option, + eq_helper_fn_idx: &std::collections::HashMap, + self_fn_idx: Option, ) -> Result<(), WasmGcError> { let record_idx = registry .record_type_idx(record_name) @@ -1649,24 +1864,53 @@ pub(super) fn emit_record_eq_inline( }); // emit per-field eq → i32 match field_ty.trim() { - "Int" => f.instruction(&Instruction::I64Eq), - "Bool" => f.instruction(&Instruction::I32Eq), - "Float" => f.instruction(&Instruction::F64Eq), + "Int" => { + f.instruction(&Instruction::I64Eq); + } + "Bool" => { + f.instruction(&Instruction::I32Eq); + } + "Float" => { + f.instruction(&Instruction::F64Eq); + } "String" => { let eq_fn = string_eq_fn_idx.ok_or(WasmGcError::Validation( "List.contains record field of String type needs \ __wasmgc_string_eq registered" .into(), ))?; - f.instruction(&Instruction::Call(eq_fn)) + f.instruction(&Instruction::Call(eq_fn)); } - _ => { - return Err(WasmGcError::Unimplemented( - "phase 4 — record field type in List.contains \ - not in {Int, Float, Bool, String}", - )); + other if other == record_name && self_fn_idx.is_some() => { + // Recursive ref to the same record — call self. + f.instruction(&Instruction::Call(self_fn_idx.unwrap())); } - }; + other => { + // Look up `__eq_` — for compound types (List/ + // Vector/Map/Option/Result/Tuple) the registry keys + // are whitespace-stripped, but a field type from + // `record_fields` can carry source-side spacing. + // Try both forms before erroring. + let normalized = super::types::normalize_compound(other); + let key = if eq_helper_fn_idx.contains_key(other) { + Some(other.to_string()) + } else if eq_helper_fn_idx.contains_key(normalized.as_str()) { + Some(normalized.clone()) + } else { + None + }; + if let Some(k) = key { + let idx = eq_helper_fn_idx[k.as_str()]; + f.instruction(&Instruction::Call(idx)); + } else { + return Err(WasmGcError::Validation(format!( + "record `{record_name}` field type `{other}` has no eq dispatch \ + (not in {{Int, Float, Bool, String}}, no `__eq_{other}` helper, \ + not self-recursive)" + ))); + } + } + } if i > 0 { f.instruction(&Instruction::I32And); } @@ -1679,6 +1923,7 @@ pub(super) fn emit_record_eq_inline( /// and needle have that concrete type. If both: cast + field-by- /// field eq, push result. If only one: push 0 (different variants). /// Final i32 on stack: 1 = equal, 0 = different. +#[allow(clippy::too_many_arguments)] pub(super) fn emit_sum_eq_inline( f: &mut Function, parent_name: &str, @@ -1686,6 +1931,8 @@ pub(super) fn emit_sum_eq_inline( head_local: u32, needle_local: u32, string_eq_fn_idx: Option, + eq_helper_fn_idx: &std::collections::HashMap, + self_fn_idx: Option, ) -> Result<(), WasmGcError> { // Collect all variants of this sum (use a stable order — names // sorted ascending — so two compiler runs produce identical wasm). @@ -1733,24 +1980,44 @@ pub(super) fn emit_sum_eq_inline( field_index: i as u32, }); match field_ty.trim() { - "Int" => f.instruction(&Instruction::I64Eq), - "Bool" => f.instruction(&Instruction::I32Eq), - "Float" => f.instruction(&Instruction::F64Eq), + "Int" => { + f.instruction(&Instruction::I64Eq); + } + "Bool" => { + f.instruction(&Instruction::I32Eq); + } + "Float" => { + f.instruction(&Instruction::F64Eq); + } "String" => { let eq_fn = string_eq_fn_idx.ok_or(WasmGcError::Validation( "List.contains sum field of String type needs \ __wasmgc_string_eq registered" .into(), ))?; - f.instruction(&Instruction::Call(eq_fn)) + f.instruction(&Instruction::Call(eq_fn)); } - _ => { - return Err(WasmGcError::Unimplemented( - "phase 4 — sum-variant field type in List.contains \ - not in {Int, Float, Bool, String}", - )); + other if other == parent_name && self_fn_idx.is_some() => { + // Recursive ref to the same sum (Tree.Node carrying + // Tree fields, ditto Cons-cell shapes) — call self. + f.instruction(&Instruction::Call(self_fn_idx.unwrap())); } - }; + other if eq_helper_fn_idx.contains_key(other) => { + // Nested record/sum field with its own __eq_ + // helper — dispatch by fn idx. Field refs are + // subtypes of eqref, the call's typed args + // accept implicit upcast. + let idx = eq_helper_fn_idx[other]; + f.instruction(&Instruction::Call(idx)); + } + other => { + return Err(WasmGcError::Validation(format!( + "sum `{parent_name}` variant field type `{other}` has no eq \ + dispatch (not primitive, no `__eq_{other}` helper, not \ + self-recursive)" + ))); + } + } if i > 0 { f.instruction(&Instruction::I32And); } @@ -1932,12 +2199,14 @@ fn emit_list_zip( /// when both lists end at the same step with all heads equal; 0 /// otherwise. Same `T` constraint as contains — only emitted when T /// has a `list_eq_kind`. +#[allow(clippy::too_many_arguments)] fn emit_list_eq( canonical: &str, registry: &TypeRegistry, kind: ListEqKind, string_eq_fn_idx: Option, self_fn_idx: u32, + eq_helper_fn_idx: &std::collections::HashMap, ) -> Result { let list_idx = list_idx_of(canonical, registry)?; // params: 0 = la, 1 = lb. No locals — short body. @@ -1969,25 +2238,36 @@ fn emit_list_eq( field_index: 0, }); match &kind { - ListEqKind::I64 => f.instruction(&Instruction::I64Eq), - ListEqKind::F64 => f.instruction(&Instruction::F64Eq), - ListEqKind::I32 => f.instruction(&Instruction::I32Eq), + ListEqKind::I64 => { + f.instruction(&Instruction::I64Eq); + } + ListEqKind::F64 => { + f.instruction(&Instruction::F64Eq); + } + ListEqKind::I32 => { + f.instruction(&Instruction::I32Eq); + } ListEqKind::StringEq => { let eq_fn = string_eq_fn_idx.ok_or(WasmGcError::Validation( "List eq over String needs __wasmgc_string_eq".into(), ))?; - f.instruction(&Instruction::Call(eq_fn)) + f.instruction(&Instruction::Call(eq_fn)); } - ListEqKind::RecordEq(_) | ListEqKind::SumEq(_) => { - // Same `all_simple` constraint as contains — these - // variants don't reach this emit path. - panic!( - "internal compiler error: emit_list_eq reached RecordEq/SumEq; \ - list_eq_kind must return None for record/sum elements. \ - Please file at https://github.com/jasisz/aver/issues" - ) + ListEqKind::RecordEq(name) | ListEqKind::SumEq(name) | ListEqKind::CarrierEq(name) => { + // Nominal/carrier element — dispatch to the per-type + // `__eq_` helper. Its signature is + // `(eqref, eqref) -> i32`; both refs on the stack are + // subtypes of eqref so the implicit upcast is fine. + let idx = eq_helper_fn_idx + .get(name) + .copied() + .ok_or(WasmGcError::Validation(format!( + "List eq over `{name}`: __eq_{name} helper not registered \ + (discovery walker should have transitively flagged it)" + )))?; + f.instruction(&Instruction::Call(idx)); } - }; + } // if heads differ → 0 f.instruction(&Instruction::I32Eqz); f.instruction(&Instruction::If(BlockType::Empty)); @@ -2013,22 +2293,59 @@ fn emit_list_eq( /// `hash : (l) -> i32`. DJB2-style fold: `h = 5381; for elem: h = h /// * 33 + element_hash`. Element hash dispatched per `kind`. Same /// `T` constraint as eq. +#[allow(clippy::too_many_arguments)] fn emit_list_hash( canonical: &str, registry: &TypeRegistry, kind: ListEqKind, string_eq_fn_idx: Option, _self_fn_idx: u32, + hash_helper_fn_idx: &std::collections::HashMap, ) -> Result { let list_idx = list_idx_of(canonical, registry)?; let _ = string_eq_fn_idx; let elem = TypeRegistry::list_element_type(canonical).unwrap(); - // params: 0=l. locals: 1=cur, 2=h. + // params: 0=l. locals: 1=cur, 2=h. Plus per-kind extras for + // record / sum element hash dispatch (3=elem_ref, 4=elem_hash + // accumulator). let list_ref = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(list_idx), }); - let mut f = Function::new([(1, list_ref), (1, ValType::I32)]); + let mut locals: Vec<(u32, ValType)> = vec![(1, list_ref), (1, ValType::I32)]; + match &kind { + ListEqKind::RecordEq(record_name) => { + let r_idx = registry + .record_type_idx(record_name) + .ok_or(WasmGcError::Validation(format!( + "list hash for `List<{record_name}>`: record not registered" + )))?; + let r_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Concrete(r_idx), + }); + locals.push((1, r_ref)); // 3 = elem_ref + locals.push((1, ValType::I32)); // 4 = elem_hash + } + ListEqKind::SumEq(_) => { + // Sum types lower to `(ref null eq)` (per + // `types.rs::aver_to_wasm` for sum-parent surface + // names). Hold the head ref as eqref; per-variant + // `ref.cast` narrows it to the concrete variant idx + // before reading its fields. + let eq_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Eq, + }, + }); + locals.push((1, eq_ref)); // 3 = elem_ref (eqref carrier) + locals.push((1, ValType::I32)); // 4 = elem_hash + } + _ => {} + } + let mut f = Function::new(locals); // h = 5381 f.instruction(&Instruction::I32Const(5381)); f.instruction(&Instruction::LocalSet(2)); @@ -2051,17 +2368,24 @@ fn emit_list_hash( struct_type_index: list_idx, field_index: 0, }); - // Element hash → i32 - match elem.trim() { - "Int" => { + // Element hash → i32. Dispatch by `kind` rather than the raw + // `elem` string — newtype optimisation erases single-field + // records to their underlying primitive (`Box(n: Int)` → I64), + // so the surface name can be `"Box"` while the actual wasm + // representation is `i64`. `list_eq_kind` resolves the newtype + // before returning, so `kind` is the source of truth for + // representation. + let _ = elem; + match &kind { + ListEqKind::I64 => { f.instruction(&Instruction::I32WrapI64); } - "Bool" => {} // already i32 - "Float" => { + ListEqKind::I32 => {} // bool — already i32 + ListEqKind::F64 => { f.instruction(&Instruction::I64ReinterpretF64); f.instruction(&Instruction::I32WrapI64); } - "String" => { + ListEqKind::StringEq => { // Inline DJB2 over the (array i8) — short version // that reuses the per-fn locals would need extra // scratch. Cheap fallback: take array length as the @@ -2070,14 +2394,52 @@ fn emit_list_hash( // for non-cryptographic mix. f.instruction(&Instruction::ArrayLen); } - other => panic!( - "internal compiler error: list hash emit reached unsupported \ - element type `{other}`; upstream `list_eq_kind` must restrict \ - list-hash emission to {{Int, Bool, Float, String}}. \ - Please file at https://github.com/jasisz/aver/issues" - ), + ListEqKind::RecordEq(record_name) => { + let r_idx = registry + .record_type_idx(record_name) + .ok_or(WasmGcError::Validation(format!( + "list hash dispatch: record `{record_name}` not registered" + )))?; + let fields = registry + .record_fields + .get(record_name) + .ok_or(WasmGcError::Validation(format!( + "list hash dispatch: record `{record_name}` has no field info" + )))?; + emit_record_inline_hash( + &mut f, + r_idx, + fields, + /* elem_local */ 3, + /* elem_hash_local */ 4, + registry, + hash_helper_fn_idx, + )?; + } + ListEqKind::SumEq(parent_name) => { + emit_sum_inline_hash( + &mut f, + parent_name, + registry, + /* elem_local */ 3, + /* elem_hash_local */ 4, + hash_helper_fn_idx, + )?; + } + ListEqKind::CarrierEq(canonical) => { + // Element on stack is eqref; `Call(__hash_)` + // returns i32. The carrier's `__hash_` is registered + // alongside `__eq_` in hash_helpers. + let idx = hash_helper_fn_idx + .get(canonical) + .copied() + .ok_or(WasmGcError::Validation(format!( + "list hash over carrier `{canonical}`: __hash_{canonical} \ + not registered in hash_helpers" + )))?; + f.instruction(&Instruction::Call(idx)); + } } - let _ = kind; f.instruction(&Instruction::I32Add); f.instruction(&Instruction::LocalSet(2)); // cur = cur.tail @@ -2095,6 +2457,191 @@ fn emit_list_hash( Ok(f) } +/// Inline DJB2-style hash for a sum element. Stack on entry has the +/// element eqref; on exit has an `i32` hash. Walks variants in the +/// same sorted order as `emit_sum_eq_inline` (so two compiler runs +/// produce identical bytecode + so `eq` and `hash` agree on which +/// variant to inspect first), and per matched variant mixes its +/// `type_idx` (as a stable tag) plus DJB2-folds each primitive +/// field into `elem_hash`. Variants are disjoint subtypes of the +/// parent, so at most one `ref.test` succeeds per call — non- +/// matched arms `ref.test` to false and skip silently. +#[allow(clippy::too_many_arguments)] +fn emit_sum_inline_hash( + f: &mut Function, + parent_name: &str, + registry: &TypeRegistry, + elem_local: u32, + elem_hash_local: u32, + hash_helper_fn_idx: &std::collections::HashMap, +) -> Result<(), WasmGcError> { + // Collect variants of this sum. Same ordering as + // `emit_sum_eq_inline` for parity. + let mut variants: Vec<(String, super::types::VariantInfo)> = registry + .variants + .iter() + .flat_map(|(n, vs)| vs.iter().map(move |v| (n.clone(), v.clone()))) + .filter(|(_, v)| v.parent == parent_name) + .collect(); + variants.sort_by(|a, b| a.0.cmp(&b.0)); + if variants.is_empty() { + return Err(WasmGcError::Validation(format!( + "list hash dispatch: sum type `{parent_name}` has no variants" + ))); + } + + // Save eqref → elem_local; init elem_hash = 5381. + f.instruction(&Instruction::LocalSet(elem_local)); + f.instruction(&Instruction::I32Const(5381)); + f.instruction(&Instruction::LocalSet(elem_hash_local)); + + for (_v_name, info) in &variants { + let v_idx = info.type_idx; + let v_heap = wasm_encoder::HeapType::Concrete(v_idx); + // if ref.test V elem_ref: + f.instruction(&Instruction::LocalGet(elem_local)); + f.instruction(&Instruction::RefTestNonNull(v_heap)); + f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + // Fold variant tag (type_idx as i32) into elem_hash — + // ensures empty variants of different shape still get + // distinct hashes. + f.instruction(&Instruction::LocalGet(elem_hash_local)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(elem_hash_local)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::I32Const(v_idx as i32)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(elem_hash_local)); + // Per field, downcast then fold. + for (i, field_ty) in info.fields.iter().enumerate() { + f.instruction(&Instruction::LocalGet(elem_hash_local)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(elem_hash_local)); + f.instruction(&Instruction::I32Add); + + f.instruction(&Instruction::LocalGet(elem_local)); + f.instruction(&Instruction::RefCastNonNull(v_heap)); + f.instruction(&Instruction::StructGet { + struct_type_index: v_idx, + field_index: i as u32, + }); + let resolved: String = if let Some(under) = registry.newtype_underlying(field_ty.trim()) + { + under.to_string() + } else { + field_ty.trim().to_string() + }; + match resolved.as_str() { + "Int" => { + f.instruction(&Instruction::I32WrapI64); + } + "Bool" => {} // already i32 + "Float" => { + f.instruction(&Instruction::I64ReinterpretF64); + f.instruction(&Instruction::I32WrapI64); + } + "String" => { + f.instruction(&Instruction::ArrayLen); + } + other if hash_helper_fn_idx.contains_key(other) => { + f.instruction(&Instruction::Call(hash_helper_fn_idx[other])); + } + _ => { + // Last-resort fallback — no helper for this + // shape. Drop, contribute 0 (eq disambiguates). + f.instruction(&Instruction::Drop); + f.instruction(&Instruction::I32Const(0)); + } + } + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(elem_hash_local)); + } + f.instruction(&Instruction::End); // end if + } + + f.instruction(&Instruction::LocalGet(elem_hash_local)); + Ok(()) +} + +/// Inline DJB2-style hash for a record element. Stack on entry has +/// the element ref; on exit has an `i32` hash. `elem_local` and +/// `elem_hash_local` are pre-declared scratch slots in the calling +/// fn (typed `(ref null $record_idx)` and `i32` respectively). +/// +/// Per-field hash trick mirrors the primitive arms in +/// `emit_list_hash`: Int → wrap, Float → reinterpret+wrap, Bool → +/// already i32, String → array.len. Field shapes are restricted to +/// {Int, Bool, Float, String} by `list_eq_kind`'s `all_simple` gate; +/// nested records / lists trip `WasmGcError::Validation` here. +#[allow(clippy::too_many_arguments)] +fn emit_record_inline_hash( + f: &mut Function, + record_idx: u32, + fields: &[(String, String)], + elem_local: u32, + elem_hash_local: u32, + registry: &TypeRegistry, + hash_helper_fn_idx: &std::collections::HashMap, +) -> Result<(), WasmGcError> { + // Save record ref → elem_local for repeated struct.get. + f.instruction(&Instruction::LocalSet(elem_local)); + // elem_hash = 5381 (DJB2 init). + f.instruction(&Instruction::I32Const(5381)); + f.instruction(&Instruction::LocalSet(elem_hash_local)); + for (i, (_field_name, field_type)) in fields.iter().enumerate() { + // elem_hash = elem_hash * 33 + field_hash + // (= (elem_hash << 5) + elem_hash + field_hash, DJB2.) + f.instruction(&Instruction::LocalGet(elem_hash_local)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + f.instruction(&Instruction::LocalGet(elem_hash_local)); + f.instruction(&Instruction::I32Add); + // Push field value, then mix to i32. + f.instruction(&Instruction::LocalGet(elem_local)); + f.instruction(&Instruction::StructGet { + struct_type_index: record_idx, + field_index: i as u32, + }); + let resolved: String = if let Some(under) = registry.newtype_underlying(field_type.trim()) { + under.to_string() + } else { + field_type.trim().to_string() + }; + match resolved.as_str() { + "Int" => { + f.instruction(&Instruction::I32WrapI64); + } + "Bool" => {} // already i32 + "Float" => { + f.instruction(&Instruction::I64ReinterpretF64); + f.instruction(&Instruction::I32WrapI64); + } + "String" => { + f.instruction(&Instruction::ArrayLen); + } + other if hash_helper_fn_idx.contains_key(other) => { + f.instruction(&Instruction::Call(hash_helper_fn_idx[other])); + } + _ => { + // No helper available — drop + 0. Collision OK; eq + // disambiguates the bucket. Hits when the field type + // is a generic carrier we haven't covered yet (e.g. + // `List` field in a record). + f.instruction(&Instruction::Drop); + f.instruction(&Instruction::I32Const(0)); + } + } + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(elem_hash_local)); + } + // Push final elem_hash so the caller's mix can fold it into the + // total list hash. + f.instruction(&Instruction::LocalGet(elem_hash_local)); + Ok(()) +} + /// `eq : (Vector, Vector) -> i32`. Length check + element- /// wise eq via per-T instruction. Same `T must be eq-able` rule as /// list_eq. @@ -2103,6 +2650,7 @@ fn emit_vec_eq( registry: &TypeRegistry, kind: ListEqKind, string_eq_fn_idx: Option, + eq_helper_fn_idx: &std::collections::HashMap, ) -> Result { let (vec_idx, _) = vec_idx_of_pair(canonical, registry)?; // params: 0=va, 1=vb. locals: 2=len, 3=i. @@ -2134,22 +2682,33 @@ fn emit_vec_eq( f.instruction(&Instruction::LocalGet(3)); f.instruction(&Instruction::ArrayGet(vec_idx)); match &kind { - ListEqKind::I64 => f.instruction(&Instruction::I64Eq), - ListEqKind::F64 => f.instruction(&Instruction::F64Eq), - ListEqKind::I32 => f.instruction(&Instruction::I32Eq), + ListEqKind::I64 => { + f.instruction(&Instruction::I64Eq); + } + ListEqKind::F64 => { + f.instruction(&Instruction::F64Eq); + } + ListEqKind::I32 => { + f.instruction(&Instruction::I32Eq); + } ListEqKind::StringEq => { let eq_fn = string_eq_fn_idx.ok_or(WasmGcError::Validation( "Vector eq over String needs __wasmgc_string_eq".into(), ))?; - f.instruction(&Instruction::Call(eq_fn)) + f.instruction(&Instruction::Call(eq_fn)); } - kind => panic!( - "internal compiler error: Vector eq emit reached unsupported \ - kind {kind:?}; upstream `list_eq_kind` must filter Vector eq \ - to scalar / String elements. \ - Please file at https://github.com/jasisz/aver/issues" - ), - }; + ListEqKind::RecordEq(name) | ListEqKind::SumEq(name) | ListEqKind::CarrierEq(name) => { + // Nominal/carrier element — `Call(__eq_)`. Same eqref + // upcast shape as in `emit_list_eq`. + let idx = eq_helper_fn_idx + .get(name) + .copied() + .ok_or(WasmGcError::Validation(format!( + "Vector eq over `{name}`: __eq_{name} helper not registered" + )))?; + f.instruction(&Instruction::Call(idx)); + } + } f.instruction(&Instruction::I32Eqz); f.instruction(&Instruction::If(BlockType::Empty)); f.instruction(&Instruction::I32Const(0)); @@ -2173,12 +2732,45 @@ fn emit_vec_hash( registry: &TypeRegistry, kind: ListEqKind, _string_eq_fn_idx: Option, + hash_helper_fn_idx: &std::collections::HashMap, ) -> Result { let (vec_idx, _) = vec_idx_of_pair(canonical, registry)?; - let _ = kind; let elem = TypeRegistry::list_element_type(canonical).unwrap(); - // params: 0=v. locals: 1=h, 2=len, 3=i. - let mut f = Function::new([(1, ValType::I32), (1, ValType::I32), (1, ValType::I32)]); + // params: 0=v. locals: 1=h, 2=len, 3=i, plus per-kind extras + // (4=elem_ref, 5=elem_hash) for record/sum element dispatch — + // newtype optimisation may erase a record name down to its + // underlying primitive (kind == I64 even though `elem == "Box"`), + // so dispatch by `kind`, not the surface element string. + let mut locals: Vec<(u32, ValType)> = + vec![(1, ValType::I32), (1, ValType::I32), (1, ValType::I32)]; + match &kind { + ListEqKind::RecordEq(record_name) => { + let r_idx = registry + .record_type_idx(record_name) + .ok_or(WasmGcError::Validation(format!( + "vector hash for `Vector<{record_name}>`: record not registered" + )))?; + let r_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Concrete(r_idx), + }); + locals.push((1, r_ref)); // 4 = elem_ref + locals.push((1, ValType::I32)); // 5 = elem_hash + } + ListEqKind::SumEq(_) => { + let eq_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Eq, + }, + }); + locals.push((1, eq_ref)); // 4 = elem_ref (eqref) + locals.push((1, ValType::I32)); // 5 = elem_hash + } + _ => {} + } + let mut f = Function::new(locals); f.instruction(&Instruction::I32Const(5381)); f.instruction(&Instruction::LocalSet(1)); f.instruction(&Instruction::LocalGet(0)); @@ -2201,24 +2793,64 @@ fn emit_vec_hash( f.instruction(&Instruction::LocalGet(0)); f.instruction(&Instruction::LocalGet(3)); f.instruction(&Instruction::ArrayGet(vec_idx)); - match elem.trim() { - "Int" => { + let _ = elem; + match &kind { + ListEqKind::I64 => { f.instruction(&Instruction::I32WrapI64); } - "Bool" => {} - "Float" => { + ListEqKind::I32 => {} // bool — already i32 + ListEqKind::F64 => { f.instruction(&Instruction::I64ReinterpretF64); f.instruction(&Instruction::I32WrapI64); } - "String" => { + ListEqKind::StringEq => { f.instruction(&Instruction::ArrayLen); } - other => panic!( - "internal compiler error: Vector hash emit reached unsupported \ - element type `{other}`; upstream `list_eq_kind` must restrict \ - Vector-hash emission to {{Int, Bool, Float, String}}. \ - Please file at https://github.com/jasisz/aver/issues" - ), + ListEqKind::RecordEq(record_name) => { + let r_idx = registry + .record_type_idx(record_name) + .ok_or(WasmGcError::Validation(format!( + "vector hash dispatch: record `{record_name}` not registered" + )))?; + let fields = registry + .record_fields + .get(record_name) + .ok_or(WasmGcError::Validation(format!( + "vector hash dispatch: record `{record_name}` has no field info" + )))?; + emit_record_inline_hash( + &mut f, + r_idx, + fields, + /* elem_local */ 4, + /* elem_hash_local */ 5, + registry, + hash_helper_fn_idx, + )?; + } + ListEqKind::SumEq(parent_name) => { + emit_sum_inline_hash( + &mut f, + parent_name, + registry, + /* elem_local */ 4, + /* elem_hash_local */ 5, + hash_helper_fn_idx, + )?; + } + ListEqKind::CarrierEq(canonical) => { + // Element on stack is eqref; `Call(__hash_)` + // returns i32. Same shape as `emit_list_hash`'s carrier + // arm. + let idx = hash_helper_fn_idx + .get(canonical) + .copied() + .ok_or(WasmGcError::Validation(format!( + "vector hash over carrier `{canonical}`: \ + __hash_{canonical} not registered in hash_helpers" + )))?; + f.instruction(&Instruction::Call(idx)); + } } f.instruction(&Instruction::I32Add); f.instruction(&Instruction::LocalSet(1)); diff --git a/src/codegen/wasm_gc/maps.rs b/src/codegen/wasm_gc/maps.rs index a9902f1d..7e1061d0 100644 --- a/src/codegen/wasm_gc/maps.rs +++ b/src/codegen/wasm_gc/maps.rs @@ -86,6 +86,16 @@ pub(super) struct MapKVHelpers { /// `from_list(l) -> Map`. Walks `l`, struct.get's the /// (K, V) from each tuple, calls the per-(K, V) `set` helper. pub(super) from_list: u32, + /// `__eq_Map(a, b) -> i32`. Structural eq — `a.size == + /// b.size && ∀ k ∈ a: get(b, k) == Some(a[k])`. Insertion order + /// is intentionally ignored (matches VM's `HashMap` PartialEq + + /// the Python/Java/Rust/Haskell mainstream). + pub(super) eq: u32, + /// `__hash_Map(m) -> i32`. Order-independent commutative + /// fold — `h = 0; for (k, v) in m: h ^= djb2(k) * 33 + djb2(v)`. + /// XOR is commutative + associative so the result is invariant + /// to bucket ordering. + pub(super) hash: u32, } #[derive(Default)] @@ -105,7 +115,24 @@ pub(super) struct MapHelperRegistry { /// matches `assign_slots` / `emit_function_section` / /// `emit_helper_bodies` exactly. #[allow(clippy::type_complexity)] - kv_type_indices: HashMap, + kv_type_indices: HashMap< + String, + ( + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + ), + >, } impl MapHelperRegistry { @@ -129,7 +156,7 @@ impl MapHelperRegistry { let mut k_names: Vec = Vec::new(); let mut k_seen: std::collections::HashSet = std::collections::HashSet::new(); for canonical in map_canonicals { - let (k_aver, _) = + let (k_aver, v_aver) = super::types::parse_map_kv(canonical).ok_or(WasmGcError::Validation(format!( "MapHelperRegistry: cannot parse K, V from `{canonical}`" )))?; @@ -152,12 +179,26 @@ impl MapHelperRegistry { .filter(|v| v.parent == k_aver) .any(|v| v.fields.iter().any(|t| t.trim() == "String")); } + // Map's structural eq + hash dispatches V via the + // same `__hash_` / `__eq_` helper map K uses, so V + // is force-registered as pseudo-K too. Skips primitive V + // (the body emitter falls back to inline cmp for those). + let v_aver_trim = v_aver.trim(); + if v_aver_trim == "String" { + needs_string = true; + } if needs_string && k_seen.insert("String".into()) { k_names.push("String".into()); } if k_seen.insert(k_aver.to_string()) { k_names.push(k_aver.to_string()); } + if !super::types::TypeRegistry::is_primitive_map_key(v_aver_trim) + && v_aver_trim != "String" + && k_seen.insert(v_aver_trim.to_string()) + { + k_names.push(v_aver_trim.to_string()); + } } // For every record / sum K, recursively collect all @@ -204,11 +245,23 @@ impl MapHelperRegistry { .values() .flat_map(|v| v.iter()) .any(|v| v.parent == ft); - if (is_record || is_sum) && k_seen.insert(ft.clone()) { + let is_carrier = ft.starts_with("Option<") + || ft.starts_with("Result<") + || ft.starts_with("Tuple<"); + if (is_record || is_sum || is_carrier) && k_seen.insert(ft.clone()) { k_names.push(ft.clone()); - to_visit.push(ft.clone()); + if !is_carrier { + // Records / sums recurse into their own + // fields. Carriers don't (their helper bodies + // delegate via Call to eq_helpers/hash_ + // helpers, which handle inner types + // themselves). + to_visit.push(ft.clone()); + } // String inside the nested type's fields → - // force-register String. + // force-register String. Carriers' name + // contains "String" only when an inner type is + // literally `String`; cheap heuristic. let mut nested_needs_string = false; if let Some(fs) = registry.record_fields.get(&ft) { nested_needs_string |= fs.iter().any(|(_, t)| t.trim() == "String"); @@ -221,6 +274,9 @@ impl MapHelperRegistry { .filter(|v| v.parent == ft) .any(|v| v.fields.iter().any(|t| t.trim() == "String")); } + if is_carrier { + nested_needs_string |= ft.contains("String"); + } if nested_needs_string && k_seen.insert("String".into()) { k_names.push("String".into()); } @@ -282,6 +338,10 @@ impl MapHelperRegistry { *next_type_idx += 1; let from_list_type_idx = *next_type_idx; *next_type_idx += 1; + let eq_type_idx = *next_type_idx; + *next_type_idx += 1; + let hash_type_idx = *next_type_idx; + *next_type_idx += 1; let empty_fn = *next_wasm_fn_idx; *next_wasm_fn_idx += 1; let set_fn = *next_wasm_fn_idx; @@ -304,6 +364,10 @@ impl MapHelperRegistry { *next_wasm_fn_idx += 1; let from_list_fn = *next_wasm_fn_idx; *next_wasm_fn_idx += 1; + let eq_fn = *next_wasm_fn_idx; + *next_wasm_fn_idx += 1; + let hash_fn = *next_wasm_fn_idx; + *next_wasm_fn_idx += 1; // K can be String, a user-defined record (field-by-field // hash + eq), or a primitive (Int / Float / Bool). Primitive @@ -318,13 +382,23 @@ impl MapHelperRegistry { .values() .flat_map(|v| v.iter()) .any(|v| v.parent == k_aver); + let is_carrier_k = k_aver.starts_with("Option<") + || k_aver.starts_with("Result<") + || k_aver.starts_with("Tuple<"); + let is_list_or_vec_k = k_aver.starts_with("List<") || k_aver.starts_with("Vector<"); + let is_map_k = k_aver.starts_with("Map<"); if k_aver != "String" && registry.record_type_idx(k_aver).is_none() && !is_primitive_k && !is_sum_k + && !is_carrier_k + && !is_list_or_vec_k + && !is_map_k { return Err(WasmGcError::Unimplemented( - "phase 3c — Map with K not String / user-record / sum / primitive", + "phase 3c — Map with K not String / user-record / sum / \ + primitive / generic-carrier (Option/Result/Tuple) / List / \ + Vector / Map", )); } @@ -342,6 +416,8 @@ impl MapHelperRegistry { remove: remove_fn, entries: entries_fn, from_list: from_list_fn, + eq: eq_fn, + hash: hash_fn, }, ); self.kv_type_indices.insert( @@ -358,6 +434,8 @@ impl MapHelperRegistry { remove_type_idx, entries_type_idx, from_list_type_idx, + eq_type_idx, + hash_type_idx, ), ); self.kv_order.push(canonical.clone()); @@ -471,6 +549,20 @@ impl MapHelperRegistry { types.ty().function([map_ref], [lt_ref]); // from_list : (List>) -> Map types.ty().function([lt_ref], [map_ref]); + // __eq_Map : (eqref, eqref) -> i32. Eqref params so + // record/sum/list/vec field dispatch can call uniformly + // (the body ref.casts both args to the typed map ref). + let eq_ref = ValType::Ref(RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Eq, + }, + }); + types.ty().function([eq_ref, eq_ref], [ValType::I32]); + // __hash_Map : (eqref) -> i32. Same eqref-shape + // calling convention as the carrier hash helpers. + types.ty().function([eq_ref], [ValType::I32]); let _ = opt_ref; let _ = tup_idx; } @@ -486,7 +578,8 @@ impl MapHelperRegistry { funcs.function(e); } for canonical in &self.kv_order { - let (em, st, gt, ln, god, pair, ks, vs, rm, en, fl) = self.kv_type_indices[canonical]; + let (em, st, gt, ln, god, pair, ks, vs, rm, en, fl, eq, hs) = + self.kv_type_indices[canonical]; funcs.function(em); funcs.function(st); funcs.function(gt); @@ -498,6 +591,8 @@ impl MapHelperRegistry { funcs.function(rm); funcs.function(en); funcs.function(fl); + funcs.function(eq); + funcs.function(hs); } } @@ -508,15 +603,29 @@ impl MapHelperRegistry { codes: &mut CodeSection, registry: &TypeRegistry, list_eq_hash: &HashMap, + carrier_eq_hash: &HashMap, ) -> Result<(), WasmGcError> { let string_key_helpers = self.key.get("String").copied(); // Snapshot every K's helpers — record hash/eq dispatch // needs to call helpers for nested record fields. Plus // virtual entries for `List` field types so hash/eq // dispatch can call into list_helpers without a - // separate cross-module lookup. + // separate cross-module lookup. Carriers (Option / Result + // / Tuple) come in through `carrier_eq_hash` from the + // `__eq_` / `__hash_` registries (eq_helpers / + // hash_helpers); their map-key body is a thin proxy that + // Calls into those. let mut all_key_helpers: HashMap = self.key.iter().map(|(k, h)| (k.clone(), *h)).collect(); + for (carrier, &(eq_fn, hash_fn)) in carrier_eq_hash { + all_key_helpers.insert( + carrier.clone(), + KeyHelpers { + hash: hash_fn, + eq: eq_fn, + }, + ); + } for (list_canonical, &(eq_fn, hash_fn)) in list_eq_hash { all_key_helpers.insert( list_canonical.clone(), @@ -561,11 +670,51 @@ impl MapHelperRegistry { let helpers = self.kv[canonical]; codes.function(&emit_map_entries(canonical, registry)?); codes.function(&emit_map_from_list(canonical, registry, helpers.set)?); + // Structural eq + commutative hash for `Map`. V's + // hash + eq fn idxs come from `all_key_helpers` (same + // table that drives K dispatch — V is just another + // shape that needs the same family of helpers when V is + // not a primitive). + let v_helpers = v_helper_for(canonical, &all_key_helpers, registry)?; + codes.function(&emit_map_eq( + canonical, + registry, + key_h, + v_helpers, + helpers.get, + )?); + codes.function(&emit_map_hash(canonical, registry, key_h, v_helpers)?); } Ok(()) } } +/// Resolve hash + eq fn idx for V — looks the same shape up as K. V +/// can be a primitive (hash/eq are inline instructions, not fn calls +/// — return None and let the body emitter pick the inline path), or +/// a ref-shaped K kind we already registered (records / sums / +/// carriers / List / Vector / Map). Returns the proxy fn idxs. +fn v_helper_for( + canonical: &str, + all_key_helpers: &HashMap, + _registry: &TypeRegistry, +) -> Result, WasmGcError> { + let (_, v_aver) = super::types::parse_map_kv(canonical).ok_or(WasmGcError::Validation( + format!("v_helper_for: bad canonical `{canonical}`"), + ))?; + let v_aver = v_aver.trim(); + if super::types::TypeRegistry::is_primitive_map_key(v_aver) { + // Primitive V (Int / Float / Bool) — body emitter inlines + // the comparison + hash, no helper dispatch. + return Ok(None); + } + // String + every other ref V flows through `all_key_helpers`, + // which assign_slots force-registered as pseudo-K (so the + // helpers exist regardless of whether the program actually + // holds `Map`). + Ok(all_key_helpers.get(v_aver).copied()) +} + /// `hash : (K) -> i32`. K can be `String` (DJB2 over the bytes) or /// any user-defined record (field-by-field combine, delegating to /// the per-K helper for String fields). @@ -592,6 +741,29 @@ fn emit_hash_for( { return emit_hash_sum(k_aver, registry, string_key_helpers); } + if k_aver.starts_with("Option<") + || k_aver.starts_with("Result<") + || k_aver.starts_with("Tuple<") + || k_aver.starts_with("List<") + || k_aver.starts_with("Vector<") + || k_aver.starts_with("Map<") + { + // Same shape as carrier eq: proxy to the per-instantiation + // `__hash_` helper. Carriers come from hash_helpers, + // List/Vector/Map from their own registries; all merged into + // `all_key_helpers` via the compound lookup at module + // assembly. + let helpers = all_key_helpers + .get(k_aver) + .ok_or(WasmGcError::Validation(format!( + "hash_for: compound `{k_aver}` has no registered hash helper" + )))?; + let mut f = Function::new([]); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::Call(helpers.hash)); + f.instruction(&Instruction::End); + return Ok(f); + } Err(WasmGcError::Unimplemented( "phase 3c — hash for unsupported K kind", )) @@ -620,6 +792,29 @@ fn emit_eq_for( { return emit_eq_sum(k_aver, registry, string_key_helpers); } + if k_aver.starts_with("Option<") + || k_aver.starts_with("Result<") + || k_aver.starts_with("Tuple<") + || k_aver.starts_with("List<") + || k_aver.starts_with("Vector<") + || k_aver.starts_with("Map<") + { + // Map-key eq for compounds proxies to `__eq_` (carriers + // from eq_helpers, List/Vector from list_helpers, Map from + // MapHelperRegistry::kv). All merged into `all_key_helpers` + // at module assembly time. + let helpers = all_key_helpers + .get(k_aver) + .ok_or(WasmGcError::Validation(format!( + "eq_for: compound `{k_aver}` has no registered eq helper" + )))?; + let mut f = Function::new([]); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::Call(helpers.eq)); + f.instruction(&Instruction::End); + return Ok(f); + } Err(WasmGcError::Unimplemented( "phase 3c — eq for unsupported K kind", )) @@ -716,6 +911,49 @@ fn emit_box_key(f: &mut Function, k_aver: &str, registry: &TypeRegistry) { } } +/// Heap type used by `Map.remove` for `ref.null` in the keys-array +/// store-back. Concrete idx for K kinds with their own struct type +/// (primitive K boxes, String, record, carrier, List, Vector); abstract +/// Eq for sum K (whose wasm rep is eqref since variants share no +/// concrete struct type). +fn key_storage_null_heap(k_aver: &str, registry: &TypeRegistry) -> HeapType { + if let Some(box_idx) = registry.primitive_key_box_idx(k_aver) { + return HeapType::Concrete(box_idx); + } + if k_aver == "String" + && let Some(s) = registry.string_array_type_idx + { + return HeapType::Concrete(s); + } + if let Some(r) = registry.record_type_idx(k_aver) { + return HeapType::Concrete(r); + } + if let Some(o) = registry.option_type_idx(k_aver) { + return HeapType::Concrete(o); + } + if let Some(r) = registry.result_type_idx(k_aver) { + return HeapType::Concrete(r); + } + if let Some(t) = registry.tuple_type_idx(k_aver) { + return HeapType::Concrete(t); + } + if let Some(l) = registry.list_type_idx(k_aver) { + return HeapType::Concrete(l); + } + if let Some(v) = registry.vector_type_idx(k_aver) { + return HeapType::Concrete(v); + } + if let Some(slots) = registry.map_slots(k_aver) { + return HeapType::Concrete(slots.map); + } + // Sum K — eqref. ref.null of abstract Eq is a subtype of every + // concrete variant ref, so the array.set typechecks. + HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Eq, + } +} + fn string_idx(registry: &TypeRegistry) -> Result { registry .string_array_type_idx @@ -1438,30 +1676,38 @@ fn emit_hash_record( // Nested record / List field. Both dispatch via // `all_key_helpers` — records were force-registered // as pseudo-K in `assign_slots`; list canonicals were - // injected by `emit_helper_bodies` from list_helpers. + // injected by `emit_helper_bodies` from list_helpers; + // carriers (Option/Result/Tuple) come from + // `carrier_eq_hash`. let lookup_key = if other.starts_with("List<") || other.starts_with("Vector<") { super::types::normalize_compound(other).to_string() } else { other.to_string() }; let is_compound = other.starts_with("List<") || other.starts_with("Vector<"); + let is_carrier = other.starts_with("Option<") + || other.starts_with("Result<") + || other.starts_with("Tuple<"); let is_sum = registry .variants .values() .flat_map(|v| v.iter()) .any(|v| v.parent == other); - if registry.record_type_idx(other).is_some() || is_compound || is_sum { + if registry.record_type_idx(other).is_some() || is_compound || is_sum || is_carrier + { let inner = all_key_helpers .get(&lookup_key) .ok_or(WasmGcError::Validation(format!( "hash_record: field `{other}` has no key helpers \ - (record / list / vector / sum T may need force-registration)" + (record / list / vector / sum / Option / Result / Tuple T \ + may need force-registration)" )))?; f.instruction(&Instruction::Call(inner.hash)); } else { return Err(WasmGcError::Unimplemented( "phase 3c — record-key field type not in \ - {Int, Float, Bool, String, nested record, List, Vector, sum}", + {Int, Float, Bool, String, nested record, List, Vector, sum, \ + Option/Result/Tuple}", )); } } @@ -1523,12 +1769,16 @@ fn emit_eq_record( other.to_string() }; let is_compound = other.starts_with("List<") || other.starts_with("Vector<"); + let is_carrier = other.starts_with("Option<") + || other.starts_with("Result<") + || other.starts_with("Tuple<"); let is_sum = registry .variants .values() .flat_map(|v| v.iter()) .any(|v| v.parent == other); - if registry.record_type_idx(other).is_some() || is_compound || is_sum { + if registry.record_type_idx(other).is_some() || is_compound || is_sum || is_carrier + { let inner = all_key_helpers .get(&lookup_key) .ok_or(WasmGcError::Validation(format!( @@ -1538,7 +1788,8 @@ fn emit_eq_record( } else { return Err(WasmGcError::Unimplemented( "phase 3c — record-key field type not in \ - {Int, Float, Bool, String, nested record, List, Vector, sum}", + {Int, Float, Bool, String, nested record, List, Vector, sum, \ + Option/Result/Tuple}", )); } } @@ -1738,6 +1989,334 @@ fn emit_map_walk_values_to_list( Ok(f) } +/// `__eq_Map(a: eqref, b: eqref) -> i32`. Structural eq — +/// `a.size == b.size && ∀ k ∈ a: get(b, k) == Some(a[k])`. Order- +/// independent (matches VM's Rust HashMap PartialEq). +fn emit_map_eq( + canonical: &str, + registry: &TypeRegistry, + keyh: KeyHelpers, + v_helpers: Option, + get_fn_idx: u32, +) -> Result { + let slots = slots_for(canonical, registry)?; + let (k_aver, v_aver) = super::types::parse_map_kv(canonical).unwrap(); + let _ = keyh; + let map_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Concrete(slots.map), + }); + let keys_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Concrete(slots.keys_array), + }); + let values_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Concrete(slots.values_array), + }); + let v_val = + super::types::aver_to_wasm(v_aver, Some(registry))?.ok_or(WasmGcError::Validation( + format!("Map<{k_aver},{v_aver}>.eq: V `{v_aver}` has no wasm rep"), + ))?; + let opt_idx = registry + .option_type_idx(&format!("Option<{v_aver}>")) + .ok_or(WasmGcError::Validation(format!( + "Map.eq: `Option<{v_aver}>` not registered" + )))?; + let opt_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Concrete(opt_idx), + }); + // Locals: 2=typed map_a, 3=typed map_b, 4=cap, 5=i, 6=keys_a, + // 7=values_a, 8=cur_key (boxed), 9=opt result, 10=v_a, 11=v_b + let mut f = Function::new(vec![ + (1, map_ref), + (1, map_ref), + (1, ValType::I32), + (1, ValType::I32), + (1, keys_ref), + (1, values_ref), + (1, key_storage_val_type(k_aver, registry)?), + (1, opt_ref), + (1, v_val), + (1, v_val), + ]); + let map_heap = HeapType::Concrete(slots.map); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(map_heap)); + f.instruction(&Instruction::LocalSet(2)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::RefCastNonNull(map_heap)); + f.instruction(&Instruction::LocalSet(3)); + // if a.size != b.size return 0 + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: slots.map, + field_index: 0, + }); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::StructGet { + struct_type_index: slots.map, + field_index: 0, + }); + f.instruction(&Instruction::I32Ne); + f.instruction(&Instruction::If(BlockType::Empty)); + f.instruction(&Instruction::I32Const(0)); + f.instruction(&Instruction::Return); + f.instruction(&Instruction::End); + // cap = a.cap; keys_a = a.keys; values_a = a.values; i = 0 + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: slots.map, + field_index: 1, + }); + f.instruction(&Instruction::LocalSet(4)); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: slots.map, + field_index: 2, + }); + f.instruction(&Instruction::LocalSet(6)); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::StructGet { + struct_type_index: slots.map, + field_index: 3, + }); + f.instruction(&Instruction::LocalSet(7)); + f.instruction(&Instruction::I32Const(0)); + f.instruction(&Instruction::LocalSet(5)); + // for i in 0..cap + f.instruction(&Instruction::Block(BlockType::Empty)); + f.instruction(&Instruction::Loop(BlockType::Empty)); + f.instruction(&Instruction::LocalGet(5)); + f.instruction(&Instruction::LocalGet(4)); + f.instruction(&Instruction::I32GeS); + f.instruction(&Instruction::BrIf(1)); + // cur_key = keys_a[i] + f.instruction(&Instruction::LocalGet(6)); + f.instruction(&Instruction::LocalGet(5)); + f.instruction(&Instruction::ArrayGet(slots.keys_array)); + f.instruction(&Instruction::LocalSet(8)); + // if cur_key != null: probe b + f.instruction(&Instruction::LocalGet(8)); + f.instruction(&Instruction::RefIsNull); + f.instruction(&Instruction::I32Eqz); + f.instruction(&Instruction::If(BlockType::Empty)); + // opt = get_fn(b, unbox(cur_key)) + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::LocalGet(8)); + emit_unbox_key(&mut f, k_aver, registry); + f.instruction(&Instruction::Call(get_fn_idx)); + f.instruction(&Instruction::LocalSet(9)); + // if opt.tag == 0 → return 0 + f.instruction(&Instruction::LocalGet(9)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 0, + }); + f.instruction(&Instruction::I32Eqz); + f.instruction(&Instruction::If(BlockType::Empty)); + f.instruction(&Instruction::I32Const(0)); + f.instruction(&Instruction::Return); + f.instruction(&Instruction::End); + // v_a = values_a[i]; v_b = opt.value + f.instruction(&Instruction::LocalGet(7)); + f.instruction(&Instruction::LocalGet(5)); + f.instruction(&Instruction::ArrayGet(slots.values_array)); + f.instruction(&Instruction::LocalSet(10)); + f.instruction(&Instruction::LocalGet(9)); + f.instruction(&Instruction::StructGet { + struct_type_index: opt_idx, + field_index: 1, + }); + f.instruction(&Instruction::LocalSet(11)); + // if v_a != v_b → return 0 + f.instruction(&Instruction::LocalGet(10)); + f.instruction(&Instruction::LocalGet(11)); + emit_v_eq(&mut f, v_aver, v_helpers)?; + f.instruction(&Instruction::I32Eqz); + f.instruction(&Instruction::If(BlockType::Empty)); + f.instruction(&Instruction::I32Const(0)); + f.instruction(&Instruction::Return); + f.instruction(&Instruction::End); + f.instruction(&Instruction::End); + // i++ + f.instruction(&Instruction::LocalGet(5)); + f.instruction(&Instruction::I32Const(1)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(5)); + f.instruction(&Instruction::Br(0)); + f.instruction(&Instruction::End); + f.instruction(&Instruction::End); + f.instruction(&Instruction::I32Const(1)); + f.instruction(&Instruction::End); + Ok(f) +} + +/// Stack: `[v_a, v_b]` of Aver type `v_aver`. Push i32 (1=eq, 0=ne). +/// Primitive V → inline cmp; ref V → `Call(v_helpers.eq)`. +fn emit_v_eq( + f: &mut Function, + v_aver: &str, + v_helpers: Option, +) -> Result<(), WasmGcError> { + match v_aver.trim() { + "Int" => { + f.instruction(&Instruction::I64Eq); + } + "Bool" => { + f.instruction(&Instruction::I32Eq); + } + "Float" => { + f.instruction(&Instruction::F64Eq); + } + _ => { + let h = v_helpers.ok_or(WasmGcError::Validation(format!( + "emit_v_eq: V `{v_aver}` needs ref helpers (record/sum/carrier/list/vec/map)" + )))?; + f.instruction(&Instruction::Call(h.eq)); + } + } + Ok(()) +} + +/// Stack: `[v]` of Aver type `v_aver`. Push i32 hash. Primitive V → +/// inline DJB2-style mix; ref V → `Call(v_helpers.hash)`. +fn emit_v_hash( + f: &mut Function, + v_aver: &str, + v_helpers: Option, +) -> Result<(), WasmGcError> { + match v_aver.trim() { + "Int" => { + f.instruction(&Instruction::I32WrapI64); + } + "Bool" => {} // already i32 + "Float" => { + f.instruction(&Instruction::I64ReinterpretF64); + f.instruction(&Instruction::I32WrapI64); + } + _ => { + let h = v_helpers.ok_or(WasmGcError::Validation(format!( + "emit_v_hash: V `{v_aver}` needs ref helpers" + )))?; + f.instruction(&Instruction::Call(h.hash)); + } + } + Ok(()) +} + +/// `__hash_Map(m: eqref) -> i32`. XOR-fold per occupied entry of +/// `djb2(k) * 33 + djb2(v)`. XOR is commutative + associative → the +/// result is invariant to bucket / insertion order. +fn emit_map_hash( + canonical: &str, + registry: &TypeRegistry, + keyh: KeyHelpers, + v_helpers: Option, +) -> Result { + let slots = slots_for(canonical, registry)?; + let (k_aver, v_aver) = super::types::parse_map_kv(canonical).unwrap(); + let map_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Concrete(slots.map), + }); + let keys_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Concrete(slots.keys_array), + }); + let values_ref = ValType::Ref(RefType { + nullable: true, + heap_type: HeapType::Concrete(slots.values_array), + }); + // Locals: 1=typed map, 2=cap, 3=i, 4=keys, 5=values, 6=cur_key + // (boxed), 7=h (i32 accumulator), 8=entry_h (i32 per-entry mix). + let mut f = Function::new(vec![ + (1, map_ref), + (1, ValType::I32), + (1, ValType::I32), + (1, keys_ref), + (1, values_ref), + (1, key_storage_val_type(k_aver, registry)?), + (1, ValType::I32), + (1, ValType::I32), + ]); + let map_heap = HeapType::Concrete(slots.map); + f.instruction(&Instruction::LocalGet(0)); + f.instruction(&Instruction::RefCastNonNull(map_heap)); + f.instruction(&Instruction::LocalSet(1)); + f.instruction(&Instruction::I32Const(0)); + f.instruction(&Instruction::LocalSet(7)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: slots.map, + field_index: 1, + }); + f.instruction(&Instruction::LocalSet(2)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: slots.map, + field_index: 2, + }); + f.instruction(&Instruction::LocalSet(4)); + f.instruction(&Instruction::LocalGet(1)); + f.instruction(&Instruction::StructGet { + struct_type_index: slots.map, + field_index: 3, + }); + f.instruction(&Instruction::LocalSet(5)); + f.instruction(&Instruction::I32Const(0)); + f.instruction(&Instruction::LocalSet(3)); + f.instruction(&Instruction::Block(BlockType::Empty)); + f.instruction(&Instruction::Loop(BlockType::Empty)); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::LocalGet(2)); + f.instruction(&Instruction::I32GeS); + f.instruction(&Instruction::BrIf(1)); + f.instruction(&Instruction::LocalGet(4)); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::ArrayGet(slots.keys_array)); + f.instruction(&Instruction::LocalSet(6)); + f.instruction(&Instruction::LocalGet(6)); + f.instruction(&Instruction::RefIsNull); + f.instruction(&Instruction::I32Eqz); + f.instruction(&Instruction::If(BlockType::Empty)); + // entry_h = hash_K(unbox(cur_key)) * 33 + hash_V(values[i]) + f.instruction(&Instruction::LocalGet(6)); + emit_unbox_key(&mut f, k_aver, registry); + f.instruction(&Instruction::Call(keyh.hash)); + f.instruction(&Instruction::I32Const(5)); + f.instruction(&Instruction::I32Shl); + // h_k * 32 (will add h_k below to become *33; OR more accurate: + // shift-add for *33). Cheaper: do `(kh<<5) + kh + vh`. + f.instruction(&Instruction::LocalGet(6)); + emit_unbox_key(&mut f, k_aver, registry); + f.instruction(&Instruction::Call(keyh.hash)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalGet(5)); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::ArrayGet(slots.values_array)); + emit_v_hash(&mut f, v_aver, v_helpers)?; + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(8)); + // h ^= entry_h + f.instruction(&Instruction::LocalGet(7)); + f.instruction(&Instruction::LocalGet(8)); + f.instruction(&Instruction::I32Xor); + f.instruction(&Instruction::LocalSet(7)); + f.instruction(&Instruction::End); + f.instruction(&Instruction::LocalGet(3)); + f.instruction(&Instruction::I32Const(1)); + f.instruction(&Instruction::I32Add); + f.instruction(&Instruction::LocalSet(3)); + f.instruction(&Instruction::Br(0)); + f.instruction(&Instruction::End); + f.instruction(&Instruction::End); + f.instruction(&Instruction::LocalGet(7)); + f.instruction(&Instruction::End); + Ok(f) +} + /// `remove(map, k) -> map`. Linear-probe locate the entry; if not /// found, return the map unchanged. If found, do a backwards-shift /// over the contiguous probe chain so subsequent `get` calls still @@ -1922,19 +2501,14 @@ fn emit_map_remove( f.instruction(&Instruction::End); f.instruction(&Instruction::End); - // keys[i] = null. Heap type matches the keys array element ref: - // primitive-key box for primitive K, String slot for K=String, - // record slot for K=record. - let null_heap_idx = registry - .primitive_key_box_idx(k_aver) - .or(registry - .string_array_type_idx - .filter(|_| k_aver == "String")) - .or_else(|| registry.record_type_idx(k_aver)) - .unwrap_or(0); + // keys[i] = null. Heap type matches the keys array element ref — + // see `key_storage_null_heap` for the per-K-kind table (primitive + // box / String / record / carrier / List / Vector concrete idx, + // abstract Eq for sum K). + let null_heap = key_storage_null_heap(k_aver, registry); f.instruction(&Instruction::LocalGet(4)); f.instruction(&Instruction::LocalGet(7)); - f.instruction(&Instruction::RefNull(HeapType::Concrete(null_heap_idx))); + f.instruction(&Instruction::RefNull(null_heap)); f.instruction(&Instruction::ArraySet(slots.keys_array)); // map.size = map.size - 1 diff --git a/src/codegen/wasm_gc/module.rs b/src/codegen/wasm_gc/module.rs index b40b80e4..68a22729 100644 --- a/src/codegen/wasm_gc/module.rs +++ b/src/codegen/wasm_gc/module.rs @@ -26,6 +26,7 @@ use wasm_encoder::{ use super::WasmGcError; use super::body::eq_helpers::{EqHelperRegistry, EqKind}; +use super::body::hash_helpers::{HashHelperRegistry, HashKind}; use super::body::{FnEntry, FnMap, emit_fn_body}; use super::builtins::{BuiltinName, BuiltinRegistry}; use super::effects::{EffectName, EffectRegistry}; @@ -42,6 +43,14 @@ pub(super) fn emit_module( ) -> Result, WasmGcError> { let registry = TypeRegistry::build_with_handler(items, handler_name.is_some()); + // Lazy caller_fn name registry — populated during user-fn body + // emit by `emit_caller_fn_idx` call sites. Threaded into every + // `emit_fn_body` call via `EmitCtx::caller_fn_collector`. The + // post-emit phase reads `collector.names` to materialise the + // exported caller-fn name table (`__caller_fn_count` + + // `__caller_fn_name`) and the matching passive data segments. + let caller_fn_collector = std::cell::RefCell::new(super::body::CallerFnCollector::default()); + let fn_defs: Vec<&FnDef> = items .iter() .filter_map(|it| match it { @@ -58,6 +67,7 @@ pub(super) fn emit_module( let mut builtin_registry = BuiltinRegistry::new(); let mut effect_registry = EffectRegistry::new(); let mut eq_helpers_registry = EqHelperRegistry::new(); + let mut hash_helpers_registry = HashHelperRegistry::new(); for fd in &fn_defs { discover_builtins_in_fn( fd, @@ -67,6 +77,78 @@ pub(super) fn emit_module( ®istry, ); } + // Sweep nominal element types of every registered List / Vector + // and key types of every registered Map. The list/vec helper + // bodies dispatch nominal element eq/hash via `Call(__eq_)` + // (since 0.16.3); without auto-registering those types here, a + // program that holds `List` without ever writing + // `list == list` directly would still get a list helper body + // that calls into an unregistered `__eq_Item`. Keys of `Map` + // need the same: maps.rs `emit_eq_for(K)` reaches into + // `__eq_` helpers when K is a record/sum field-of-field. + let mut nominal_seed: Vec = Vec::new(); + for canonical in ®istry.list_order { + if let Some(elem) = super::types::TypeRegistry::list_element_type(canonical) { + nominal_seed.push(elem.trim().to_string()); + } + } + for canonical in ®istry.vector_order { + if let Some(elem) = super::types::TypeRegistry::vector_element_type(canonical) { + nominal_seed.push(elem.trim().to_string()); + } + } + for canonical in ®istry.map_order { + if let Some((k, _v)) = super::types::parse_map_kv(canonical) { + nominal_seed.push(k.trim().to_string()); + } + } + for name in &nominal_seed { + if registry.record_fields.contains_key(name) { + eq_helpers_registry.register_transitive(name, EqKind::Record, ®istry); + hash_helpers_registry.register_transitive(name, HashKind::Record, ®istry); + } else if registry + .variants + .values() + .flat_map(|v| v.iter()) + .any(|v| &v.parent == name) + { + eq_helpers_registry.register_transitive(name, EqKind::Sum, ®istry); + hash_helpers_registry.register_transitive(name, HashKind::Sum, ®istry); + } else if name.starts_with("Option<") && name.ends_with('>') { + // Carrier element of List> / Vector> + // / Map, _> — list/vec eq+hash bodies dispatch + // each element via `Call(__eq_Option)` which means + // eq_helpers must hold the slot. Same logic for the hash + // side. Inner type registration happens transitively. + eq_helpers_registry.register_transitive(name, EqKind::OptionEq, ®istry); + hash_helpers_registry.register_transitive(name, HashKind::OptionHash, ®istry); + } else if name.starts_with("Result<") && name.ends_with('>') { + eq_helpers_registry.register_transitive(name, EqKind::ResultEq, ®istry); + hash_helpers_registry.register_transitive(name, HashKind::ResultHash, ®istry); + } else if name.starts_with("Tuple<") && name.ends_with('>') { + eq_helpers_registry.register_transitive(name, EqKind::TupleEq, ®istry); + hash_helpers_registry.register_transitive(name, HashKind::TupleHash, ®istry); + } + } + // Mirror eq registry's transitive shape — every type registered + // for eq dispatch also needs a hash helper, since list/vec/map + // helpers and per-record/sum hash bodies dispatch through + // `Call(__hash_)` for non-primitive fields. Walk the eq + // registry post-seed and register matching hash slots. + let eq_snapshot: Vec<(String, EqKind)> = eq_helpers_registry + .iter() + .map(|(n, k)| (n.to_string(), k)) + .collect(); + for (name, kind) in &eq_snapshot { + let hk = match kind { + EqKind::Record => HashKind::Record, + EqKind::Sum => HashKind::Sum, + EqKind::OptionEq => HashKind::OptionHash, + EqKind::ResultEq => HashKind::ResultHash, + EqKind::TupleEq => HashKind::TupleHash, + }; + hash_helpers_registry.register_transitive(name, hk, ®istry); + } // Eq helpers over records / sums with String fields need // `__wasmgc_string_eq` — force-register so the slot is allocated // before bodies emit. @@ -207,6 +289,8 @@ pub(super) fn emit_module( // `__wasmgc_string_eq` registered above). eq_helpers_registry.assign_slots(&mut next_builtin_fn_idx, &mut next_type_idx); eq_helpers_registry.emit_helper_types(&mut types); + hash_helpers_registry.assign_slots(&mut next_builtin_fn_idx, &mut next_type_idx); + hash_helpers_registry.emit_helper_types(&mut types); // 8a) `aver_http_handle` wrapper — `--handler X` synthesises a // no-arg fn that reads request fields via the `Request.*` @@ -307,6 +391,37 @@ pub(super) fn emit_module( &effect_registry, )?; + // 10) Caller-fn name table exports. `__caller_fn_count() -> i32` + // and `__caller_fn_name(i32) -> ref null $string`. Host walks + // `0..count` once at instantiation, decodes each ref via the + // LM bridge, caches in a `Vec`. Per effect call: `i32` + // idx flows through `params.last()` → vector index lookup, + // no LM round-trip on the hot path. + // + // Allocated only when the program has the String slot (i.e. + // any fn def, since `needs_string` forces the slot whenever + // `has_fn_defs`). Programs without fns never emit caller_fn + // anywhere so the exports would be unused. + let caller_fn_table_types: Option<(u32, u32)> = + if let Some(string_type_idx) = registry.string_array_type_idx { + // count: () -> i32 + types.ty().function([], [ValType::I32]); + let count_type_idx = next_type_idx; + next_type_idx += 1; + // name: (i32) -> (ref null $string) + let string_ref_ty = ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(string_type_idx), + }); + types.ty().function([ValType::I32], [string_ref_ty]); + let name_type_idx = next_type_idx; + // Last type allocation in this fn — `next_type_idx` + // increment dropped to silence `unused_assignments`. + Some((count_type_idx, name_type_idx)) + } else { + None + }; + module.section(&types); // ── Import section ───────────────────────────────────────────── @@ -343,6 +458,13 @@ pub(super) fn emit_module( .expect("registered eq helper has type idx after assign_slots"); funcs.function(t_idx); } + // Hash helpers — same shape (one entry per registered slot). + for (name, _kind) in hash_helpers_registry.iter() { + let t_idx = hash_helpers_registry + .lookup_type_idx(name) + .expect("registered hash helper has type idx after assign_slots"); + funcs.function(t_idx); + } if let Some(hw) = &handler_wrapper { funcs.function(hw.wrapper_type); funcs.function(hw.list_cons_type); @@ -354,20 +476,18 @@ pub(super) fn emit_module( funcs.function(b.grow_type); } factory_exports.emit_function_entries(&mut funcs); - // `__init_globals`: wasm-level start fn that lazy-fills the - // caller_fn globals via `array.new_data`. Has to be a separate - // fn (not part of `_start`) because the host invokes `main` - // directly when both are exported, bypassing `_start` — - // wasm-level start fns are auto-run at instantiation regardless - // of which export the host calls. Allocated last so its idx is - // import_count + funcs.len() once the entry is appended. - let init_globals_fn_idx: Option = if !registry.caller_fn_global_order.is_empty() { - let idx = import_count + funcs.len(); - funcs.function(start_type_idx); - Some(idx) - } else { - None - }; + // Caller-fn name table fns — fixed-shape entries (count + name), + // their bodies land at the very end of the code section once + // `caller_fn_collector` has all names. Idxs are recorded so + // `module.section(&exports)` can wire them up without re-deriving + // the position. + let caller_fn_table_fns: Option<(u32, u32)> = caller_fn_table_types.map(|(c_ty, n_ty)| { + let count_fn_idx = import_count + funcs.len(); + funcs.function(c_ty); + let name_fn_idx = import_count + funcs.len(); + funcs.function(n_ty); + (count_fn_idx, name_fn_idx) + }); module.section(&funcs); // ── Memory section (bridge LM only) ──────────────────────────── @@ -391,40 +511,10 @@ pub(super) fn emit_module( module.section(&memories); } - // ── Global section ───────────────────────────────────────────── - // One mutable `(ref null $string)` global per fn def, declared as - // `ref.null` and lazy-initialised at the top of `_start` via - // `array.new_data $string $segment 0 N`. The wasm-gc spec doesn't - // permit `array.new_data` in const-expr position, so the init has - // to land in a code body — `_start` runs once per instantiation - // and dominates every effect call site, which is what we need. - // Body emit pushes the caller-fn ref through - // `global.get $caller_fn_` at every effect call site — the - // alloc happens once at startup, the hot path costs one global - // load. - if !registry.caller_fn_global_order.is_empty() { - let string_idx = registry - .string_array_type_idx - .expect("caller_fn globals require the $string slot — TypeRegistry forces it on"); - let mut globals = wasm_encoder::GlobalSection::new(); - for _ in ®istry.caller_fn_global_order { - let init = wasm_encoder::ConstExpr::extended([Instruction::RefNull( - wasm_encoder::HeapType::Concrete(string_idx), - )]); - globals.global( - wasm_encoder::GlobalType { - val_type: wasm_encoder::ValType::Ref(wasm_encoder::RefType { - nullable: true, - heap_type: wasm_encoder::HeapType::Concrete(string_idx), - }), - mutable: true, - shared: false, - }, - &init, - ); - } - module.section(&globals); - } + // (caller_fn delivery moved from per-fn globals to an exported + // name table; segment append + `__caller_fn_*` exports are + // wired in the post-emit phase further down. Globals + their + // start-fn init are gone.) // Build the fn-name → wasm-fn-idx map. With K imports: // imports at idx 0..K @@ -487,6 +577,16 @@ pub(super) fn emit_module( eq_helpers_lookup.insert(name.to_string(), fn_idx); } } + // Map structural-eq fn idxs flow through the same lookup so + // BinOp::Eq on a Map dispatches via `Call(__eq_Map)` (sum_ + // or_record_eq_fn → ctx.fn_map.eq_helpers). Whitespace-free + // canonical matches what the operand's `.ty().display()` produces + // at the call site. + for canonical in ®istry.map_order { + if let Some(h) = map_helpers.kv_helpers(canonical) { + eq_helpers_lookup.insert(canonical.clone(), h.eq); + } + } let fn_map = FnMap { by_name, builtins: builtin_idx_lookup, @@ -534,22 +634,45 @@ pub(super) fn emit_module( ); } } + if let Some((count_fn_idx, name_fn_idx)) = caller_fn_table_fns { + exports.export("__caller_fn_count", ExportKind::Func, count_fn_idx); + exports.export("__caller_fn_name", ExportKind::Func, name_fn_idx); + } module.section(&exports); - // ── Start section ────────────────────────────────────────────── - // Wasm-level start fn — auto-runs once at instantiation, ahead of - // any host-invoked export. Used for caller_fn global init. - if let Some(idx) = init_globals_fn_idx { - module.section(&wasm_encoder::StartSection { - function_index: idx, - }); + // (No StartSection — 0.16.2's caller_fn globals init is gone; + // host reads the caller_fn name table via `__caller_fn_count` + // + `__caller_fn_name(i)` exports at instantiation instead.) + + // Pre-pass over user fn bodies — populates `caller_fn_collector` + // with every fn name that emits caller_fn at a call site. Needed + // before data count + data section emit because the count of + // passive segments is `string_literals + collector.names`, and + // data count section must precede the code section. Real body + // emit later in the code section calls `register` again with the + // same names; the collector is idempotent so the idx assignment + // matches what the call sites observed during this probe. + for (i, fd) in fn_defs.iter().enumerate() { + let self_wasm_idx = import_count + 1 + (i as u32); + let mut probe = Function::new([]); + let _ = emit_fn_body( + &mut probe, + fd, + &fn_map, + self_wasm_idx, + ®istry, + &effect_idx_lookup, + &caller_fn_collector, + )?; } + let caller_fn_segment_count = caller_fn_collector.borrow().names.len() as u32; // ── Data count section (must precede code when using passive // segments via array.new_data / data.drop). - if !registry.string_literals.is_empty() { + let total_segment_count = registry.string_literals.len() as u32 + caller_fn_segment_count; + if total_segment_count > 0 { let count = DataCountSection { - count: registry.string_literals.len() as u32, + count: total_segment_count, }; module.section(&count); } @@ -585,6 +708,7 @@ pub(super) fn emit_module( self_wasm_idx, ®istry, &effect_idx_lookup, + &caller_fn_collector, )?; let local_groups: Vec<(u32, ValType)> = extra_locals_dry.iter().map(|v| (1, *v)).collect(); @@ -596,6 +720,7 @@ pub(super) fn emit_module( self_wasm_idx, ®istry, &effect_idx_lookup, + &caller_fn_collector, )?; codes.function(&func); } @@ -630,16 +755,98 @@ pub(super) fn emit_module( compound_eq_hash_lookup.insert(format!("Vector<{}>", elem.trim()), (eq_fn, hash_fn)); } } - map_helpers.emit_helper_bodies(&mut codes, ®istry, &compound_eq_hash_lookup)?; + // Map structural eq + commutative hash — per-instantiation + // helpers live in MapHelperRegistry::kv. Threading them into the + // compound lookup lets record/sum/list/vec field dispatch call + // `__eq_Map` / `__hash_Map` uniformly with carriers. + for canonical in ®istry.map_order { + if let Some(h) = map_helpers.kv_helpers(canonical) { + compound_eq_hash_lookup.insert(canonical.clone(), (h.eq, h.hash)); + } + } + // Carrier eq+hash lookup — Option/Result/Tuple instantiations + // get their helpers from eq_helpers / hash_helpers; map keys + // proxy through these. Build the pair map by zipping the two + // registries' fn idxs by canonical. + let mut carrier_eq_hash_lookup: HashMap = HashMap::new(); + for (name, kind) in eq_helpers_registry.iter() { + use super::body::eq_helpers::EqKind as EK; + if matches!(kind, EK::OptionEq | EK::ResultEq | EK::TupleEq) + && let Some(eq_fn) = eq_helpers_registry.lookup_fn_idx(name) + && let Some(hash_fn) = hash_helpers_registry.lookup_fn_idx(name) + { + carrier_eq_hash_lookup.insert(name.to_string(), (eq_fn, hash_fn)); + } + } + map_helpers.emit_helper_bodies( + &mut codes, + ®istry, + &compound_eq_hash_lookup, + &carrier_eq_hash_lookup, + )?; // List / Vector.fromList / String.split-join helper bodies. + // Snapshot eq-helper fn idxs so list/vec eq+hash bodies can + // dispatch nominal-element `==`/hash through `Call(__eq_)`. + // Merge in list_helpers' own list/vec canonicals so `List>` + // / `List>` element dispatch finds the inner helper. let string_eq_fn_idx = builtin_registry.lookup_wasm_fn_idx(BuiltinName::StringEq); - list_helpers.emit_helper_bodies(&mut codes, ®istry, string_eq_fn_idx)?; + let mut eq_helper_fn_idx_map: HashMap = eq_helpers_registry + .iter() + .filter_map(|(n, _k)| { + eq_helpers_registry + .lookup_fn_idx(n) + .map(|i| (n.to_string(), i)) + }) + .collect(); + let mut hash_helper_fn_idx_map: HashMap = hash_helpers_registry + .iter() + .filter_map(|(n, _k)| { + hash_helpers_registry + .lookup_fn_idx(n) + .map(|i| (n.to_string(), i)) + }) + .collect(); + for (canonical, (eq_fn, hash_fn)) in &compound_eq_hash_lookup { + eq_helper_fn_idx_map.insert(canonical.clone(), *eq_fn); + hash_helper_fn_idx_map.insert(canonical.clone(), *hash_fn); + } + list_helpers.emit_helper_bodies( + &mut codes, + ®istry, + string_eq_fn_idx, + &eq_helper_fn_idx_map, + &hash_helper_fn_idx_map, + )?; // Per-(record/sum) `__eq_` helper bodies — emit after // list helpers so any String fields can call `__wasmgc_string_eq` - // by the index recorded above. - eq_helpers_registry.emit_helper_bodies(&mut codes, ®istry, string_eq_fn_idx)?; + // by the index recorded above. The compound eq lookup forwards + // `List` / `Vector` fn idxs so a record field of type + // `List>` (etc.) can dispatch via + // `Call(__eq_List<…>)`. Same shape on the hash side. + let compound_eq_lookup: HashMap = compound_eq_hash_lookup + .iter() + .map(|(n, (eq, _))| (n.clone(), *eq)) + .collect(); + let compound_hash_lookup: HashMap = compound_eq_hash_lookup + .iter() + .map(|(n, (_, h))| (n.clone(), *h)) + .collect(); + eq_helpers_registry.emit_helper_bodies( + &mut codes, + ®istry, + string_eq_fn_idx, + &compound_eq_lookup, + )?; + // `__hash_` helper bodies — emitted right after eq helpers so + // every nominal/carrier hash dispatch finds its target fn_idx. + hash_helpers_registry.emit_helper_bodies( + &mut codes, + ®istry, + string_eq_fn_idx, + &compound_hash_lookup, + )?; if let Some(hw) = &handler_wrapper { let user_handler_wasm_idx = import_count + 1 + (hw.user_handler_idx as u32); @@ -658,31 +865,68 @@ pub(super) fn emit_module( factory_exports.emit_bodies(&mut codes, ®istry)?; - // `__init_globals` body — one `array.new_data → global.set` per - // registered caller_fn name. Walks `caller_fn_global_order` so - // global idx i in the section matches the `i`-th name in the - // walker output. Runs at instantiation via the StartSection - // emitted below. - if init_globals_fn_idx.is_some() { + // `__caller_fn_count` + `__caller_fn_name` bodies. Emitted after + // every helper so their fn idxs land last in the code section, + // matching the function section allocation order. The collector + // is fully populated at this point — every user-fn body ran + // through the pre-pass and the real-emit pass. + if let Some((_count_fn_idx, _name_fn_idx)) = caller_fn_table_fns { + let names = caller_fn_collector.borrow(); let string_idx = registry .string_array_type_idx - .expect("caller_fn globals require the $string slot"); - let mut init = Function::new([]); - for (idx, fn_name) in registry.caller_fn_global_order.iter().enumerate() { + .expect("caller_fn name table requires the $string slot"); + // Caller-fn name segments occupy the data section slot range + // [string_literals.len()..string_literals.len()+names.len()]; + // `array.new_data` in `__caller_fn_name(i)` reads from those + // idxs. + let segment_base = registry.string_literals.len() as u32; + + // __caller_fn_count: pure constant. + let mut count_fn = Function::new([]); + count_fn.instruction(&Instruction::I32Const(names.names.len() as i32)); + count_fn.instruction(&Instruction::End); + codes.function(&count_fn); + + // __caller_fn_name(idx) -> ref null $string. Switch on idx + // via `br_table`; each arm materialises the matching String + // ref via `array.new_data`. A trailing default arm returns + // ref.null for out-of-range idxs (host shouldn't pass them, + // but the wasm validator wants a fallthrough). + let string_ref_ty = ValType::Ref(wasm_encoder::RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Concrete(string_idx), + }); + let mut name_fn = Function::new([]); + let block_ty = wasm_encoder::BlockType::Result(string_ref_ty); + name_fn.instruction(&Instruction::Block(block_ty)); + for (i, fn_name) in names.names.iter().enumerate() { let bytes = fn_name.as_bytes(); - let segment_idx = registry - .string_literal_segment(bytes) - .expect("fn-name passive segment registered alongside the global"); - init.instruction(&Instruction::I32Const(0)); - init.instruction(&Instruction::I32Const(bytes.len() as i32)); - init.instruction(&Instruction::ArrayNewData { + // Inner block: if idx == i, this arm emits the ref and + // breaks out of the outer block. Otherwise falls through + // to the next arm. + name_fn.instruction(&Instruction::Block(wasm_encoder::BlockType::Empty)); + // if local 0 != i { br 0 } — skip to next arm. + name_fn.instruction(&Instruction::LocalGet(0)); + name_fn.instruction(&Instruction::I32Const(i as i32)); + name_fn.instruction(&Instruction::I32Ne); + name_fn.instruction(&Instruction::BrIf(0)); + // Match: emit ref + break to outer. + name_fn.instruction(&Instruction::I32Const(0)); + name_fn.instruction(&Instruction::I32Const(bytes.len() as i32)); + name_fn.instruction(&Instruction::ArrayNewData { array_type_index: string_idx, - array_data_index: segment_idx, + array_data_index: segment_base + i as u32, }); - init.instruction(&Instruction::GlobalSet(idx as u32)); + name_fn.instruction(&Instruction::Br(1)); + name_fn.instruction(&Instruction::End); } - init.instruction(&Instruction::End); - codes.function(&init); + // Default arm — out-of-range idx returns ref.null. + name_fn.instruction(&Instruction::RefNull(wasm_encoder::HeapType::Concrete( + string_idx, + ))); + name_fn.instruction(&Instruction::End); + name_fn.instruction(&Instruction::End); + codes.function(&name_fn); } module.section(&codes); @@ -690,11 +934,20 @@ pub(super) fn emit_module( // ── Data section ─────────────────────────────────────────────── // Passive segments holding String literal byte sequences. Emitted // last; `array.new_data $string $segment_idx` reads from these. - if !registry.string_literals.is_empty() { + // Order: pre-walked program literals first, caller_fn names + // second. `__caller_fn_name`'s body uses + // `segment_base = registry.string_literals.len()` so its arms + // hit the right slots regardless of how many literals the + // program has. + if total_segment_count > 0 { let mut data = DataSection::new(); for bytes in ®istry.string_literals { data.passive(bytes.iter().copied()); } + let names = caller_fn_collector.borrow(); + for fn_name in &names.names { + data.passive(fn_name.as_bytes().iter().copied()); + } module.section(&data); } @@ -1121,6 +1374,58 @@ fn discover_builtins_in_stmt( } } +/// Recursively walks `t` and registers every nominal record/sum it +/// reaches in `eq_helpers`. Needed for `==` on collection types +/// whose element/key/value type is nominal — `List`, +/// `Map`, `Option`, etc. Without this, the +/// helper-body emit (`emit_list_eq`, `emit_record_eq_inline`, +/// `emit_eq_record`) would dispatch by `Call(__eq_)` against +/// an unregistered slot. +fn register_nominal_in_type( + t: &AverType, + eq_helpers: &mut EqHelperRegistry, + type_registry: &super::types::TypeRegistry, +) { + let canonical: String = t.display().chars().filter(|c| !c.is_whitespace()).collect(); + match t { + AverType::Named(name) => { + if type_registry.record_fields.contains_key(name) { + eq_helpers.register_transitive(name, EqKind::Record, type_registry); + } else if type_registry + .variants + .values() + .flat_map(|v| v.iter()) + .any(|v| &v.parent == name) + { + eq_helpers.register_transitive(name, EqKind::Sum, type_registry); + } + } + AverType::Option(inner) => { + eq_helpers.register_transitive(&canonical, EqKind::OptionEq, type_registry); + register_nominal_in_type(inner, eq_helpers, type_registry); + } + AverType::Result(ok, err) => { + eq_helpers.register_transitive(&canonical, EqKind::ResultEq, type_registry); + register_nominal_in_type(ok, eq_helpers, type_registry); + register_nominal_in_type(err, eq_helpers, type_registry); + } + AverType::Tuple(items) => { + eq_helpers.register_transitive(&canonical, EqKind::TupleEq, type_registry); + for item in items { + register_nominal_in_type(item, eq_helpers, type_registry); + } + } + AverType::List(inner) | AverType::Vector(inner) => { + register_nominal_in_type(inner, eq_helpers, type_registry); + } + AverType::Map(k, v) => { + register_nominal_in_type(k, eq_helpers, type_registry); + register_nominal_in_type(v, eq_helpers, type_registry); + } + _ => {} + } +} + fn discover_builtins_in_expr( expr: &Expr, builtins: &mut BuiltinRegistry, @@ -1183,16 +1488,26 @@ fn discover_builtins_in_expr( && let AverType::Named(name) = t { if type_registry.record_fields.contains_key(name) { - eq_helpers.register(name, EqKind::Record); + eq_helpers.register_transitive(name, EqKind::Record, type_registry); } else if type_registry .variants .values() .flat_map(|v| v.iter()) .any(|v| &v.parent == name) { - eq_helpers.register(name, EqKind::Sum); + eq_helpers.register_transitive(name, EqKind::Sum, type_registry); } } + // List / Vector / Map / Option / Result / Tuple `==` — + // dispatch reaches the per-element/key __eq_ through + // the helper bodies, so any nominal element type also + // needs an __eq slot. Walk the operand type recursively + // and register every nominal we hit. + if matches!(op, Op::Eq | Op::Neq) + && let Some(t) = l.ty() + { + register_nominal_in_type(t, eq_helpers, type_registry); + } discover_builtins_in_expr(&l.node, builtins, effects, eq_helpers, type_registry); discover_builtins_in_expr(&r.node, builtins, effects, eq_helpers, type_registry); } diff --git a/src/codegen/wasm_gc/types.rs b/src/codegen/wasm_gc/types.rs index 8b0582a4..685ff5db 100644 --- a/src/codegen/wasm_gc/types.rs +++ b/src/codegen/wasm_gc/types.rs @@ -115,16 +115,6 @@ pub(super) struct TypeRegistry { /// $string $segment_idx` with offset=0, size=len. pub(super) string_literals: Vec>, pub(super) string_literal_idx: HashMap, u32>, - /// Per-fn `(ref null $string)` global, init by `array.new_data` - /// from the fn-name passive segment. Codegen emits `global.get` - /// at every effect call site instead of allocating a fresh - /// `(array i8)` per call — module-instantiation pays the alloc - /// once, the runtime hot path costs one global load. - pub(super) caller_fn_globals: HashMap, - /// Emit order for the caller-fn globals, matched to the - /// idx values stored above. The `GlobalSection` walks this - /// vector so wasm idx 0..N matches the map values. - pub(super) caller_fn_global_order: Vec, /// Type names that must NOT be erased to their underlying /// primitive by the newtype optimisation. Populated with every /// record/variant used as a `Map` key — Map's open- @@ -325,6 +315,23 @@ impl TypeRegistry { &mut result_order, &mut next_idx, ); + // Walk binding annotations too — `let r: Result = …` + // wouldn't be picked up by the builtin-uses scan (which only + // looks at known dotted calls like `Disk.readText`). Without this, + // user-typed `Result` values fail to find their + // type slot at construction time. + use crate::ast::{FnBody, Stmt}; + let FnBody::Block(stmts) = fd.body.as_ref(); + for stmt in stmts { + if let Stmt::Binding(_, Some(annot), _) = stmt { + collect_results_from_str( + annot, + &mut result_types, + &mut result_order, + &mut next_idx, + ); + } + } } } let mut list_types: HashMap = HashMap::new(); @@ -521,6 +528,21 @@ impl TypeRegistry { for (_, ty) in &fd.params { collect_maps_from_str(ty, &mut pending_maps); } + // Walk let-binding annotations too — `let m: Map = + // Map.set(…)` won't show up in fn signatures and the discovery + // walker would otherwise miss the canonical entirely. Mirrors + // what the Result walker added a few commits back. + use crate::ast::{FnBody, Stmt}; + let FnBody::Block(stmts) = fd.body.as_ref(); + for stmt in stmts { + if let Stmt::Binding(_, Some(annot), _) = stmt { + collect_maps_from_str(annot, &mut pending_maps); + } + let expr = match stmt { + Stmt::Binding(_, _, e) | Stmt::Expr(e) => e, + }; + collect_maps_from_expr(expr, &mut pending_maps); + } } } // Dedup in encounter order. @@ -699,33 +721,13 @@ impl TypeRegistry { collect_string_literals_in_fn(fd, &mut string_literals, &mut string_literal_idx); } } - // Pre-register fn names as passive String literals AND - // allocate `(ref null $string)` wasm globals — but only for - // fns whose body actually emits caller_fn at a call site (a - // dotted call `Foo.bar(...)` or an `?!`/`!` independent - // product, which lowers to group/branch markers). Pure fns - // and plain forwarders that only call other user fns don't - // need a slot. - let mut caller_fn_globals: HashMap = HashMap::new(); - let mut caller_fn_global_order: Vec = Vec::new(); - for item in items { - if let TopLevel::FnDef(fd) = item { - if !fn_body_emits_effect_call(fd) { - continue; - } - let bytes = fd.name.as_bytes().to_vec(); - string_literal_idx.entry(bytes.clone()).or_insert_with(|| { - let idx = string_literals.len() as u32; - string_literals.push(bytes); - idx - }); - caller_fn_globals.entry(fd.name.clone()).or_insert_with(|| { - let idx = caller_fn_global_order.len() as u32; - caller_fn_global_order.push(fd.name.clone()); - idx - }); - } - } + // Note: caller_fn name registration moved out of `TypeRegistry` + // — `body::CallerFnCollector` lazy-registers names during + // codegen (every site that calls `emit_caller_fn_idx` registers + // its self_fn_name on demand), and the post-emit phase appends + // the resulting names as fresh passive segments after the + // pre-walked literal segments above. Single source of truth, + // zero AST-walker false positives. // `Int.mod` lowers to a boxed `Result` whose Err // arm carries a fixed "Division by zero" message — register // its bytes as a passive segment so the emitter has an idx @@ -789,8 +791,6 @@ impl TypeRegistry { string_array_type_idx, string_literals, string_literal_idx, - caller_fn_globals, - caller_fn_global_order, non_newtypable_keys, } } @@ -1854,6 +1854,69 @@ fn collect_maps_from_str(type_str: &str, out: &mut Vec) { } } +/// Walk every sub-expression and harvest `Map` canonicals from +/// each `Spanned::ty()` stamp. Catches map literals (`{a => 3}` +/// without annotation) and expressions whose type is a Map but the +/// canonical never shows up in any signature — symmetrical with +/// `collect_tuples_from_expr`. Recurses into sub-expressions so +/// nested literals / fn args / match arms all get their stamps +/// harvested. +fn collect_maps_from_expr(expr: &crate::ast::Spanned, out: &mut Vec) { + use crate::ast::Expr; + if let Some(ty) = expr.ty() { + let display = ty.display(); + collect_maps_from_str(&display, out); + } + match &expr.node { + Expr::FnCall(callee, args) => { + collect_maps_from_expr(callee, out); + for a in args { + collect_maps_from_expr(a, out); + } + } + Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => { + for it in items { + collect_maps_from_expr(it, out); + } + } + Expr::MapLiteral(entries) => { + for (k, v) in entries { + collect_maps_from_expr(k, out); + collect_maps_from_expr(v, out); + } + } + Expr::RecordCreate { fields, .. } => { + for (_, v) in fields { + collect_maps_from_expr(v, out); + } + } + Expr::RecordUpdate { base, updates, .. } => { + collect_maps_from_expr(base, out); + for (_, v) in updates { + collect_maps_from_expr(v, out); + } + } + Expr::Constructor(_, Some(arg)) => collect_maps_from_expr(arg, out), + Expr::Match { subject, arms } => { + collect_maps_from_expr(subject, out); + for arm in arms { + collect_maps_from_expr(&arm.body, out); + } + } + Expr::BinOp(_, l, r) => { + collect_maps_from_expr(l, out); + collect_maps_from_expr(r, out); + } + Expr::Attr(e, _) | Expr::ErrorProp(e) => collect_maps_from_expr(e, out), + Expr::TailCall(tc) => { + for a in &tc.args { + collect_maps_from_expr(a, out); + } + } + _ => {} + } +} + /// Split a canonical `Map` into its `K` and `V` parts (both /// borrowed slices of the input). Returns `None` if the string /// doesn't match the expected shape. @@ -1902,11 +1965,10 @@ fn expr_uses_string(expr: &crate::ast::Expr) -> bool { let dotted = format!("{p}.{member}"); if matches!( dotted.as_str(), - "Int.toString" - | "Float.toString" + "String.fromInt" + | "String.fromFloat" | "String.len" | "String.length" - | "String.concat" | "String.startsWith" | "String.contains" | "String.slice" @@ -1916,8 +1978,6 @@ fn expr_uses_string(expr: &crate::ast::Expr) -> bool { | "String.replace" | "String.split" | "String.join" - | "String.fromInt" - | "String.fromFloat" | "String.fromBool" | "String.endsWith" | "String.charAt" @@ -2035,45 +2095,6 @@ fn fn_body_calls_int_mod(fd: &crate::ast::FnDef) -> bool { }) } -/// Returns true when the fn body contains anything that emits a -/// caller_fn slot at codegen — a dotted call (any `Attr(_, _)` -/// callee, including nested-module shape `Module.Sub.fn(args)`) or -/// an independent product (`?!` / `!`, lowers to group/branch -/// markers). Used to skip allocating a global for fns that never -/// need one. Conservative on dotted: builtin namespace calls like -/// `List.length` are also flagged; false positives cost one segment -/// + one global per fn, false negatives crash wasm validation. -fn fn_body_emits_effect_call(fd: &crate::ast::FnDef) -> bool { - use crate::ast::{Expr, FnBody, Stmt}; - fn walk(e: &Expr) -> bool { - match e { - Expr::FnCall(callee, args) => { - let dotted = matches!(&callee.node, Expr::Attr(_, _)); - dotted || walk(&callee.node) || args.iter().any(|a| walk(&a.node)) - } - Expr::IndependentProduct(_, _) => true, - Expr::Match { subject, arms } => { - walk(&subject.node) || arms.iter().any(|a| walk(&a.body.node)) - } - Expr::BinOp(_, l, r) => walk(&l.node) || walk(&r.node), - Expr::Attr(o, _) => walk(&o.node), - Expr::ErrorProp(i) => walk(&i.node), - Expr::TailCall(b) => b.args.iter().any(|a| walk(&a.node)), - Expr::List(xs) | Expr::Tuple(xs) => xs.iter().any(|x| walk(&x.node)), - Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| walk(&e.node)), - Expr::RecordUpdate { base, updates, .. } => { - walk(&base.node) || updates.iter().any(|(_, e)| walk(&e.node)) - } - Expr::Constructor(_, p) => p.as_deref().is_some_and(|x| walk(&x.node)), - _ => false, - } - } - let FnBody::Block(stmts) = fd.body.as_ref(); - stmts.iter().any(|stmt| match stmt { - Stmt::Binding(_, _, e) | Stmt::Expr(e) => walk(&e.node), - }) -} - fn collect_string_literals_in_fn( fd: &crate::ast::FnDef, out: &mut Vec>, diff --git a/src/ir/alloc_info.rs b/src/ir/alloc_info.rs index 1548908d..942ea8d5 100644 --- a/src/ir/alloc_info.rs +++ b/src/ir/alloc_info.rs @@ -316,7 +316,7 @@ mod tests { impl AllocPolicy for TestPolicy { fn builtin_allocates(&self, name: &str) -> bool { - name.starts_with("Map.") || name == "Int.toString" + name.starts_with("Map.") || name == "String.fromInt" } fn constructor_allocates(&self, _name: &str, _has_payload: bool) -> bool { false @@ -363,11 +363,11 @@ mod tests { #[test] fn allocating_builtin_call_allocates() { - // Int.toString(42) + // String.fromInt(42) let call = Expr::FnCall( Box::new(sp(Expr::Attr( - Box::new(sp(Expr::Ident("Int".into()))), - "toString".into(), + Box::new(sp(Expr::Ident("String".into()))), + "fromInt".into(), ))), vec![lit_int(42)], ); diff --git a/src/ir/analyze.rs b/src/ir/analyze.rs index 991a1ec8..25979e6a 100644 --- a/src/ir/analyze.rs +++ b/src/ir/analyze.rs @@ -73,7 +73,7 @@ impl AllocPolicy for NeutralAllocPolicy { "Int.abs" | "Int.min" | "Int.max" - | "Int.toFloat" + | "Float.fromInt" | "Float.abs" | "Float.floor" | "Float.ceil" @@ -86,7 +86,6 @@ impl AllocPolicy for NeutralAllocPolicy { | "Float.pow" | "Float.atan2" | "Float.pi" - | "Float.fromInt" | "Char.toCode" | "String.len" | "String.byteLength" diff --git a/src/main/run_wasm_gc.rs b/src/main/run_wasm_gc.rs index b7acece4..2da72f9b 100644 --- a/src/main/run_wasm_gc.rs +++ b/src/main/run_wasm_gc.rs @@ -193,6 +193,84 @@ pub(super) struct RunWasmGcHost { /// output. `None` is the production path — zero overhead beyond the /// `Option::is_some` check per effect call. pub(super) recorder: Option, + /// Caller-fn name table, materialised at instantiation by walking + /// `__caller_fn_count` + `__caller_fn_name(0..count)`. Per effect + /// call, `imports.rs::dispatch_aver_import` looks up + /// `caller_fn_table[idx]` to stamp the recorded effect's + /// `caller_fn` field. Cleared (kept empty) for modules that don't + /// export the table. + pub(super) caller_fn_table: Vec, +} + +/// Walk `__caller_fn_count` + `__caller_fn_name(0..count)` once at +/// instance creation, decode each name via the LM bridge, return the +/// resulting `Vec` so per-call dispatch can index into it +/// without per-call LM round-trips. Empty when the module doesn't +/// export the table (programs without effect-emitting fns). +#[cfg(feature = "wasm")] +fn build_caller_fn_table( + store: &mut wasmtime::Store, + instance: &wasmtime::Instance, +) -> Result, String> { + use wasmtime::Val; + let count_fn = match instance.get_func(&mut *store, "__caller_fn_count") { + Some(f) => f, + None => return Ok(Vec::new()), + }; + let name_fn = match instance.get_func(&mut *store, "__caller_fn_name") { + Some(f) => f, + None => return Ok(Vec::new()), + }; + let to_lm = match instance.get_func(&mut *store, "__rt_string_to_lm") { + Some(f) => f, + None => return Ok(Vec::new()), + }; + let memory = match instance.get_memory(&mut *store, "memory") { + Some(m) => m, + None => return Ok(Vec::new()), + }; + + let mut count_out = [Val::I32(0)]; + count_fn + .call(&mut *store, &[], &mut count_out) + .map_err(|e| format!("__caller_fn_count: {e:#}"))?; + let count = match count_out[0] { + Val::I32(n) => n.max(0) as usize, + _ => 0, + }; + + let mut out = Vec::with_capacity(count); + let mut name_out = [Val::AnyRef(None)]; + let mut len_out = [Val::I32(0)]; + for i in 0..count { + name_fn + .call(&mut *store, &[Val::I32(i as i32)], &mut name_out) + .map_err(|e| format!("__caller_fn_name({i}): {e:#}"))?; + // Decode via __rt_string_to_lm: writes bytes into LM at + // offset 0, returns the byte length on the i32 return. + let any_ref = match &name_out[0] { + Val::AnyRef(Some(r)) => Val::AnyRef(Some(*r)), + _ => { + out.push("main".to_string()); + continue; + } + }; + to_lm + .call(&mut *store, &[any_ref], &mut len_out) + .map_err(|e| format!("__rt_string_to_lm: {e:#}"))?; + let len = match len_out[0] { + Val::I32(n) => n.max(0) as usize, + _ => 0, + }; + let mut buf = vec![0u8; len]; + if len > 0 { + memory + .read(&*store, 0, &mut buf) + .map_err(|e| format!("read caller_fn name {i}: {e:#}"))?; + } + out.push(String::from_utf8_lossy(&buf).into_owned()); + } + Ok(out) } /// Walk the parsed AST for a `fn (...) -> T` definition and @@ -257,6 +335,7 @@ fn run_wasm_gc_with_host( RunWasmGcHost { program_args: program_args.to_vec(), recorder: recorder.take(), + caller_fn_table: Vec::new(), }, ); let mut linker: Linker = Linker::new(&engine); @@ -317,6 +396,17 @@ fn run_wasm_gc_with_host( .instantiate(&mut store, &module) .map_err(|e| format!("instantiate: {e:#}"))?; + // Materialise the caller-fn name table. The compiler exports + // `__caller_fn_count() -> i32` and `__caller_fn_name(i32) -> ref + // null $string` whenever any user fn might emit caller_fn (i.e. + // the program has fn defs); we walk `0..count` once, decode each + // ref via the LM bridge, and cache the strings in + // `RunWasmGcHost::caller_fn_table`. Per effect call, + // `imports.rs::dispatch_aver_import` reads the trailing `i32` + // arg as an idx into this vector — no LM round-trip per call. + let caller_fn_table = build_caller_fn_table(&mut store, &instance)?; + store.data_mut().caller_fn_table = caller_fn_table; + // Two entry shapes: // // - `entry_info = Some((fn_name, args))` — `aver run --wasm-gc -e diff --git a/src/main/run_wasm_gc/imports.rs b/src/main/run_wasm_gc/imports.rs index a76a1fb9..3d29bacd 100644 --- a/src/main/run_wasm_gc/imports.rs +++ b/src/main/run_wasm_gc/imports.rs @@ -60,7 +60,6 @@ pub(super) use factories::{ pub(super) use lm::lm_string_from_host; use http::{HttpVerb, http_body_dispatch, http_simple_dispatch}; -use lm::lm_string_to_host; pub(super) fn dispatch_aver_import( name: &str, @@ -78,8 +77,22 @@ pub(super) fn dispatch_aver_import( // dispatch chain. Falls back to "main" when the trailing arg // is missing or not decodable — keeps replay of older recorded // traces / hand-rolled wasm-gc modules working. - let caller_fn = lm_string_to_host(caller, params.last())? - .filter(|s| !s.is_empty()) + // Trailing arg is now an `i32` idx into the caller-fn name table + // the host materialised at instantiation (see + // `run_wasm_gc::build_caller_fn_table`). Vector index lookup + // replaces the per-call LM round-trip from 0.16.2. + let caller_fn_idx = params + .last() + .and_then(|v| match v { + wasmtime::Val::I32(n) => Some(*n), + _ => None, + }) + .unwrap_or(-1); + let caller_fn: String = caller + .data() + .caller_fn_table + .get(caller_fn_idx as usize) + .cloned() .unwrap_or_else(|| "main".to_string()); let caller_fn_ref: &str = &caller_fn; let real_params: &[wasmtime::Val] = if params.is_empty() { diff --git a/src/types/checker/builtins.rs b/src/types/checker/builtins.rs index 0a869886..c3b99ad9 100644 --- a/src/types/checker/builtins.rs +++ b/src/types/checker/builtins.rs @@ -439,12 +439,10 @@ impl TypeChecker { let int_sigs: &[(&str, &[Type], Type, &[&str])] = &[ ("Int.fromString", &[Type::Str], int_result(), &[]), ("Int.fromFloat", &[Type::Float], Type::Int, &[]), - ("Int.toString", &[Type::Int], Type::Str, &[]), ("Int.abs", &[Type::Int], Type::Int, &[]), ("Int.min", &[Type::Int, Type::Int], Type::Int, &[]), ("Int.max", &[Type::Int, Type::Int], Type::Int, &[]), ("Int.mod", &[Type::Int, Type::Int], int_result(), &[]), - ("Int.toFloat", &[Type::Int], Type::Float, &[]), ]; for (name, params, ret, effects) in int_sigs { self.insert_sig(name, params, ret.clone(), effects); @@ -455,7 +453,6 @@ impl TypeChecker { let float_sigs: &[(&str, &[Type], Type, &[&str])] = &[ ("Float.fromString", &[Type::Str], float_result(), &[]), ("Float.fromInt", &[Type::Int], Type::Float, &[]), - ("Float.toString", &[Type::Float], Type::Str, &[]), ("Float.abs", &[Type::Float], Type::Float, &[]), ("Float.floor", &[Type::Float], Type::Int, &[]), ("Float.ceil", &[Type::Float], Type::Int, &[]), @@ -555,7 +552,6 @@ impl TypeChecker { let v_var = || Type::Var("V".to_string()); let map_kv = || Type::Map(Box::new(k_var()), Box::new(v_var())); let map_sigs: &[(&str, &[Type], Type, &[&str])] = &[ - ("Map.empty", &[], map_kv(), &[]), ("Map.set", &[map_kv(), k_var(), v_var()], map_kv(), &[]), ( "Map.get", @@ -614,7 +610,7 @@ impl TypeChecker { &[], ), ( - "Vector.toList", + "List.fromVector", &[vec_t()], Type::List(Box::new(t_var())), &[], diff --git a/src/types/checker/infer/expr.rs b/src/types/checker/infer/expr.rs index 1ddafd4c..6e904089 100644 --- a/src/types/checker/infer/expr.rs +++ b/src/types/checker/infer/expr.rs @@ -201,9 +201,6 @@ impl TypeChecker { Expr::FnCall(callee, args) => { let key = Self::callee_key(&callee.node)?; match (key.as_str(), args.len(), expected) { - // Map.empty() — no args, expected must be Map. - ("Map.empty", 0, Type::Map(_, _)) => Some(expected.clone()), - // Map.fromList(xs) — expected Map gives xs the // concrete List<(K, V)> element type. ("Map.fromList", 1, Type::Map(k, v)) => { diff --git a/src/types/checker/infer/map_calls.rs b/src/types/checker/infer/map_calls.rs index 74e8a0e9..44e320b1 100644 --- a/src/types/checker/infer/map_calls.rs +++ b/src/types/checker/infer/map_calls.rs @@ -10,8 +10,6 @@ impl TypeChecker { let option_ty = |v: Type| Type::Option(Box::new(v)); let list_ty = |v: Type| Type::List(Box::new(v)); let tuple2 = |k: Type, v: Type| Type::Tuple(vec![k, v]); - let k_var = || Type::Var("K".to_string()); - let v_var = || Type::Var("V".to_string()); let is_hashable_key_type = |ty: &Type| { // The Map runtime hashes any heap value through rt_deep_hash, // so user-defined types (variants/records/tuples/lists) are @@ -58,12 +56,6 @@ impl TypeChecker { }; match name { - "Map.empty" => { - if let Err(fallback) = expect_arity(self, 0, map_ty(Type::Invalid, Type::Invalid)) { - return Some(fallback); - } - Some(map_ty(k_var(), v_var())) - } "Map.len" => { if let Err(fallback) = expect_arity(self, 1, Type::Int) { return Some(fallback); diff --git a/src/types/checker/infer/vector_calls.rs b/src/types/checker/infer/vector_calls.rs index 2cf6c136..ef5a8254 100644 --- a/src/types/checker/infer/vector_calls.rs +++ b/src/types/checker/infer/vector_calls.rs @@ -117,7 +117,7 @@ impl TypeChecker { }; Some(vector_ty(elem)) } - "Vector.toList" => { + "List.fromVector" => { if let Err(fallback) = expect_arity(self, 1, list_ty(Type::Invalid)) { return Some(fallback); } diff --git a/src/types/checker/tests.rs b/src/types/checker/tests.rs index 43e41770..4b9ec421 100644 --- a/src/types/checker/tests.rs +++ b/src/types/checker/tests.rs @@ -309,7 +309,7 @@ record AppCtx config: String fn handler(ctx: AppCtx, req: HttpRequest) -> HttpResponse - HttpResponse(status = 200, body = "ok", headers = Map.empty()) + HttpResponse(status = 200, body = "ok", headers = {}) fn main() -> Unit ! [HttpServer.listenWith] @@ -333,7 +333,7 @@ record AppCtx config: String fn handler(ctx: AppCtx, req: HttpRequest) -> HttpResponse - HttpResponse(status = 200, body = ctx.config, headers = Map.empty()) + HttpResponse(status = 200, body = ctx.config, headers = {}) fn main(ctx: AppCtx) -> Unit ! [HttpServer.listenWith] diff --git a/src/types/float.rs b/src/types/float.rs index 1445f995..93656a31 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -3,7 +3,6 @@ /// Methods: /// Float.fromString(s) → Result — parse string to float /// Float.fromInt(n) → Float — widen int to float -/// Float.toString(f) → String — format float as string /// Float.abs(f) → Float — absolute value /// Float.floor(f) → Int — floor to int /// Float.ceil(f) → Int — ceil to int @@ -29,7 +28,6 @@ pub fn register(global: &mut HashMap) { for method in &[ "fromString", "fromInt", - "toString", "abs", "floor", "ceil", @@ -66,7 +64,6 @@ pub fn call(name: &str, args: &[Value]) -> Option> { match name { "Float.fromString" => Some(from_string(args)), "Float.fromInt" => Some(from_int(args)), - "Float.toString" => Some(to_string(args)), "Float.abs" => Some(abs(args)), "Float.floor" => Some(floor(args)), "Float.ceil" => Some(ceil(args)), @@ -111,16 +108,6 @@ fn from_int(args: &[Value]) -> Result { Ok(Value::Float(*n as f64)) } -fn to_string(args: &[Value]) -> Result { - let [val] = one_arg("Float.toString", args)?; - let Value::Float(f) = val else { - return Err(RuntimeError::Error( - "Float.toString: argument must be a Float".to_string(), - )); - }; - Ok(Value::Str(format!("{}", f))) -} - fn abs(args: &[Value]) -> Result { let [val] = one_arg("Float.abs", args)?; let Value::Float(f) = val else { @@ -271,7 +258,6 @@ pub fn register_nv(global: &mut HashMap, arena: &mut Arena) { let methods = &[ "fromString", "fromInt", - "toString", "abs", "floor", "ceil", @@ -305,7 +291,6 @@ pub fn call_nv( match name { "Float.fromString" => Some(from_string_nv(args, arena)), "Float.fromInt" => Some(from_int_nv(args, arena)), - "Float.toString" => Some(to_string_nv(args, arena)), "Float.abs" => Some(abs_nv(args, arena)), "Float.floor" => Some(floor_nv(args, arena)), "Float.ceil" => Some(ceil_nv(args, arena)), @@ -375,17 +360,6 @@ fn from_int_nv(args: &[NanValue], arena: &mut Arena) -> Result Result { - let v = nv_check1("Float.toString", args)?; - if !v.is_float() { - return Err(RuntimeError::Error( - "Float.toString: argument must be a Float".to_string(), - )); - } - let s = format!("{}", v.as_float()); - Ok(NanValue::new_string_value(&s, arena)) -} - fn abs_nv(args: &[NanValue], _arena: &mut Arena) -> Result { let v = nv_check1("Float.abs", args)?; if !v.is_float() { diff --git a/src/types/int.rs b/src/types/int.rs index 60a8b132..43fe379e 100644 --- a/src/types/int.rs +++ b/src/types/int.rs @@ -3,12 +3,13 @@ /// Methods: /// Int.fromString(s) → Result — parse string to int /// Int.fromFloat(f) → Int — truncate float to int -/// Int.toString(n) → String — format int as string /// Int.abs(n) → Int — absolute value /// Int.min(a, b) → Int — minimum of two ints /// Int.max(a, b) → Int — maximum of two ints /// Int.mod(a, b) → Result — modulo (error on b=0) -/// Int.toFloat(n) → Float — widen int to float +/// +/// Stringification goes through `String.fromInt` (or `"{n}"` interpolation); +/// widening to Float goes through `Float.fromInt`. /// /// No effects required. use std::collections::HashMap; @@ -19,16 +20,7 @@ use crate::value::{RuntimeError, Value}; pub fn register(global: &mut HashMap) { let mut members = HashMap::new(); - for method in &[ - "fromString", - "fromFloat", - "toString", - "abs", - "min", - "max", - "mod", - "toFloat", - ] { + for method in &["fromString", "fromFloat", "abs", "min", "max", "mod"] { members.insert( method.to_string(), Value::Builtin(format!("Int.{}", method)), @@ -52,12 +44,10 @@ pub fn call(name: &str, args: &[Value]) -> Option> { match name { "Int.fromString" => Some(from_string(args)), "Int.fromFloat" => Some(from_float(args)), - "Int.toString" => Some(to_string(args)), "Int.abs" => Some(abs(args)), "Int.min" => Some(min(args)), "Int.max" => Some(max(args)), "Int.mod" => Some(modulo(args)), - "Int.toFloat" => Some(to_float(args)), _ => None, } } @@ -90,16 +80,6 @@ fn from_float(args: &[Value]) -> Result { Ok(Value::Int(*f as i64)) } -fn to_string(args: &[Value]) -> Result { - let [val] = one_arg("Int.toString", args)?; - let Value::Int(n) = val else { - return Err(RuntimeError::Error( - "Int.toString: argument must be an Int".to_string(), - )); - }; - Ok(Value::Str(format!("{}", n))) -} - fn abs(args: &[Value]) -> Result { let [val] = one_arg("Int.abs", args)?; let Value::Int(n) = val else { @@ -146,16 +126,6 @@ fn modulo(args: &[Value]) -> Result { } } -fn to_float(args: &[Value]) -> Result { - let [val] = one_arg("Int.toFloat", args)?; - let Value::Int(n) = val else { - return Err(RuntimeError::Error( - "Int.toFloat: argument must be an Int".to_string(), - )); - }; - Ok(Value::Float(*n as f64)) -} - // ─── Helpers ──────────────────────────────────────────────────────────────── fn one_arg<'a>(name: &str, args: &'a [Value]) -> Result<[&'a Value; 1], RuntimeError> { @@ -183,16 +153,7 @@ fn two_args<'a>(name: &str, args: &'a [Value]) -> Result<[&'a Value; 2], Runtime // ─── NanValue-native API ───────────────────────────────────────────────────── pub fn register_nv(global: &mut HashMap, arena: &mut Arena) { - let methods = &[ - "fromString", - "fromFloat", - "toString", - "abs", - "min", - "max", - "mod", - "toFloat", - ]; + let methods = &["fromString", "fromFloat", "abs", "min", "max", "mod"]; let mut members: Vec<(Rc, NanValue)> = Vec::with_capacity(methods.len()); for method in methods { let idx = arena.push_builtin(&format!("Int.{}", method)); @@ -213,12 +174,10 @@ pub fn call_nv( match name { "Int.fromString" => Some(from_string_nv(args, arena)), "Int.fromFloat" => Some(from_float_nv(args, arena)), - "Int.toString" => Some(to_string_nv(args, arena)), "Int.abs" => Some(abs_nv(args, arena)), "Int.min" => Some(min_nv(args, arena)), "Int.max" => Some(max_nv(args, arena)), "Int.mod" => Some(modulo_nv(args, arena)), - "Int.toFloat" => Some(to_float_nv(args, arena)), _ => None, } } @@ -276,17 +235,6 @@ fn from_float_nv(args: &[NanValue], arena: &mut Arena) -> Result Result { - let v = nv_check1("Int.toString", args)?; - if !v.is_int() { - return Err(RuntimeError::Error( - "Int.toString: argument must be an Int".to_string(), - )); - } - let s = format!("{}", v.as_int(arena)); - Ok(NanValue::new_string_value(&s, arena)) -} - fn abs_nv(args: &[NanValue], arena: &mut Arena) -> Result { let v = nv_check1("Int.abs", args)?; if !v.is_int() { @@ -340,13 +288,3 @@ fn modulo_nv(args: &[NanValue], arena: &mut Arena) -> Result Result { - let v = nv_check1("Int.toFloat", args)?; - if !v.is_int() { - return Err(RuntimeError::Error( - "Int.toFloat: argument must be an Int".to_string(), - )); - } - Ok(NanValue::new_float(v.as_int(arena) as f64)) -} diff --git a/src/types/list.rs b/src/types/list.rs index 526b7c36..5bc3116d 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -22,7 +22,15 @@ use crate::value::{ pub fn register(global: &mut HashMap) { let mut members = HashMap::new(); for method in &[ - "len", "prepend", "take", "drop", "concat", "reverse", "contains", "zip", + "len", + "prepend", + "take", + "drop", + "concat", + "reverse", + "contains", + "zip", + "fromVector", ] { members.insert( method.to_string(), @@ -197,7 +205,15 @@ fn zip(args: &[Value]) -> Result { pub fn register_nv(global: &mut HashMap, arena: &mut Arena) { let methods = &[ - "len", "prepend", "take", "drop", "concat", "reverse", "contains", "zip", + "len", + "prepend", + "take", + "drop", + "concat", + "reverse", + "contains", + "zip", + "fromVector", ]; let mut members: Vec<(Rc, NanValue)> = Vec::with_capacity(methods.len()); for method in methods { diff --git a/src/types/map.rs b/src/types/map.rs index 215748ce..72e3aede 100644 --- a/src/types/map.rs +++ b/src/types/map.rs @@ -1,7 +1,6 @@ /// Map namespace — immutable key/value map helpers. /// /// Methods: -/// Map.empty() → Map /// Map.set(map, key, value) → Map /// Map.get(map, key) → Option /// Map.remove(map, key) → Map @@ -12,6 +11,9 @@ /// Map.len(map) → Int /// Map.fromList(pairs) → Map where each pair is (key, value) /// +/// The empty map is the literal `{}` (with type from context); there is +/// no `Map.empty()` builtin since 0.17 — symmetric with `[]` for List. +/// /// Key constraint: only scalar keys are allowed (Int, Float, String, Bool). /// /// No effects required. @@ -25,7 +27,7 @@ use crate::value::{RuntimeError, Value, aver_repr, list_from_vec, list_view}; pub fn register(global: &mut HashMap) { let mut members = HashMap::new(); for method in &[ - "empty", "set", "get", "remove", "has", "keys", "values", "entries", "len", "fromList", + "set", "get", "remove", "has", "keys", "values", "entries", "len", "fromList", ] { members.insert( method.to_string(), @@ -48,7 +50,6 @@ pub fn effects(_name: &str) -> &'static [&'static str] { /// Returns `Some(result)` when `name` is owned by this namespace, `None` otherwise. pub fn call(name: &str, args: &[Value]) -> Option> { match name { - "Map.empty" => Some(empty(args)), "Map.set" => Some(set(args)), "Map.get" => Some(get(args)), "Map.remove" => Some(remove(args)), @@ -62,16 +63,6 @@ pub fn call(name: &str, args: &[Value]) -> Option> { } } -fn empty(args: &[Value]) -> Result { - if !args.is_empty() { - return Err(RuntimeError::Error(format!( - "Map.empty() takes 0 arguments, got {}", - args.len() - ))); - } - Ok(Value::Map(HashMap::new())) -} - fn set(args: &[Value]) -> Result { let [map_val, key, value] = three_args("Map.set", args)?; let Value::Map(map) = map_val else { @@ -294,7 +285,6 @@ pub fn call_nv( arena: &mut Arena, ) -> Option> { match name { - "Map.empty" => Some(empty_nv(args, arena)), "Map.set" => Some(set_nv(args, arena)), "Map.get" => Some(get_nv(args, arena)), "Map.remove" => Some(remove_nv(args, arena)), @@ -331,17 +321,6 @@ fn nv_key_bits(v: NanValue, arena: &Arena) -> u64 { v.map_key_hash_deep(arena) } -fn empty_nv(args: &[NanValue], arena: &mut Arena) -> Result { - if !args.is_empty() { - return Err(RuntimeError::Error(format!( - "Map.empty() takes 0 arguments, got {}", - args.len() - ))); - } - let _ = arena; - Ok(NanValue::EMPTY_MAP) -} - /// Map.set with sole-owned first argument — takes instead of cloning. pub fn set_nv_owned(args: &[NanValue], arena: &mut Arena) -> Result { if args.len() != 3 { diff --git a/src/types/vector.rs b/src/types/vector.rs index bab413f5..3ce46569 100644 --- a/src/types/vector.rs +++ b/src/types/vector.rs @@ -6,7 +6,7 @@ /// Vector.set(vec, idx, val) → Option> /// Vector.len(vec) → Int /// Vector.fromList(xs) → Vector -/// Vector.toList(vec) → List +/// List.fromVector(vec) → List /// /// No effects required. use std::collections::HashMap; @@ -19,7 +19,7 @@ use crate::value::{RuntimeError, Value, list_from_vec, list_view}; pub fn register(global: &mut HashMap) { let mut members = HashMap::new(); - for method in &["new", "get", "set", "len", "fromList", "toList"] { + for method in &["new", "get", "set", "len", "fromList"] { members.insert( method.to_string(), Value::Builtin(format!("Vector.{}", method)), @@ -45,7 +45,7 @@ pub fn call(name: &str, args: &[Value]) -> Option> { "Vector.set" => Some(vec_set(args)), "Vector.len" => Some(vec_len(args)), "Vector.fromList" => Some(vec_from_list(args)), - "Vector.toList" => Some(vec_to_list(args)), + "List.fromVector" => Some(vec_to_list(args)), _ => None, } } @@ -156,13 +156,13 @@ fn vec_from_list(args: &[Value]) -> Result { fn vec_to_list(args: &[Value]) -> Result { if args.len() != 1 { return Err(RuntimeError::Error(format!( - "Vector.toList() takes 1 argument, got {}", + "List.fromVector() takes 1 argument, got {}", args.len() ))); } let Value::Vector(vec) = &args[0] else { return Err(RuntimeError::Error( - "Vector.toList: argument must be a Vector".to_string(), + "List.fromVector: argument must be a Vector".to_string(), )); }; Ok(list_from_vec(vec.to_vec())) @@ -171,7 +171,7 @@ fn vec_to_list(args: &[Value]) -> Result { // ─── NanValue-native API ───────────────────────────────────────────────────── pub fn register_nv(global: &mut HashMap, arena: &mut Arena) { - let methods = &["new", "get", "set", "len", "fromList", "toList"]; + let methods = &["new", "get", "set", "len", "fromList"]; let mut members: Vec<(Rc, NanValue)> = Vec::with_capacity(methods.len()); for method in methods { let idx = arena.push_builtin(&format!("Vector.{}", method)); @@ -195,7 +195,7 @@ pub fn call_nv( "Vector.set" => Some(vec_set_nv(args, arena)), "Vector.len" => Some(vec_len_nv(args, arena)), "Vector.fromList" => Some(vec_from_list_nv(args, arena)), - "Vector.toList" => Some(vec_to_list_nv(args, arena)), + "List.fromVector" => Some(vec_to_list_nv(args, arena)), _ => None, } } @@ -364,13 +364,13 @@ fn vec_from_list_nv(args: &[NanValue], arena: &mut Arena) -> Result Result { if args.len() != 1 { return Err(RuntimeError::Error(format!( - "Vector.toList() takes 1 argument, got {}", + "List.fromVector() takes 1 argument, got {}", args.len() ))); } if !args[0].is_vector() { return Err(RuntimeError::Error( - "Vector.toList: argument must be a Vector".to_string(), + "List.fromVector: argument must be a Vector".to_string(), )); } let items = arena.clone_vector_value(args[0]); diff --git a/src/vm/alloc_policy.rs b/src/vm/alloc_policy.rs index 4a36c96d..4ffc79ba 100644 --- a/src/vm/alloc_policy.rs +++ b/src/vm/alloc_policy.rs @@ -32,7 +32,7 @@ fn is_pure_non_alloc_builtin(name: &str) -> bool { "Int.abs" | "Int.min" | "Int.max" - | "Int.toFloat" + | "Float.fromInt" | "Float.abs" | "Float.floor" | "Float.ceil" @@ -45,7 +45,6 @@ fn is_pure_non_alloc_builtin(name: &str) -> bool { | "Float.pow" | "Float.atan2" | "Float.pi" - | "Float.fromInt" | "Char.toCode" | "String.len" | "String.byteLength" diff --git a/src/vm/builtin.rs b/src/vm/builtin.rs index a0a35122..317bc873 100644 --- a/src/vm/builtin.rs +++ b/src/vm/builtin.rs @@ -96,16 +96,13 @@ vm_builtins! { IntFromString => "Int.fromString", IntFromFloat => "Int.fromFloat", - IntToString => "Int.toString", IntAbs => "Int.abs", IntMin => "Int.min", IntMax => "Int.max", IntMod => "Int.mod", - IntToFloat => "Int.toFloat", FloatFromString => "Float.fromString", FloatFromInt => "Float.fromInt", - FloatToString => "Float.toString", FloatAbs => "Float.abs", FloatFloor => "Float.floor", FloatCeil => "Float.ceil", @@ -146,7 +143,6 @@ vm_builtins! { ListContains => "List.contains", ListZip => "List.zip", - MapEmpty => "Map.empty", MapSet => "Map.set", MapGet => "Map.get", MapRemove => "Map.remove", @@ -162,7 +158,7 @@ vm_builtins! { VectorSet => "Vector.set", VectorLen => "Vector.len", VectorFromList => "Vector.fromList", - VectorToList => "Vector.toList", + ListFromVector => "List.fromVector", OptionWithDefault => "Option.withDefault", OptionToResult => "Option.toResult", @@ -204,7 +200,6 @@ impl VmBuiltin { | Self::IntAbs | Self::IntMin | Self::IntMax - | Self::IntToFloat | Self::FloatFromInt | Self::FloatAbs | Self::FloatFloor @@ -224,7 +219,6 @@ impl VmBuiltin { | Self::StringEndsWith | Self::StringContains | Self::ListContains - | Self::MapEmpty | Self::MapLen | Self::MapHas | Self::VectorLen @@ -422,16 +416,13 @@ impl VmBuiltin { Self::IntFromString | Self::IntFromFloat - | Self::IntToString | Self::IntAbs | Self::IntMin | Self::IntMax - | Self::IntMod - | Self::IntToFloat => int::call_nv(self.name(), args, arena), + | Self::IntMod => int::call_nv(self.name(), args, arena), Self::FloatFromString | Self::FloatFromInt - | Self::FloatToString | Self::FloatAbs | Self::FloatFloor | Self::FloatCeil @@ -472,8 +463,7 @@ impl VmBuiltin { | Self::ListContains | Self::ListZip => list::call_nv(self.name(), args, arena), - Self::MapEmpty - | Self::MapSet + Self::MapSet | Self::MapGet | Self::MapRemove | Self::MapHas @@ -488,7 +478,7 @@ impl VmBuiltin { | Self::VectorSet | Self::VectorLen | Self::VectorFromList - | Self::VectorToList => crate::types::vector::call_nv(self.name(), args, arena), + | Self::ListFromVector => crate::types::vector::call_nv(self.name(), args, arena), Self::OptionWithDefault | Self::OptionToResult => { option::call_nv(self.name(), args, arena) diff --git a/tools/edge/bench.av b/tools/edge/bench.av index 3ad8ecc8..d0a3950a 100644 --- a/tools/edge/bench.av +++ b/tools/edge/bench.av @@ -12,4 +12,4 @@ fn main() -> Unit seahorse = Fractal.viewFromQuery("cx=-0.7463&cy=0.1102&w=0.012") zoom = Fractal.render(200, 120, 220, seahorse) t2 = Time.unixMs() - Console.print("full(200x120,100)={Int.toString(t1 - t0)}ms[{Int.toString(String.len(full))}B] zoom(200x120,220)={Int.toString(t2 - t1)}ms[{Int.toString(String.len(zoom))}B] total={Int.toString(t2 - t0)}ms") + Console.print("full(200x120,100)={String.fromInt(t1 - t0)}ms[{String.fromInt(String.len(full))}B] zoom(200x120,220)={String.fromInt(t2 - t1)}ms[{String.fromInt(String.len(zoom))}B] total={String.fromInt(t2 - t0)}ms") diff --git a/tools/edge/fractal.av b/tools/edge/fractal.av index 0b6cccde..ee8abf18 100644 --- a/tools/edge/fractal.av +++ b/tools/edge/fractal.av @@ -141,9 +141,9 @@ verify gridHFor fn navLink(cx: Float, cy: Float, w: Float, label: String, hint: String) -> String ? "One nav anchor — encodes (cx, cy, w) into a /fractal query string. Browsers fire a fresh request per click; the new view is parsed back out by `viewFromQuery` on the server." String.join([ - "", label, "" ], "") @@ -189,22 +189,22 @@ fn mandelStep(n: Int, x: Float, y: Float, prevZ2: Float, cx: Float, cy: Float, m ? "One iteration of z = z² + c. On escape (|z|² > 4) returns a *continuous* escape time using linear interpolation between the previous and current |z|²: nu = (n - 1) + (R² - prevZ²) / (curZ² - prevZ²). This sub-iter precision drives the Bayer-dither palette lookup so iso-bands transition smoothly across pixels instead of stepping at integer iter boundaries." curZ2 = x * x + y * y match curZ2 > 4.0 - true -> Int.toFloat(n - 1) + (4.0 - prevZ2) / (curZ2 - prevZ2) + true -> Float.fromInt(n - 1) + (4.0 - prevZ2) / (curZ2 - prevZ2) false -> mandelIter(n + 1, x * x - y * y + cx, 2.0 * x * y + cy, curZ2, cx, cy, maxIter) verify mandelStep - mandelStep(7, 3.0, 0.0, 1.0, 0.0, 0.0, 50) => Int.toFloat(7 - 1) + (4.0 - 1.0) / (9.0 - 1.0) - mandelStep(0, 0.0, 0.0, 0.0, 0.0, 0.0, 50) => Int.toFloat(50) + mandelStep(7, 3.0, 0.0, 1.0, 0.0, 0.0, 50) => Float.fromInt(7 - 1) + (4.0 - 1.0) / (9.0 - 1.0) + mandelStep(0, 0.0, 0.0, 0.0, 0.0, 0.0, 50) => Float.fromInt(50) fn mandelIter(n: Int, x: Float, y: Float, prevZ2: Float, cx: Float, cy: Float, maxIter: Int) -> Float - ? "Iterates z = z² + c until |z|² > 4 or maxIter; returns continuous escape time as Float. In-set points (n hits maxIter) return Int.toFloat(maxIter). TCO-eligible mutual recursion via mandelStep." + ? "Iterates z = z² + c until |z|² > 4 or maxIter; returns continuous escape time as Float. In-set points (n hits maxIter) return Float.fromInt(maxIter). TCO-eligible mutual recursion via mandelStep." match n >= maxIter - true -> Int.toFloat(maxIter) + true -> Float.fromInt(maxIter) false -> mandelStep(n, x, y, prevZ2, cx, cy, maxIter) verify mandelIter - mandelIter(0, 0.0, 0.0, 0.0, 0.0, 0.0, 50) => Int.toFloat(50) - mandelIter(50, 0.0, 0.0, 0.0, 0.0, 0.0, 50) => Int.toFloat(50) + mandelIter(0, 0.0, 0.0, 0.0, 0.0, 0.0, 50) => Float.fromInt(50) + mandelIter(50, 0.0, 0.0, 0.0, 0.0, 0.0, 50) => Float.fromInt(50) fn insideCardioid(cx: Float, cy: Float) -> Bool ? "True if (cx, cy) lies inside the main cardioid (the largest connected component of the set). Roughly 50% of inside-set points fall here; this test costs ~5 ops and avoids running mandelIter to maxIter on every one of them." @@ -232,21 +232,21 @@ fn escapeIterSlow(cx: Float, cy: Float, maxIter: Int) -> Float fn escapeIterBulb(cx: Float, cy: Float, maxIter: Int) -> Float ? "Cardioid missed — try the period-2 bulb, else fall through." match insideBulb(cx, cy) - true -> Int.toFloat(maxIter) + true -> Float.fromInt(maxIter) false -> escapeIterSlow(cx, cy, maxIter) verify escapeIterBulb - escapeIterBulb(0.0 - 1.0, 0.0, 50) => Int.toFloat(50) + escapeIterBulb(0.0 - 1.0, 0.0, 50) => Float.fromInt(50) fn escapeIter(cx: Float, cy: Float, maxIter: Int) -> Float - ? "Returns the continuous escape time for (cx, cy) under the given iteration cap. Returns Int.toFloat(maxIter) for inside-set. Uses the cardioid + period-2 bulb pre-tests to short-circuit ~70% of inside-set points before any iteration runs. Sub-iter precision (Float vs Int) drives Bayer-dither palette lookup downstream." + ? "Returns the continuous escape time for (cx, cy) under the given iteration cap. Returns Float.fromInt(maxIter) for inside-set. Uses the cardioid + period-2 bulb pre-tests to short-circuit ~70% of inside-set points before any iteration runs. Sub-iter precision (Float vs Int) drives Bayer-dither palette lookup downstream." match insideCardioid(cx, cy) - true -> Int.toFloat(maxIter) + true -> Float.fromInt(maxIter) false -> escapeIterBulb(cx, cy, maxIter) verify escapeIter - escapeIter(0.0, 0.0, 50) => Int.toFloat(50) - escapeIter(0.0 - 1.0, 0.0, 50) => Int.toFloat(50) + escapeIter(0.0, 0.0, 50) => Float.fromInt(50) + escapeIter(0.0 - 1.0, 0.0, 50) => Float.fromInt(50) fn ditherThreshold(col: Int, row: Int) -> Float ? "4x4 ordered Bayer dither threshold for pixel (col, row). Standard recursive 4x4 Bayer matrix divided by 16 (centred so frac=0 never upgrades and frac=1 always upgrades). 16 distinct thresholds across a 4x4 block give 16-level fractional palette interpolation — fine enough that the dither pattern is barely visible at normal viewing distance while still smoothly bridging adjacent palette entries." @@ -279,28 +279,28 @@ verify ditherThreshold fn paletteIdx(nu: Float, col: Int, row: Int, maxIter: Int) -> Int ? "Maps a continuous escape-time `nu` to a palette index 0..15 with 2x2 ordered-Bayer dither. Inside-set (nu >= maxIter) → 0 (deep navy). Escaped pixels: `n = floor(nu)`, `frac = nu - n`. Compare `frac` against the position-dependent dither threshold; if greater, show the next palette entry, else the current one. The result is that fractional `nu` translates into per-cell choice between two adjacent palette entries, breaking up integer iso-band stepping into smooth 2x2 transitions." - match nu >= Int.toFloat(maxIter) + match nu >= Float.fromInt(maxIter) true -> 0 - false -> match (nu - Int.toFloat(Float.floor(nu))) > ditherThreshold(col, row) + false -> match (nu - Float.fromInt(Float.floor(nu))) > ditherThreshold(col, row) true -> Result.withDefault(Int.mod(Float.floor(nu) + 1, 30), 0) + 1 false -> Result.withDefault(Int.mod(Float.floor(nu), 30), 0) + 1 verify paletteIdx - paletteIdx(Int.toFloat(50), 0, 0, 50) => 0 + paletteIdx(Float.fromInt(50), 0, 0, 50) => 0 paletteIdx(0.0, 0, 0, 50) => 1 paletteIdx(29.0, 0, 0, 50) => 30 paletteIdx(30.0, 0, 0, 50) => 1 fn pixelCx(pixCol: Int, pixelsW: Int, view: View) -> Float ? "Pixel column → Mandelbrot cx coordinate." - Int.toFloat(pixCol) / Int.toFloat(pixelsW) * view.cxRange + view.cxMin + Float.fromInt(pixCol) / Float.fromInt(pixelsW) * view.cxRange + view.cxMin verify pixelCx pixelCx(0, 1, fullView()) => 0.0 - 2.0 fn pixelCy(pixRow: Int, pixelsH: Int, view: View) -> Float ? "Pixel row → Mandelbrot cy coordinate." - Int.toFloat(pixRow) / Int.toFloat(pixelsH) * view.cyRange + view.cyMin + Float.fromInt(pixRow) / Float.fromInt(pixelsH) * view.cyRange + view.cyMin verify pixelCy pixelCy(0, 1, fullView()) => 0.0 - 1.5 diff --git a/tools/website/index.html b/tools/website/index.html index b7f909e4..bfceb1dd 100644 --- a/tools/website/index.html +++ b/tools/website/index.html @@ -220,42 +220,42 @@

What Aver deliberately omits

Native WASM, shared runtime

-

Seven games compiled directly from Aver to WebAssembly GC. Engine handles GC and tail calls — no NaN-boxing, no custom heap. Snake ships at 4.4 KiB; a full roguelike with procedural generation is 28.0 KiB. Modern browsers only (Chrome 119+ / Firefox 120+ / Safari 18.2+).

+

Seven games compiled directly from Aver to WebAssembly GC. Engine handles GC and tail calls — no NaN-boxing, no custom heap. Snake ships at 4.3 KiB; a full roguelike with procedural generation is 25.6 KiB. Modern browsers only (Chrome 119+ / Firefox 120+ / Safari 18.2+).

diff --git a/tools/website/llms.txt b/tools/website/llms.txt index 0ead6c76..3db11b4e 100644 --- a/tools/website/llms.txt +++ b/tools/website/llms.txt @@ -104,7 +104,7 @@ Compound: Notes: - top-level named functions can be passed where `Fn(...)` is expected - there are no lambdas and no closures -- no implicit type promotion; use `Int.toFloat` / `Float.fromInt` +- no implicit type promotion; use `Float.fromInt` / `Float.fromInt` ### User-defined types @@ -136,7 +136,7 @@ Rules: ```aver match value - Result.Ok(v) -> Int.toString(v) + Result.Ok(v) -> String.fromInt(v) Result.Err(e) -> e ``` @@ -309,8 +309,8 @@ Common pure namespaces: Key `String` API: - `String.len`, `String.contains`, `String.startsWith`, `String.endsWith` - `String.toUpper`, `String.toLower`, `String.trim` -- `String.concat`, `String.join`, `String.split`, `String.chars` -- `Int.fromString : String -> Result`, `Int.toString : Int -> String` +- `String.join`, `String.split`, `String.chars` — concat is the `+` operator +- `Int.fromString : String -> Result`, `String.fromInt : Int -> String` - string interpolation: `"Hello, {name}!"` is the idiomatic concat Key `List` API (small, recursion-first): @@ -321,7 +321,7 @@ Key `Vector` API (O(1) indexed access): - `Vector.new(n, default)`, `Vector.get(v, i) -> Option`, `Vector.set(v, i, val) -> Option>` Key `Map` API: -- `Map.empty()`, `Map.fromList(pairs)`, `Map.get(m, k) -> Option`, `Map.set(m, k, v)`, `Map.has(m, k)`, `Map.remove(m, k)`, `Map.keys(m)`, `Map.len(m)` +- `{}`, `Map.fromList(pairs)`, `Map.get(m, k) -> Option`, `Map.set(m, k, v)`, `Map.has(m, k)`, `Map.remove(m, k)`, `Map.keys(m)`, `Map.len(m)` Effectful namespaces: - `Console`: print, error, warn, readLine — **`print`/`error`/`warn` take `String`**, not arbitrary values. Stringify at the call site: interpolation `"{x}"` for primitives, a per-type render fn (`fn show(r: Result) -> String`) for compound shapes. diff --git a/tools/website/playground/checkers.wasm b/tools/website/playground/checkers.wasm index 48969772..f3382aee 100644 Binary files a/tools/website/playground/checkers.wasm and b/tools/website/playground/checkers.wasm differ diff --git a/tools/website/playground/doom.wasm b/tools/website/playground/doom.wasm index 9f0e0074..0497634a 100644 Binary files a/tools/website/playground/doom.wasm and b/tools/website/playground/doom.wasm differ diff --git a/tools/website/playground/index.html b/tools/website/playground/index.html index f1965a12..c6124e80 100644 --- a/tools/website/playground/index.html +++ b/tools/website/playground/index.html @@ -34,13 +34,13 @@ What is Aver? About WASM GitHub @@ -225,7 +225,7 @@

Native WASM

Tiny binaries - Snake ships at 4.4 KiB. Tetris is 8.5 KiB. A full roguelike with procedural generation is 28.0 KiB. Built with --target wasm-gc --optimize size — engine GC + native tail-calls; per-program binary, no shared runtime to fetch. + Snake ships at 4.3 KiB. Tetris is 8.2 KiB. A full roguelike with procedural generation is 25.6 KiB. Built with --target wasm-gc --optimize size — engine GC + native tail-calls; per-program binary, no shared runtime to fetch.
Capability-based imports diff --git a/tools/website/playground/life.wasm b/tools/website/playground/life.wasm index 8f169d52..627c2c1b 100644 Binary files a/tools/website/playground/life.wasm and b/tools/website/playground/life.wasm differ diff --git a/tools/website/playground/rogue.wasm b/tools/website/playground/rogue.wasm index a98908e9..4335bdd8 100644 Binary files a/tools/website/playground/rogue.wasm and b/tools/website/playground/rogue.wasm differ diff --git a/tools/website/playground/snake.wasm b/tools/website/playground/snake.wasm index d3eb0c82..6d1accdf 100644 Binary files a/tools/website/playground/snake.wasm and b/tools/website/playground/snake.wasm differ diff --git a/tools/website/playground/sources/examples/games/checkers/main.av b/tools/website/playground/sources/examples/games/checkers/main.av index d36b5e0d..73794513 100644 --- a/tools/website/playground/sources/examples/games/checkers/main.av +++ b/tools/website/playground/sources/examples/games/checkers/main.av @@ -210,11 +210,11 @@ fn renderAiPanel(result: AiResult) -> Unit ! [Terminal.moveTo, Terminal.print, Terminal.resetColor, Terminal.setColor] Terminal.moveTo(22, 1) Terminal.setColor("cyan") - Terminal.print("AI depth={Int.toString(result.depth)} {Int.toString(result.thinkMs)}ms") + Terminal.print("AI depth={String.fromInt(result.depth)} {String.fromInt(result.thinkMs)}ms") Terminal.resetColor() Terminal.moveTo(22, 2) Terminal.setColor("yellow") - Terminal.print("root-parallel x{Int.toString(Ai.rootParallelWidth())} | {Int.toString(List.len(result.candidates))} moves") + Terminal.print("root-parallel x{String.fromInt(Ai.rootParallelWidth())} | {String.fromInt(List.len(result.candidates))} moves") Terminal.resetColor() renderCandidates(result.candidates, result.move, 0) @@ -242,7 +242,7 @@ fn renderChosenCandidate(sm: ScoredMove, i: Int) -> Unit Terminal.setColor("green") Terminal.print("★ ") Terminal.print(moveLabel(sm.move)) - Terminal.print(" {Int.toString(sm.score)}") + Terminal.print(" {String.fromInt(sm.score)}") Terminal.resetColor() fn renderOtherCandidate(sm: ScoredMove, i: Int) -> Unit @@ -250,7 +250,7 @@ fn renderOtherCandidate(sm: ScoredMove, i: Int) -> Unit ! [Terminal.print] Terminal.print(" ") Terminal.print(moveLabel(sm.move)) - Terminal.print(" {Int.toString(sm.score)}") + Terminal.print(" {String.fromInt(sm.score)}") fn isChosenMove(a: List, b: List) -> Bool ? "True if two moves are the same (compare first and last points)" @@ -295,7 +295,7 @@ fn moveLabel(move: List) -> String ? "Human-readable label for a move path" match firstPoint(move) Option.Some(from) -> match lastPoint(move) - Option.Some(to) -> "{colChar(from.x)}{Int.toString(8 - from.y)}→{colChar(to.x)}{Int.toString(8 - to.y)}" + Option.Some(to) -> "{colChar(from.x)}{String.fromInt(8 - from.y)}→{colChar(to.x)}{String.fromInt(8 - to.y)}" Option.None -> "?" Option.None -> "?" @@ -307,7 +307,7 @@ fn renderHelp(state: GameState) -> Unit ! [Terminal.moveTo, Terminal.print, Terminal.resetColor, Terminal.setColor] Terminal.moveTo(0, 14) Terminal.setColor("green") - Terminal.print(" You (black) vs AI (white) · depth {Int.toString(state.searchDepth)} · +/- to change") + Terminal.print(" You (black) vs AI (white) · depth {String.fromInt(state.searchDepth)} · +/- to change") Terminal.resetColor() Terminal.moveTo(0, 15) match state.selected @@ -415,7 +415,7 @@ fn aiPhase(state: GameState) -> Result ] renderFrame(state) Terminal.moveTo(0, 14) - Terminal.print("AI thinking (depth {Int.toString(state.searchDepth)}, root-parallel x{Int.toString(Ai.rootParallelWidth())})...") + Terminal.print("AI thinking (depth {String.fromInt(state.searchDepth)}, root-parallel x{String.fromInt(Ai.rootParallelWidth())})...") Terminal.flush() t0 = Time.unixMs() aiState = handleAiTurn(state) @@ -443,7 +443,7 @@ fn gameOverResult(state: GameState) -> Result Terminal.print("Game over!") Terminal.flush() Time.sleep(2000) - Result.Ok("Game over! Move {Int.toString(state.moveNum)}") + Result.Ok("Game over! Move {String.fromInt(state.moveNum)}") fn gameTurn(state: GameState) -> Result ? "Dispatch to player or AI" diff --git a/tools/website/playground/sources/examples/games/checkers/render.av b/tools/website/playground/sources/examples/games/checkers/render.av index 5e3e472f..f0145779 100644 --- a/tools/website/playground/sources/examples/games/checkers/render.av +++ b/tools/website/playground/sources/examples/games/checkers/render.av @@ -240,7 +240,7 @@ fn renderRowLabelsNext(r: Int) -> Unit fn rowLabel(r: Int) -> String ? "Row label: row 0 = 8, row 7 = 1" - Int.toString(8 - r) + String.fromInt(8 - r) verify rowLabel rowLabel(0) => "8" @@ -281,7 +281,7 @@ fn renderStatus(currentPlayer: Color, moveNum: Int) -> Unit Terminal.setColor(playerColor(currentPlayer)) Terminal.print("{colorName(currentPlayer)}") Terminal.resetColor() - Terminal.print(" to move | Move #{Int.toString(moveNum)}") + Terminal.print(" to move | Move #{String.fromInt(moveNum)}") fn renderAiTrace(move: List, score: Int, alternatives: Int, depth: Int) -> Unit ? "Show AI decision trace" @@ -290,4 +290,4 @@ fn renderAiTrace(move: List, score: Int, alternatives: Int, depth: Int) - Terminal.setColor("cyan") Terminal.print("AI: ") Terminal.resetColor() - Terminal.print("score={Int.toString(score)} alts={Int.toString(alternatives)} depth={Int.toString(depth)}") + Terminal.print("score={String.fromInt(score)} alts={String.fromInt(alternatives)} depth={String.fromInt(depth)}") diff --git a/tools/website/playground/sources/examples/games/doom/enemy.av b/tools/website/playground/sources/examples/games/doom/enemy.av index 670ab64e..e99d3f5e 100644 --- a/tools/website/playground/sources/examples/games/doom/enemy.av +++ b/tools/website/playground/sources/examples/games/doom/enemy.av @@ -49,8 +49,8 @@ fn ghostTeleport(enemy: Enemy, seed: Int, levelMap: List) -> Enemy ? "Teleport ghost to a random nearby floor tile" s1 = Rng.nextSeed(seed) s2 = Rng.nextSeed(s1) - dx = Int.toFloat(Rng.seedToRange(s1, 0 - 4, 4)) - dy = Int.toFloat(Rng.seedToRange(s2, 0 - 4, 4)) + dx = Float.fromInt(Rng.seedToRange(s1, 0 - 4, 4)) + dy = Float.fromInt(Rng.seedToRange(s2, 0 - 4, 4)) nx = enemy.x + dx ny = enemy.y + dy match Level.isWall(levelMap, Float.floor(nx), Float.floor(ny)) @@ -251,7 +251,7 @@ fn applyShotVec(enemies: Vector, idx: Int) -> (List, String) ? "Apply 5 damage to enemy at index using indexed vector access" match Vector.get(enemies, idx) Option.Some(e) -> applyDamageToEnemyVec(enemies, idx, e) - Option.None -> (Vector.toList(enemies), "Shot missed!") + Option.None -> (List.fromVector(enemies), "Shot missed!") verify applyShotVec applyShotVec(Vector.fromList([]), 0) => ([], "Shot missed!") @@ -268,7 +268,7 @@ fn applyDamageToEnemyVec(enemies: Vector, idx: Int, e: Enemy) -> (List (removeEnemyVec(enemies, idx, 0, []), "Killed {Types.enemyGlyph(e.kind)}!") - false -> (replaceEnemyVec(enemies, idx, Enemy.update(e, hp = newHp), 0, []), "Hit {Types.enemyGlyph(e.kind)}! ({Int.toString(newHp)} HP)") + false -> (replaceEnemyVec(enemies, idx, Enemy.update(e, hp = newHp), 0, []), "Hit {Types.enemyGlyph(e.kind)}! ({String.fromInt(newHp)} HP)") verify applyDamageToEnemyVec applyDamageToEnemyVec(Vector.fromList([Types.mkEnemy(1.0, 1.0, EnemyKind.Imp, 3)]), 0, Types.mkEnemy(1.0, 1.0, EnemyKind.Imp, 3)) => ([], "Killed !!") diff --git a/tools/website/playground/sources/examples/games/doom/level.av b/tools/website/playground/sources/examples/games/doom/level.av index e83b0e4b..7c72e3d1 100644 --- a/tools/website/playground/sources/examples/games/doom/level.av +++ b/tools/website/playground/sources/examples/games/doom/level.av @@ -280,7 +280,7 @@ verify roomFromSeed fn roomCenter(room: Room) -> (Float, Float) ? "Center of a room as float coordinates" - (Int.toFloat(room.x + room.w / 2) + 0.5, Int.toFloat(room.y + room.h / 2) + 0.5) + (Float.fromInt(room.x + room.w / 2) + 0.5, Float.fromInt(room.y + room.h / 2) + 0.5) verify roomCenter roomCenter(Room(x = 4, y = 4, w = 6, h = 6)) => (7.5, 7.5) diff --git a/tools/website/playground/sources/examples/games/doom/main.av b/tools/website/playground/sources/examples/games/doom/main.av index 089bcf05..836b8aba 100644 --- a/tools/website/playground/sources/examples/games/doom/main.av +++ b/tools/website/playground/sources/examples/games/doom/main.av @@ -33,8 +33,8 @@ verify turnSpeed fn spawnInRoom(room: Level.Room, seed: Int) -> Enemy ? "Spawn an enemy at the center of a room" kind = pickKind(seed) - cx = Int.toFloat(room.x + room.w / 2) + 0.5 - cy = Int.toFloat(room.y + room.h / 2) + 0.5 + cx = Float.fromInt(room.x + room.w / 2) + 0.5 + cy = Float.fromInt(room.y + room.h / 2) + 0.5 Types.mkEnemy(cx, cy, kind, Types.enemyMaxHp(kind)) verify spawnInRoom @@ -214,7 +214,7 @@ fn updateTurn(state: GameState) -> GameState msgs = match isOver true -> ["You died!"] false -> match damaged.hp < state.player.hp - true -> List.concat(state.messages, ["Ouch! HP: {Int.toString(damaged.hp)}"]) + true -> List.concat(state.messages, ["Ouch! HP: {String.fromInt(damaged.hp)}"]) false -> state.messages trimmedMsgs = trimMessages(msgs) GameState.update(state, enemies = newEnemies, player = damaged, turnCount = state.turnCount + 1, gameOver = isOver, messages = trimmedMsgs) diff --git a/tools/website/playground/sources/examples/games/doom/math.av b/tools/website/playground/sources/examples/games/doom/math.av index 9b89add2..15f90514 100644 --- a/tools/website/playground/sources/examples/games/doom/math.av +++ b/tools/website/playground/sources/examples/games/doom/math.av @@ -29,7 +29,7 @@ verify radToDeg fn normalizeAngle(a: Float) -> Float ? "Wrap angle to [0, 2*pi)" twoPi = Float.pi() * 2.0 - rem = a - Int.toFloat(Float.floor(a / twoPi)) * twoPi + rem = a - Float.fromInt(Float.floor(a / twoPi)) * twoPi match rem < 0.0 true -> rem + twoPi false -> rem diff --git a/tools/website/playground/sources/examples/games/doom/render.av b/tools/website/playground/sources/examples/games/doom/render.av index 823002fc..1123b5ea 100644 --- a/tools/website/playground/sources/examples/games/doom/render.av +++ b/tools/website/playground/sources/examples/games/doom/render.av @@ -134,7 +134,7 @@ fn wallHeight(distance: Float) -> Int ? "Pixel height of the wall strip for a given corrected distance" match distance < 0.1 true -> pixelH() - false -> Math.clampInt(Float.floor(Int.toFloat(pixelH()) / distance), 0, pixelH()) + false -> Math.clampInt(Float.floor(Float.fromInt(pixelH()) / distance), 0, pixelH()) verify wallHeight wallHeight(1.0) => 80 @@ -282,7 +282,7 @@ verify buildZBuffer fn buildZBufferCol(levelMap: List, wallMap: List, px: Float, py: Float, angle: Float, col: Int, acc: List) -> List ? "Cast one ray and append to buffer" - rayAngle = angle - fov() / 2.0 + Int.toFloat(col) * fov() / Int.toFloat(screenW()) + rayAngle = angle - fov() / 2.0 + Float.fromInt(col) * fov() / Float.fromInt(screenW()) hit = castRay(levelMap, wallMap, px, py, rayAngle) corrected = RayHit.update(hit, dist = hit.dist * Float.cos(rayAngle - angle)) buildZBuffer(levelMap, wallMap, px, py, angle, col + 1, List.concat(acc, [corrected])) @@ -375,7 +375,7 @@ fn renderEnemySprite(enemies: Vector, zbuf: Vector, px: Float, py eDist = Math.dist(px, py, e.x, e.y) eAngle = Float.atan2(dy, dx) relAngle = Math.normalizeAngle(eAngle - pAngle + Float.pi()) - Float.pi() - screenCol = Float.floor(Int.toFloat(screenW()) / 2.0 + relAngle * Int.toFloat(screenW()) / fov()) + screenCol = Float.floor(Float.fromInt(screenW()) / 2.0 + relAngle * Float.fromInt(screenW()) / fov()) match isEnemyVisibleVec(screenCol, eDist, zbuf) true -> drawEnemyChar(e, screenCol, eDist, enemies, zbuf, px, py, pAngle, i) false -> renderEnemiesVec(enemies, zbuf, px, py, pAngle, i + 1) @@ -409,8 +409,8 @@ verify isEnemyVisibleVec fn drawEnemyChar(e: Enemy, screenCol: Int, eDist: Float, enemies: Vector, zbuf: Vector, px: Float, py: Float, pAngle: Float, i: Int) -> Unit ? "Draw enemy using solid Unicode block characters" ! [Terminal.flush, Terminal.moveTo, Terminal.print, Terminal.resetColor, Terminal.setColor] - spriteH = Math.clampInt(Float.floor(2.5 / eDist * Int.toFloat(screenH())), 1, 10) - spriteW = Math.clampInt(Float.floor(5.0 / eDist * Int.toFloat(screenW()) / 10.0), 3, 13) + spriteH = Math.clampInt(Float.floor(2.5 / eDist * Float.fromInt(screenH())), 1, 10) + spriteW = Math.clampInt(Float.floor(5.0 / eDist * Float.fromInt(screenW()) / 10.0), 3, 13) topRow = screenH() / 2 - spriteH / 2 leftCol = screenCol - spriteW / 2 drawBlockGrid(e, leftCol, topRow, spriteH, spriteW, 0, 0) @@ -463,9 +463,9 @@ verify fullBlock fn isInsideShape(kind: EnemyKind, col: Int, row: Int, spriteW: Int, spriteH: Int) -> Bool ? "Check if (col, row) is inside the enemy silhouette" - halfW = Int.toFloat(spriteW) / 2.0 - cx = Float.abs(Int.toFloat(col) - halfW + 0.5) / halfW - cy = Int.toFloat(row) / Int.toFloat(spriteH) + halfW = Float.fromInt(spriteW) / 2.0 + cx = Float.abs(Float.fromInt(col) - halfW + 0.5) / halfW + cy = Float.fromInt(row) / Float.fromInt(spriteH) maxW = shapeWidth(kind, cy) cx < maxW @@ -557,13 +557,13 @@ fn renderHud(state: GameState) -> Unit Terminal.print("BRAILLE DOOM") Terminal.setColor("red") Terminal.moveTo(hudX, 3) - Terminal.print("HP: {Int.toString(state.player.hp)}/100") + Terminal.print("HP: {String.fromInt(state.player.hp)}/100") Terminal.setColor("white") Terminal.moveTo(hudX, 4) deg = Float.floor(Math.radToDeg(state.player.angle)) - Terminal.print("angle: {Int.toString(deg)}") + Terminal.print("angle: {String.fromInt(deg)}") Terminal.moveTo(hudX, 5) - Terminal.print("enemies: {Int.toString(List.len(state.enemies))}") + Terminal.print("enemies: {String.fromInt(List.len(state.enemies))}") Terminal.setColor("cyan") Terminal.moveTo(hudX, 7) Terminal.print("W/S forward/back") diff --git a/tools/website/playground/sources/examples/games/doom/rng.av b/tools/website/playground/sources/examples/games/doom/rng.av index ca76be06..52312e63 100644 --- a/tools/website/playground/sources/examples/games/doom/rng.av +++ b/tools/website/playground/sources/examples/games/doom/rng.av @@ -38,7 +38,7 @@ verify advanceSeed fn randomFloat(s: Int) -> Float ? "Map seed to [0.0, 1.0)" - Int.toFloat(Result.withDefault(Int.mod(Int.abs(s), 10000), 0)) / 10000.0 + Float.fromInt(Result.withDefault(Int.mod(Int.abs(s), 10000), 0)) / 10000.0 verify randomFloat randomFloat(0) => 0.0 diff --git a/tools/website/playground/sources/examples/games/life.av b/tools/website/playground/sources/examples/games/life.av index b5488dec..cf25411e 100644 --- a/tools/website/playground/sources/examples/games/life.av +++ b/tools/website/playground/sources/examples/games/life.av @@ -301,7 +301,7 @@ fn drawOneRow(grid: Vector, s: Int, w: Int, h: Int, row: Int) -> Unit fn drawHud(st: SimState, mode: String) -> Unit ? "Draw two-line HUD: status on line 0, controls on line 1." ! [Terminal.moveTo, Terminal.print, Terminal.resetColor, Terminal.setColor] - genStr = Int.toString(st.gen) + genStr = String.fromInt(st.gen) Terminal.setColor("green") Terminal.moveTo(0, 0) line1 = match mode == "edit" @@ -310,7 +310,7 @@ fn drawHud(st: SimState, mode: String) -> Unit true -> " PAUSED Gen:{genStr} " false -> match st.delay == 0 true -> " {st.fps.display}fps Gen:{genStr} BENCHMARK " - false -> " {st.fps.display}fps Gen:{genStr} {Int.toString(st.delay)}ms " + false -> " {st.fps.display}fps Gen:{genStr} {String.fromInt(st.delay)}ms " Terminal.print(line1) Terminal.moveTo(0, 1) Terminal.setColor("cyan") @@ -327,7 +327,7 @@ fn formatFps(frames: Int, elapsed: Int) -> String tenths = frames * 10000 / elapsed whole = tenths / 10 frac = Result.withDefault(Int.mod(tenths, 10), 0) - Int.toString(whole) + "." + Int.toString(frac) + String.fromInt(whole) + "." + String.fromInt(frac) verify formatFps formatFps(2, 1000) => "2.0" @@ -452,7 +452,7 @@ fn simAction(k: String, st: SimState) -> Result true -> 0 false -> 1 match k - "q" -> Result.Ok("Stopped at generation {Int.toString(st.gen)}.") + "q" -> Result.Ok("Stopped at generation {String.fromInt(st.gen)}.") " " -> gameLoop(SimState.update(st, paused = newPaused)) "e" -> simToEditor(st) "+" -> gameLoop(SimState.update(st, delay = Int.max(st.delay - 10, 0))) @@ -470,7 +470,7 @@ fn simToEditor(st: SimState) -> Result result = editorLoop(st.grid, st.stride, st.width, st.height, st.width / 2, st.height / 2) match result Result.Ok(newGrid) -> gameLoop(SimState.update(st, grid = newGrid, gen = 0)) - Result.Err(_) -> Result.Ok("Stopped at generation {Int.toString(st.gen)}.") + Result.Err(_) -> Result.Ok("Stopped at generation {String.fromInt(st.gen)}.") // ─── Entry ────────────────────────────────────────────────── diff --git a/tools/website/playground/sources/examples/games/rogue/main.av b/tools/website/playground/sources/examples/games/rogue/main.av index 565b39dd..07b7cbed 100644 --- a/tools/website/playground/sources/examples/games/rogue/main.av +++ b/tools/website/playground/sources/examples/games/rogue/main.av @@ -354,7 +354,7 @@ fn resolveAttack(state: GameState, target: Entity, idx: Int) -> GameState hit = Combat.applyDamageToEntity(target, dmg) match Combat.isEntityDead(hit) true -> killEntity(state, target, idx, dmg) - false -> alertNearby(addMessage(GameState.update(state, entities = Combat.replaceEntity(state.entities, idx, hit, 0)), "You hit {target.name} for {Int.toString(dmg)}!"), state.player.pos, 12, 0) + false -> alertNearby(addMessage(GameState.update(state, entities = Combat.replaceEntity(state.entities, idx, hit, 0)), "You hit {target.name} for {String.fromInt(dmg)}!"), state.player.pos, 12, 0) fn killEntity(state: GameState, target: Entity, idx: Int, dmg: Int) -> GameState ? "Kill entity — for-loop gets one respawn (off-by-one error)" @@ -373,7 +373,7 @@ fn loopRespawn(state: GameState, target: Entity, idx: Int) -> GameState fn actualKill(state: GameState, target: Entity, idx: Int) -> GameState ? "Actually remove entity and gain exp" newPlayer = Combat.gainExp(state.player, Combat.expForKill(target.kind)) - addMessage(GameState.update(state, entities = Combat.removeAt(state.entities, idx, 0), player = newPlayer), "You defeated {target.name}! (+{Int.toString(Combat.expForKill(target.kind))} exp)") + addMessage(GameState.update(state, entities = Combat.removeAt(state.entities, idx, 0), player = newPlayer), "You defeated {target.name}! (+{String.fromInt(Combat.expForKill(target.kind))} exp)") verify actualKill List.len(actualKill(GameState(gameMap = [], player = Types.makePlayer(0, 0), entities = [Types.makeIfElse(1, 1)], items = [], visible = [], visGrid = [], remembered = [], messages = [], seed = 0, gameOver = false), Types.makeIfElse(1, 1), 0).entities) => 0 @@ -400,9 +400,9 @@ verify pickUpItem fn applyItem(state: GameState, item: Item, idx: Int) -> GameState ? "Apply item effect" match item.kind - ItemKind.PotionOfPurity -> addMessage(GameState.update(state, player = Combat.healPlayer(state.player, item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You drink {item.name}. Purity restored! (+{Int.toString(item.value)} HP)") - ItemKind.ScrollOfPatternMatch -> addMessage(GameState.update(state, player = Player.update(state.player, attack = state.player.attack + item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You read {item.name}. Your pattern matching grows stronger! (+{Int.toString(item.value)} ATK)") - ItemKind.ShieldOfImmutability -> addMessage(GameState.update(state, player = Player.update(state.player, defense = state.player.defense + item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You equip {item.name}. Nothing can change you! (+{Int.toString(item.value)} DEF)") + ItemKind.PotionOfPurity -> addMessage(GameState.update(state, player = Combat.healPlayer(state.player, item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You drink {item.name}. Purity restored! (+{String.fromInt(item.value)} HP)") + ItemKind.ScrollOfPatternMatch -> addMessage(GameState.update(state, player = Player.update(state.player, attack = state.player.attack + item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You read {item.name}. Your pattern matching grows stronger! (+{String.fromInt(item.value)} ATK)") + ItemKind.ShieldOfImmutability -> addMessage(GameState.update(state, player = Player.update(state.player, defense = state.player.defense + item.value), items = Combat.removeItemAt(state.items, idx, 0)), "You equip {item.name}. Nothing can change you! (+{String.fromInt(item.value)} DEF)") verify applyItem applyItem(GameState(gameMap = [], player = Player.update(Types.makePlayer(0, 0), hp = 10), entities = [], items = [Types.makePurity(0, 0)], visible = [], visGrid = [], remembered = [], messages = [], seed = 0, gameOver = false), Types.makePurity(0, 0), 0).player.hp => 18 @@ -533,8 +533,8 @@ fn ifElseAttack(state: GameState, entity: Entity, i: Int) -> GameState match Combat.isPlayerDead(newPlayer) true -> addMessage(GameState.update(state, player = newPlayer, gameOver = true), "{entity.name} branch-predicts your death!") false -> match doubled == 1 - true -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} takes BOTH branches! {Int.toString(totalDmg)} dmg!") - false -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} hits you for {Int.toString(dmg)}.") + true -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} takes BOTH branches! {String.fromInt(totalDmg)} dmg!") + false -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} hits you for {String.fromInt(dmg)}.") fn loopAttack(state: GameState, entity: Entity, i: Int) -> GameState ? "for-loop: normal attack but respawns once when killed (off-by-one)" @@ -542,7 +542,7 @@ fn loopAttack(state: GameState, entity: Entity, i: Int) -> GameState newPlayer = Combat.applyDamageToPlayer(state.player, dmg) match Combat.isPlayerDead(newPlayer) true -> addMessage(GameState.update(state, player = newPlayer, gameOver = true), "{entity.name} iterates you to death!") - false -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} iterates on you for {Int.toString(dmg)}!") + false -> addMessage(GameState.update(state, player = newPlayer), "{entity.name} iterates on you for {String.fromInt(dmg)}!") verify loopAttack loopAttack(GameState(gameMap = [], player = Types.makePlayer(0, 0), entities = [Types.makeLoop(1, 0)], items = [], visible = [], visGrid = [], remembered = [], messages = [], seed = 0, gameOver = false), Types.makeLoop(1, 0), 0).player.hp < 20 => true @@ -551,7 +551,7 @@ fn nullAttack(state: GameState, entity: Entity, i: Int) -> GameState ? "null: steals EXP instead of dealing HP damage (null dereference — wipes memory)" stolen = Int.max(1, state.player.exp / 4) newPlayer = Player.update(state.player, exp = Int.max(0, state.player.exp - stolen)) - addMessage(GameState.update(state, player = newPlayer), "{entity.name} dereferences your memory! -{Int.toString(stolen)} EXP!") + addMessage(GameState.update(state, player = newPlayer), "{entity.name} dereferences your memory! -{String.fromInt(stolen)} EXP!") verify nullAttack nullAttack(GameState(gameMap = [], player = Player.update(Types.makePlayer(0, 0), exp = 40), entities = [Types.makeNull(1, 0)], items = [], visible = [], visGrid = [], remembered = [], messages = [], seed = 0, gameOver = false), Types.makeNull(1, 0), 0).player.exp => 30 @@ -655,7 +655,7 @@ fn descend(state: GameState) -> GameState itms = spawnItems(newSeed, rooms, 0) vis = Fov.computeFov(newMap, Types.pt(px, py), 8) rem = markRemembered(emptyRemembered(0), vis, 0) - addMessage(GameState(gameMap = newMap, player = newPlayer, entities = ents, items = itms, visible = vis, visGrid = buildVisGrid(vis, emptyRemembered(0), 0), remembered = rem, messages = state.messages, seed = Map.nextSeed(newSeed), gameOver = false), "You descend to floor {Int.toString(newFloor)} of Non-Aver!") + addMessage(GameState(gameMap = newMap, player = newPlayer, entities = ents, items = itms, visible = vis, visGrid = buildVisGrid(vis, emptyRemembered(0), 0), remembered = rem, messages = state.messages, seed = Map.nextSeed(newSeed), gameOver = false), "You descend to floor {String.fromInt(newFloor)} of Non-Aver!") verify descend descend(initGame(42)).player.floor => 2 @@ -715,7 +715,7 @@ fn gameLoop(state: GameState) -> Result ] renderFrame(state) match state.gameOver - true -> Result.Ok("Game over on floor {Int.toString(state.player.floor)}. EXP: {Int.toString(state.player.exp)}. The Aver way prevails!") + true -> Result.Ok("Game over on floor {String.fromInt(state.player.floor)}. EXP: {String.fromInt(state.player.exp)}. The Aver way prevails!") false -> waitAndProcess(state) fn waitAndProcess(state: GameState) -> Result diff --git a/tools/website/playground/sources/examples/games/snake.av b/tools/website/playground/sources/examples/games/snake.av index e10a086b..c169a9b0 100644 --- a/tools/website/playground/sources/examples/games/snake.av +++ b/tools/website/playground/sources/examples/games/snake.av @@ -274,7 +274,7 @@ fn render(state: GameState) -> Unit drawAt(state.food.x, state.food.y, "██") Terminal.resetColor() Terminal.moveTo(0, state.height) - Terminal.print("Score: {Int.toString(state.score)} | Q / ESC to quit") + Terminal.print("Score: {String.fromInt(state.score)} | Q / ESC to quit") Terminal.flush() fn handleKey(state: GameState, k: String) -> GameState @@ -304,7 +304,7 @@ fn tick(state: GameState) -> Result Time.sleep, ] match state.gameOver - true -> Result.Ok("Game over! Score: {Int.toString(state.score)}") + true -> Result.Ok("Game over! Score: {String.fromInt(state.score)}") false -> tickMove(moveSnake(state)) fn tickMove(moved: GameState) -> Result @@ -315,7 +315,7 @@ fn tickMove(moved: GameState) -> Result Time.sleep, ] match checkCollision(moved) - true -> Result.Ok("Game over! Score: {Int.toString(moved.score)}") + true -> Result.Ok("Game over! Score: {String.fromInt(moved.score)}") false -> match didEatFood(moved) true -> gameLoop(growSnake(randomFood(moved))) false -> gameLoop(moved) diff --git a/tools/website/playground/sources/examples/games/tetris/main.av b/tools/website/playground/sources/examples/games/tetris/main.av index 41172ebb..db30b14f 100644 --- a/tools/website/playground/sources/examples/games/tetris/main.av +++ b/tools/website/playground/sources/examples/games/tetris/main.av @@ -127,11 +127,11 @@ fn renderInfo(state: GameState) -> Unit ? "Draw score, level, lines info" ! [Terminal.moveTo, Terminal.print] Terminal.moveTo(26, 2) - Terminal.print("Score: {Int.toString(state.score)}") + Terminal.print("Score: {String.fromInt(state.score)}") Terminal.moveTo(26, 4) - Terminal.print("Level: {Int.toString(state.level)}") + Terminal.print("Level: {String.fromInt(state.level)}") Terminal.moveTo(26, 6) - Terminal.print("Lines: {Int.toString(state.lines)}") + Terminal.print("Lines: {String.fromInt(state.lines)}") Terminal.moveTo(26, 10) Terminal.print("Controls:") Terminal.moveTo(26, 11) @@ -204,7 +204,7 @@ fn tick(state: GameState) -> Result Time.sleep, ] match state.gameOver - true -> Result.Ok("Game over! Score: {Int.toString(state.score)} Lines: {Int.toString(state.lines)}") + true -> Result.Ok("Game over! Score: {String.fromInt(state.score)} Lines: {String.fromInt(state.lines)}") false -> tickGravity(state) fn gameLoop(state: GameState) -> Result diff --git a/tools/website/playground/sources/examples/games/wumpus.av b/tools/website/playground/sources/examples/games/wumpus.av index a1847be4..721c4638 100644 --- a/tools/website/playground/sources/examples/games/wumpus.av +++ b/tools/website/playground/sources/examples/games/wumpus.av @@ -48,7 +48,7 @@ verify validRoom fn roomListItem(r: Int, rest: List) -> String ? "Format a single room entry and recurse on remainder." - rs = Int.toString(r) + rs = String.fromInt(r) match rest [] -> rs _ -> "{rs}, {roomList(rest)}" @@ -246,10 +246,10 @@ fn showRoom(game: Game) -> Unit ? "Display current room, tunnels, and hazard warnings." ! [Console.print] Console.print("") - Console.print("You are in room {Int.toString(game.player)}.") + Console.print("You are in room {String.fromInt(game.player)}.") Console.print("Tunnels lead to: {roomList(neighbors(game.player))}") Console.print(warningText(game)) - Console.print("Arrows: {Int.toString(game.arrows)}") + Console.print("Arrows: {String.fromInt(game.arrows)}") fn gameLoop(game: Game) -> Result ? "Main game loop — one turn per recursive call." @@ -311,7 +311,7 @@ fn teleportAndCheck(game: Game) -> Result ? "Bats carry you to a random room; check hazards there." ! [Console.print, Console.readLine, Random.int] dest = randRoom() - Console.print("Super bats carry you to room {Int.toString(dest)}!") + Console.print("Super bats carry you to room {String.fromInt(dest)}!") resolveRoomCheck(checkRoom(Game.update(game, player = dest))) fn retry(game: Game, msg: String) -> Result diff --git a/tools/website/playground/sources/examples/shapes.av b/tools/website/playground/sources/examples/shapes.av index f3ad7b23..197efc5b 100644 --- a/tools/website/playground/sources/examples/shapes.av +++ b/tools/website/playground/sources/examples/shapes.av @@ -21,5 +21,5 @@ fn main() -> Unit ! [Console.print] c = Shape.Circle(5.0) r = Shape.Rectangle(3.0, 4.0) - Console.print("circle area = {Float.toString(area(c))}") - Console.print("rect area = {Float.toString(area(r))}") + Console.print("circle area = {String.fromFloat(area(c))}") + Console.print("rect area = {String.fromFloat(area(r))}") diff --git a/tools/website/playground/tetris.wasm b/tools/website/playground/tetris.wasm index de771be6..0dceea1f 100644 Binary files a/tools/website/playground/tetris.wasm and b/tools/website/playground/tetris.wasm differ diff --git a/tools/website/playground/wasm/aver.js b/tools/website/playground/wasm/aver.js index 32c65f4d..2eb3bdaf 100644 --- a/tools/website/playground/wasm/aver.js +++ b/tools/website/playground/wasm/aver.js @@ -675,7 +675,7 @@ function __wbg_get_imports() { __wbg___wbindgen_throw_6ddd609b62940d55: function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }, - __wbg_error_d8ad12e60840c9e8: function(arg0, arg1) { + __wbg_error_1659bd874679429f: function(arg0, arg1) { console.error(getStringFromWasm0(arg0, arg1)); }, __wbg_getRandomValues_3f44b700395062e5: function() { return handleError(function (arg0, arg1) { diff --git a/tools/website/playground/wasm/aver_bg.wasm b/tools/website/playground/wasm/aver_bg.wasm index 31d78386..024b723c 100644 Binary files a/tools/website/playground/wasm/aver_bg.wasm and b/tools/website/playground/wasm/aver_bg.wasm differ diff --git a/tools/website/playground/wasm_host.js b/tools/website/playground/wasm_host.js index 377a8820..bf9ef53f 100644 --- a/tools/website/playground/wasm_host.js +++ b/tools/website/playground/wasm_host.js @@ -111,6 +111,35 @@ export class AverBrowserHost { setInstance(instance) { this.instance = instance; + this.callerFnTable = this.materialiseCallerFnTable(instance); + } + + /// Read the caller-fn name table once at instance creation. + /// Compiler exports `__caller_fn_count() -> i32` and + /// `__caller_fn_name(i32) -> ref null $string`; we walk + /// `0..count`, decode each ref via `averToJs` (which uses the + /// existing LM bridge), cache the JS strings in an array. + /// Per effect call the trailing `i32` arg indexes into this + /// array — no LM round-trip on the hot path. + materialiseCallerFnTable(instance) { + const exports = instance.exports; + if (typeof exports.__caller_fn_count !== "function" || + typeof exports.__caller_fn_name !== "function") { + return []; + } + const count = exports.__caller_fn_count(); + const out = []; + for (let i = 0; i < count; i++) { + const ref = exports.__caller_fn_name(i); + out.push(ref == null ? "main" : this.averToJs(ref)); + } + return out; + } + + callerFnFromIdx(idx) { + if (typeof idx !== "number") return "main"; + const name = this.callerFnTable && this.callerFnTable[idx]; + return name || "main"; } setTerminalSize(cols, rows) { @@ -273,25 +302,25 @@ export class AverBrowserHost { createImports() { // Every effect import declares a trailing `caller_fn: // any_ref` param now (see `effects.rs::params`). Each - // callback below picks it up as `callerRef`, decodes via + // callback below picks it up as `callerIdx`, decodes via // LM transport, and pipes the resulting JS string through // `recordOrDispatch` as the recorder's caller_fn stamp. // Pure imports (`float_*`) and the group markers ignore // the trailing arg — JS just lets the extra value drop on // the floor. - const dec = (callerRef) => this.averToJs(callerRef); + const dec = (callerIdx) => this.averToJs(callerIdx); return { aver: { - args_len: (callerRef) => + args_len: (callerIdx) => this.recordOrDispatch( "Args.len", [], () => BigInt(this.programArgs.length), (json) => BigInt(json ?? 0), (v) => Number(v), - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - args_get: (index, callerRef) => { + args_get: (index, callerIdx) => { const idx = Number(index); return this.recordOrDispatch( "Args.get", @@ -308,10 +337,10 @@ export class AverBrowserHost { idx >= 0 && idx < this.programArgs.length ? this.programArgs[idx] : "", - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - console_print: (sref, callerRef) => { + console_print: (sref, callerIdx) => { const text = this.averToJs(sref); this.recordOrDispatch( "Console.print", @@ -319,10 +348,10 @@ export class AverBrowserHost { () => this.postConsole("stdout", text), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - console_error: (sref, callerRef) => { + console_error: (sref, callerIdx) => { const text = this.averToJs(sref); this.recordOrDispatch( "Console.error", @@ -330,10 +359,10 @@ export class AverBrowserHost { () => this.postConsole("stderr", text), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - console_warn: (sref, callerRef) => { + console_warn: (sref, callerIdx) => { const text = this.averToJs(sref); this.recordOrDispatch( "Console.warn", @@ -341,10 +370,10 @@ export class AverBrowserHost { () => this.postConsole("stderr", text), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - console_read_line: (callerRef) => { + console_read_line: (callerIdx) => { const exports = this.instance.exports; return this.recordOrDispatch( "Console.readLine", @@ -370,63 +399,63 @@ export class AverBrowserHost { : ""; return { $ok: peek }; }, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - random_int: (min, max, callerRef) => + random_int: (min, max, callerIdx) => this.recordOrDispatch( "Random.int", [Number(min), Number(max)], () => chooseRandomInt(min, max), (json) => BigInt(json ?? 0), (v) => Number(v), - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - random_float: (callerRef) => + random_float: (callerIdx) => this.recordOrDispatch( "Random.float", [], () => Math.random(), (json) => Number(json ?? 0), (v) => Number(v), - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - time_unix_ms: (callerRef) => + time_unix_ms: (callerIdx) => this.recordOrDispatch( "Time.unixMs", [], () => BigInt(Date.now()), (json) => BigInt(json ?? 0), (v) => Number(v), - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - time_now: (callerRef) => + time_now: (callerIdx) => this.recordOrDispatch( "Time.now", [], () => this.jsToAver(new Date().toISOString()), (json) => this.jsToAver(json ?? ""), () => new Date().toISOString(), - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - time_sleep: (millis, callerRef) => + time_sleep: (millis, callerIdx) => this.recordOrDispatch( "Time.sleep", [Number(millis)], () => sleepMillis(millis), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), // Float math is pure — no recording, no replay. The // wasm-gc imports list these because the engine // doesn't expose `f64.sin` directly. The trailing - // `callerRef` arg is ignored. - float_sin: (x, _callerRef) => Math.sin(x), - float_cos: (x, _callerRef) => Math.cos(x), - float_atan2: (y, x, _callerRef) => Math.atan2(y, x), - float_pow: (b, e, _callerRef) => Math.pow(b, e), - terminal_enable_raw_mode: (callerRef) => + // `callerIdx` arg is ignored. + float_sin: (x, _callerIdx) => Math.sin(x), + float_cos: (x, _callerIdx) => Math.cos(x), + float_atan2: (y, x, _callerIdx) => Math.atan2(y, x), + float_pow: (b, e, _callerIdx) => Math.pow(b, e), + terminal_enable_raw_mode: (callerIdx) => this.recordOrDispatch( "Terminal.enableRawMode", [], @@ -436,9 +465,9 @@ export class AverBrowserHost { }, () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - terminal_disable_raw_mode: (callerRef) => + terminal_disable_raw_mode: (callerIdx) => this.recordOrDispatch( "Terminal.disableRawMode", [], @@ -448,18 +477,18 @@ export class AverBrowserHost { }, () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - terminal_clear: (callerRef) => + terminal_clear: (callerIdx) => this.recordOrDispatch( "Terminal.clear", [], () => this.terminal.clear(), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - terminal_move_to: (x, y, callerRef) => { + terminal_move_to: (x, y, callerIdx) => { const xi = Number(x); const yi = Number(y); this.recordOrDispatch( @@ -468,10 +497,10 @@ export class AverBrowserHost { () => this.terminal.moveTo(xi, yi), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - terminal_print: (sref, callerRef) => { + terminal_print: (sref, callerIdx) => { const text = this.averToJs(sref); this.recordOrDispatch( "Terminal.print", @@ -479,10 +508,10 @@ export class AverBrowserHost { () => this.terminal.print(text), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - terminal_set_color: (sref, callerRef) => { + terminal_set_color: (sref, callerIdx) => { const color = this.averToJs(sref); this.recordOrDispatch( "Terminal.setColor", @@ -493,19 +522,19 @@ export class AverBrowserHost { ), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - terminal_reset_color: (callerRef) => + terminal_reset_color: (callerIdx) => this.recordOrDispatch( "Terminal.resetColor", [], () => this.terminal.resetColor(), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - terminal_read_key: (callerRef) => { + terminal_read_key: (callerIdx) => { const exports = this.instance.exports; return this.recordOrDispatch( "Terminal.readKey", @@ -529,10 +558,10 @@ export class AverBrowserHost { ? { $some: head } : { $none: true }; }, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - terminal_size: (callerRef) => { + terminal_size: (callerIdx) => { const exports = this.instance.exports; const cols = this.terminal.cols; const rows = this.terminal.rows; @@ -557,47 +586,47 @@ export class AverBrowserHost { fields: { width: cols, height: rows }, }, }), - dec(callerRef), + this.callerFnFromIdx(callerIdx), ); }, - terminal_hide_cursor: (callerRef) => + terminal_hide_cursor: (callerIdx) => this.recordOrDispatch( "Terminal.hideCursor", [], () => this.terminal.hideCursor(), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - terminal_show_cursor: (callerRef) => + terminal_show_cursor: (callerIdx) => this.recordOrDispatch( "Terminal.showCursor", [], () => this.terminal.showCursor(), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), - terminal_flush: (callerRef) => + terminal_flush: (callerIdx) => this.recordOrDispatch( "Terminal.flush", [], () => this.postTerminalSnapshot(), () => undefined, () => null, - dec(callerRef), + this.callerFnFromIdx(callerIdx), ), // Independent-product structural-scope markers — same // contract the wasm-gc CLI host enforces. Trailing - // `callerRef` ignored (group state lives in the + // `callerIdx` ignored (group state lives in the // recorder, not in trace records). - record_enter_group: (_callerRef) => { + record_enter_group: (_callerIdx) => { this.recorder.enterGroup(); }, - record_set_branch: (i, _callerRef) => { + record_set_branch: (i, _callerIdx) => { this.recorder.setBranch(Number(i)); }, - record_exit_group: (_callerRef) => { + record_exit_group: (_callerIdx) => { this.recorder.exitGroup(); }, }, diff --git a/tools/website/playground/wumpus.wasm b/tools/website/playground/wumpus.wasm index aeba3498..b982609a 100644 Binary files a/tools/website/playground/wumpus.wasm and b/tools/website/playground/wumpus.wasm differ