Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/compiler/src/backend/emission/emit-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
9 changes: 7 additions & 2 deletions packages/compiler/src/coverage/surface-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
SET_COMBINE_METHODS,
SET_METHODS,
STATIC_MATH_FNS,
STATIC_MATH_PROPS,
STATIC_NUMBER_METHODS,
STR_METHODS,
UNSUPPORTED_EXPR,
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 5 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-island.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions packages/compiler/src/frontend/lowering/surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,13 @@ export const ISLAND_SURFACE = {
export const STATIC_MATH_FNS: Record<string, { fn: IrLibFn; arity: number } | undefined> = {
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
Expand All @@ -534,6 +541,13 @@ export const STATIC_MATH_FNS: Record<string, { fn: IrLibFn; arity: number } | un
random: { fn: "math.random", arity: 0 },
};

/** Read-only Math constants whose IEEE-754 values are emitted directly into
* the IR. They need neither a runtime call nor the embedded dynamic engine. */
export const STATIC_MATH_PROPS: Readonly<Record<string, number | undefined>> = {
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. */
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/src/ir/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/src/ir/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"math.minArr": { argTypes: [arrayOf(F64)], result: F64 },
"math.floor": { argTypes: [F64], result: F64 },
"math.abs": { argTypes: [F64], result: F64 },
"math.sin": { argTypes: [F64], result: F64 },
"math.cos": { argTypes: [F64], result: F64 },
"math.sqrt": { argTypes: [F64], result: F64 },
"math.exp": { argTypes: [F64], result: F64 },
"math.log": { argTypes: [F64], result: F64 },
"math.pow": { argTypes: [F64, F64], result: F64 },
"math.fround": { argTypes: [F64], result: F64 },
"math.round": { argTypes: [F64], result: F64 },
"math.trunc": { argTypes: [F64], result: F64 },
"math.ceil": { argTypes: [F64], result: F64 },
Expand Down
37 changes: 21 additions & 16 deletions packages/compiler/surface-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2560,15 +2560,13 @@
"id": "stdlib.math.E",
"kind": "stdlib",
"name": "Math.E",
"status": "dynamic-only",
"code": "SC2012"
"status": "static"
},
{
"id": "stdlib.math.PI",
"kind": "stdlib",
"name": "Math.PI",
"status": "dynamic-only",
"code": "SC2012"
"status": "static"
},
{
"id": "stdlib.math.abs",
Expand Down Expand Up @@ -2623,15 +2621,15 @@
"id": "stdlib.math.cos",
"kind": "stdlib",
"name": "Math.cos",
"status": "dynamic-only",
"code": "SC2012"
"status": "static",
"note": "compiles statically at arity 1; other declared call shapes run only in the embedded dynamic engine (SC2012 without --dynamic)"
},
{
"id": "stdlib.math.exp",
"kind": "stdlib",
"name": "Math.exp",
"status": "dynamic-only",
"code": "SC2012"
"status": "static",
"note": "compiles statically at arity 1; other declared call shapes run only in the embedded dynamic engine (SC2012 without --dynamic)"
},
{
"id": "stdlib.math.floor",
Expand All @@ -2640,6 +2638,13 @@
"status": "static",
"note": "compiles statically at arity 1"
},
{
"id": "stdlib.math.fround",
"kind": "stdlib",
"name": "Math.fround",
"status": "static",
"note": "compiles statically at arity 1"
},
{
"id": "stdlib.math.hypot",
"kind": "stdlib",
Expand All @@ -2651,8 +2656,8 @@
"id": "stdlib.math.log",
"kind": "stdlib",
"name": "Math.log",
"status": "dynamic-only",
"code": "SC2012"
"status": "static",
"note": "compiles statically at arity 1; other declared call shapes run only in the embedded dynamic engine (SC2012 without --dynamic)"
},
{
"id": "stdlib.math.log10",
Expand Down Expand Up @@ -2686,8 +2691,8 @@
"id": "stdlib.math.pow",
"kind": "stdlib",
"name": "Math.pow",
"status": "dynamic-only",
"code": "SC2012"
"status": "static",
"note": "compiles statically at arity 2; other declared call shapes run only in the embedded dynamic engine (SC2012 without --dynamic)"
},
{
"id": "stdlib.math.random",
Expand All @@ -2714,15 +2719,15 @@
"id": "stdlib.math.sin",
"kind": "stdlib",
"name": "Math.sin",
"status": "dynamic-only",
"code": "SC2012"
"status": "static",
"note": "compiles statically at arity 1; other declared call shapes run only in the embedded dynamic engine (SC2012 without --dynamic)"
},
{
"id": "stdlib.math.sqrt",
"kind": "stdlib",
"name": "Math.sqrt",
"status": "dynamic-only",
"code": "SC2012"
"status": "static",
"note": "compiles statically at arity 1; other declared call shapes run only in the embedded dynamic engine (SC2012 without --dynamic)"
},
{
"id": "stdlib.math.tan",
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/test/ts7/baselines/order-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -5089,6 +5089,12 @@
],
"diags": []
},
"<repo>/tests/corpus/2607-math-dsp-static.js": {
"order": [
"<repo>/tests/corpus/2607-math-dsp-static.js"
],
"diags": []
},
"<repo>/tests/corpus/2608-regex-named-groups.ts": {
"order": [
"<repo>/tests/corpus/2608-regex-named-groups.ts"
Expand Down
14 changes: 14 additions & 0 deletions tests/corpus/2607-math-dsp-static.js
Original file line number Diff line number Diff line change
@@ -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));
2 changes: 1 addition & 1 deletion tests/coverage-fixtures/dynamic-mix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions tests/diagnostics/dynamic-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion tests/harness/__snapshots__/coverage-dynamic-mix.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 8 additions & 8 deletions tests/harness/__snapshots__/dynamic-surface.ts.txt
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
14 changes: 7 additions & 7 deletions tests/harness/library-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Loading
Loading