From 80802e1bb0e75be5ccb00687306d21ed86d05a12 Mon Sep 17 00:00:00 2001 From: Skye <63878374+MierenManz@users.noreply.github.com> Date: Wed, 26 Apr 2023 22:03:46 +0200 Subject: [PATCH] feat: add `I64LEB128` (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elias Sjögreen --- .github/workflows/checks.yml | 4 +- benchmarks/string.ts | 4 +- benchmarks/struct.ts | 3 +- benchmarks/tuple.ts | 3 +- deno.json | 10 ++ deno.lock | 20 +++ mod.ts | 1 + scripts/build_i64leb128.ts | 43 +++++ .../array/{arrayBuffer.ts => array_buffer.ts} | 0 types/array/mod.ts | 4 +- types/array/{typedArray.ts => typed_array.ts} | 0 types/leb128/_i64leb128.ts | 27 ++++ types/leb128/_i64leb128.wat | 109 +++++++++++++ .../{varint/leb128.ts => leb128/i32leb128.ts} | 2 +- types/leb128/i32leb128_test.ts | 80 ++++++++++ types/leb128/i64leb128.ts | 27 ++++ types/leb128/i64leb128_test.ts | 151 ++++++++++++++++++ types/leb128/mod.ts | 2 + types/mod.ts | 1 + types/string/null_terminated_test.ts | 5 +- types/varint/leb128_test.ts | 94 ----------- types/varint/mod.ts | 1 - 22 files changed, 481 insertions(+), 110 deletions(-) create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 scripts/build_i64leb128.ts rename types/array/{arrayBuffer.ts => array_buffer.ts} (100%) rename types/array/{typedArray.ts => typed_array.ts} (100%) create mode 100644 types/leb128/_i64leb128.ts create mode 100644 types/leb128/_i64leb128.wat rename types/{varint/leb128.ts => leb128/i32leb128.ts} (95%) create mode 100644 types/leb128/i32leb128_test.ts create mode 100644 types/leb128/i64leb128.ts create mode 100644 types/leb128/i64leb128_test.ts create mode 100644 types/leb128/mod.ts delete mode 100644 types/varint/leb128_test.ts delete mode 100644 types/varint/mod.ts diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c105855..6aa1dd1 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup latest deno version uses: denoland/setup-deno@main @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup latest deno version uses: denoland/setup-deno@main diff --git a/benchmarks/string.ts b/benchmarks/string.ts index 7dbb4c6..cda5e8d 100644 --- a/benchmarks/string.ts +++ b/benchmarks/string.ts @@ -1,6 +1,6 @@ -import { FixedUTF8String } from "../types/string/mod.ts"; +import { FixedLengthString } from "../mod.ts"; -const stringThing = new FixedUTF8String(12); +const stringThing = new FixedLengthString(12); const ab = new TextEncoder().encode("Hello World!").buffer; const dt = new DataView(ab); diff --git a/benchmarks/struct.ts b/benchmarks/struct.ts index 92d12af..702ed69 100644 --- a/benchmarks/struct.ts +++ b/benchmarks/struct.ts @@ -1,5 +1,4 @@ -import { AlignedStruct } from "../types/struct/mod.ts"; -import { u32 } from "../types/primitive/mod.ts"; +import { AlignedStruct, u32 } from "../mod.ts"; const data = new DataView(new ArrayBuffer(8)); diff --git a/benchmarks/tuple.ts b/benchmarks/tuple.ts index b5a7c72..e1dff2d 100644 --- a/benchmarks/tuple.ts +++ b/benchmarks/tuple.ts @@ -1,5 +1,4 @@ -import { Tuple } from "../types/tuple/mod.ts"; -import { u32 } from "../types/primitive/u32.ts"; +import { Tuple, u32 } from "../mod.ts"; const benchTuple = new Tuple([u32, u32]); const u32arr = new Uint32Array([2, 4]); diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..6e5f1ab --- /dev/null +++ b/deno.json @@ -0,0 +1,10 @@ +{ + "imports": { + "std/": "https://deno.land/std@0.184.0/", + "wabt": "npm:wabt@1.0.32" + }, + "tasks": { + "build": "deno task build_i64leb128", + "build_i64leb128": "deno run --allow-read --allow-write ./scripts/build_i64leb128.ts" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..55b8a59 --- /dev/null +++ b/deno.lock @@ -0,0 +1,20 @@ +{ + "version": "2", + "remote": { + "https://deno.land/std@0.184.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.184.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.184.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.184.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f" + }, + "npm": { + "specifiers": { + "wabt": "wabt@1.0.32" + }, + "packages": { + "wabt@1.0.32": { + "integrity": "sha512-1aHvkKaSrrl7qFtAbQ1RWVHLuJApRh7PtUdYvRtiUEKEhk0MOV0sTuz5cLF6jL5jPLRyifLbZcR65AEga/xBhQ==", + "dependencies": {} + } + } + } +} diff --git a/mod.ts b/mod.ts index 06816bf..95ee59e 100644 --- a/mod.ts +++ b/mod.ts @@ -1 +1,2 @@ export * from "./types/mod.ts"; +export * from "./utils.ts"; diff --git a/scripts/build_i64leb128.ts b/scripts/build_i64leb128.ts new file mode 100644 index 0000000..44059b1 --- /dev/null +++ b/scripts/build_i64leb128.ts @@ -0,0 +1,43 @@ +import wabt from "wabt"; + +const { parseWat } = await wabt(); + +const source = "./types/leb128/_i64leb128.wat"; +const destination = "./types/leb128/_i64leb128.ts"; + +const wat = await Deno.readTextFile(source); +const module = parseWat(source, wat); +const wasm = module.toBinary({}).buffer; +const encoded = btoa(String.fromCharCode(...wasm)); + +const content = `\ +// Copyright 2023 the Blocktopus authors. All rights reserved. MIT license. +// Copyright 2023 the denosaurs team. All rights reserved. MIT license. + +const bytes = Uint8Array.from( + atob( + "${encoded}" + ), + (c) => c.charCodeAt(0) +); +const { instance } = await WebAssembly.instantiate(bytes); + +const exports = instance.exports as { + memory: WebAssembly.Memory; + read: (pointer: number) => [bigint, number]; + write: (value: bigint) => number; +}; + +const memory = new Uint8Array(exports.memory.buffer); + +export function read(buffer: Uint8Array): [bigint, number] { + memory.set(buffer, 0); + return exports.read(0); +} + +export function write(value: bigint): Uint8Array { + return memory.subarray(0, exports.write(value)); +} +`; + +await Deno.writeTextFile(destination, content); diff --git a/types/array/arrayBuffer.ts b/types/array/array_buffer.ts similarity index 100% rename from types/array/arrayBuffer.ts rename to types/array/array_buffer.ts diff --git a/types/array/mod.ts b/types/array/mod.ts index 0b153d3..feda794 100644 --- a/types/array/mod.ts +++ b/types/array/mod.ts @@ -1,3 +1,3 @@ export * from "./array.ts"; -export * from "./arrayBuffer.ts"; -export * from "./typedArray.ts"; +export * from "./array_buffer.ts"; +export * from "./typed_array.ts"; diff --git a/types/array/typedArray.ts b/types/array/typed_array.ts similarity index 100% rename from types/array/typedArray.ts rename to types/array/typed_array.ts diff --git a/types/leb128/_i64leb128.ts b/types/leb128/_i64leb128.ts new file mode 100644 index 0000000..4d717f5 --- /dev/null +++ b/types/leb128/_i64leb128.ts @@ -0,0 +1,27 @@ +// Copyright 2023 the Blocktopus authors. All rights reserved. MIT license. +// Copyright 2023 the denosaurs team. All rights reserved. MIT license. + +const bytes = Uint8Array.from( + atob( + "AGFzbQEAAAABDAJgAX8Cfn9gAX4BfwMDAgABBQMBAAEHGQMGbWVtb3J5AgAEcmVhZAAABXdyaXRlAAEKhAECQwEDfgJAA0AgADEAACIDQv8AgyAChiABhCEBIAJCB3wiAiADQoABg1ANASAAQQFqIQBCxgBUDQALAAsgASACQgeApws+AQF/AkADQCAAQoB/g1ANASABIABC/wCDQoABhDwAACAAQgeIIQAgAUEBaiEBDAALCyABIAA8AABBASABags=", + ), + (c) => c.charCodeAt(0), +); +const { instance } = await WebAssembly.instantiate(bytes); + +const exports = instance.exports as { + memory: WebAssembly.Memory; + read: (pointer: number) => [bigint, number]; + write: (value: bigint) => number; +}; + +const memory = new Uint8Array(exports.memory.buffer); + +export function read(buffer: Uint8Array): [bigint, number] { + memory.set(buffer, 0); + return exports.read(0); +} + +export function write(value: bigint): Uint8Array { + return memory.subarray(0, exports.write(value)); +} diff --git a/types/leb128/_i64leb128.wat b/types/leb128/_i64leb128.wat new file mode 100644 index 0000000..6034300 --- /dev/null +++ b/types/leb128/_i64leb128.wat @@ -0,0 +1,109 @@ +;; Copyright 2023 the Blocktopus authors. All rights reserved. MIT license. +;; Copyright 2023 the denosaurs team. All rights reserved. MIT license. +;; This file is here as a reference as to what the embedded wasm binary is. +(module + (memory (export "memory") 1) + (func $read + (export "read") + (param $ptr i32) + (result i64 i32) + (local $v i64) + (local $length i64) + (local $temp i64) + + (block $B0 + (loop $L0 + ;; CurrentByte + local.get $ptr + i64.load8_u + local.tee $temp + i64.const 127 + i64.and + + ;; << 7 * length + local.get $length + i64.shl + ;; value |= i64.shl + local.get $v + i64.or + local.set $v + + ;; length++; + local.get $length + i64.const 7 + i64.add + local.tee $length + + ;; CurrentByte + local.get $temp + i64.const 128 + i64.and + i64.eqz + br_if $B0 + + ;; Move to next iteration + local.get $ptr + i32.const 1 + i32.add + local.set $ptr + + ;; Branch if not over 70 + i64.const 70 + i64.lt_u + br_if $L0 + ) + unreachable + ) + + local.get $v + local.get $length + i64.const 7 + i64.div_u + i32.wrap_i64 + ) + + (func $write + (export "write") + (param $value i64) + (result i32) + (local $length i32) + + (block $B0 + (loop $L0 + local.get $value + i64.const -128 + i64.and + i64.eqz + br_if $B0 + + local.get $length + + local.get $value + i64.const 127 + i64.and + i64.const 128 + i64.or + i64.store8 + + local.get $value + i64.const 7 + i64.shr_u + local.set $value + + local.get $length + i32.const 1 + i32.add + local.set $length + + br $L0 + ) + ) + local.get $length + local.get $value + i64.store8 + + i32.const 1 + local.get $length + i32.add + ) +) diff --git a/types/varint/leb128.ts b/types/leb128/i32leb128.ts similarity index 95% rename from types/varint/leb128.ts rename to types/leb128/i32leb128.ts index 3a93921..b7d6ecd 100644 --- a/types/varint/leb128.ts +++ b/types/leb128/i32leb128.ts @@ -1,4 +1,4 @@ -import { Type } from "../types.ts"; +import type { Type } from "../types.ts"; const SEGMENT_BITS = 0x7F; const CONTINUE_BIT = 0x80; diff --git a/types/leb128/i32leb128_test.ts b/types/leb128/i32leb128_test.ts new file mode 100644 index 0000000..a9a00f0 --- /dev/null +++ b/types/leb128/i32leb128_test.ts @@ -0,0 +1,80 @@ +import { assertEquals, assertThrows } from "std/testing/asserts.ts"; +import { i32leb128 } from "./mod.ts"; + +Deno.test("i32leb128", async ({ step }) => { + await step("read", async ({ step }) => { + await step("positive", () => { + let data = Uint8Array.of(127); + let result = i32leb128.read(new DataView(data.buffer)); + assertEquals(result, 127); + + data = Uint8Array.of(128, 1); + result = i32leb128.read(new DataView(data.buffer)); + assertEquals(result, 128); + + data = Uint8Array.of(221, 199, 1); + result = i32leb128.read(new DataView(data.buffer)); + assertEquals(result, 25565); + + data = Uint8Array.of(255, 255, 255, 255, 7); + result = i32leb128.read(new DataView(data.buffer)); + assertEquals(result, 2147483647); + }); + + await step("negative", () => { + let data = Uint8Array.of(255, 255, 255, 255, 15); + let result = i32leb128.read(new DataView(data.buffer)); + assertEquals(result, -1); + + data = Uint8Array.of(128, 128, 128, 128, 8); + result = i32leb128.read(new DataView(data.buffer)); + assertEquals(result, -2147483648); + }); + + await step("bad", () => { + const data = Uint8Array.of(255, 255, 255, 255, 255, 15); + assertThrows(() => i32leb128.read(new DataView(data.buffer))); + }); + + await step("i32 max", () => { + const data = Uint8Array.of(255, 255, 255, 255, 7); + assertEquals(i32leb128.read(new DataView(data.buffer)), 2147483647); + }); + }); + + await step("write", async ({ step }) => { + await step("positive", () => { + let data = new Uint8Array(1); + i32leb128.write(127, new DataView(data.buffer)); + assertEquals(data, Uint8Array.of(127)); + + data = new Uint8Array(2); + i32leb128.write(128, new DataView(data.buffer)); + assertEquals(data, Uint8Array.of(128, 1)); + + data = new Uint8Array(3); + i32leb128.write(25565, new DataView(data.buffer)); + assertEquals(data, Uint8Array.of(221, 199, 1)); + + data = new Uint8Array(5); + i32leb128.write(2147483647, new DataView(data.buffer)); + assertEquals(data, Uint8Array.of(255, 255, 255, 255, 7)); + }); + + await step("negative", () => { + let data = new Uint8Array(5); + i32leb128.write(-1, new DataView(data.buffer)); + assertEquals(data, Uint8Array.of(255, 255, 255, 255, 15)); + + data = new Uint8Array(5); + i32leb128.write(-2147483648, new DataView(data.buffer)); + assertEquals(data, Uint8Array.of(128, 128, 128, 128, 8)); + }); + + await step("i32 max", () => { + const data = new Uint8Array(5); + i32leb128.write(2147483647, new DataView(data.buffer)); + assertEquals(data, Uint8Array.of(255, 255, 255, 255, 7)); + }); + }); +}); diff --git a/types/leb128/i64leb128.ts b/types/leb128/i64leb128.ts new file mode 100644 index 0000000..9d30aaf --- /dev/null +++ b/types/leb128/i64leb128.ts @@ -0,0 +1,27 @@ +import type { Type } from "../types.ts"; +import { read, write } from "./_i64leb128.ts"; + +export class I64LEB128 implements Type { + read(dataView: DataView, byteOffset = 0): bigint { + try { + const [value, _bytesRead] = read( + new Uint8Array(dataView.buffer, dataView.byteOffset + byteOffset), + ); + return value; + } catch { + throw new RangeError("I64LEB128 is too large"); + } + } + + write(value: bigint, dataView: DataView, byteOffset = 0): void { + const view = write(value); + const writeView = new Uint8Array( + dataView.buffer, + dataView.byteOffset + byteOffset, + dataView.byteLength, + ); + writeView.set(view, 0); + } +} + +export const i64leb128 = new I64LEB128(); diff --git a/types/leb128/i64leb128_test.ts b/types/leb128/i64leb128_test.ts new file mode 100644 index 0000000..b6ae427 --- /dev/null +++ b/types/leb128/i64leb128_test.ts @@ -0,0 +1,151 @@ +import { assertEquals, assertThrows } from "std/testing/asserts.ts"; +import { i64leb128 } from "./i64leb128.ts"; + +Deno.test("i64leb128", async ({ step }) => { + await step("read", async ({ step }) => { + await step("positive", () => { + assertEquals( + i64leb128.read(new DataView(Uint8Array.of(0x01).buffer)), + 1n, + ); + assertEquals( + i64leb128.read(new DataView(Uint8Array.of(0xff, 0x01).buffer)), + 255n, + ); + assertEquals( + i64leb128.read( + new DataView(Uint8Array.of(0xff, 0xff, 0xff, 0xff, 0x07).buffer), + ), + 2147483647n, + ); + assertEquals( + i64leb128.read( + new DataView( + Uint8Array.of( + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0x7f, + ).buffer, + ), + ), + 9223372036854775807n, + ); + }); + + await step("negative", () => { + assertEquals( + i64leb128.read( + new DataView( + Uint8Array.of( + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0x01, + ).buffer, + ), + ), + -1n, + ); + assertEquals( + i64leb128.read( + new DataView( + Uint8Array.of( + 0x80, + 0x80, + 0x80, + 0x80, + 0xf8, + 0xff, + 0xff, + 0xff, + 0xff, + 0x01, + ).buffer, + ), + ), + -2147483648n, + ); + assertEquals( + i64leb128.read( + new DataView( + Uint8Array.of( + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x01, + ).buffer, + ), + ), + -9223372036854775808n, + ); + }); + + await step("bad", () => { + assertThrows(() => + i64leb128.read( + new DataView( + Uint8Array.of( + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x80, + 0x01, + ).buffer, + ), + ) + ); + }); + }); + + await step("write", async ({ step }) => { + await step("positive", () => { + const buff = new Uint8Array(1); + i64leb128.write(10n, new DataView(buff.buffer)); + assertEquals(buff, Uint8Array.of(10)); + }); + + await step("negative", () => { + const buff = new Uint8Array(10); + i64leb128.write(-1n, new DataView(buff.buffer)); + assertEquals( + buff, + Uint8Array.of( + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0x01, + ), + ); + }); + }); +}); diff --git a/types/leb128/mod.ts b/types/leb128/mod.ts new file mode 100644 index 0000000..357d1d4 --- /dev/null +++ b/types/leb128/mod.ts @@ -0,0 +1,2 @@ +export * from "./i32leb128.ts"; +export * from "./i64leb128.ts"; diff --git a/types/mod.ts b/types/mod.ts index d3bd892..f28d055 100644 --- a/types/mod.ts +++ b/types/mod.ts @@ -1,5 +1,6 @@ export * from "./array/mod.ts"; export * from "./bitflags/mod.ts"; +export * from "./leb128/mod.ts"; export * from "./misc/mod.ts"; export * from "./primitive/mod.ts"; export * from "./string/mod.ts"; diff --git a/types/string/null_terminated_test.ts b/types/string/null_terminated_test.ts index 88024df..098866c 100644 --- a/types/string/null_terminated_test.ts +++ b/types/string/null_terminated_test.ts @@ -1,7 +1,4 @@ -import { - assertEquals, - assertThrows, -} from "https://deno.land/std@0.178.0/testing/asserts.ts"; +import { assertEquals, assertThrows } from "std/testing/asserts.ts"; import { nullTerminatedString } from "./null_terminated.ts"; const encoder = new TextEncoder(); diff --git a/types/varint/leb128_test.ts b/types/varint/leb128_test.ts deleted file mode 100644 index 35b455a..0000000 --- a/types/varint/leb128_test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { I32LEB128 } from "./mod.ts"; -import { - assertEquals, - assertThrows, -} from "https://deno.land/std@0.183.0/testing/asserts.ts"; - -Deno.test({ - name: "Read Positive varint", - fn: () => { - let data = Uint8Array.of(127); - let result = new I32LEB128().read(new DataView(data.buffer)); - assertEquals(result, 127); - - data = Uint8Array.of(128, 1); - result = new I32LEB128().read(new DataView(data.buffer)); - assertEquals(result, 128); - - data = Uint8Array.of(221, 199, 1); - result = new I32LEB128().read(new DataView(data.buffer)); - assertEquals(result, 25565); - - data = Uint8Array.of(255, 255, 255, 255, 7); - result = new I32LEB128().read(new DataView(data.buffer)); - assertEquals(result, 2147483647); - }, -}); - -Deno.test({ - name: "Read Negative varint", - fn: () => { - let data = Uint8Array.of(255, 255, 255, 255, 15); - let result = new I32LEB128().read(new DataView(data.buffer)); - assertEquals(result, -1); - - data = Uint8Array.of(128, 128, 128, 128, 8); - result = new I32LEB128().read(new DataView(data.buffer)); - assertEquals(result, -2147483648); - }, -}); - -Deno.test({ - name: "Read Bad varint", - fn: () => { - const data = Uint8Array.of(255, 255, 255, 255, 255, 15); - assertThrows(() => new I32LEB128().read(new DataView(data.buffer))); - }, -}); - -Deno.test({ - name: "Write Positive varint", - fn: () => { - let data = new Uint8Array(1); - new I32LEB128().write(127, new DataView(data.buffer)); - assertEquals(data, Uint8Array.of(127)); - - data = new Uint8Array(2); - new I32LEB128().write(128, new DataView(data.buffer)); - assertEquals(data, Uint8Array.of(128, 1)); - - data = new Uint8Array(3); - new I32LEB128().write(25565, new DataView(data.buffer)); - assertEquals(data, Uint8Array.of(221, 199, 1)); - - data = new Uint8Array(5); - new I32LEB128().write(2147483647, new DataView(data.buffer)); - assertEquals(data, Uint8Array.of(255, 255, 255, 255, 7)); - }, -}); - -Deno.test({ - name: "Write Negative varint", - fn: () => { - let data = new Uint8Array(5); - new I32LEB128().write(-1, new DataView(data.buffer)); - assertEquals(data, Uint8Array.of(255, 255, 255, 255, 15)); - - data = new Uint8Array(5); - new I32LEB128().write(-2147483648, new DataView(data.buffer)); - assertEquals(data, Uint8Array.of(128, 128, 128, 128, 8)); - }, -}); - -Deno.test({ - name: "Write & read i32 MAX", - fn: () => { - const value = 2_147_483_647; - const decoder = new I32LEB128(); - const bytes = new Uint8Array(5); - const dt = new DataView(bytes.buffer); - decoder.write(value, dt, 0); - const decodedValue = decoder.read(dt); - assertEquals(decodedValue, value); - }, -}); diff --git a/types/varint/mod.ts b/types/varint/mod.ts deleted file mode 100644 index 53fea10..0000000 --- a/types/varint/mod.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./leb128.ts";