mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
Lower Math.PI and Math.E statically (#279)
Co-authored-by: yowainwright <1074042+yowainwright@users.noreply.github.com>
This commit is contained in:
co-authored by
yowainwright
parent
1cec922d95
commit
1071b87d77
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user