From fc2361eaeb7adce8fc40322a6a51d7290e7525a8 Mon Sep 17 00:00:00 2001 From: Tom Lauwaerts Date: Thu, 13 Aug 2026 09:09:48 +0200 Subject: [PATCH 1/4] Add example #45 --- package.json | 1 + tests/integration/precision.ts | 29 +++++++++++++++++++++++++++++ tests/integration/precision.wast | 14 ++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 tests/integration/precision.ts create mode 100644 tests/integration/precision.wast diff --git a/package.json b/package.json index 0dab90d..db93586 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "test": "npm run test:all", "test:all": "npm run test:prebuild && npm run test:ava", "test:ava": "ava", + "test:integration": "npx ts-node ./tests/integration/precision.ts", "test:example": "npx ts-node ./tests/examples/example.ts", "coverage:test:ava": "c8 --src src/ --all ava" }, diff --git a/tests/integration/precision.ts b/tests/integration/precision.ts new file mode 100644 index 0000000..aa0e377 --- /dev/null +++ b/tests/integration/precision.ts @@ -0,0 +1,29 @@ +import { + EmulatorSpecification, + Framework, + Invoker, + Verbosity, + WASM +} from '../../src/index'; + +const framework = Framework.getImplementation(); + +const precision = framework.suite('Test i64 precision'); + +precision.testee('emulator[:8100]', new EmulatorSpecification(8100)); + +const maxI64 = 9223372036854775807n; + +precision.test({ + title: 'Preserve i64 precision outside the JavaScript safe integer range', + program: 'tests/integration/precision.wast', + dependencies: [], + steps: [ + new Invoker('i64_get_max', [], WASM.i64(maxI64)), + new Invoker('i64_eq_max', [WASM.i64(maxI64)], WASM.i32(1n)), + new Invoker('i64_eq_max_wrong', [WASM.i64(maxI64)], WASM.i32(0n)) + ] +}); + +framework.reporter.verbosity(Verbosity.more); +framework.analyse([precision]); diff --git a/tests/integration/precision.wast b/tests/integration/precision.wast new file mode 100644 index 0000000..b49171d --- /dev/null +++ b/tests/integration/precision.wast @@ -0,0 +1,14 @@ +(module + (func (export "i64_get_max") (result i64) + (i64.const 9223372036854775807)) + + (func (export "i64_eq_max") (param i64) (result i32) + (i64.eq + (local.get 0) + (i64.const 9223372036854775807))) + + (func (export "i64_eq_max_wrong") (param i64) (result i32) + (i64.eq + (local.get 0) + (i64.const 9223372036854776000))) +) From 86f9e3584d6a06bd23c7bb8579d7edd0a50865ec Mon Sep 17 00:00:00 2001 From: Tom Lauwaerts Date: Thu, 13 Aug 2026 10:40:01 +0200 Subject: [PATCH 2/4] Add failing spec tests --- tests/integration/precision.ts | 11 ++++---- tests/integration/precision.wast | 47 ++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/tests/integration/precision.ts b/tests/integration/precision.ts index aa0e377..b2946ba 100644 --- a/tests/integration/precision.ts +++ b/tests/integration/precision.ts @@ -12,16 +12,15 @@ const precision = framework.suite('Test i64 precision'); precision.testee('emulator[:8100]', new EmulatorSpecification(8100)); -const maxI64 = 9223372036854775807n; - precision.test({ - title: 'Preserve i64 precision outside the JavaScript safe integer range', + title: 'i64 rem_s returns zero for INT64_MIN modulo -1', program: 'tests/integration/precision.wast', dependencies: [], steps: [ - new Invoker('i64_get_max', [], WASM.i64(maxI64)), - new Invoker('i64_eq_max', [WASM.i64(maxI64)], WASM.i32(1n)), - new Invoker('i64_eq_max_wrong', [WASM.i64(maxI64)], WASM.i32(0n)) + new Invoker('rem_s', [ + WASM.i64(-9223372036854775808n), + WASM.i64(-1n) + ], WASM.i64(0n)) ] }); diff --git a/tests/integration/precision.wast b/tests/integration/precision.wast index b49171d..97da996 100644 --- a/tests/integration/precision.wast +++ b/tests/integration/precision.wast @@ -1,14 +1,37 @@ (module - (func (export "i64_get_max") (result i64) - (i64.const 9223372036854775807)) - - (func (export "i64_eq_max") (param i64) (result i32) - (i64.eq - (local.get 0) - (i64.const 9223372036854775807))) - - (func (export "i64_eq_max_wrong") (param i64) (result i32) - (i64.eq - (local.get 0) - (i64.const 9223372036854776000))) + (func (export "add") (param $x i64) (param $y i64) (result i64) (i64.add (local.get $x) (local.get $y))) + (func (export "sub") (param $x i64) (param $y i64) (result i64) (i64.sub (local.get $x) (local.get $y))) + (func (export "mul") (param $x i64) (param $y i64) (result i64) (i64.mul (local.get $x) (local.get $y))) + (func (export "div_s") (param $x i64) (param $y i64) (result i64) (i64.div_s (local.get $x) (local.get $y))) + (func (export "div_u") (param $x i64) (param $y i64) (result i64) (i64.div_u (local.get $x) (local.get $y))) + (func (export "rem_s") (param $x i64) (param $y i64) (result i64) (i64.rem_s (local.get $x) (local.get $y))) + (func (export "rem_u") (param $x i64) (param $y i64) (result i64) (i64.rem_u (local.get $x) (local.get $y))) + (func (export "and") (param $x i64) (param $y i64) (result i64) (i64.and (local.get $x) (local.get $y))) + (func (export "or") (param $x i64) (param $y i64) (result i64) (i64.or (local.get $x) (local.get $y))) + (func (export "xor") (param $x i64) (param $y i64) (result i64) (i64.xor (local.get $x) (local.get $y))) + (func (export "shl") (param $x i64) (param $y i64) (result i64) (i64.shl (local.get $x) (local.get $y))) + (func (export "shr_s") (param $x i64) (param $y i64) (result i64) (i64.shr_s (local.get $x) (local.get $y))) + (func (export "shr_u") (param $x i64) (param $y i64) (result i64) (i64.shr_u (local.get $x) (local.get $y))) + (func (export "rotl") (param $x i64) (param $y i64) (result i64) (i64.rotl (local.get $x) (local.get $y))) + (func (export "rotr") (param $x i64) (param $y i64) (result i64) (i64.rotr (local.get $x) (local.get $y))) + (func (export "clz") (param $x i64) (result i64) (i64.clz (local.get $x))) + (func (export "ctz") (param $x i64) (result i64) (i64.ctz (local.get $x))) + (func (export "popcnt") (param $x i64) (result i64) (i64.popcnt (local.get $x))) + (func (export "extend8_s") (param $x i64) (result i64) (i64.extend8_s (local.get $x))) + (func (export "extend16_s") (param $x i64) (result i64) (i64.extend16_s (local.get $x))) + (func (export "extend32_s") (param $x i64) (result i64) (i64.extend32_s (local.get $x))) + (func (export "eqz") (param $x i64) (result i32) (i64.eqz (local.get $x))) + (func (export "eq") (param $x i64) (param $y i64) (result i32) (i64.eq (local.get $x) (local.get $y))) + (func (export "ne") (param $x i64) (param $y i64) (result i32) (i64.ne (local.get $x) (local.get $y))) + (func (export "lt_s") (param $x i64) (param $y i64) (result i32) (i64.lt_s (local.get $x) (local.get $y))) + (func (export "lt_u") (param $x i64) (param $y i64) (result i32) (i64.lt_u (local.get $x) (local.get $y))) + (func (export "le_s") (param $x i64) (param $y i64) (result i32) (i64.le_s (local.get $x) (local.get $y))) + (func (export "le_u") (param $x i64) (param $y i64) (result i32) (i64.le_u (local.get $x) (local.get $y))) + (func (export "gt_s") (param $x i64) (param $y i64) (result i32) (i64.gt_s (local.get $x) (local.get $y))) + (func (export "gt_u") (param $x i64) (param $y i64) (result i32) (i64.gt_u (local.get $x) (local.get $y))) + (func (export "ge_s") (param $x i64) (param $y i64) (result i32) (i64.ge_s (local.get $x) (local.get $y))) + (func (export "ge_u") (param $x i64) (param $y i64) (result i32) (i64.ge_u (local.get $x) (local.get $y))) + (func (export "i64_get_max") (result i64) (i64.const 9223372036854775807)) + (func (export "i64_eq_max") (param i64) (result i32) (i64.eq (local.get 0) (i64.const 9223372036854775807))) + (func (export "i64_eq_max_wrong") (param i64) (result i32) (i64.eq (local.get 0) (i64.const 9223372036854776000))) ) From 3f7764a4adac968c47e11ed3b722e298999e9433 Mon Sep 17 00:00:00 2001 From: Tom Lauwaerts Date: Thu, 13 Aug 2026 11:11:15 +0200 Subject: [PATCH 3/4] Fix 64-bit precision --- src/messaging/Message.ts | 4 +++- src/sourcemap/Wasm.ts | 15 +++++++-------- tests/unit/sourcemap.test.ts | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/messaging/Message.ts b/src/messaging/Message.ts index ba7e439..1924204 100644 --- a/src/messaging/Message.ts +++ b/src/messaging/Message.ts @@ -181,8 +181,10 @@ export namespace Message { const buff = Buffer.alloc(arg.type === Float.f32 ? 4 : 8); write(buff, Number(arg.value), 0, true, arg.type === Float.f32 ? 23 : 52, buff.length); // todo fix precision loss payload += buff.toString('hex'); + } else if (WASM.isInteger(arg.type)) { + payload += WASM.leb128((arg.value as WASM.WasmInt).toBigInt()); } else { - payload += WASM.leb128(arg.value); + throw Error(`Cannot invoke a function with a ${arg.type} argument.`); } }); return payload; diff --git a/src/sourcemap/Wasm.ts b/src/sourcemap/Wasm.ts index e3515fb..b71401e 100644 --- a/src/sourcemap/Wasm.ts +++ b/src/sourcemap/Wasm.ts @@ -178,16 +178,15 @@ export namespace WASM { bytes: Uint8Array; } - export function leb128(value: bigint | number): string { // TODO can only handle 32 bit - let a = Number(value); - a |= 0; + export function leb128(value: bigint | number): string { + let a = BigInt(value); const result = []; while (true) { - const byte_ = a & 0x7f; - a >>= 7; + const byte_ = Number(a & 0x7fn); + a >>= 7n; if ( - (a === 0 && (byte_ & 0x40) === 0) || - (a === -1 && (byte_ & 0x40) !== 0) + (a === 0n && (byte_ & 0x40) === 0) || + (a === -1n && (byte_ & 0x40) !== 0) ) { result.push(byte_.toString(16).padStart(2, '0')); return result.join('').toUpperCase(); @@ -196,4 +195,4 @@ export namespace WASM { } } -} \ No newline at end of file +} diff --git a/tests/unit/sourcemap.test.ts b/tests/unit/sourcemap.test.ts index 3f31b04..0eccef7 100644 --- a/tests/unit/sourcemap.test.ts +++ b/tests/unit/sourcemap.test.ts @@ -18,6 +18,7 @@ test('[leb128] : test encoding', t => { t.is(WASM.leb128(64n), 'C000'); t.is(WASM.leb128(128n), '8001'); t.is(WASM.leb128(1202n), 'B209'); + t.is(WASM.leb128(9223372036854775807n), 'FFFFFFFFFFFFFFFFFF00'); }); test('[extractLineInfo] : test against artifacts (1)', async t => { @@ -81,4 +82,4 @@ function initialize(): string { const tmp: string = mkdtempSync('test'); copyFileSync(`${artifacts}/upload.wasm`, `${tmp}/upload.wasm`); return tmp; -} \ No newline at end of file +} From 79fecca4100024a05f4d2a81553a764c263613ee Mon Sep 17 00:00:00 2001 From: Tom Lauwaerts Date: Thu, 13 Aug 2026 11:21:15 +0200 Subject: [PATCH 4/4] Use `@thi.ng/leb128` --- src/messaging/Message.ts | 11 +++++++---- src/sourcemap/Wasm.ts | 17 ----------------- tests/unit/messaging.test.ts | 20 +++++++++++++++++++- tests/unit/sourcemap.test.ts | 15 --------------- 4 files changed, 26 insertions(+), 37 deletions(-) diff --git a/src/messaging/Message.ts b/src/messaging/Message.ts index 1924204..261b605 100644 --- a/src/messaging/Message.ts +++ b/src/messaging/Message.ts @@ -7,6 +7,7 @@ import {SourceMap} from '../sourcemap/SourceMap'; import {readFileSync} from 'fs'; import {CompileOutput, CompilerFactory} from '../manage/Compiler'; import {WABT} from '../util/env'; +import {encodeSLEB128, encodeULEB128} from '@thi.ng/leb128'; import Interrupt = WARDuino.Interrupt; import State = WARDuino.State; import Value = WASM.Value; @@ -139,7 +140,7 @@ export namespace Message { export function updateModule(wasm: string): Request { function payload(binary: Buffer): string { const w = new Uint8Array(binary); - const sizeHex: string = WASM.leb128(BigInt(w.length)); + const sizeHex = Buffer.from(encodeULEB128(w.length)).toString('hex'); const sizeBuffer = Buffer.allocUnsafe(4); sizeBuffer.writeUint32BE(w.length); const wasmHex = Buffer.from(w).toString('hex'); @@ -181,8 +182,10 @@ export namespace Message { const buff = Buffer.alloc(arg.type === Float.f32 ? 4 : 8); write(buff, Number(arg.value), 0, true, arg.type === Float.f32 ? 23 : 52, buff.length); // todo fix precision loss payload += buff.toString('hex'); - } else if (WASM.isInteger(arg.type)) { - payload += WASM.leb128((arg.value as WASM.WasmInt).toBigInt()); + } else if (arg.type === WASM.Integer.i32 || arg.type === WASM.Integer.i64) { + payload += Buffer.from(encodeSLEB128((arg.value as WASM.WasmInt).toBigInt())).toString('hex'); + } else if (arg.type === WASM.Integer.u32 || arg.type === WASM.Integer.u64) { + payload += Buffer.from(encodeULEB128((arg.value as WASM.WasmInt).toBigInt())).toString('hex'); } else { throw Error(`Cannot invoke a function with a ${arg.type} argument.`); } @@ -192,7 +195,7 @@ export namespace Message { return { type: Interrupt.invoke, - payload: (map: SourceMap.Mapping) => `${WASM.leb128(BigInt(fidx(map, func)))}${convert(args)}`, + payload: (map: SourceMap.Mapping) => `${Buffer.from(encodeULEB128(fidx(map, func))).toString('hex')}${convert(args)}`, parser: invokeParser } } diff --git a/src/sourcemap/Wasm.ts b/src/sourcemap/Wasm.ts index b71401e..2b8b076 100644 --- a/src/sourcemap/Wasm.ts +++ b/src/sourcemap/Wasm.ts @@ -178,21 +178,4 @@ export namespace WASM { bytes: Uint8Array; } - export function leb128(value: bigint | number): string { - let a = BigInt(value); - const result = []; - while (true) { - const byte_ = Number(a & 0x7fn); - a >>= 7n; - if ( - (a === 0n && (byte_ & 0x40) === 0) || - (a === -1n && (byte_ & 0x40) !== 0) - ) { - result.push(byte_.toString(16).padStart(2, '0')); - return result.join('').toUpperCase(); - } - result.push((byte_ | 0x80).toString(16).padStart(2, '0')); - } - } - } diff --git a/tests/unit/messaging.test.ts b/tests/unit/messaging.test.ts index 9f64070..6134f39 100644 --- a/tests/unit/messaging.test.ts +++ b/tests/unit/messaging.test.ts @@ -1,8 +1,26 @@ import test from 'ava'; import {MessageQueue} from '../../src/messaging/MessageQueue'; +import {Message} from '../../src/messaging/Message'; +import {WASM} from '../../src/sourcemap/Wasm'; +import {SourceMap} from '../../src/sourcemap/SourceMap'; const alphanumerical = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split(''); +test('[Message.invoke] : encode 64-bit integer arguments without precision loss', t => { + const mapping = new SourceMap.Mapping().init([], [{ + index: 0, + name: 'rem_s', + arguments: [], + locals: [] + }], [], []); + const request = Message.invoke('rem_s', [ + WASM.i64(-9223372036854775808n), + WASM.i64(-1n) + ]); + + t.is(request.payload!(mapping), '008080808080808080807f7f'); +}); + test('[MessageQueue] : test EOM detection', t => { const fuzzer = fuzzy(alphanumerical); const newline = new MessageQueue('\n'); @@ -75,4 +93,4 @@ const fuzzy = (characters: string[]): (n: number) => string => { } return result; }; -} \ No newline at end of file +} diff --git a/tests/unit/sourcemap.test.ts b/tests/unit/sourcemap.test.ts index 0eccef7..e0574f4 100644 --- a/tests/unit/sourcemap.test.ts +++ b/tests/unit/sourcemap.test.ts @@ -1,5 +1,4 @@ import test from 'ava'; -import {WASM} from '../../src/sourcemap/Wasm'; import {WatMapper} from '../../src/sourcemap/SourceMapper'; import {SourceMap} from '../../src/sourcemap/SourceMap'; import {WABT} from '../../src/util/env'; @@ -7,20 +6,6 @@ import {copyFileSync, mkdtempSync, readFileSync, rmSync} from 'fs'; const artifacts = `${__dirname}/../../../tests/artifacts`; -/** - * Check LEB 128 encoding - */ -test('[leb128] : test encoding', t => { - t.is(WASM.leb128(0n), '00'); - t.is(WASM.leb128(1n), '01'); - t.is(WASM.leb128(8n), '08'); - t.is(WASM.leb128(32n), '20'); - t.is(WASM.leb128(64n), 'C000'); - t.is(WASM.leb128(128n), '8001'); - t.is(WASM.leb128(1202n), 'B209'); - t.is(WASM.leb128(9223372036854775807n), 'FFFFFFFFFFFFFFFFFF00'); -}); - test('[extractLineInfo] : test against artifacts (1)', async t => { await check(`${artifacts}/compile.output`, (mapping: SourceMap.Mapping) => { // check line information