From 39c84df6f866cf49ae90f1e209e18488909c7101 Mon Sep 17 00:00:00 2001 From: Tom Lauwaerts Date: Fri, 29 May 2026 10:24:17 +0200 Subject: [PATCH 1/3] Unsigned to signed conversion --- package.json | 1 + src/index.ts | 1 - src/messaging/Parsers.ts | 14 +++++++++++++- src/sourcemap/Wasm.ts | 12 ++++++++++++ tests/unit/parsing.test.ts | 18 ++++++++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/unit/parsing.test.ts diff --git a/package.json b/package.json index efee6e8..ca598c2 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "files": [ "out/tests/unit/describers.test.js", "out/tests/unit/messaging.test.js", + "out/tests/unit/parsing.test.js", "out/tests/unit/sourcemap.test.js", "out/tests/unit/util.test.js" ], diff --git a/src/index.ts b/src/index.ts index 49e7049..a250307 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ export * from './util/env'; export * from './framework/scenario/Actions'; export * from './framework/Testee'; export * from './framework/Framework'; -export * from './messaging/Parsers'; export * from './messaging/Message'; export * from './framework/Scheduler'; export * from './sourcemap/Wasm'; diff --git a/src/messaging/Parsers.ts b/src/messaging/Parsers.ts index ed30b02..2050cac 100644 --- a/src/messaging/Parsers.ts +++ b/src/messaging/Parsers.ts @@ -58,15 +58,27 @@ export function breakpointHitParser(text: string): Breakpoint { throw new Error('Could not messaging BREAKPOINT address in ack.'); } +export function signed(value: number, bits = 32) { + let x = BigInt(value); + const sign = 1n << BigInt(bits - 1); + const mod = 1n << BigInt(bits); + return x >= sign ? x - mod : x; + +} + function stacking(objects: {value: any, type: any}[]): WASM.Value[] { const stacked: WASM.Value[] = []; for (const object of objects) { const type: WASM.Type = WASM.typing.get(object.type.toLowerCase()) ?? WASM.Type.unknown; let buff; switch (type) { + case WASM.Type.u32: + case WASM.Type.u64: + stacked.push({value: Number(object.value), type: type}); + break; case WASM.Type.i32: case WASM.Type.i64: - stacked.push({value: Number(object.value), type: type}); + stacked.push({value: Number(signed(object.value)), type: type}); break; case WASM.Type.f32: buff = Buffer.from(Number(object.value.toString(16)).toString(16), 'hex'); diff --git a/src/sourcemap/Wasm.ts b/src/sourcemap/Wasm.ts index 54e185e..a4cd772 100644 --- a/src/sourcemap/Wasm.ts +++ b/src/sourcemap/Wasm.ts @@ -2,7 +2,9 @@ export namespace WASM { export enum Type { f32, f64, + u32, i32, + u64, i64, nothing, unknown @@ -11,7 +13,9 @@ export namespace WASM { export const typing = new Map([ ['f32', Type.f32], ['f64', Type.f64], + ['u32', Type.u32], ['i32', Type.i32], + ['u64', Type.u64], ['i64', Type.i64] ]); @@ -26,6 +30,10 @@ export namespace WASM { type: Type.nothing, value: 0 } + export function u32(n: number): WASM.Value { + return {value: n, type: Type.u32}; + } + export function i32(n: number): WASM.Value { return {value: n, type: Type.i32}; } @@ -38,6 +46,10 @@ export namespace WASM { return {value: n, type: Type.f64}; } + export function u64(n: number): WASM.Value { + return {value: n, type: Type.u64}; + } + export function i64(n: number): WASM.Value { return {value: n, type: Type.i64}; } diff --git a/tests/unit/parsing.test.ts b/tests/unit/parsing.test.ts new file mode 100644 index 0000000..7cd6a66 --- /dev/null +++ b/tests/unit/parsing.test.ts @@ -0,0 +1,18 @@ +import test from 'ava'; +import {signed} from "../../src/messaging/Parsers"; + +/** + * Check unsigned 32-bit integer to signed conversion + */ +test('[parser] : 32-bit unsigned to signed', t => { + t.is(signed(0, 32), 0n); + t.is(signed(4294967295, 32), -1n); +}); + +/** + * Check unsigned 64-bit integer to signed conversion + */ +test('[parser] : 64-bit unsigned to signed', t => { + t.is(signed(0, 64), 0n); + t.is(signed(4294967295, 64), 4294967295n); +}); \ No newline at end of file From b0dc27ef424f474e62f1c9cfef1109b48a34c120 Mon Sep 17 00:00:00 2001 From: Tom Lauwaerts Date: Sat, 30 May 2026 15:06:05 +0200 Subject: [PATCH 2/3] Fix parser --- package-lock.json | 19 +++++++++-- package.json | 3 +- src/debug/WARDuino.ts | 4 +-- src/framework/Verifier.ts | 5 +-- src/framework/scenario/Invoker.ts | 8 ++--- src/messaging/Message.ts | 12 +++---- src/messaging/Parsers.ts | 23 ++++++++------ src/sourcemap/Wasm.ts | 21 +++++++------ tests/unit/parsing.test.ts | 52 ++++++++++++++++++++++++++----- tests/unit/sourcemap.test.ts | 14 ++++----- 10 files changed, 110 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index a667930..4cb989f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "latch", - "version": "0.4.3", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "latch", - "version": "0.4.3", + "version": "0.5.1", "dependencies": { "@thi.ng/leb128": "^3.1.75", "ansi-colors": "^4.1.3", "ieee754": "^1.2.1", + "json-with-bigint": "^3.5.8", "ora": "^8.0.1", "source-map": "^0.7.4", "ts-node": "^10.5.0", @@ -43,6 +44,7 @@ "integrity": "sha512-+8oDYc4J5cCaWZh1VUbyc+cegGplJO9FqHpqR4LVAVx8fRLVRaYlC4yyA6cqHJ1vWP23Ff/ECS5U68Zz6OLZlg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "escape-string-regexp": "^5.0.0", "execa": "^9.6.0" @@ -82,6 +84,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1184,6 +1187,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1241,6 +1245,7 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -1469,6 +1474,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1795,6 +1801,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -2485,6 +2492,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3564,6 +3572,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-with-bigint": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5131,6 +5145,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index ca598c2..e82b683 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "bin" ], "scripts": { - "clean": "rm -rf dist", + "clean": "rm -rf dist out", "build": "tsc --project tsconfig.build.json", "build:tests": "tsc --outDir out --project tsconfig.tests.json", "watch": "tsc -watch -p ./", @@ -29,6 +29,7 @@ "@thi.ng/leb128": "^3.1.75", "ansi-colors": "^4.1.3", "ieee754": "^1.2.1", + "json-with-bigint": "^3.5.8", "ora": "^8.0.1", "source-map": "^0.7.4", "ts-node": "^10.5.0", diff --git a/src/debug/WARDuino.ts b/src/debug/WARDuino.ts index f5fe5ea..1984683 100644 --- a/src/debug/WARDuino.ts +++ b/src/debug/WARDuino.ts @@ -77,9 +77,9 @@ export namespace WARDuino { pc_error?: number; exception_msg?: string; breakpoints?: number[]; - stack?: Value[]; + stack?: Value[]; callstack?: Frame[]; - globals?: Value[]; + globals?: Value[]; table?: Table; memory?: Memory; br_table?: BRTable; diff --git a/src/framework/Verifier.ts b/src/framework/Verifier.ts index bb20835..e20a031 100644 --- a/src/framework/Verifier.ts +++ b/src/framework/Verifier.ts @@ -3,6 +3,7 @@ import {StepOutcome} from '../reporter/Results'; import {getValue} from './Testee'; import {Outcome} from '../reporter/describers/Describer'; import {bold} from 'ansi-colors'; +import {JSONStringify} from "json-with-bigint"; // decorator for Step class export class Verifier { @@ -32,7 +33,7 @@ export class Verifier { result = this.expectBehaviour(value, getValue(previous, field), entry.value); } } catch { - return this.error(`Failure: ${JSON.stringify(actual)} state does not contain '${field}'.`); + return this.error(`Failure: ${JSONStringify(actual)} state does not contain '${field}'.`); } if (result.outcome !== Outcome.succeeded) { @@ -124,5 +125,5 @@ export class Verifier { /* eslint @typescript-eslint/no-explicit-any: off */ function deepEqual(a: any, b: any): boolean { - return a === b || (isNaN(a) && isNaN(b)); + return a === b || (isNaN(Number(a)) && isNaN(Number(b))); } diff --git a/src/framework/scenario/Invoker.ts b/src/framework/scenario/Invoker.ts index 27fb62b..14964ba 100644 --- a/src/framework/scenario/Invoker.ts +++ b/src/framework/scenario/Invoker.ts @@ -12,7 +12,7 @@ export class Invoker implements Step { readonly expected?: Expectation[]; readonly target?: Target; - constructor(func: string, args: Value[], result: Value | undefined, target?: Target) { + constructor(func: string, args: Value[], result: Value | undefined, target?: Target) { let prefix = ''; this.instruction = invoke(func, args); this.expected = (result == undefined) ? returns(nothing) : returns(result); @@ -24,13 +24,13 @@ export class Invoker implements Step { } } -export function invoke(func: string, args: Value[]): Instruction { +export function invoke(func: string, args: Value[]): Instruction { return {kind: Kind.Request, value: Message.invoke(func, args)}; } -export function returns(n: Value): Expectation[] { +export function returns(n: Value): Expectation[] { if (n.type == Type.nothing) { return [{'value': {kind: 'primitive', value: undefined} as Expected}] } - return [{'value': {kind: 'primitive', value: n.value} as Expected}] + return [{'value': {kind: 'primitive', value: n.value} as Expected}] } diff --git a/src/messaging/Message.ts b/src/messaging/Message.ts index 61dc29e..e156078 100644 --- a/src/messaging/Message.ts +++ b/src/messaging/Message.ts @@ -136,7 +136,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(w.length); + const sizeHex: string = WASM.leb128(BigInt(w.length)); const sizeBuffer = Buffer.allocUnsafe(4); sizeBuffer.writeUint32BE(w.length); const wasmHex = Buffer.from(w).toString('hex'); @@ -162,7 +162,7 @@ export namespace Message { } } - export function invoke(func: string, args: Value[]): Request { + export function invoke(func: string, args: Value[]): Request | Exception> { function fidx(map: SourceMap.Mapping, func: string): number { const fidx: number | void = map.functions.find((closure: SourceMap.Closure) => closure.name === func)?.index; if (fidx === undefined) { @@ -171,14 +171,14 @@ export namespace Message { return fidx!; } - function convert(args: Value[]) { + function convert(args: Value[]) { let payload: string = ''; - args.forEach((arg: Value) => { + args.forEach((arg: Value) => { if (arg.type === Type.i32 || arg.type === Type.i64) { payload += WASM.leb128(arg.value); } else { const buff = Buffer.alloc(arg.type === Type.f32 ? 4 : 8); - write(buff, arg.value, 0, true, arg.type === Type.f32 ? 23 : 52, buff.length); + write(buff, Number(arg.value), 0, true, arg.type === Type.f32 ? 23 : 52, buff.length); // todo fix precision loss payload += buff.toString('hex'); } }); @@ -187,7 +187,7 @@ export namespace Message { return { type: Interrupt.invoke, - payload: (map: SourceMap.Mapping) => `${WASM.leb128(fidx(map, func))}${convert(args)}`, + payload: (map: SourceMap.Mapping) => `${WASM.leb128(BigInt(fidx(map, func)))}${convert(args)}`, parser: invokeParser } } diff --git a/src/messaging/Parsers.ts b/src/messaging/Parsers.ts index 2050cac..61c9ecd 100644 --- a/src/messaging/Parsers.ts +++ b/src/messaging/Parsers.ts @@ -3,6 +3,7 @@ import * as ieee754 from 'ieee754'; import {Ack, Exception} from './Message'; import {Breakpoint} from '../debug/Breakpoint'; import {WARDuino} from '../debug/WARDuino'; +import {JSONParse} from 'json-with-bigint'; import State = WARDuino.State; import nothing = WASM.nothing; @@ -11,10 +12,10 @@ export function identityParser(text: string) { } export function stateParser(text: string): State { - return JSON.parse(text); + return JSONParse(text); } -export function invokeParser(text: string): WASM.Value | Exception { +export function invokeParser(text: string): WASM.Value | Exception { if (exception(text)) { return {text: text}; } @@ -58,7 +59,7 @@ export function breakpointHitParser(text: string): Breakpoint { throw new Error('Could not messaging BREAKPOINT address in ack.'); } -export function signed(value: number, bits = 32) { +export function signed(value: bigint, bits = 32) { let x = BigInt(value); const sign = 1n << BigInt(bits - 1); const mod = 1n << BigInt(bits); @@ -66,27 +67,29 @@ export function signed(value: number, bits = 32) { } -function stacking(objects: {value: any, type: any}[]): WASM.Value[] { - const stacked: WASM.Value[] = []; +function stacking(objects: {value: bigint, type: any}[]): WASM.Value[] { + const stacked: WASM.Value[] = []; for (const object of objects) { const type: WASM.Type = WASM.typing.get(object.type.toLowerCase()) ?? WASM.Type.unknown; let buff; switch (type) { case WASM.Type.u32: case WASM.Type.u64: - stacked.push({value: Number(object.value), type: type}); + stacked.push({value: object.value, type: type}); break; case WASM.Type.i32: + stacked.push({value: signed(object.value, 32), type: type}); + break; case WASM.Type.i64: - stacked.push({value: Number(signed(object.value)), type: type}); + stacked.push({value: signed(object.value, 64), type: type}); break; case WASM.Type.f32: buff = Buffer.from(Number(object.value.toString(16)).toString(16), 'hex'); - stacked.push({value: ieee754.read(buff, 0, false, 23, buff.length), type: type}); + stacked.push({value: Number(ieee754.read(buff, 0, false, 23, buff.length)), type: type}); break; case WASM.Type.f64: - buff = Buffer.from(BigInt(object.value.toString(16)).toString(16), 'hex'); - stacked.push({value: ieee754.read(buff, 0, false, 52, buff.length), type: type}); + buff = Buffer.from(Number(object.value.toString(16)).toString(16), 'hex'); + stacked.push({value: Number(ieee754.read(buff, 0, false, 52, buff.length)), type: type}); // todo fix precision loss break; case WASM.Type.unknown: break; diff --git a/src/sourcemap/Wasm.ts b/src/sourcemap/Wasm.ts index a4cd772..a2d31af 100644 --- a/src/sourcemap/Wasm.ts +++ b/src/sourcemap/Wasm.ts @@ -19,38 +19,38 @@ export namespace WASM { ['i64', Type.i64] ]); - export interface Value { + export interface Value { type: Type; - value: number; + value: T; } - export interface Nothing extends Value {} + export interface Nothing extends Value {} export const nothing: Nothing = { type: Type.nothing, value: 0 } - export function u32(n: number): WASM.Value { + export function u32(n: bigint): WASM.Value { return {value: n, type: Type.u32}; } - export function i32(n: number): WASM.Value { + export function i32(n: bigint): WASM.Value { return {value: n, type: Type.i32}; } - export function f32(n: number): WASM.Value { + export function f32(n: number): WASM.Value { return {value: n, type: Type.f32}; } - export function f64(n: number): WASM.Value { + export function f64(n: number): WASM.Value { return {value: n, type: Type.f64}; } - export function u64(n: number): WASM.Value { + export function u64(n: bigint): WASM.Value { return {value: n, type: Type.u64}; } - export function i64(n: number): WASM.Value { + export function i64(n: bigint): WASM.Value { return {value: n, type: Type.i64}; } @@ -77,7 +77,8 @@ export namespace WASM { bytes: Uint8Array; } - export function leb128(a: number): string { // TODO can only handle 32 bit + export function leb128(value: bigint | number): string { // TODO can only handle 32 bit + let a = Number(value); a |= 0; const result = []; while (true) { diff --git a/tests/unit/parsing.test.ts b/tests/unit/parsing.test.ts index 7cd6a66..73b3efa 100644 --- a/tests/unit/parsing.test.ts +++ b/tests/unit/parsing.test.ts @@ -1,18 +1,56 @@ import test from 'ava'; -import {signed} from "../../src/messaging/Parsers"; +import {signed, stateParser} from "../../src/messaging/Parsers"; +import {WARDuino} from "../../src"; +import State = WARDuino.State; /** * Check unsigned 32-bit integer to signed conversion */ -test('[parser] : 32-bit unsigned to signed', t => { - t.is(signed(0, 32), 0n); - t.is(signed(4294967295, 32), -1n); +test('[signed] : 32-bit unsigned to signed', t => { + t.is(signed(0n, 32), 0n); + t.is(signed(1n, 32), 1n); + t.is(signed(127n, 32), 127n); + t.is(signed(128n, 32), 128n); + t.is(signed(2147483647n, 32), 2147483647n); + t.is(signed(2147483648n, 32), -2147483648n); + t.is(signed(4294967294n, 32), -2n); + t.is(signed(4294967295n, 32), -1n); }); /** * Check unsigned 64-bit integer to signed conversion */ -test('[parser] : 64-bit unsigned to signed', t => { - t.is(signed(0, 64), 0n); - t.is(signed(4294967295, 64), 4294967295n); +test('[signed] : 64-bit unsigned to signed', t => { + t.is(signed(0n, 64), 0n); + t.is(signed(1n, 64), 1n); + t.is(signed(127n, 64), 127n); + t.is(signed(128n, 64), 128n); + t.is(signed(2147483648n, 64), 2147483648n); + t.is(signed(4294967295n, 64), 4294967295n); + t.is(signed(18446744073709551615n, 64), -1n); + t.is(signed(18446744073709551489n, 64), -127n); +}); + +/** + * Check for precision loss in state parser + */ +const equality = (a: bigint | number | undefined, b: bigint) => + // false if a is undefined or a float + (typeof a === 'bigint' && a === b) || // both bigint + (a !== undefined && Number.isInteger(a) && BigInt(a) === b); // compare integer number with bigint + +test('[state] : 32-bit integer precision', t => { + const values: bigint[] = [1n, 127n, 2147483648n, 4294967294n]; + for (const value of values) { + const state: State = stateParser(`{\"stack\": [{\"idx\":0,\"type\":\"i32\",\"value\":${value}}]}\n`); + t.true(equality(state.stack?.[0].value, value)); + } +}); + +test('[state parser] : 64-bit integer precision', t => { + const values: bigint[] = [1n, 127n, 2147483648n, 4294967294n, 18446744073709551615n, 18446744073709551489n]; + for (const value of values) { + const state: State = stateParser(`{\"stack\": [{\"idx\":0,\"type\":\"i32\",\"value\":${value}}]}\n`); + t.true(equality(state.stack?.[0].value, value)); + } }); \ No newline at end of file diff --git a/tests/unit/sourcemap.test.ts b/tests/unit/sourcemap.test.ts index 6e21d96..3f31b04 100644 --- a/tests/unit/sourcemap.test.ts +++ b/tests/unit/sourcemap.test.ts @@ -11,13 +11,13 @@ const artifacts = `${__dirname}/../../../tests/artifacts`; * Check LEB 128 encoding */ test('[leb128] : test encoding', t => { - t.is(WASM.leb128(0), '00'); - t.is(WASM.leb128(1), '01'); - t.is(WASM.leb128(8), '08'); - t.is(WASM.leb128(32), '20'); - t.is(WASM.leb128(64), 'C000'); - t.is(WASM.leb128(128), '8001'); - t.is(WASM.leb128(1202), 'B209'); + 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'); }); test('[extractLineInfo] : test against artifacts (1)', async t => { From ba4022219c62191f794bb8d165e0a9c4a8ac6a31 Mon Sep 17 00:00:00 2001 From: Tom Lauwaerts Date: Sat, 30 May 2026 15:51:05 +0200 Subject: [PATCH 3/3] Fix precision loss in reading f64 --- src/messaging/Parsers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/messaging/Parsers.ts b/src/messaging/Parsers.ts index 61c9ecd..d9f6f35 100644 --- a/src/messaging/Parsers.ts +++ b/src/messaging/Parsers.ts @@ -85,11 +85,11 @@ function stacking(objects: {value: bigint, type: any}[]): WASM.Value