diff --git a/packages/compiler/src/backend/emission/emit-exprs.ts b/packages/compiler/src/backend/emission/emit-exprs.ts index a710947e8..30476ad6b 100644 --- a/packages/compiler/src/backend/emission/emit-exprs.ts +++ b/packages/compiler/src/backend/emission/emit-exprs.ts @@ -3376,6 +3376,20 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp { // halves and naive floor(x+0.5) drifts at the epsilon boundary). case "math.abs": return finish(`fabs(${arg(0)})`); + case "math.sin": + return finish(`sin(${arg(0)})`); + case "math.cos": + return finish(`cos(${arg(0)})`); + case "math.sqrt": + return finish(`sqrt(${arg(0)})`); + case "math.exp": + return finish(`exp(${arg(0)})`); + case "math.log": + return finish(`log(${arg(0)})`); + case "math.pow": + return finish(`pow(${arg(0)}, ${arg(1)})`); + case "math.fround": + return finish(`(double)(float)(${arg(0)})`); case "math.round": return finish(`scr_math_round(${arg(0)})`); // The scalar Math.min/max (scr_lib.c — fmin/fmax drop NaN, so diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 2f78eeb94..7100c6de0 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -13729,6 +13729,35 @@ class LlEmitter { B.line(`${t} = call double @llvm.fabs.f64(double ${v.name})`); return { name: t, type: e.type }; } + if ( + e.fn === "math.sin" || + e.fn === "math.cos" || + e.fn === "math.sqrt" || + e.fn === "math.exp" || + e.fn === "math.log" + ) { + const v = this.emitExpr(e.args[0]!); + this.declare(`declare double @llvm.${e.fn.slice(5)}.f64(double)`); + const t = B.tmp(); + B.line(`${t} = call double @llvm.${e.fn.slice(5)}.f64(double ${v.name})`); + return { name: t, type: e.type }; + } + if (e.fn === "math.pow") { + const left = this.emitExpr(e.args[0]!); + const right = this.emitExpr(e.args[1]!); + this.declare(`declare double @llvm.pow.f64(double, double)`); + const t = B.tmp(); + B.line(`${t} = call double @llvm.pow.f64(double ${left.name}, double ${right.name})`); + return { name: t, type: e.type }; + } + if (e.fn === "math.fround") { + const v = this.emitExpr(e.args[0]!); + const narrowed = B.tmp(); + const widened = B.tmp(); + B.line(`${narrowed} = fptrunc double ${v.name} to float`); + B.line(`${widened} = fpext float ${narrowed} to double`); + return { name: widened, type: e.type }; + } if (e.fn === "num.isNaN") { const v = this.emitExpr(e.args[0]!); const t = B.tmp(); diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index 0291c060d..e6fdb41d5 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -52,6 +52,7 @@ import { SET_COMBINE_METHODS, SET_METHODS, STATIC_MATH_FNS, + STATIC_MATH_PROPS, STATIC_NUMBER_METHODS, STR_METHODS, UNSUPPORTED_EXPR, @@ -221,8 +222,12 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes add({ id: `stdlib.math.${name}`, kind: "stdlib", name: `Math.${name}`, status: "dynamic-only", code: "SC2012" }); } } - for (const name of Object.keys(ISLAND_SURFACE.math.props)) { - add({ id: `stdlib.math.${name}`, kind: "stdlib", name: `Math.${name}`, status: "dynamic-only", code: "SC2012" }); + for (const name of new Set([...Object.keys(STATIC_MATH_PROPS), ...Object.keys(ISLAND_SURFACE.math.props)])) { + if (STATIC_MATH_PROPS[name] !== undefined) { + add({ id: `stdlib.math.${name}`, kind: "stdlib", name: `Math.${name}`, status: "static" }); + } else { + add({ id: `stdlib.math.${name}`, kind: "stdlib", name: `Math.${name}`, status: "dynamic-only", code: "SC2012" }); + } } const numberNames = new Set([ ...Object.keys(STATIC_NUMBER_METHODS), diff --git a/packages/compiler/src/frontend/lowering/lower-island.ts b/packages/compiler/src/frontend/lowering/lower-island.ts index b5a7023c4..666f578b2 100644 --- a/packages/compiler/src/frontend/lowering/lower-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-island.ts @@ -5,7 +5,7 @@ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { BOOL, BYTES_U8, DYN, F64, IrExpr, IrStmt, IrType, JSVAL, MAX_ISLAND_CALLBACK_ARITY, STRING, VOID, canConvertToDyn, canMarshalTypedFuncIntoIsland, islandPromisePayloadTag, isUnitType } from "../../ir/nodes.js"; -import { ISLAND_SURFACE, IslandFnEntry, STATIC_MATH_FNS, boundaryIntoIslandMsg } from "./surfaces.js"; +import { ISLAND_SURFACE, IslandFnEntry, STATIC_MATH_FNS, STATIC_MATH_PROPS, boundaryIntoIslandMsg } from "./surfaces.js"; import { requiresDynamicApiDiag, requiresDynamicPackageDiag } from "../../diagnostics/diagnostic.js"; import { isCjsJsFile, isJsSourceFile, locOf, npmPackageNameOf } from "../program.js"; import { foldedStringKeyOf, lowerDynObjectLiteral, pureReemittable } from "./lower-exprs.js"; @@ -3335,6 +3335,10 @@ export function lowerStaticReadableStreamReaderCall( const member = L.stdlibGlobalMember(expr, "Math"); if (member === null) return null; const loc = locOf(expr); + const staticProp = own(STATIC_MATH_PROPS, member); + if (staticProp !== undefined) { + return { kind: "numLit", value: staticProp, type: F64, loc }; + } const propType = own(ISLAND_SURFACE.math.props, member); if (propType !== undefined) { L.requireDynamicApi(`'Math.${member}'`, expr); diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 4ccf0c2e5..1743ef28d 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -523,6 +523,13 @@ export const ISLAND_SURFACE = { export const STATIC_MATH_FNS: Record = { floor: { fn: "math.floor", arity: 1 }, abs: { fn: "math.abs", arity: 1 }, + sin: { fn: "math.sin", arity: 1 }, + cos: { fn: "math.cos", arity: 1 }, + sqrt: { fn: "math.sqrt", arity: 1 }, + exp: { fn: "math.exp", arity: 1 }, + log: { fn: "math.log", arity: 1 }, + pow: { fn: "math.pow", arity: 2 }, + fround: { fn: "math.fround", arity: 1 }, round: { fn: "math.round", arity: 1 }, // trunc/ceil joined the static table with ask 4: they are the // integer-boundary inference's wholeness-discharge operators (C @@ -534,6 +541,13 @@ export const STATIC_MATH_FNS: Record> = { + PI: Math.PI, + E: Math.E, +}; + /** Number prototype methods with dedicated STATIC lowering paths. The * libCall spellings are also the compiled-graph witnesses used by library * fences, while the arity range is the surface manifest's support claim. */ diff --git a/packages/compiler/src/ir/nodes.ts b/packages/compiler/src/ir/nodes.ts index ca36c7cf9..35b7031f6 100644 --- a/packages/compiler/src/ir/nodes.ts +++ b/packages/compiler/src/ir/nodes.ts @@ -1930,6 +1930,13 @@ export type IrLibFn = * round() is half-away-from-zero and floor(x+0.5) drifts at the * epsilon boundary). Borrow nothing; never throw. */ | "math.abs" + | "math.sin" + | "math.cos" + | "math.sqrt" + | "math.exp" + | "math.log" + | "math.pow" + | "math.fround" | "math.round" /** Math.trunc / Math.ceil — C trunc()/ceil() ARE the JS operations * (NaN/±0/±Infinity pass through bit-exactly; ceil(-0.5) is -0 in IEEE diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 51a52b2ee..324d1b817 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -210,6 +210,13 @@ export const LIB_FN_SIGS: Record/tests/corpus/2607-math-dsp-static.js": { + "order": [ + "/tests/corpus/2607-math-dsp-static.js" + ], + "diags": [] + }, "/tests/corpus/2608-regex-named-groups.ts": { "order": [ "/tests/corpus/2608-regex-named-groups.ts" diff --git a/tests/corpus/2607-math-dsp-static.js b/tests/corpus/2607-math-dsp-static.js new file mode 100644 index 000000000..fcddc7816 --- /dev/null +++ b/tests/corpus/2607-math-dsp-static.js @@ -0,0 +1,14 @@ +// The DSP-oriented scalar Math surface compiles without the dynamic engine. +// Transcendentals print at a precision that is stable across V8's fdlibm and +// the target libc while fround and the constants pin exact JavaScript values. +console.log( + Math.sin(1).toFixed(9), + Math.cos(1).toFixed(9), + Math.sqrt(2).toFixed(9), + Math.exp(1).toFixed(9), + Math.log(10).toFixed(9), + Math.pow(2, 0.5).toFixed(9), +); +console.log(Math.PI.toFixed(12), Math.E.toFixed(12)); +console.log(Math.fround(1 / 3), Math.fround(16777217), 1 / Math.fround(-0)); +console.log(Math.sqrt(-1), Math.log(0), Math.pow(0, -1)); diff --git a/tests/coverage-fixtures/dynamic-mix.ts b/tests/coverage-fixtures/dynamic-mix.ts index 940a289cc..88f368406 100644 --- a/tests/coverage-fixtures/dynamic-mix.ts +++ b/tests/coverage-fixtures/dynamic-mix.ts @@ -5,7 +5,7 @@ // fixes. const v: any = 21; const doubled = v * 2; -const root = Math.sqrt(81); +const root = Math.cbrt(27); const up = (19.99).toPrecision(3); const parsed = Number.parseFloat("1.5"); // the global's string form is static now; the Number static keeps the island const raw = __island_eval("6 * 7"); diff --git a/tests/diagnostics/dynamic-surface.ts b/tests/diagnostics/dynamic-surface.ts index ad7cb07f6..40203b503 100644 --- a/tests/diagnostics/dynamic-surface.ts +++ b/tests/diagnostics/dynamic-surface.ts @@ -7,8 +7,8 @@ // trim/pad variants, parseInt, isNaN, and the global parseFloat/isFinite // over exactly-typed arguments compile statically now and no longer // appear here.) -const up = Math.sqrt(2); -const tau = Math.PI * 2; +const up = Math.cbrt(8); +const tau = Math.atan2(0, -1) * 2; const price = (19.99).toPrecision(4); const swapped = "banana".replace("an", "AN"); const ch = "hello".at(0); diff --git a/tests/harness/__snapshots__/coverage-dynamic-mix.txt b/tests/harness/__snapshots__/coverage-dynamic-mix.txt index 99b414c4d..1a44c1ace 100644 --- a/tests/harness/__snapshots__/coverage-dynamic-mix.txt +++ b/tests/harness/__snapshots__/coverage-dynamic-mix.txt @@ -6,7 +6,7 @@ scriptc coverage tests/coverage-fixtures/dynamic-mix.ts runs with --dynamic 5 sites (embeds a JS engine, ~620KB — static stays the default) ×1 '__island_eval' requires the embedded dynamic engine, which this build does not include SC2010 ×1 the '*' operator on 'any'-typed values runs in the embedded dynamic engine, which this build does not include SC2011 - ×1 'Math.sqrt' runs in the embedded dynamic engine, which this build does not include SC2012 + ×1 'Math.cbrt' runs in the embedded dynamic engine, which this build does not include SC2012 ×1 '.toPrecision()' on numbers runs in the embedded dynamic engine, which this build does not include SC2012 ×1 'Number.parseFloat' runs in the embedded dynamic engine, which this build does not include SC2012 diff --git a/tests/harness/__snapshots__/dynamic-surface.ts.txt b/tests/harness/__snapshots__/dynamic-surface.ts.txt index 47310acd2..eee5f808f 100644 --- a/tests/harness/__snapshots__/dynamic-surface.ts.txt +++ b/tests/harness/__snapshots__/dynamic-surface.ts.txt @@ -1,24 +1,24 @@ -dynamic-surface.ts:10:12 - error SC2012: 'Math.sqrt' runs in the embedded dynamic engine, which this build does not include +dynamic-surface.ts:10:12 - error SC2012: 'Math.cbrt' runs in the embedded dynamic engine, which this build does not include 9 | // appear here.) - 10 | const up = Math.sqrt(2); + 10 | const up = Math.cbrt(8); | ^~~~~~~~~~~~ - 11 | const tau = Math.PI * 2; + 11 | const tau = Math.atan2(0, -1) * 2; hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it -dynamic-surface.ts:11:13 - error SC2012: 'Math.PI' runs in the embedded dynamic engine, which this build does not include +dynamic-surface.ts:11:13 - error SC2012: 'Math.atan2' runs in the embedded dynamic engine, which this build does not include - 10 | const up = Math.sqrt(2); - 11 | const tau = Math.PI * 2; - | ^~~~~~~ + 10 | const up = Math.cbrt(8); + 11 | const tau = Math.atan2(0, -1) * 2; + | ^~~~~~~~~~~~~~~~~ 12 | const price = (19.99).toPrecision(4); hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it dynamic-surface.ts:12:15 - error SC2012: '.toPrecision()' on numbers runs in the embedded dynamic engine, which this build does not include - 11 | const tau = Math.PI * 2; + 11 | const tau = Math.atan2(0, -1) * 2; 12 | const price = (19.99).toPrecision(4); | ^~~~~~~~~~~~~~~~~~~~~~ 13 | const swapped = "banana".replace("an", "AN"); diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index ad93d3628..e2bff9258 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -937,32 +937,32 @@ describe.each(EMISSIONS)("K14: determinism fences, %s emission", (emission) => { test("a manifest-id-keyed teachings entry attaches to that surface's own refusal", async () => { const diags = await refusal( - `export function f(): number { return Math.sin(1); }\n`, + `export function f(): number { return Math.cbrt(8); }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], - determinism: { teachings: { "stdlib.math.sin": "trig runs in the host; request it as an effect" } }, + determinism: { teachings: { "stdlib.math.cbrt": "cube roots run in the host; request them as an effect" } }, }, emission, ); // The surface's own code, not a fence code: the id key attaches text // to the refusal that already fires. expect(diags[0]!.code).toBe("SC2012"); - expect(diags[0]!.message).toContain("Math.sin"); - expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: trig runs in the host; request it as an effect"); + expect(diags[0]!.message).toContain("Math.cbrt"); + expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: cube roots run in the host; request them as an effect"); }); test("fencing a surface the static tier refuses anyway changes only the message", async () => { const diags = await refusal( - `export function f(): number { return Math.sin(1); }\n`, + `export function f(): number { return Math.cbrt(8); }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], - determinism: { fences: [{ id: "stdlib.math.sin", teaching: "trig is host math" }] }, + determinism: { fences: [{ id: "stdlib.math.cbrt", teaching: "cube roots are host math" }] }, }, emission, ); // The existing refusal's code survives — the fence never re-codes a // surface that already refuses; its teaching rides as the note. expect(diags[0]!.code).toBe("SC2012"); - expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: trig is host math"); + expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: cube roots are host math"); }); }); diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index bbe81fc75..c8c7151bc 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -349,7 +349,7 @@ describe("library profile fences", () => { fences: [ { id: "stdlib.math.random", teaching: "randomness is an effect", remediation: "ask the host" }, { prefix: "node-builtin.fs.", teaching: "files are effects" }, - { id: "stdlib.math.sin", teaching: "trig is host math", remediation: "request it as an effect" }, + { id: "stdlib.math.cbrt", teaching: "cube roots are host math", remediation: "request it as an effect" }, ], }, }), @@ -369,9 +369,9 @@ describe("library profile fences", () => { expect(fsIds).toContain("node-builtin.fs.promises.readFile"); // A fenced dynamic-only surface carries its own refusal code and no // detector: the teaching rides the refusal that already fires. - const sin = r.profile.fences[2]!.surfaces[0]!; - expect(sin.code).toBe("SC2012"); - expect(sin.detector).toBeUndefined(); + const cbrt = r.profile.fences[2]!.surfaces[0]!; + expect(cbrt.code).toBe("SC2012"); + expect(cbrt.detector).toBeUndefined(); }); test("a fence remediation feeds the trap-remediation lookup through covered codes", () => { @@ -381,7 +381,7 @@ describe("library profile fences", () => { determinism: { remediations: { SC2012: "the explicit map key wins" }, fences: [ - { id: "stdlib.math.sin", remediation: "request it as an effect" }, + { id: "stdlib.math.cbrt", remediation: "request it as an effect" }, { id: "node-builtin.crypto.createHash", remediation: "digests come from the host" }, ], }, diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index ac3cc2d05..22eee062d 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -114,6 +114,8 @@ const PROBES: Probe[] = [ { id: "stdlib.array.unshift", source: "const xs: number[] = [2];\nconsole.log(xs.unshift(1), xs[0]);\n" }, { id: "stdlib.array.reverse", source: "const xs: number[] = [1, 2];\nconsole.log(xs.reverse()[0]);\n" }, { id: "stdlib.math.floor", source: "console.log(Math.floor(1.5));\n" }, + { id: "stdlib.math.sqrt", source: "console.log(Math.sqrt(2));\n" }, + { id: "stdlib.math.PI", source: "console.log(Math.PI);\n" }, { id: "stdlib.map.has", source: 'const m = new Map();\nm.set("a", 1);\nconsole.log(m.has("a"));\n' }, { id: "stdlib.date.now", source: "console.log(Date.now() > 0);\n" }, { id: "stdlib.number.toFixed", source: "const n = 1.2345;\nconsole.log(n.toFixed(2));\n" }, @@ -140,8 +142,7 @@ const PROBES: Probe[] = [ { id: "node-builtin.os.EOL", source: 'import { EOL } from "node:os";\nconsole.log(EOL.length);\n' }, // status dynamic-only — refused with the entry's code statically, // analyzed clean under --dynamic - { id: "stdlib.math.sqrt", source: "console.log(Math.sqrt(2));\n" }, - { id: "stdlib.math.PI", source: "console.log(Math.PI);\n" }, + { id: "stdlib.math.cbrt", source: "console.log(Math.cbrt(8));\n" }, { id: "stdlib.string.replace", source: 'console.log("aa".replace("a", "b"));\n' }, { id: "stdlib.headers.entries",