Skip to content

Commit 333a0df

Browse files
feat(stdlib): STEP 4-A binary I/O — Bytes constructor + LE getters/setters (Refs #239, closes standards#326) (#507)
Adds the raw binary I/O surface that estate ABI-test ports need (`raze-tui`'s 16-byte `RazeEvent` record, future tree-sitter-k9 / tree-sitter-a2ml grammar fixtures, half of bofig's contract tests). Companion to the read-only `bytesLength` / `bytesByteAt` / `bytesAsciiSlice` accessors shipped in affinescript#504 (STEP 3 / standards#242). Together the two PRs give AffineScript first-class binary-buffer support. ## What lands ### `stdlib/Deno.affine` (+10 externs) | extern | lowers to | notes | |---|---|---| | `bytes_new(n) -> Bytes` | `new Uint8Array(n)` | zeroed | | `bytes_fill(n, byte) -> Bytes` | `(new Uint8Array(n)).fill(byte & 0xFF)` | all-byte buffer | | `bytes_set_u8(b, off, v) -> Int` | `… .setUint8(off, v & 0xFF), 0` | returns 0 | | `bytes_set_u16_le(b, off, v) -> Int` | `… .setUint16(off, v & 0xFFFF, true), 0` | LE | | `bytes_set_u32_le(b, off, v) -> Int` | `… .setUint32(off, v >>> 0, true), 0` | LE | | `bytes_set_i32_le(b, off, v) -> Int` | `… .setInt32(off, v \| 0, true), 0` | LE | | `bytes_get_u8(b, off) -> Int` | `… .getUint8(off)` | | | `bytes_get_u16_le(b, off) -> Int` | `… .getUint16(off, true)` | LE | | `bytes_get_u32_le(b, off) -> Int` | `… .getUint32(off, true)` | LE | | `bytes_get_i32_le(b, off) -> Int` | `… .getInt32(off, true)` | LE | All multi-byte integer variants are little-endian — the estate's C ABI contracts (raze-tui `raze-events.ads`, Idris2 `Events.idr`) are LE-pinned. Setters return `Int = 0` so they compose in an expression-statement position; the caller is responsible for the buffer-bounds invariant (an out-of-range offset throws `RangeError` at the host boundary). Bounds-check via `bytesLength` from #504. ### Tests `tests/codegen-deno/bytes_binary_io.{affine,deno.js,harness.mjs}` — round-trips a raze-tui-shaped RazeEvent record (LE i32 + u32 + u8 + u16 × 2) with field-level equality, plus boundary cases: - u32 max (`0xFFFFFFFF`) round-trips - i32 -1 preserves sign - LE byte order verified byte-by-byte via `DataView` (`0x12345678` writes low byte at offset 4, high byte at offset 7) - `bytes_fill` masks `256 → 0`, `-1 → 0xFF`, `0xFF → 0xFF` - `bytes_new` produces a zero-initialised buffer ## Verification | step | result | |---|---| | `dune build bin/main.exe` | ✅ | | `dune runtest` | ✅ 353/353 | | `./tools/run_codegen_deno_tests.sh` | ✅ all harnesses (incl. the new one) | ## Out of scope - **Big-endian variants** — not needed by any current estate ABI; revisit if/when an external API forces BE. - **64-bit getters/setters** (`*_i64_le`) — defer; current STEP 4 candidates max out at 32-bit. ## Relation to #504 (STEP 3) Both this PR and #504 add externs to `stdlib/Deno.affine` and lowerings to `lib/codegen_deno.ml`. The two sets are disjoint at the symbol level: - #504: `statIsFile`, `statIsDirectory`, `bytesLength`, `bytesByteAt`, `bytesAsciiSlice`, `importMetaUrl`, `pub fn ends_with`, wildcard-let codegen fix. - this PR: 10 binary I/O externs in a separate section. The bytes test fixture uses uniquely-named `let _r0 = …`, `let _r4 = …`, etc. instead of the `let _ = …` form so this PR is independently mergeable from #504; once #504 lands, the fixture could be simplified to use the wildcard pattern. ## Refs - Closes standards#326 - Refs: standards#239 (umbrella), standards#242 / #504 (STEP 3 predecessor), standards#243 (STEP 4 per-repo unblock target) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 815af9f commit 333a0df

5 files changed

Lines changed: 516 additions & 0 deletions

File tree

lib/codegen_deno.ml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,19 @@ let () =
545545
(* Recursive file walk — depth-first, returns every file path under
546546
`root`. Filtering by extension is the caller's responsibility. *)
547547
b "walkRecursive" (fun a -> Printf.sprintf "__as_walkRecursive(%s)" (arg 0 a));
548+
(* Bytes I/O — construction + LE getters/setters (campaign #239 STEP
549+
4-A / standards#326). Setters return 0 so they compose in an
550+
expression-statement position. All multi-byte ints are LE. *)
551+
b "bytes_new" (fun a -> Printf.sprintf "new Uint8Array(%s)" (arg 0 a));
552+
b "bytes_fill" (fun a -> Printf.sprintf "(new Uint8Array(%s)).fill((%s) & 0xFF)" (arg 0 a) (arg 1 a));
553+
b "bytes_set_u8" (fun a -> Printf.sprintf "((new DataView((%s).buffer, (%s).byteOffset, (%s).byteLength)).setUint8(%s, (%s) & 0xFF), 0)" (arg 0 a) (arg 0 a) (arg 0 a) (arg 1 a) (arg 2 a));
554+
b "bytes_set_u16_le" (fun a -> Printf.sprintf "((new DataView((%s).buffer, (%s).byteOffset, (%s).byteLength)).setUint16(%s, (%s) & 0xFFFF, true), 0)" (arg 0 a) (arg 0 a) (arg 0 a) (arg 1 a) (arg 2 a));
555+
b "bytes_set_u32_le" (fun a -> Printf.sprintf "((new DataView((%s).buffer, (%s).byteOffset, (%s).byteLength)).setUint32(%s, (%s) >>> 0, true), 0)" (arg 0 a) (arg 0 a) (arg 0 a) (arg 1 a) (arg 2 a));
556+
b "bytes_set_i32_le" (fun a -> Printf.sprintf "((new DataView((%s).buffer, (%s).byteOffset, (%s).byteLength)).setInt32(%s, (%s) | 0, true), 0)" (arg 0 a) (arg 0 a) (arg 0 a) (arg 1 a) (arg 2 a));
557+
b "bytes_get_u8" (fun a -> Printf.sprintf "(new DataView((%s).buffer, (%s).byteOffset, (%s).byteLength)).getUint8(%s)" (arg 0 a) (arg 0 a) (arg 0 a) (arg 1 a));
558+
b "bytes_get_u16_le" (fun a -> Printf.sprintf "(new DataView((%s).buffer, (%s).byteOffset, (%s).byteLength)).getUint16(%s, true)" (arg 0 a) (arg 0 a) (arg 0 a) (arg 1 a));
559+
b "bytes_get_u32_le" (fun a -> Printf.sprintf "(new DataView((%s).buffer, (%s).byteOffset, (%s).byteLength)).getUint32(%s, true)" (arg 0 a) (arg 0 a) (arg 0 a) (arg 1 a));
560+
b "bytes_get_i32_le" (fun a -> Printf.sprintf "(new DataView((%s).buffer, (%s).byteOffset, (%s).byteLength)).getInt32(%s, true)" (arg 0 a) (arg 0 a) (arg 0 a) (arg 1 a));
548561
(* `new RegExp(pat).test(s)` — minimal regex surface. Invalid `pat`
549562
throws at call time (RegExp constructor error). *)
550563
b "regexMatch" (fun a -> Printf.sprintf "__as_regexMatch(%s, %s)" (arg 0 a) (arg 1 a));

stdlib/Deno.affine

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,49 @@ pub extern fn statSize(path: String) -> Int;
7373
/// filter by extension). Throws on a missing root via `Deno.readDirSync`.
7474
pub extern fn walkRecursive(root: String) -> [String];
7575

76+
// ── Bytes I/O (construction + LE getters/setters) ──────────────────
77+
//
78+
// Construction + per-field read/write at byte offsets. Companion to
79+
// the read-only `bytesLength` / `bytesByteAt` / `bytesAsciiSlice`
80+
// accessors (campaign #239 STEP 3 / standards#242). All multi-byte
81+
// integer variants are little-endian — the estate's C ABI contracts
82+
// (raze-tui `raze-events.ads`, Idris2 `Events.idr`) are LE-pinned.
83+
//
84+
// Setters return `Int = 0` so they compose in expression-statement
85+
// position; the caller is responsible for the buffer-bounds invariant
86+
// (an out-of-range offset throws `RangeError` at the host boundary).
87+
// Bounds-check via `bytesLength` from STEP 3.
88+
89+
/// `new Uint8Array(n)` — zeroed buffer of `n` bytes.
90+
pub extern fn bytes_new(n: Int) -> Bytes;
91+
92+
/// `new Uint8Array(n).fill(byte & 0xFF)` — all-`byte` buffer.
93+
pub extern fn bytes_fill(n: Int, byte: Int) -> Bytes;
94+
95+
/// Write `v & 0xFF` to byte `offset`.
96+
pub extern fn bytes_set_u8(b: Bytes, offset: Int, v: Int) -> Int;
97+
98+
/// Write `v & 0xFFFF` to bytes `[offset, offset+2)` as little-endian u16.
99+
pub extern fn bytes_set_u16_le(b: Bytes, offset: Int, v: Int) -> Int;
100+
101+
/// Write `v >>> 0` to bytes `[offset, offset+4)` as little-endian u32.
102+
pub extern fn bytes_set_u32_le(b: Bytes, offset: Int, v: Int) -> Int;
103+
104+
/// Write `v | 0` to bytes `[offset, offset+4)` as little-endian i32.
105+
pub extern fn bytes_set_i32_le(b: Bytes, offset: Int, v: Int) -> Int;
106+
107+
/// Read byte at `offset` (0..255).
108+
pub extern fn bytes_get_u8(b: Bytes, offset: Int) -> Int;
109+
110+
/// Read bytes `[offset, offset+2)` as little-endian u16 (0..65535).
111+
pub extern fn bytes_get_u16_le(b: Bytes, offset: Int) -> Int;
112+
113+
/// Read bytes `[offset, offset+4)` as little-endian u32 (0..4294967295).
114+
pub extern fn bytes_get_u32_le(b: Bytes, offset: Int) -> Int;
115+
116+
/// Read bytes `[offset, offset+4)` as little-endian i32 (-2147483648..2147483647).
117+
pub extern fn bytes_get_i32_le(b: Bytes, offset: Int) -> Int;
118+
76119
// ── Path ───────────────────────────────────────────────────────────
77120

78121
/// Single-segment join with a `/` separator (idempotent on a trailing
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// campaign #239 STEP 4-A — binary I/O smoke (raze-tui RazeEvent shape).
3+
//
4+
// Round-trips the 16-byte event record that estate ABI tests build and
5+
// parse byte-by-byte:
6+
//
7+
// offset 0 size 4 i32_le kind
8+
// offset 4 size 4 u32_le key_code
9+
// offset 8 size 1 u8 modifiers
10+
// offset 9 size 1 pad
11+
// offset 10 size 2 u16_le mouse_x
12+
// offset 12 size 2 u16_le mouse_y
13+
// offset 14 size 2 pad
14+
// Total: 16 bytes
15+
16+
use Deno::{ Bytes, bytes_new, bytes_fill,
17+
bytes_set_u8, bytes_set_u16_le, bytes_set_u32_le, bytes_set_i32_le,
18+
bytes_get_u8, bytes_get_u16_le, bytes_get_u32_le, bytes_get_i32_le };
19+
20+
pub fn build_event(kind: Int, key_code: Int, modifiers: Int, mouse_x: Int, mouse_y: Int) -> Bytes {
21+
// Uniquely-named `let` discards keep the codegen pre-#504-compatible
22+
// (the `PatWildcard` fix from #504 also works, but isn't required).
23+
let b = bytes_new(16);
24+
let _r0 = bytes_set_i32_le(b, 0, kind);
25+
let _r4 = bytes_set_u32_le(b, 4, key_code);
26+
let _r8 = bytes_set_u8(b, 8, modifiers);
27+
let _ra = bytes_set_u16_le(b, 10, mouse_x);
28+
let _rc = bytes_set_u16_le(b, 12, mouse_y);
29+
b
30+
}
31+
32+
pub fn read_kind(b: Bytes) -> Int { bytes_get_i32_le(b, 0) }
33+
pub fn read_key_code(b: Bytes) -> Int { bytes_get_u32_le(b, 4) }
34+
pub fn read_modifiers(b: Bytes) -> Int { bytes_get_u8(b, 8) }
35+
pub fn read_mouse_x(b: Bytes) -> Int { bytes_get_u16_le(b, 10) }
36+
pub fn read_mouse_y(b: Bytes) -> Int { bytes_get_u16_le(b, 12) }
37+
38+
pub fn fill_byte(n: Int, byte: Int) -> Bytes { bytes_fill(n, byte) }
39+
40+
pub fn fill_first(n: Int, byte: Int) -> Int {
41+
let b = bytes_fill(n, byte);
42+
bytes_get_u8(b, 0)
43+
}

0 commit comments

Comments
 (0)