From 772b61232e8dde87619f20309e9940cc1b364c52 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Wed, 3 Dec 2025 08:56:37 -0800 Subject: [PATCH 1/3] Add test case --- packages/compiler/tools/testCases.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/compiler/tools/testCases.ts b/packages/compiler/tools/testCases.ts index ca39c47..02df87e 100644 --- a/packages/compiler/tools/testCases.ts +++ b/packages/compiler/tools/testCases.ts @@ -1,4 +1,6 @@ const testCases: [string, string, number][] = [ + // https://github.com/captbaritone/eel-wasm/issues/61 + ["Mod with large value", "g = 56991359368.52679 % 2;", 0], ["Expressions", "g = ((6- -7)+ 3);", 16], ["Number", "g = 5;", 5], ["Number with decimal", "g = 5.5;", 5.5], From 2f59086e2f29b14b62c8562426dd33e43df91190 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sun, 14 Dec 2025 18:39:27 -0800 Subject: [PATCH 2/3] Improve handling of overflow in mod operator --- packages/compiler/src/encoding.ts | 11 +++-- packages/compiler/src/wasmFunctions.ts | 59 ++++++++++++++++++++++---- packages/compiler/tools/testCases.ts | 14 ++++-- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/packages/compiler/src/encoding.ts b/packages/compiler/src/encoding.ts index 4be0f71..4b73792 100644 --- a/packages/compiler/src/encoding.ts +++ b/packages/compiler/src/encoding.ts @@ -96,10 +96,11 @@ export const op = { * Numeric Instructions * https://webassembly.github.io/spec/core/binary/instructions.html#numeric-instructions */ - i32_const: (i: number) => [0x41, ...signedLEB128(i)], - // i64_const: 0x42, + // TODO: Add bounds checking + i32_const: (n: number) => [0x41, ...signedLEB128(n)], + i64_const: (n: number) => [0x42, ...signedLEB128(n)], // f32_const: 0x43, - f64_const: (i: number) => [0x44, ...encodef64(i)], + f64_const: (n: number) => [0x44, ...encodef64(n)], i32_eqz: 0x45, // i32_eq: 0x46, @@ -114,6 +115,7 @@ export const op = { // i32_ge_u: 0x4f, // [0x50...0x5a] i64 + i64_ne: 0x52, // [0x5b...0x60] f32 f64_eq: 0x61, @@ -164,6 +166,9 @@ export const op = { i64_trunc_s_f64: 0xb0, f64_convert_i64_s: 0xb9, f64_convert_i32_s: 0xb7, + + i64_trunc_f64_s: 0xb0, + i64_trunc_f64_u: 0xb1, }; // https://webassembly.github.io/spec/core/binary/instructions.html#binary-blocktype diff --git a/packages/compiler/src/wasmFunctions.ts b/packages/compiler/src/wasmFunctions.ts index 564f076..81b8149 100644 --- a/packages/compiler/src/wasmFunctions.ts +++ b/packages/compiler/src/wasmFunctions.ts @@ -2,6 +2,14 @@ import { op, VAL_TYPE, IS_NOT_ZEROISH, EPSILON, BLOCK } from "./encoding.js"; import { FunctionDefinition } from "./types.js"; import { BUFFER_SIZE } from "./constants.js"; +// Minimum value that x87 would treat as out of range for conversion +// from float to int. +const MIN_OUT_OF_RANGE = op.f64_const(Math.pow(2, 31)); +// Arbitrary value that x87 uses when trying to convert an out of range, or NaN, +// float to an int. + +const INDETERMINATE = op.f64_const(Math.pow(2, 31)); + export const localFuncMap: { [functionName: string]: FunctionDefinition } = { sqr: { args: [VAL_TYPE.f64], @@ -53,20 +61,55 @@ export const localFuncMap: { [functionName: string]: FunctionDefinition } = { mod: { args: [VAL_TYPE.f64, VAL_TYPE.f64], returns: [VAL_TYPE.f64], - localVariables: [VAL_TYPE.i32], - // TODO: Simplify all this type coercion + localVariables: [VAL_TYPE.i64], + // Tuned to match https://github.com/WACUP/vis_milk2/blob/de9625a89e724afe23ed273b96b8e48496095b6c/ns-eel2/asm-nseel-x86-gcc.c#L507-L531 + // Specifically, we handle the nuances of how x87 handles out of range values in the + // `fist` instruction. + // See https://godbolt.org/z/rjf769E3n for some examples binary: [ + ...op.local_get(0), + op.f64_abs, + ...op.local_set(0), + ...op.local_get(1), - op.i32_trunc_f64_s, + op.f64_abs, + ...op.local_set(1), + + // Bound the input + ...op.local_get(0), + ...MIN_OUT_OF_RANGE, + op.f64_lt, + // We purposefully make "out of range" the false case so that + // we can also catch NaNs. + ...op.if(BLOCK.void), + // + op.else, + ...INDETERMINATE, + ...op.local_set(0), + op.end, + + // Bound the input 2 + ...op.local_get(1), + ...MIN_OUT_OF_RANGE, + op.f64_lt, + ...op.if(BLOCK.void), + // + op.else, + ...INDETERMINATE, + ...op.local_set(1), + op.end, + + ...op.local_get(1), + op.i64_trunc_f64_s, ...op.local_tee(2), - ...op.i32_const(0), - op.i32_ne, + ...op.i64_const(0), + op.i64_ne, ...op.if(BLOCK.f64), ...op.local_get(0), - op.i32_trunc_f64_s, + op.i64_trunc_f64_s, ...op.local_get(2), - op.i32_rem_s, - op.f64_convert_i32_s, + op.i64_rem_s, + op.f64_convert_i64_s, op.else, ...op.f64_const(0), op.end, diff --git a/packages/compiler/tools/testCases.ts b/packages/compiler/tools/testCases.ts index 02df87e..e8703d4 100644 --- a/packages/compiler/tools/testCases.ts +++ b/packages/compiler/tools/testCases.ts @@ -1,6 +1,11 @@ const testCases: [string, string, number][] = [ // https://github.com/captbaritone/eel-wasm/issues/61 - ["Mod with large value", "g = 56991359368.52679 % 2;", 0], + ["Mod with NaN in dividend", "g = asin(2) % 2;", 0], // The indeterminate is even + ["Mod with large even value in dividend", "g = 56991359368.52679 % 2;", 0], // The indeterminate is even + ["Mod with large odd value in dividend", "g = 56991359367.52679 % 2;", 0], // The indeterminate is even + ["Mod with large even value in divisor", "g = 300 % 56991359368.52679;", 300], + ["Mod with large odd value in divisor", "g = 300 % 56991359367.52679;", 300], + ["Mod with NaN in divisor", "g = 300 % asin(2);", 300], ["Expressions", "g = ((6- -7)+ 3);", 16], ["Number", "g = 5;", 5], ["Number with decimal", "g = 5.5;", 5.5], @@ -41,6 +46,7 @@ const testCases: [string, string, number][] = [ ["Cos", "g = cos(10);", Math.cos(10)], ["Tan", "g = tan(10);", Math.tan(10)], ["Asin", "g = asin(0.5);", Math.asin(0.5)], + ["Asin", "g = asin(2);", NaN], ["Acos", "g = acos(0.5);", Math.acos(0.5)], ["Atan", "g = atan(0.5);", Math.atan(0.5)], ["Atan2", "g = atan2(1, 1);", Math.atan2(1, 1)], @@ -48,11 +54,11 @@ const testCases: [string, string, number][] = [ ["Read globals", "g = x;", 10], ["Multiple statements", "g = 10; g = 20;", 20], ["Multiple statements expression", "(g = 10; g = 20;);", 20], - ["Multiple statements expression implcit return", "g = (0; 20 + 5;);", 25], + ["Multiple statements expression implicit return", "g = (0; 20 + 5;);", 25], ["if", "g = if(0, 20, 10);", 10], ["if", "g = if(0, 20, 10);", 10], - ["if does short-circit (consiquent)", "if(0, (g = 10;), 10);", 0], - ["if does short-circit (alternate)", "if(1, (10), (g = 10;));", 0], + ["if does short-circuit (consequent)", "if(0, (g = 10;), 10);", 0], + ["if does short-circuit (alternate)", "if(1, (10), (g = 10;));", 0], ["above (true)", "g = above(10, 4);", 1], ["above (false)", "g = above(4, 10);", 0], ["below (true)", "g = below(4, 10);", 1], From f7c7ff3d8ce8be8fe4ad433b4dc2a48a568b28aa Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Mon, 15 Dec 2025 12:47:32 -0800 Subject: [PATCH 3/3] Extract logic to get bounded value --- packages/compiler/src/wasmFunctions.ts | 66 +++++++++++--------------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/packages/compiler/src/wasmFunctions.ts b/packages/compiler/src/wasmFunctions.ts index 81b8149..8555fa2 100644 --- a/packages/compiler/src/wasmFunctions.ts +++ b/packages/compiler/src/wasmFunctions.ts @@ -10,6 +10,31 @@ const MIN_OUT_OF_RANGE = op.f64_const(Math.pow(2, 31)); const INDETERMINATE = op.f64_const(Math.pow(2, 31)); +/** + * Reimplements the details of how x87 handles out of range values in the `fist` + * instruction. + * + * Expects a local argument offset and leaves an i64 value on the stack. We use + * i64 since teh indeterminate value is 2^31, which is just out of the range of + * i32. + * + * See https://godbolt.org/z/rjf769E3n for some examples + */ +function localGetAsBoundedPositiveInt(argOffset: number): number[] { + return [ + ...op.local_get(argOffset), + ...MIN_OUT_OF_RANGE, + op.f64_lt, + ...op.if(BLOCK.f64), + ...op.local_get(argOffset), + op.f64_abs, + op.else, + ...INDETERMINATE, + op.end, + op.i64_trunc_f64_s, + ]; +} + export const localFuncMap: { [functionName: string]: FunctionDefinition } = { sqr: { args: [VAL_TYPE.f64], @@ -63,50 +88,13 @@ export const localFuncMap: { [functionName: string]: FunctionDefinition } = { returns: [VAL_TYPE.f64], localVariables: [VAL_TYPE.i64], // Tuned to match https://github.com/WACUP/vis_milk2/blob/de9625a89e724afe23ed273b96b8e48496095b6c/ns-eel2/asm-nseel-x86-gcc.c#L507-L531 - // Specifically, we handle the nuances of how x87 handles out of range values in the - // `fist` instruction. - // See https://godbolt.org/z/rjf769E3n for some examples binary: [ - ...op.local_get(0), - op.f64_abs, - ...op.local_set(0), - - ...op.local_get(1), - op.f64_abs, - ...op.local_set(1), - - // Bound the input - ...op.local_get(0), - ...MIN_OUT_OF_RANGE, - op.f64_lt, - // We purposefully make "out of range" the false case so that - // we can also catch NaNs. - ...op.if(BLOCK.void), - // - op.else, - ...INDETERMINATE, - ...op.local_set(0), - op.end, - - // Bound the input 2 - ...op.local_get(1), - ...MIN_OUT_OF_RANGE, - op.f64_lt, - ...op.if(BLOCK.void), - // - op.else, - ...INDETERMINATE, - ...op.local_set(1), - op.end, - - ...op.local_get(1), - op.i64_trunc_f64_s, + ...localGetAsBoundedPositiveInt(1), ...op.local_tee(2), ...op.i64_const(0), op.i64_ne, ...op.if(BLOCK.f64), - ...op.local_get(0), - op.i64_trunc_f64_s, + ...localGetAsBoundedPositiveInt(0), ...op.local_get(2), op.i64_rem_s, op.f64_convert_i64_s,