From e1a4cd72c45e8e339b08f4b4036d61922c5ebbb0 Mon Sep 17 00:00:00 2001 From: Tom Lauwaerts Date: Thu, 18 Dec 2025 09:09:14 +0100 Subject: [PATCH 1/2] Split Integers and Floats + use BigInts --- src/debug/WARDuino.ts | 5 ++- src/framework/Testee.ts | 2 +- src/framework/scenario/Invoker.ts | 8 ++-- src/messaging/Message.ts | 32 +++++++++------ src/messaging/Parsers.ts | 14 +++---- src/sourcemap/Wasm.ts | 66 +++++++++++++++---------------- tests/examples/example.ts | 4 +- tests/unit/sourcemap.test.ts | 31 +++++++++++---- 8 files changed, 91 insertions(+), 71 deletions(-) diff --git a/src/debug/WARDuino.ts b/src/debug/WARDuino.ts index ac859a8..c4dec9f 100644 --- a/src/debug/WARDuino.ts +++ b/src/debug/WARDuino.ts @@ -5,6 +5,7 @@ export namespace WARDuino { import Frame = WASM.Frame; import Table = WASM.Table; import Memory = WASM.Memory; + import Type = WASM.Type; export interface CallbackMapping { callbackid: string; @@ -63,9 +64,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/Testee.ts b/src/framework/Testee.ts index 2143e42..e9afb2e 100644 --- a/src/framework/Testee.ts +++ b/src/framework/Testee.ts @@ -27,7 +27,7 @@ export function timeout(label: string, time: number, promise: Promise): Pr * @param field dot string describing the field of the value (or path) */ export function getValue(object: any, field: string): any { - if (object?.type == WASM.Type.nothing) { + if (object?.type == WASM.Special.nothing) { return undefined; } diff --git a/src/framework/scenario/Invoker.ts b/src/framework/scenario/Invoker.ts index 27fb62b..b64395f 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,12 +24,12 @@ 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[] { - if (n.type == Type.nothing) { +export function returns(n: Value): Expectation[] { + if (n.type === WASM.Special.nothing) { return [{'value': {kind: 'primitive', value: undefined} as Expected}] } return [{'value': {kind: 'primitive', value: n.value} as Expected}] diff --git a/src/messaging/Message.ts b/src/messaging/Message.ts index 0a9a99d..5a92624 100644 --- a/src/messaging/Message.ts +++ b/src/messaging/Message.ts @@ -2,7 +2,7 @@ import {WARDuino} from '../debug/WARDuino'; import {ackParser, breakpointParser, invokeParser, stateParser} from './Parsers'; import {Breakpoint} from '../debug/Breakpoint'; import {WASM} from '../sourcemap/Wasm'; -import {write} from 'ieee754'; +import ieee754 from 'ieee754'; import {SourceMap} from '../sourcemap/SourceMap'; import {readFileSync} from 'fs'; import {CompileOutput, CompilerFactory} from '../manage/Compiler'; @@ -28,6 +28,7 @@ export interface Request { } export namespace Message { + import Float = WASM.Float; export const run: Request = { type: Interrupt.run, parser: (line: string) => { @@ -135,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'); @@ -161,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) { @@ -170,15 +171,22 @@ export namespace Message { return fidx!; } - function convert(args: Value[]) { + function convert(args: Value[]) { let payload: string = ''; - 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); - payload += buff.toString('hex'); + args.forEach((arg: Value) => { + switch (arg.type) { + case WASM.Float.f32: + case WASM.Float.f64: + const buff = Buffer.alloc(arg.type === Float.f32 ? 4 : 8); + ieee754.write(buff, arg.value, 0, true, arg.type === Float.f32 ? 23 : 52, buff.length); // TODO write BigInt without loss of precision (don't use ieee754.write) + payload += buff.toString('hex'); + break; + case WASM.Integer.i32: + case WASM.Integer.i64: + payload += WASM.leb128(arg.value); + break; + default: + break; } }); return payload; @@ -186,7 +194,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 46b1824..dfbba4f 100644 --- a/src/messaging/Parsers.ts +++ b/src/messaging/Parsers.ts @@ -5,6 +5,7 @@ import {Breakpoint} from '../debug/Breakpoint'; import {WARDuino} from '../debug/WARDuino'; import State = WARDuino.State; import nothing = WASM.nothing; +import Type = WASM.Type; export function identityParser(text: string) { return stripEnd(text); @@ -14,7 +15,7 @@ export function stateParser(text: string): State { return JSON.parse(text); } -export function invokeParser(text: string): WASM.Value | Exception { +export function invokeParser(text: string): WASM.Value | Exception { if (exception(text)) { return {text: text}; } @@ -58,21 +59,20 @@ export function breakpointHitParser(text: string): Breakpoint { throw new Error('Could not messaging BREAKPOINT address in ack.'); } -function stacking(objects: {value: any, type: any}[]): WASM.Value[] { - const stacked: WASM.Value[] = []; +function stacking(objects: {value: any, type: any}[]): WASM.Value[] { + const stacked: WASM.Value[] = []; for (const object of objects) { let value: number = object.value; - const type: WASM.Type = WASM.typing.get(object.type.toLowerCase()) ?? WASM.Type.unknown; - if (type === WASM.Type.f32 || type === WASM.Type.f64) { + const type: WASM.Type = WASM.typing.get(object.type.toLowerCase()) ?? WASM.Special.unknown; + if (type === WASM.Float.f32 || type === WASM.Float.f64) { const buff = Buffer.from(object.value, 'hex'); - value = ieee754.read(buff, 0, false, type === WASM.Type.f32 ? 23 : 52, buff.length); + value = ieee754.read(buff, 0, false, type === WASM.Float.f32 ? 23 : 52, buff.length); } stacked.push({value: value, type: type}); } return stacked; } - // Strips all trailing newlines function stripEnd(text: string): string { return text.replace(/\s+$/g, ''); diff --git a/src/sourcemap/Wasm.ts b/src/sourcemap/Wasm.ts index 54e185e..9f9fc58 100644 --- a/src/sourcemap/Wasm.ts +++ b/src/sourcemap/Wasm.ts @@ -1,45 +1,57 @@ +import * as leb from "@thi.ng/leb128"; + export namespace WASM { - export enum Type { + + export enum Float { f32, f64, + } + + export enum Integer { i32, i64, + } + + export enum Special { nothing, unknown } + export type Type = Float | Integer | Special; + + export const typing = new Map([ - ['f32', Type.f32], - ['f64', Type.f64], - ['i32', Type.i32], - ['i64', Type.i64] + ['f32', Float.f32], + ['f64', Float.f64], + ['i32', Integer.i32], + ['i64', Integer.i64] ]); - export interface Value { - type: Type; - value: number; + export interface Value { + type: T; + value: T extends Float ? number : bigint; } - export interface Nothing extends Value {} + export interface Nothing extends Value {} export const nothing: Nothing = { - type: Type.nothing, value: 0 + type: Special.nothing, value: 0 } - export function i32(n: number): WASM.Value { - return {value: n, type: Type.i32}; + export function i32(n: bigint): WASM.Value { + return {value: n, type: Integer.i32}; } - export function f32(n: number): WASM.Value { - return {value: n, type: Type.f32}; + export function f32(n: number): WASM.Value { + return {value: n, type: Float.f32}; } - export function f64(n: number): WASM.Value { - return {value: n, type: Type.f64}; + export function f64(n: number): WASM.Value { + return {value: n, type: Float.f64}; } - export function i64(n: number): WASM.Value { - return {value: n, type: Type.i64}; + export function i64(n: bigint): WASM.Value { + return {value: n, type: Integer.i64}; } export interface Frame { @@ -65,21 +77,5 @@ export namespace WASM { bytes: Uint8Array; } - export function leb128(a: number): string { // TODO can only handle 32 bit - a |= 0; - const result = []; - while (true) { - const byte_ = a & 0x7f; - a >>= 7; - if ( - (a === 0 && (byte_ & 0x40) === 0) || - (a === -1 && (byte_ & 0x40) !== 0) - ) { - result.push(byte_.toString(16).padStart(2, '0')); - return result.join('').toUpperCase(); - } - result.push((byte_ | 0x80).toString(16).padStart(2, '0')); - } - } - + export const leb128 = (v: number | bigint) => Buffer.from(leb.encodeSLEB128(v)).toString('hex').toUpperCase().padStart(2, '0') } \ No newline at end of file diff --git a/tests/examples/example.ts b/tests/examples/example.ts index 7911aa7..6210ad7 100644 --- a/tests/examples/example.ts +++ b/tests/examples/example.ts @@ -22,10 +22,10 @@ spec.testee('emulator[:8100]', new EmulatorSpecification(8100)); const steps: Step[] = []; // ✔ ((invoke "8u_good1" (i32.const 0)) (i32.const 97)) -steps.push(new Invoker('8u_good1', [WASM.i32(0)], WASM.i32(97))); +steps.push(new Invoker('8u_good1', [WASM.i32(BigInt(0))], WASM.i32(BigInt(97)))); // ✔ ((invoke "8u_good3" (i32.const 0)) (i32.const 98)) -steps.push(new Invoker('8u_good3', [WASM.i32(0)], WASM.i32(98))); +steps.push(new Invoker('8u_good3', [WASM.i32(BigInt(0))], WASM.i32(BigInt(98)))); // ✔ ((invoke "func-unwind-by-br")) steps.push(new Invoker('func-unwind-by-br', [], undefined)); diff --git a/tests/unit/sourcemap.test.ts b/tests/unit/sourcemap.test.ts index 6e21d96..83ef2f7 100644 --- a/tests/unit/sourcemap.test.ts +++ b/tests/unit/sourcemap.test.ts @@ -5,19 +5,34 @@ import {SourceMap} from '../../src/sourcemap/SourceMap'; import {WABT} from '../../src/util/env'; import {copyFileSync, mkdtempSync, readFileSync, rmSync} from 'fs'; +import * as leb from '@thi.ng/leb128'; + 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'); +test('[leb128] : test encode positive numbers', t => { + t.is(WASM.leb128(BigInt(0)), '00'); + t.is(WASM.leb128(BigInt(1)), '01'); + t.is(WASM.leb128(BigInt(8)), '08'); + t.is(WASM.leb128(BigInt(32)), '20'); + t.is(WASM.leb128(BigInt(64)), 'C000'); + t.is(WASM.leb128(BigInt(127)), 'FF00'); + t.is(WASM.leb128(BigInt(128)), '8001'); + t.is(WASM.leb128(BigInt(1202)), 'B209'); + t.is(WASM.leb128(BigInt(2147483647)), 'FFFFFFFF07'); + t.is(WASM.leb128(BigInt(4294967295)), 'FFFFFFFF0F'); + t.is(WASM.leb128(Number.MAX_SAFE_INTEGER), 'FFFFFFFFFFFFFF0F'); +}); + +test('[leb128] : test encode negative numbers', t => { + t.is(WASM.leb128(BigInt(-1)), '7F'); + t.is(WASM.leb128(BigInt(-8)), '78'); + t.is(WASM.leb128(BigInt(-32)), '60'); + t.is(WASM.leb128(BigInt(-64)), '40'); + t.is(WASM.leb128(BigInt(-127)), '817F'); + t.is(WASM.leb128(BigInt(-128)), '807F'); }); test('[extractLineInfo] : test against artifacts (1)', async t => { From 6ba8d510f293cf8451c48816f7638a577ad1e9c9 Mon Sep 17 00:00:00 2001 From: Tom Lauwaerts Date: Thu, 18 Dec 2025 09:35:18 +0100 Subject: [PATCH 2/2] Fix eslint errors --- .github/workflows/lint.yml | 4 ++-- .eslint.config.mjs => eslint.config.mjs | 2 +- src/messaging/Message.ts | 11 ++++++++--- src/messaging/Parsers.ts | 1 + src/sourcemap/Wasm.ts | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) rename .eslint.config.mjs => eslint.config.mjs (90%) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fa65fce..a418d1e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,13 +28,13 @@ jobs: env: SARIF_ESLINT_IGNORE_SUPPRESSED: "true" run: npx eslint src/**/* - --config .eslint.config.mjs + --config eslint.config.mjs --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif continue-on-error: true - name: Upload analysis results to GitHub - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: eslint-results.sarif wait-for-processing: true diff --git a/.eslint.config.mjs b/eslint.config.mjs similarity index 90% rename from .eslint.config.mjs rename to eslint.config.mjs index 771747b..aae517d 100644 --- a/.eslint.config.mjs +++ b/eslint.config.mjs @@ -11,7 +11,7 @@ export default [ { plugins: { '@stylistic/js': stylisticJs }, rules: { - '@stylistic/js/indent': ['error', 4], + '@stylistic/js/indent': ['error', 4, { "SwitchCase": 1 }], '@typescript-eslint/no-wrapper-object-types': 'off' } }, diff --git a/src/messaging/Message.ts b/src/messaging/Message.ts index 5a92624..0288571 100644 --- a/src/messaging/Message.ts +++ b/src/messaging/Message.ts @@ -11,6 +11,7 @@ import Interrupt = WARDuino.Interrupt; import State = WARDuino.State; import Value = WASM.Value; import Type = WASM.Type; +import Float = WASM.Float; // An acknowledgement returned by the debugger export interface Ack { @@ -177,9 +178,7 @@ export namespace Message { switch (arg.type) { case WASM.Float.f32: case WASM.Float.f64: - const buff = Buffer.alloc(arg.type === Float.f32 ? 4 : 8); - ieee754.write(buff, arg.value, 0, true, arg.type === Float.f32 ? 23 : 52, buff.length); // TODO write BigInt without loss of precision (don't use ieee754.write) - payload += buff.toString('hex'); + payload += ieeefloat(>arg) break; case WASM.Integer.i32: case WASM.Integer.i64: @@ -226,3 +225,9 @@ export namespace Message { } }; } + +function ieeefloat(arg: Value): String { + const buff = Buffer.alloc(arg.type === Float.f32 ? 4 : 8); + ieee754.write(buff, arg.value, 0, true, arg.type === Float.f32 ? 23 : 52, buff.length); // TODO write BigInt without loss of precision (don't use ieee754.write) + return buff.toString('hex'); +} \ No newline at end of file diff --git a/src/messaging/Parsers.ts b/src/messaging/Parsers.ts index dfbba4f..2f287e7 100644 --- a/src/messaging/Parsers.ts +++ b/src/messaging/Parsers.ts @@ -59,6 +59,7 @@ export function breakpointHitParser(text: string): Breakpoint { throw new Error('Could not messaging BREAKPOINT address in ack.'); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function stacking(objects: {value: any, type: any}[]): WASM.Value[] { const stacked: WASM.Value[] = []; for (const object of objects) { diff --git a/src/sourcemap/Wasm.ts b/src/sourcemap/Wasm.ts index 9f9fc58..f9d3115 100644 --- a/src/sourcemap/Wasm.ts +++ b/src/sourcemap/Wasm.ts @@ -32,7 +32,7 @@ export namespace WASM { value: T extends Float ? number : bigint; } - export interface Nothing extends Value {} + export type Nothing = Value export const nothing: Nothing = { type: Special.nothing, value: 0