Lower Math.PI and Math.E statically (#279)

Co-authored-by: yowainwright <1074042+yowainwright@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-09-01 00:47:23 -05:00
committed by GitHub
co-authored by yowainwright
parent 1cec922d95
commit 1071b87d77
12 changed files with 95 additions and 25 deletions
@@ -53,6 +53,7 @@ import {
SET_COMBINE_METHODS,
SET_METHODS,
STATIC_MATH_FNS,
STATIC_MATH_PROPS,
STATIC_NUMBER_METHODS,
STR_METHODS,
UNSUPPORTED_EXPR,
@@ -225,6 +226,15 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
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 Object.keys(STATIC_MATH_PROPS)) {
add({
id: `stdlib.math.${name}`,
kind: "stdlib",
name: `Math.${name}`,
status: "static",
note: "the constant read compiles to a numeric literal; no runtime Math read is performed",
});
}
const numberNames = new Set([
...Object.keys(STATIC_NUMBER_METHODS),
...Object.keys(ISLAND_SURFACE.number),
@@ -6,7 +6,7 @@ import { InternalCompilerError } from "../../errors.js";
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/ir.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";
@@ -3218,14 +3218,19 @@ export function lowerStaticReadableStreamReaderCall(
return finish(lowerer.jsvalIn(lowerer.lowerExpr(access.expression), access.expression), entry);
}
/** `Math.PI` / `Math.E` property READS: getProp off globalGet("Math"),
* exiting to the declared number type. Math methods referenced without a
* call are rejected specifically (no value form exists, --dynamic or
* not). Null for non-Math receivers (the property chain keeps trying). */
/** Canonical `Math.PI` / `Math.E` property reads become typed numeric
* literals. Remaining Math properties retain the island/fence path. Math
* methods referenced without a call are rejected specifically (no value form
* exists, --dynamic or not). Null for non-Math receivers (the property chain
* keeps trying). */
export function lowerMathProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
const member = lowerer.stdlibGlobalMember(expr, "Math");
if (member === null) return null;
const loc = locOf(expr);
const staticValue = own(STATIC_MATH_PROPS, member);
if (staticValue !== undefined) {
return { kind: "numLit", value: staticValue, type: F64, loc };
}
const propType = own(ISLAND_SURFACE.math.props, member);
if (propType !== undefined) {
lowerer.requireDynamicApi(`'Math.${member}'`, expr);
@@ -448,7 +448,8 @@ export const boundaryOutOfIslandMsg = (typeName: string): string =>
`and 'T | undefined' over those)`;
/** The island-backed surface — standard-library APIs with no static
* runtime implementation (Math.*, number/string methods beyond the
* runtime implementation (Math methods/properties beyond the compile-time
* constants, number/string methods beyond the
* intrinsic set, parseFloat, ...). ONE table drives both sides of the
* gate: under --dynamic each entry lowers to marshal → engine execution →
* validated exit to the declared return type; without the flag each use
@@ -463,7 +464,8 @@ export const boundaryOutOfIslandMsg = (typeName: string): string =>
* user. */
export const ISLAND_SURFACE = {
/** `Math.<fn>(...)` lowers to callMethod(globalGet("Math"), fn, args);
* the readonly number props (`Math.PI`) to getProp(globalGet("Math")).
* Math.PI and Math.E are compile-time numeric literals in STATIC_MATH_PROPS;
* remaining Math properties retain island/fence behavior.
* min/max/atan2/hypot/pow are declared with exactly two parameters
* (rest/optional parameters aren't representable). */
math: {
@@ -476,7 +478,8 @@ export const ISLAND_SURFACE = {
round: ISL_N1,
sign: ISL_N1, sin: ISL_N1, sqrt: ISL_N1, tan: ISL_N1, trunc: ISL_N1,
} as Record<string, IslandFnEntry | undefined>,
props: { PI: F64, E: F64 } as Record<string, IrType | undefined>,
// Math constants are compile-time literals, not island properties.
props: {} as Record<string, IrType | undefined>,
},
/** Methods on `number` receivers. The receiver marshals by value; the
* engine auto-boxes primitives on method calls, so `this` binds the
@@ -510,6 +513,16 @@ export const ISLAND_SURFACE = {
} as Record<string, IslandFnEntry | undefined>,
};
/** Math properties with a STATIC lowering: each read becomes the exact
* JavaScript numeric constant in the typed IR. No runtime Math object read or
* dynamic-engine dependency is involved. Remaining Math properties retain
* their island/fence behavior through ISLAND_SURFACE.math.props and the
* generic standard-library member fence. */
export const STATIC_MATH_PROPS: Record<string, number | undefined> = {
PI: 3.141592653589793,
E: 2.718281828459045,
};
/** Math members with a STATIC lowering — each is one C call that IS the
* JS operation, at the tabled arity (floor: libm's floor; min/max: the
* NaN-poisoning ±0-ordered scalar folds; random: arc4random-backed
+1 -1
View File
@@ -174,7 +174,7 @@ export {
} from "./library/sidecar.js";
export { validateSidecar } from "./library/sidecar-validate.js";
export { BUILD_ID_SEED, SOURCE_HASH_SEED, hex16, lengthPrefixedStream, wyhash64 } from "./library/wyhash.js";
export { ISLAND_SURFACE, type IslandFnEntry } from "./frontend/lowering/surfaces.js";
export { ISLAND_SURFACE, STATIC_MATH_PROPS, type IslandFnEntry } from "./frontend/lowering/surfaces.js";
export { ambientDtsPath, isExactExternalTypeSpecifier, overridesDtsPath } from "./frontend/program.js";
export { resolveProvenanceSources } from "./frontend/provenance.js";
export { wasiGuestPath, type HostPathFlavor } from "./wasi-paths.js";
@@ -56,6 +56,7 @@ import {
BUILTIN_MODULE_FNS,
BUILTIN_MODULE_FN_ALIASES,
STATIC_MATH_FNS,
STATIC_MATH_PROPS,
STATIC_NUMBER_METHODS,
STR_METHODS,
} from "../frontend/lowering/surfaces.js";
@@ -165,6 +166,7 @@ function fenceTaxonomy(): FenceTaxonomy {
for (const [mod, members] of Object.entries(BUILTIN_MODULE_CONSTS)) {
for (const member of Object.keys(members!)) foldedIds.add(`${builtinRootId(mod)}.${member}`);
}
for (const member of Object.keys(STATIC_MATH_PROPS)) foldedIds.add(`stdlib.math.${member}`);
const ambientFns = new Map(AMBIENT_SURFACE_FNS.map((row) => [row.id, row.fns as readonly string[]]));
taxonomy = { byId, ids: manifest.entries.map((e) => e.id), builtinFns, builtinRoots, foldedIds, ambientFns };
return taxonomy;
+4 -4
View File
@@ -2560,15 +2560,15 @@
"id": "stdlib.math.E",
"kind": "stdlib",
"name": "Math.E",
"status": "dynamic-only",
"code": "SC2012"
"status": "static",
"note": "the constant read compiles to a numeric literal; no runtime Math read is performed"
},
{
"id": "stdlib.math.PI",
"kind": "stdlib",
"name": "Math.PI",
"status": "dynamic-only",
"code": "SC2012"
"status": "static",
"note": "the constant read compiles to a numeric literal; no runtime Math read is performed"
},
{
"id": "stdlib.math.abs",
+10
View File
@@ -0,0 +1,10 @@
// Math.PI and Math.E are compile-time numeric constants: they remain usable
// in static arithmetic and through static number operations.
const pi = Math.PI;
const e = Math.E;
console.log(Math.PI, Math.E);
console.log(pi + e, pi - e, -pi, -e);
console.log(Math.PI * 2, Math.E * 2);
console.log(pi.toFixed(6), e.toFixed(6));
console.log(Math.PI, Math.PI, Math.E, Math.E);
+1 -1
View File
@@ -1,4 +1,4 @@
// The island-backed ambient surface (Math beyond the static members,
// The island-backed ambient surface (Math beyond the static PI/E members,
// 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
@@ -7,15 +7,6 @@ dynamic-surface.ts:10:12 - error SC2012: 'Math.sqrt' runs in the embedded dynami
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
10 | const up = Math.sqrt(2);
11 | const tau = Math.PI * 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;
+23 -1
View File
@@ -26,7 +26,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, describe, expect, test } from "vitest";
import * as ts from "../../packages/compiler/src/frontend/ts7/adapter.js";
import { ambientDtsPath, ir, ISLAND_SURFACE, overridesDtsPath, type IslandFnEntry } from "@scriptc/compiler";
import { ambientDtsPath, ir, ISLAND_SURFACE, overridesDtsPath, STATIC_MATH_PROPS, type IslandFnEntry } from "@scriptc/compiler";
import { loadProgram } from "../../packages/compiler/src/frontend/program.js";
function placeholder(t: ir.IrType): string {
@@ -70,6 +70,12 @@ const propProbes = Object.entries(ISLAND_SURFACE.math.props).map(([name, propTyp
type: propType!,
}));
const staticMathPropProbes = Object.entries(STATIC_MATH_PROPS).map(([name, value]) => ({
what: `Math.${name}`,
expr: `Math.${name}`,
value: value!,
}));
// One probe program through the compiler's own loader, containing every
// table entry called in exactly the form the island lowering emits.
const dir = mkdtempSync(join(tmpdir(), "scr-island-surface-"));
@@ -85,6 +91,7 @@ writeFileSync(
(p, i) => `const __fn${i} = ${p.callee}(${p.entry.args.map(placeholder).join(", ")});`,
),
...propProbes.map((p, i) => `const __prop${i} = ${p.expr};`),
...staticMathPropProbes.map((p, i) => `const __staticProp${i} = ${p.expr};`),
].join("\n"),
);
const load = loadProgram(probePath);
@@ -169,3 +176,18 @@ describe("every ISLAND_SURFACE entry is declared standard-library surface", () =
).toBe(resultText(probe.type));
});
});
describe("static Math properties", () => {
test("PI and E are declared in the static table, not the dynamic island table", () => {
expect(Object.keys(ISLAND_SURFACE.math.props)).not.toEqual(expect.arrayContaining(["PI", "E"]));
expect(STATIC_MATH_PROPS).toEqual({ PI: 3.141592653589793, E: 2.718281828459045 });
});
test.for(staticMathPropProbes.map((p, i) => [p.what, p, i] as const))("%s is a number", ([, probe, i]) => {
const decl = decls.get(`__staticProp${i}`);
expect(decl, `${probe.what}: probe declaration missing`).toBeDefined();
expect(isStdlibDeclared(calleeSymbol(decl!)), `${probe.what}: no standard-library declaration`).toBe(true);
expect(checker.typeToString(checker.getTypeAtLocation(decl!.name))).toBe("number");
expect(probe.value).toBeTypeOf("number");
});
});
+16
View File
@@ -546,6 +546,22 @@ describe("library profile fences", () => {
if (!r.ok) return;
expect(r.profile.fences[0]!.surfaces.map((s) => s.id)).not.toContain("node-builtin.os.EOL");
expect(r.profile.fences[0]!.surfaces.map((s) => s.id)).toContain("node-builtin.os.homedir");
// Math.PI is the same kind of per-binary numeric literal. An exact fence
// must refuse because there is no runtime read to deny.
expectSc4001(
{ ...good, determinism: { fences: [{ id: "stdlib.math.PI" }] } },
"fold",
);
const math = loadLibraryProfile(
writeProfile({ ...good, determinism: { fences: [{ prefix: "stdlib.math." }] } }),
);
expect(math.ok).toBe(true);
if (!math.ok) return;
const mathSurfaces = math.profile.fences[0]!.surfaces;
expect(mathSurfaces.map((s) => s.id)).not.toContain("stdlib.math.PI");
expect(mathSurfaces.map((s) => s.id)).not.toContain("stdlib.math.E");
expect(mathSurfaces.find((s) => s.id === "stdlib.math.random")?.detector).toBeDefined();
});
test("a desugared surface no detector can police refuses, id and prefix alike", () => {
+2 -1
View File
@@ -140,10 +140,11 @@ const PROBES: Probe[] = [
{ id: "node-builtin.perf_hooks.performance.now", source: "console.log(performance.now() >= 0);\n" },
{ id: "node-builtin.path.join", source: 'import { join } from "node:path";\nconsole.log(join("a", "b"));\n' },
{ id: "node-builtin.os.EOL", source: 'import { EOL } from "node:os";\nconsole.log(EOL.length);\n' },
{ id: "stdlib.math.PI", source: "console.log(Math.PI);\n" },
{ 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.sqrt", source: "console.log(Math.sqrt(2));\n" },
{ id: "stdlib.math.PI", source: "console.log(Math.PI);\n" },
{ id: "stdlib.string.replace", source: 'console.log("aa".replace("a", "b"));\n' },
{
id: "stdlib.headers.entries",