diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index 9d6de7e..7ff4bfa 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -464,6 +464,19 @@ let () = (* Recursive file walk — depth-first, returns every file path under `root`. Filtering by extension is the caller's responsibility. *) b "walkRecursive" (fun a -> Printf.sprintf "__as_walkRecursive(%s)" (arg 0 a)); + (* Bytes I/O — construction + LE getters/setters (campaign #239 STEP + 4-A / standards#326). Setters return 0 so they compose in an + expression-statement position. All multi-byte ints are LE. *) + b "bytes_new" (fun a -> Printf.sprintf "new Uint8Array(%s)" (arg 0 a)); + b "bytes_fill" (fun a -> Printf.sprintf "(new Uint8Array(%s)).fill((%s) & 0xFF)" (arg 0 a) (arg 1 a)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); (* `new RegExp(pat).test(s)` — minimal regex surface. Invalid `pat` throws at call time (RegExp constructor error). *) b "regexMatch" (fun a -> Printf.sprintf "__as_regexMatch(%s, %s)" (arg 0 a) (arg 1 a)); diff --git a/stdlib/Deno.affine b/stdlib/Deno.affine index a69c0b8..f222906 100644 --- a/stdlib/Deno.affine +++ b/stdlib/Deno.affine @@ -73,6 +73,49 @@ pub extern fn statSize(path: String) -> Int; /// filter by extension). Throws on a missing root via `Deno.readDirSync`. pub extern fn walkRecursive(root: String) -> [String]; +// ── Bytes I/O (construction + LE getters/setters) ────────────────── +// +// Construction + per-field read/write at byte offsets. Companion to +// the read-only `bytesLength` / `bytesByteAt` / `bytesAsciiSlice` +// accessors (campaign #239 STEP 3 / standards#242). 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 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 STEP 3. + +/// `new Uint8Array(n)` — zeroed buffer of `n` bytes. +pub extern fn bytes_new(n: Int) -> Bytes; + +/// `new Uint8Array(n).fill(byte & 0xFF)` — all-`byte` buffer. +pub extern fn bytes_fill(n: Int, byte: Int) -> Bytes; + +/// Write `v & 0xFF` to byte `offset`. +pub extern fn bytes_set_u8(b: Bytes, offset: Int, v: Int) -> Int; + +/// Write `v & 0xFFFF` to bytes `[offset, offset+2)` as little-endian u16. +pub extern fn bytes_set_u16_le(b: Bytes, offset: Int, v: Int) -> Int; + +/// Write `v >>> 0` to bytes `[offset, offset+4)` as little-endian u32. +pub extern fn bytes_set_u32_le(b: Bytes, offset: Int, v: Int) -> Int; + +/// Write `v | 0` to bytes `[offset, offset+4)` as little-endian i32. +pub extern fn bytes_set_i32_le(b: Bytes, offset: Int, v: Int) -> Int; + +/// Read byte at `offset` (0..255). +pub extern fn bytes_get_u8(b: Bytes, offset: Int) -> Int; + +/// Read bytes `[offset, offset+2)` as little-endian u16 (0..65535). +pub extern fn bytes_get_u16_le(b: Bytes, offset: Int) -> Int; + +/// Read bytes `[offset, offset+4)` as little-endian u32 (0..4294967295). +pub extern fn bytes_get_u32_le(b: Bytes, offset: Int) -> Int; + +/// Read bytes `[offset, offset+4)` as little-endian i32 (-2147483648..2147483647). +pub extern fn bytes_get_i32_le(b: Bytes, offset: Int) -> Int; + // ── Path ─────────────────────────────────────────────────────────── /// Single-segment join with a `/` separator (idempotent on a trailing diff --git a/tests/codegen-deno/bytes_binary_io.affine b/tests/codegen-deno/bytes_binary_io.affine new file mode 100644 index 0000000..8af3c15 --- /dev/null +++ b/tests/codegen-deno/bytes_binary_io.affine @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MPL-2.0 +// campaign #239 STEP 4-A — binary I/O smoke (raze-tui RazeEvent shape). +// +// Round-trips the 16-byte event record that estate ABI tests build and +// parse byte-by-byte: +// +// offset 0 size 4 i32_le kind +// offset 4 size 4 u32_le key_code +// offset 8 size 1 u8 modifiers +// offset 9 size 1 pad +// offset 10 size 2 u16_le mouse_x +// offset 12 size 2 u16_le mouse_y +// offset 14 size 2 pad +// Total: 16 bytes + +use Deno::{ Bytes, bytes_new, bytes_fill, + bytes_set_u8, bytes_set_u16_le, bytes_set_u32_le, bytes_set_i32_le, + bytes_get_u8, bytes_get_u16_le, bytes_get_u32_le, bytes_get_i32_le }; + +pub fn build_event(kind: Int, key_code: Int, modifiers: Int, mouse_x: Int, mouse_y: Int) -> Bytes { + // Uniquely-named `let` discards keep the codegen pre-#504-compatible + // (the `PatWildcard` fix from #504 also works, but isn't required). + let b = bytes_new(16); + let _r0 = bytes_set_i32_le(b, 0, kind); + let _r4 = bytes_set_u32_le(b, 4, key_code); + let _r8 = bytes_set_u8(b, 8, modifiers); + let _ra = bytes_set_u16_le(b, 10, mouse_x); + let _rc = bytes_set_u16_le(b, 12, mouse_y); + b +} + +pub fn read_kind(b: Bytes) -> Int { bytes_get_i32_le(b, 0) } +pub fn read_key_code(b: Bytes) -> Int { bytes_get_u32_le(b, 4) } +pub fn read_modifiers(b: Bytes) -> Int { bytes_get_u8(b, 8) } +pub fn read_mouse_x(b: Bytes) -> Int { bytes_get_u16_le(b, 10) } +pub fn read_mouse_y(b: Bytes) -> Int { bytes_get_u16_le(b, 12) } + +pub fn fill_byte(n: Int, byte: Int) -> Bytes { bytes_fill(n, byte) } + +pub fn fill_first(n: Int, byte: Int) -> Int { + let b = bytes_fill(n, byte); + bytes_get_u8(b, 0) +} diff --git a/tests/codegen-deno/bytes_binary_io.deno.js b/tests/codegen-deno/bytes_binary_io.deno.js new file mode 100644 index 0000000..f70701b --- /dev/null +++ b/tests/codegen-deno/bytes_binary_io.deno.js @@ -0,0 +1,325 @@ +// Generated by AffineScript compiler (Deno-ESM target, issue #122) +// SPDX-License-Identifier: MPL-2.0 +// ---- AffineScript Deno-ESM runtime ---- +const Some = (value) => ({ tag: "Some", value }); +const None = { tag: "None" }; +const Ok = (value) => ({ tag: "Ok", value }); +const Err = (error) => ({ tag: "Err", error }); +const Unit = null; +const print = (s) => { Deno.stdout.writeSync(new TextEncoder().encode(String(s))); }; +const println = (s) => { console.log(String(s)); }; +// ---- Deno host shims (extern fn lowering targets, issue #122) ---- +// Kept tiny + inlined so emitted modules are genuinely drop-in (no extra +// package to publish or resolve). The same surface is mirrored, for +// standalone `deno test`, by packages/affine-deno/mod.js. +const __as_ensureDir = (p) => { + try { Deno.mkdirSync(p, { recursive: true }); } + catch (e) { if (!(e instanceof Deno.errors.AlreadyExists)) throw e; } +}; +const __as_pathJoin = (a, b) => { + if (a.length === 0) return b; + const sep = a.endsWith("/") || a.endsWith("\\") ? "" : "/"; + return a + sep + b; +}; +const __as_readDirNames = (p) => { + const names = []; + for (const entry of Deno.readDirSync(p)) { + if (entry.isFile) names.push(entry.name); + } + return names; +}; +const __as_isNotFound = (e) => (e instanceof Deno.errors.NotFound); +const __as_walkRecursive = (root) => { + const out = []; + const rec = (dir) => { + for (const entry of Deno.readDirSync(dir)) { + const full = (dir.endsWith("/") ? dir : dir + "/") + entry.name; + if (entry.isFile) out.push(full); + else if (entry.isDirectory) rec(full); + } + }; + rec(root); + return out; +}; +const __as_regexMatch = (s, pat) => new RegExp(pat).test(String(s)); +const __as_wasmInstance = (bytes) => + new WebAssembly.Instance(new WebAssembly.Module(bytes)).exports; +const __as_wasmCall = (exports, name, args) => Number(exports[name](...(args || []))); +// ---- WasmValue (Deno.affine #455 — Tier 1 #5, Option B) ---- +// Opaque tagged value crossing the AS/JS boundary as `{ kind, v }`. +// `kind` is one of "i32" | "i64" | "f32" | "f64". The `v` payload is +// `BigInt` for i64 (preserves precision beyond 2^53), `Number` otherwise. +const __as_wv_i32 = (n) => ({ kind: "i32", v: (Number(n) | 0) }); +const __as_wv_i64 = (n) => ({ kind: "i64", v: BigInt(n) }); +const __as_wv_f32 = (f) => ({ kind: "f32", v: Math.fround(Number(f)) }); +const __as_wv_f64 = (f) => ({ kind: "f64", v: Number(f) }); +const __as_wv_as_int = (v) => { + if (v == null) return 0; + if (typeof v.v === "bigint") { + // i64: truncate to safe-integer Number; caller's responsibility for + // precision-sensitive paths (use wv_kind to detect). + return Number(v.v); + } + // i32 / f32 / f64: truncate toward zero per AS Int semantics. + return (Number(v.v) | 0); +}; +const __as_wv_as_float = (v) => { + if (v == null) return 0; + return typeof v.v === "bigint" ? Number(v.v) : Number(v.v); +}; +const __as_wv_kind = (v) => (v && typeof v.kind === "string") ? v.kind : ""; +const __as_wasm_export_call = (exports, name, args) => { + // Unmarshal AS-side [WasmValue] to raw JS scalars for the wasm call. + const rawArgs = (args || []).map((wv) => { + if (wv == null) return 0; + // i64 payload is BigInt; wasm i64 imports accept BigInt directly. + return wv.v; + }); + const result = exports[name](...rawArgs); + // Wrap return as f64 (lossless for any numeric; callers expecting i32/i64 + // can rebuild via wv_i32(wv_as_int(result)) or inspect wv_kind). + if (typeof result === "bigint") { + return { kind: "i64", v: result }; + } + return { kind: "f64", v: Number(result) }; +}; +// ---- motion (bindings #4): consumer-provided import ---- +// Host JS environment must expose globalThis.__as_motion (the motion +// library or a compatible mock). Tests set it in the harness before +// importing the generated module; production consumers typically do +// `import * as m from "motion"; globalThis.__as_motion = m;` once at +// module-init time. The AffineScript-side externs (stdlib/Motion.affine) +// don't see this indirection — they call __as_motion* helpers directly. +const __as_motionAnimate = (target, keyframes, options) => + globalThis.__as_motion.animate(target, keyframes, options); +const __as_motionAwait = (controls) => + Promise.resolve(controls).then(() => 0); +const __as_motionCancel = (controls) => { + if (controls && typeof controls.cancel === "function") controls.cancel(); + return 0; +}; +// `animateMini` / `tween` / `spring` / `ease` — bindings #4 follow-up +// surface. Each helper resolves the host method on globalThis.__as_motion +// at call time so a mock that only stubs a subset still works for the +// rest (the smoke harness exercises every variant). +const __as_motionAnimateMini = (target, keyframes, options) => + globalThis.__as_motion.animateMini(target, keyframes, options); +const __as_motionTween = (target, from, to, options) => + globalThis.__as_motion.tween(target, from, to, options); +const __as_motionSpring = (target, keyframes, springConfig) => + globalThis.__as_motion.spring(target, keyframes, springConfig); +const __as_motionEase = (name) => + globalThis.__as_motion.ease(name); +// ---- pixi.js (bindings #1): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi (the PIXI namespace +// from `import * as PIXI from "pixi.js"`). Tests set it in the harness +// before importing the generated module. +const __as_pixiAppInit = async (options) => { + const app = new globalThis.__as_pixi.Application(); + await app.init(options); + return app; +}; +const __as_pixiAppCanvas = (app) => app.canvas; +const __as_pixiAppStage = (app) => app.stage; +const __as_pixiAppTicker = (app) => app.ticker; +const __as_pixiAppDestroy = (app) => { app.destroy(); return 0; }; +const __as_pixiContainerNew = () => new globalThis.__as_pixi.Container(); +const __as_pixiContainerAddChild = (p, c) => { p.addChild(c); return 0; }; +const __as_pixiContainerRemoveChild = (p, c) => { p.removeChild(c); return 0; }; +const __as_pixiContainerSetPosition = (c, x, y) => { c.x = x; c.y = y; return 0; }; +const __as_pixiContainerSetVisible = (c, v) => { c.visible = v; return 0; }; +const __as_pixiContainerDestroy = (c) => { c.destroy(); return 0; }; +const __as_pixiSpriteFrom = (t) => new globalThis.__as_pixi.Sprite(t); +// Upcasts are identity — PIXI's class hierarchy makes Sprite/Graphics/ +// Text actual Container subclasses, so the JS object is the same. +const __as_pixiSpriteAsContainer = (s) => s; +const __as_pixiTextureFromUrl = (url) => globalThis.__as_pixi.Texture.from(url); +const __as_pixiGraphicsNew = () => new globalThis.__as_pixi.Graphics(); +const __as_pixiGraphicsRect = (g, x, y, w, h) => { g.rect(x, y, w, h); return 0; }; +const __as_pixiGraphicsFill = (g, color) => { g.fill({ color }); return 0; }; +const __as_pixiGraphicsClear = (g) => { g.clear(); return 0; }; +const __as_pixiGraphicsAsContainer = (g) => g; +const __as_pixiTextNew = (options) => new globalThis.__as_pixi.Text(options); +const __as_pixiTextSetText = (t, content) => { t.text = content; return 0; }; +const __as_pixiTextAsContainer = (t) => t; +const __as_pixiTickerAdd = (t, cb) => { t.add(cb); return 0; }; +const __as_pixiTickerStart = (t) => { t.start(); return 0; }; +const __as_pixiTickerStop = (t) => { t.stop(); return 0; }; +// ---- @pixi/ui (bindings #3): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi_ui (the namespace +// from `import * as PixiUI from "@pixi/ui"`). Tests set it in the +// harness before importing the generated module; production +// consumers typically do once at module-init time. The +// AffineScript-side externs (stdlib/PixiUI.affine) don't see this +// indirection — they call __as_pixiUi* helpers directly. +// +// Upcasts to Container are identity — @pixi/ui's Button / +// FancyButton / Slider / Switch are all real PIXI.Container +// subclasses, so the JS object is the same. +const __as_pixiUiButtonNew = (options) => new globalThis.__as_pixi_ui.Button(options); +const __as_pixiUiButtonOnPress = (b, cb) => { b.onPress.connect(cb); return 0; }; +const __as_pixiUiButtonAsContainer = (b) => b; +const __as_pixiUiFancyButtonNew = (options) => new globalThis.__as_pixi_ui.FancyButton(options); +const __as_pixiUiFancyButtonAsContainer = (b) => b; +const __as_pixiUiSliderNew = (options) => new globalThis.__as_pixi_ui.Slider(options); +const __as_pixiUiSliderOnUpdate = (s, cb) => { s.onUpdate.connect(cb); return 0; }; +const __as_pixiUiSliderAsContainer = (s) => s; +const __as_pixiUiSwitchNew = (options) => new globalThis.__as_pixi_ui.Switch(options); +const __as_pixiUiSwitchOnChange = (sw, cb) => { sw.onChange.connect(cb); return 0; }; +const __as_pixiUiSwitchAsContainer = (sw) => sw; +// ---- @pixi/sound (bindings #2): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi_sound (the `Sound` +// named export from `@pixi/sound`). Tests set it in the harness before +// importing the generated module; production consumers typically do +// `import { Sound } from "@pixi/sound"; globalThis.__as_pixi_sound = Sound;` +// once at module-init time. The AffineScript-side externs +// (stdlib/PixiSound.affine) don't see this indirection — they call +// __as_pixiSound* helpers directly. +const __as_pixiSoundFrom = (url) => globalThis.__as_pixi_sound.from(url); +const __as_pixiSoundPlay = (s) => { s.play(); return 0; }; +const __as_pixiSoundStop = (s) => { s.stop(); return 0; }; +const __as_pixiSoundPause = (s) => { s.pause(); return 0; }; +const __as_pixiSoundResume = (s) => { s.resume(); return 0; }; +const __as_pixiSoundSetVolume = (s, vol) => { s.volume = vol; return 0; }; +const __as_pixiSoundSetLoop = (s, loop) => { s.loop = loop; return 0; }; +// `++` is overloaded (string concat / array concat); `a + b` would +// stringify arrays. Dispatch on shape so stdlib/string.affine's +// `result ++ [x]` and `a ++ b` are both correct. +const __as_concat = (a, b) => Array.isArray(a) ? a.concat(b) : (a + b); +// Honest host/runtime primitives underpinning the AffineScript-level +// stdlib/string.affine (its is_empty/starts_with/ends_with/split/join/ +// replace/... are real AffineScript on top of these). +const __as_strSub = (s, start, n) => String(s).slice(start, start + n); +const __as_strGet = (s, i) => String(s)[i]; +const __as_strFind = (s, n) => String(s).indexOf(n); +const __as_charToInt = (c) => String(c).codePointAt(0); +const __as_intToChar = (n) => String.fromCodePoint(n); +const __as_parseInt = (s) => { + const n = parseInt(String(s), 10); + return Number.isNaN(n) ? None : Some(n); +}; +const __as_parseFloat = (s) => { + const n = parseFloat(String(s)); + return Number.isNaN(n) ? None : Some(n); +}; +const __as_show = (v) => (typeof v === "string" ? v : JSON.stringify(v)); +// ---- Http (issue #160): portable fetch round-trip ---- +// `headers` crosses the boundary as an AffineScript [(String, String)] +// assoc list == JS array of [name, value] pairs. `body` is an +// AffineScript Option == { tag: "Some", value } | { tag: "None" }. +// The result is the `Response` record shape { status, headers, body }. +const __as_httpHeadersToObject = (pairs) => { + const o = {}; + for (const kv of (pairs || [])) o[kv[0]] = kv[1]; + return o; +}; +const __as_httpHeadersFromResponse = (res) => { + const out = []; + res.headers.forEach((value, key) => out.push([key, value])); + return out; +}; +// ---- hpm-json-rsr Zig FFI shims (stdlib/json.affine v0.3) ---- +// `HpmJsonValue` is opaque to AffineScript; on Deno-ESM it's just the +// underlying JS value from JSON.parse. The shims mirror the sentinel +// conventions of the Zig exports so the AffineScript-side wrappers +// (`to_json`, `parse`) behave identically across backends. +const __as_hpmJsonParse = (s) => { + try { return Some(JSON.parse(String(s))); } catch (_e) { return None; } +}; +const __as_hpmJsonFree = (_v) => 0; +const __as_hpmJsonType = (v) => { + if (v === null || v === undefined) return 0; + if (typeof v === "boolean") return 1; + if (typeof v === "number") return Number.isInteger(v) ? 2 : 3; + if (typeof v === "string") return 4; + if (Array.isArray(v)) return 5; + if (typeof v === "object") return 6; + return -1; +}; +const __as_hpmJsonBool = (v) => (typeof v === "boolean" ? (v ? 1 : 0) : -1); +const __as_hpmJsonInt = (v) => + (typeof v === "number" ? Math.trunc(v) : Number.MIN_SAFE_INTEGER); +const __as_hpmJsonFloat = (v) => (typeof v === "number" ? v : NaN); +const __as_hpmJsonString = (v) => (typeof v === "string" ? v : ""); +const __as_hpmJsonObjectGet = (v, k) => { + if (v === null || typeof v !== "object" || Array.isArray(v)) return None; + return Object.prototype.hasOwnProperty.call(v, String(k)) + ? Some(v[String(k)]) : None; +}; +const __as_hpmJsonArrayLen = (v) => (Array.isArray(v) ? v.length : 0); +const __as_hpmJsonArrayGet = (v, i) => { + if (!Array.isArray(v)) return None; + const idx = Number(i); + return (idx >= 0 && idx < v.length) ? Some(v[idx]) : None; +}; +const __as_hpmJsonEscapeString = (s) => { + let out = ""; + const src = String(s); + for (let i = 0; i < src.length; i++) { + const c = src.charCodeAt(i); + if (c === 0x22) out += "\\\""; + else if (c === 0x5c) out += "\\\\"; + else if (c === 0x0a) out += "\\n"; + else if (c === 0x0d) out += "\\r"; + else if (c === 0x09) out += "\\t"; + else if (c === 0x08) out += "\\b"; + else if (c === 0x0c) out += "\\f"; + else if (c < 0x20) out += "\\u00" + c.toString(16).padStart(2, "0"); + else out += src[i]; + } + return out; +}; +const __as_httpFetch = async (url, method, headers, bodyOpt) => { + const init = { method, headers: __as_httpHeadersToObject(headers) }; + if (bodyOpt && bodyOpt.tag === "Some") init.body = bodyOpt.value; + // `globalThis.fetch` explicitly: the stdlib `Http.fetch` compiles to a + // module-level `function fetch`, which would otherwise shadow the host. + const res = await globalThis.fetch(url, init); + const text = await res.text(); + return { + status: res.status, + headers: __as_httpHeadersFromResponse(res), + body: text, + }; +}; +// ---- end runtime ---- + +export function build_event(kind, key_code, modifiers, mouse_x, mouse_y) { + const b = new Uint8Array(16); + const _r0 = ((new DataView((b).buffer, (b).byteOffset, (b).byteLength)).setInt32(0, (kind) | 0, true), 0); + const _r4 = ((new DataView((b).buffer, (b).byteOffset, (b).byteLength)).setUint32(4, (key_code) >>> 0, true), 0); + const _r8 = ((new DataView((b).buffer, (b).byteOffset, (b).byteLength)).setUint8(8, (modifiers) & 0xFF), 0); + const _ra = ((new DataView((b).buffer, (b).byteOffset, (b).byteLength)).setUint16(10, (mouse_x) & 0xFFFF, true), 0); + const _rc = ((new DataView((b).buffer, (b).byteOffset, (b).byteLength)).setUint16(12, (mouse_y) & 0xFFFF, true), 0); + return b; +} + +export function read_kind(b) { + return (new DataView((b).buffer, (b).byteOffset, (b).byteLength)).getInt32(0, true); +} + +export function read_key_code(b) { + return (new DataView((b).buffer, (b).byteOffset, (b).byteLength)).getUint32(4, true); +} + +export function read_modifiers(b) { + return (new DataView((b).buffer, (b).byteOffset, (b).byteLength)).getUint8(8); +} + +export function read_mouse_x(b) { + return (new DataView((b).buffer, (b).byteOffset, (b).byteLength)).getUint16(10, true); +} + +export function read_mouse_y(b) { + return (new DataView((b).buffer, (b).byteOffset, (b).byteLength)).getUint16(12, true); +} + +export function fill_byte(n, byte) { + return (new Uint8Array(n)).fill((byte) & 0xFF); +} + +export function fill_first(n, byte) { + const b = (new Uint8Array(n)).fill((byte) & 0xFF); + return (new DataView((b).buffer, (b).byteOffset, (b).byteLength)).getUint8(0); +} + diff --git a/tests/codegen-deno/bytes_binary_io.harness.mjs b/tests/codegen-deno/bytes_binary_io.harness.mjs new file mode 100644 index 0000000..ef4984d --- /dev/null +++ b/tests/codegen-deno/bytes_binary_io.harness.mjs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MPL-2.0 +// campaign #239 STEP 4-A — Node-ESM harness for binary I/O bindings. +// +// Exercises the bytes_new / bytes_fill / LE setters + getters round-trip +// against a raze-tui-shaped RazeEvent record. + +import assert from "node:assert/strict"; + +const { + build_event, + read_kind, + read_key_code, + read_modifiers, + read_mouse_x, + read_mouse_y, + fill_byte, + fill_first, +} = await import("./bytes_binary_io.deno.js"); + +// Round-trip: build a Key event and read every field back. +{ + const buf = build_event(1, 0x61, 0x03, 0, 0); + assert.equal(buf.byteLength, 16, "RazeEvent buffer is 16 bytes"); + assert.equal(read_kind(buf), 1, "kind field LE round-trip"); + assert.equal(read_key_code(buf), 0x61, "key_code field LE round-trip"); + assert.equal(read_modifiers(buf), 0x03, "modifiers field round-trip"); + assert.equal(read_mouse_x(buf), 0, "mouse_x field LE round-trip"); + assert.equal(read_mouse_y(buf), 0, "mouse_y field LE round-trip"); +} + +// Round-trip: Mouse event with non-zero coordinates. +{ + const buf = build_event(2, 0, 0, 80, 24); + assert.equal(read_kind(buf), 2, "Mouse kind"); + assert.equal(read_mouse_x(buf), 80, "mouse_x = 80"); + assert.equal(read_mouse_y(buf), 24, "mouse_y = 24"); +} + +// Boundary: u32 max value through the field. +{ + const buf = build_event(0, 0xFFFFFFFF, 0, 0, 0); + assert.equal(read_key_code(buf), 0xFFFFFFFF, "u32 max round-trips"); +} + +// Boundary: negative i32 through the kind field. +{ + const buf = build_event(-1, 0, 0, 0, 0); + assert.equal(read_kind(buf), -1, "i32 -1 round-trips with sign"); +} + +// LE byte order check — write 0x12345678 as u32 at offset 4; the first +// byte at offset 4 must be 0x78 (LE low byte) and the last 0x12. +{ + const buf = build_event(0, 0x12345678, 0, 0, 0); + // Reading bytes 4..7 individually via bytes_get_u8 would be ideal, but + // this harness can call build_event + DataView read directly. Instead, + // confirm via the read helper that the value is preserved. + assert.equal(read_key_code(buf), 0x12345678, "0x12345678 LE round-trip"); + // And via byte-level read — JS DataView side-effect-free confirmation: + const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + assert.equal(view.getUint8(4), 0x78, "LE low byte at offset 4"); + assert.equal(view.getUint8(7), 0x12, "LE high byte at offset 7"); +} + +// bytes_fill: 8-byte all-0xAA buffer. +{ + const filled = fill_byte(8, 0xAA); + assert.equal(filled.byteLength, 8, "bytes_fill respects size"); + for (let i = 0; i < 8; i++) { + assert.equal(filled[i], 0xAA, `bytes_fill byte ${i} = 0xAA`); + } +} + +// bytes_fill: byte arg is masked to 0xFF (256 → 0). +{ + assert.equal(fill_first(4, 256), 0, "bytes_fill masks 256 → 0"); + assert.equal(fill_first(4, 0xFF), 0xFF, "bytes_fill 0xFF passes through"); + assert.equal(fill_first(4, -1), 0xFF, "bytes_fill -1 masks to 0xFF"); +} + +// bytes_new: zero-initialised. +{ + const empty = build_event(0, 0, 0, 0, 0); + for (let i = 0; i < 16; i++) { + if (i !== 0) { // kind field was set to 0 by build_event explicitly + // Every offset zero by default + } + assert.equal(empty[i], 0, `bytes_new byte ${i} zero-initialised`); + } +} + +console.log("bytes_binary_io.harness.mjs OK");