Skip to content
Merged
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/c/exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5125,6 +5125,20 @@ function emitPrimitiveLibCall(state: LibCallState): Temp {
return finish(`sin(${arg(0)})`);
case "math.cos":
return finish(`cos(${arg(0)})`);
case "math.tan":
return finish(`tan(${arg(0)})`);
case "math.asin":
return finish(`asin(${arg(0)})`);
case "math.acos":
return finish(`acos(${arg(0)})`);
case "math.atan":
return finish(`atan(${arg(0)})`);
case "math.cbrt":
return finish(`cbrt(${arg(0)})`);
case "math.sign":
// Each argument was evaluated into a temp. Returning that temp
// for unordered/zero inputs preserves NaN and the sign of zero.
return finish(`(${arg(0)} > 0.0 ? 1.0 : (${arg(0)} < 0.0 ? -1.0 : ${arg(0)}))`);
case "math.exp":
return finish(`exp(${arg(0)})`);
case "math.sqrt":
Expand Down
12 changes: 12 additions & 0 deletions packages/compiler/src/backend/llvm/lib-filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,18 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr):
B.line(`${t} = call double @llvm.fabs.f64(double ${v.name})`);
return { name: t, type: e.type };
}
if (e.fn === "math.sign") {
const v = host.emitExpr(e.args[0]!);
const positive = B.tmp();
B.line(`${positive} = fcmp ogt double ${v.name}, ${f64Lit(0)}`);
const negative = B.tmp();
B.line(`${negative} = fcmp olt double ${v.name}, ${f64Lit(0)}`);
const nonPositive = B.tmp();
B.line(`${nonPositive} = select i1 ${negative}, double ${f64Lit(-1)}, double ${v.name}`);
const t = B.tmp();
B.line(`${t} = select i1 ${positive}, double ${f64Lit(1)}, double ${nonPositive}`);
return { name: t, type: e.type };
}
if (e.fn === "num.isNaN") {
const v = host.emitExpr(e.args[0]!);
const t = B.tmp();
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
"math.round": "scr_math_round",
"math.sin": "sin",
"math.cos": "cos",
"math.tan": "tan",
"math.asin": "asin",
"math.acos": "acos",
"math.atan": "atan",
"math.cbrt": "cbrt",
"math.exp": "exp",
"math.sqrt": "sqrt",
"math.log": "log",
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/frontend/lowering/surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,12 @@ export const STATIC_MATH_FNS: Record<string, { fn: IrLibFn; arity: number } | un
random: { fn: "math.random", arity: 0 },
sin: { fn: "math.sin", arity: 1 },
cos: { fn: "math.cos", arity: 1 },
tan: { fn: "math.tan", arity: 1 },
asin: { fn: "math.asin", arity: 1 },
acos: { fn: "math.acos", arity: 1 },
atan: { fn: "math.atan", arity: 1 },
cbrt: { fn: "math.cbrt", arity: 1 },
sign: { fn: "math.sign", arity: 1 },
exp: { fn: "math.exp", arity: 1 },
sqrt: { fn: "math.sqrt", arity: 1 },
log: { fn: "math.log", arity: 1 },
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/src/ir/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2157,6 +2157,13 @@ export type IrLibFn =
| "math.ceil"
| "math.sin"
| "math.cos"
| "math.tan"
| "math.asin"
| "math.acos"
| "math.atan"
| "math.cbrt"
/** Return -1, +1, or the original NaN/zero. In particular, -0 remains -0. */
| "math.sign"
| "math.exp"
| "math.sqrt"
| "math.log"
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/ir/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"math.ceil": { argTypes: [F64], result: F64 },
"math.sin": { argTypes: [F64], result: F64 },
"math.cos": { argTypes: [F64], result: F64 },
"math.tan": { argTypes: [F64], result: F64 },
"math.asin": { argTypes: [F64], result: F64 },
"math.acos": { argTypes: [F64], result: F64 },
"math.atan": { argTypes: [F64], result: F64 },
"math.cbrt": { argTypes: [F64], result: F64 },
"math.sign": { argTypes: [F64], result: F64 },
"math.exp": { argTypes: [F64], result: F64 },
"math.sqrt": { argTypes: [F64], result: F64 },
"math.log": { argTypes: [F64], result: F64 },
Expand Down
14 changes: 10 additions & 4 deletions packages/compiler/src/library/fence-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,10 +443,16 @@ function surfaceMatchesDiag(
diag: ScrDiagnostic,
): boolean {
if (surface.code === undefined || surface.code !== diag.code) return false;
// A diagnostic-fence entry IS the code family; member entries must also
// appear by name in the refusal's message (SC2020/SC2012 refusals spell
// the surface exactly as the manifest names it).
return surface.kind === "diagnostic-fence" || diag.message.includes(surface.name);
// A diagnostic-fence entry IS the code family. Method refusals use a
// receiver description instead of the manifest's prototype spelling.
if (surface.kind === "diagnostic-fence" || diag.message.includes(surface.name)) return true;
for (const [prefix, receiver] of [["number.prototype.", "numbers"], ["string.prototype.", "strings"]] as const) {
if (surface.name.startsWith(prefix)) {
const method = surface.name.slice(prefix.length);
return diag.message.includes(`'.${method}()' on ${receiver}`);
}
}
return false;
}

function teachingForRefusal(profile: FenceProfileView, diag: ScrDiagnostic): string | undefined {
Expand Down
24 changes: 12 additions & 12 deletions packages/compiler/surface-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2683,22 +2683,22 @@
"id": "stdlib.math.acos",
"kind": "stdlib",
"name": "Math.acos",
"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.asin",
"kind": "stdlib",
"name": "Math.asin",
"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.atan",
"kind": "stdlib",
"name": "Math.atan",
"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.atan2",
Expand All @@ -2711,8 +2711,8 @@
"id": "stdlib.math.cbrt",
"kind": "stdlib",
"name": "Math.cbrt",
"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.ceil",
Expand Down Expand Up @@ -2809,8 +2809,8 @@
"id": "stdlib.math.sign",
"kind": "stdlib",
"name": "Math.sign",
"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.sin",
Expand All @@ -2830,8 +2830,8 @@
"id": "stdlib.math.tan",
"kind": "stdlib",
"name": "Math.tan",
"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.trunc",
Expand Down
16 changes: 16 additions & 0 deletions tests/corpus/2952-math-static-remaining.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// The remaining one-argument Math functions compile without --dynamic.
// Transcendental results round before comparison because libm and V8 can
// differ in the last ulp; IEEE special values and signed zero compare raw.
const nan = 0 / 0;
console.log(Math.tan(0), 1 / Math.tan(-0), Math.tan(Infinity), Math.tan(nan));
console.log(Math.asin(0), 1 / Math.asin(-0), Math.asin(2), Math.asin(nan));
console.log(Math.acos(1), Math.acos(2), Math.acos(-2), Math.acos(nan));
console.log(Math.atan(0), 1 / Math.atan(-0), Math.atan(nan));
console.log(Math.cbrt(0), 1 / Math.cbrt(-0), Math.cbrt(Infinity), Math.cbrt(-Infinity), Math.cbrt(nan));
console.log(Math.sign(-42), 1 / Math.sign(-0), 1 / Math.sign(0), Math.sign(42), Math.sign(Infinity), Math.sign(-Infinity), Math.sign(nan));
console.log(Math.tan(1).toFixed(9), Math.asin(0.5).toFixed(9), Math.acos(0.5).toFixed(9));
console.log(Math.atan(1).toFixed(9), Math.cbrt(2).toFixed(9), Math.cbrt(-27).toFixed(9));
console.log(Math.asin(1).toFixed(9), Math.asin(-1).toFixed(9), Math.acos(-1).toFixed(9), Math.atan(Infinity).toFixed(9));
let seen = "";
function input(label: string, value: number): number { seen += label; return value; }
console.log(Math.sign(input("s", -8)), Math.cbrt(input("c", -8)), Math.atan(input("a", 1)).toFixed(9), seen);
4 changes: 2 additions & 2 deletions tests/diagnostics/dynamic-surface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// The island-backed ambient surface (Math methods outside the static table,
// number methods, string-pattern replace/at, the Number statics, ...)
// The island-backed ambient surface (number methods, string-pattern
// replace/at, the Number statics, ...)
// typechecks against real static types but executes in the embedded
// engine: in a static build every use site is its own SC2012 naming the
// flag — never an ICE, never a link error. Static Math methods include
Expand Down
4 changes: 2 additions & 2 deletions tests/harness/__snapshots__/coverage-dynamic-mix-dynamic.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
scriptc coverage tests/coverage-fixtures/dynamic-mix.ts

statements analyzed 9
compile statically 3 (33%)
compile dynamically 5 (55%) (island sites — the embedded engine runs them)
compile statically 4 (44%)
compile dynamically 4 (44%) (island sites — the embedded engine runs them)

blockers:
×1 loose equality (== and !=) SC1040
5 changes: 2 additions & 3 deletions tests/harness/__snapshots__/coverage-dynamic-mix.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
scriptc coverage tests/coverage-fixtures/dynamic-mix.ts

statements analyzed 9
compile statically 3 (33%)
compile statically 4 (44%)

runs with --dynamic 5 sites (embeds a JS engine, ~620KB — static stays the default)
runs with --dynamic 4 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.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
9 changes: 0 additions & 9 deletions tests/harness/__snapshots__/dynamic-surface.ts.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
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.cbrt(2);
| ^~~~~~~~~~~~
11 | const tau = Math.PI * 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: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;
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 @@ -957,32 +957,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.acos(1); }\n`,
`export function f(): number { return (1.234).toPrecision(2).length; }\n`,
{
exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }],
determinism: { teachings: { "stdlib.math.acos": "trig runs in the host; request it as an effect" } },
determinism: { teachings: { "stdlib.number.toPrecision": "formatting runs in the host; request it 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.acos");
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(".toPrecision()");
expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: formatting runs in the host; request it 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.acos(1); }\n`,
`export function f(): number { return "abc".replace("a", "b").length; }\n`,
{
exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }],
determinism: { fences: [{ id: "stdlib.math.acos", teaching: "trig is host math" }] },
determinism: { fences: [{ id: "stdlib.string.replace", teaching: "replacement is host work" }] },
},
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: replacement is host work");
});
});
9 changes: 4 additions & 5 deletions tests/harness/library-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,11 +374,10 @@ describe("library profile fences", () => {
const fsIds = r.profile.fences[1]!.surfaces.map((s) => s.id);
expect(fsIds).toContain("node-builtin.fs.readFileSync");
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.
// Math.acos now compiles statically, so its fence must have a detector.
const acos = r.profile.fences[2]!.surfaces[0]!;
expect(acos.code).toBe("SC2012");
expect(acos.detector).toBeUndefined();
expect(acos.code).toBeUndefined();
expect(acos.detector).toBeDefined();
});

test("a fence remediation feeds the trap-remediation lookup through covered codes", () => {
Expand All @@ -388,7 +387,7 @@ describe("library profile fences", () => {
determinism: {
remediations: { SC2012: "the explicit map key wins" },
fences: [
{ id: "stdlib.math.acos", remediation: "request it as an effect" },
{ id: "stdlib.string.replace", remediation: "request it as an effect" },
{ id: "node-builtin.crypto.createCipheriv", remediation: "ciphers come from the host" },
],
},
Expand Down
3 changes: 2 additions & 1 deletion tests/harness/surface-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ const PROBES: Probe[] = [
{ 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(4));\n" },
{ id: "stdlib.math.tan", source: "console.log(Math.tan(2));\n" },
{ id: "stdlib.math.sign", source: "console.log(Math.sign(-2));\n" },
{ id: "stdlib.math.hypot", source: "console.log(Math.hypot(3, 4));\n" },
{ id: "stdlib.map.has", source: 'const m = new Map<string, number>();\nm.set("a", 1);\nconsole.log(m.has("a"));\n' },
{ id: "stdlib.date.now", source: "console.log(Date.now() > 0);\n" },
Expand Down Expand Up @@ -161,7 +163,6 @@ const PROBES: Probe[] = [
{ id: "stdlib.math.E", source: "console.log(Math.E);\n" },
// status dynamic-only — refused with the entry's code statically,
// analyzed clean under --dynamic
{ id: "stdlib.math.tan", source: "console.log(Math.tan(2));\n" },
{ id: "stdlib.string.replace", source: 'console.log("aa".replace("a", "b"));\n' },
{
id: "stdlib.headers.entries",
Expand Down
Loading