From 0006db21c02b9bb92af1c85f17623d5e853bdcc9 Mon Sep 17 00:00:00 2001 From: Jeff Wainwright <1074042+yowainwright@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:01:25 -0700 Subject: [PATCH] fix(compiler): lower Math.PI and Math.E statically --- .../compiler/src/coverage/surface-manifest.ts | 4 ++ .../src/frontend/lowering/lower-island.ts | 10 ++-- .../src/frontend/lowering/surfaces.ts | 10 +++- packages/compiler/surface-manifest.json | 6 +- tests/corpus/1538-math-static-scalar.ts | 8 +++ tests/diagnostics/dynamic-surface.ts | 15 ++--- .../__snapshots__/dynamic-surface.ts.txt | 58 ++++++++----------- tests/harness/surface-manifest.test.ts | 3 +- 8 files changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index c35f9298b..86103e4bc 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -53,6 +53,7 @@ import { SET_COMBINE_METHODS, SET_METHODS, STATIC_MATH_FNS, + STATIC_MATH_PROPS, STATIC_NUMBER_METHODS, STR_METHODS, UNSUPPORTED_EXPR, @@ -222,6 +223,9 @@ 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(STATIC_MATH_PROPS)) { + add({ id: `stdlib.math.${name}`, kind: "stdlib", name: `Math.${name}`, status: "static" }); + } 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" }); } diff --git a/packages/compiler/src/frontend/lowering/lower-island.ts b/packages/compiler/src/frontend/lowering/lower-island.ts index d1a57ff61..42723f06e 100644 --- a/packages/compiler/src/frontend/lowering/lower-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-island.ts @@ -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,14 @@ 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). */ 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 staticPropValue = own(STATIC_MATH_PROPS, member); + if (staticPropValue !== undefined) { + return { kind: "numLit", value: staticPropValue, type: F64, loc }; + } const propType = own(ISLAND_SURFACE.math.props, member); if (propType !== undefined) { lowerer.requireDynamicApi(`'Math.${member}'`, expr); diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 03b58d891..0c88dd664 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -462,8 +462,7 @@ export const boundaryOutOfIslandMsg = (typeName: string): string => * own declarations — a drifted entry fails a test instead of surprising a * user. */ export const ISLAND_SURFACE = { - /** `Math.(...)` lowers to callMethod(globalGet("Math"), fn, args); - * the readonly number props (`Math.PI`) to getProp(globalGet("Math")). + /** `Math.(...)` lowers to callMethod(globalGet("Math"), fn, args). * min/max/atan2/hypot/pow are declared with exactly two parameters * (rest/optional parameters aren't representable). */ math: { @@ -476,7 +475,7 @@ export const ISLAND_SURFACE = { round: ISL_N1, sign: ISL_N1, sin: ISL_N1, sqrt: ISL_N1, tan: ISL_N1, trunc: ISL_N1, } as Record, - props: { PI: F64, E: F64 } as Record, + props: {} as Record, }, /** Methods on `number` receivers. The receiver marshals by value; the * engine auto-boxes primitives on method calls, so `this` binds the @@ -510,6 +509,11 @@ export const ISLAND_SURFACE = { } as Record, }; +export const STATIC_MATH_PROPS: Record = { + E: Math.E, + PI: Math.PI, +}; + /** 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 diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index ef28a9f01..e98dcbf7b 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -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", diff --git a/tests/corpus/1538-math-static-scalar.ts b/tests/corpus/1538-math-static-scalar.ts index dbdf76841..a249f2a75 100644 --- a/tests/corpus/1538-math-static-scalar.ts +++ b/tests/corpus/1538-math-static-scalar.ts @@ -5,6 +5,9 @@ // this whole program compiles statically. console.log(Math.min(2, -9), Math.max(2, -9), Math.min(1.5, 1.5), Math.max(-3, -3)); console.log(Math.min(Infinity, 7), Math.max(-Infinity, 7), Math.min(-Infinity, Infinity)); +const pi = Math.PI; +const e = Math.E; +console.log(pi.toFixed(12), e.toFixed(12), (pi + e).toFixed(12), (pi * e).toFixed(12)); const nan = 0 / 0; // NaN-as-a-value stays fenced; the arithmetic form compiles console.log(Math.min(nan, 1), Math.max(1, nan), Math.min(nan, nan)); // ±0: detect the sign through division (String(-0) is "0" either way). @@ -30,3 +33,8 @@ console.log(ok, low < 0.5, high >= 0.5, low !== high); const a = Math.random(); const b = Math.random(); console.log(typeof a, a === b); + +{ + const Math = { PI: 4, E: 3 }; + console.log(Math.PI, Math.E); +} diff --git a/tests/diagnostics/dynamic-surface.ts b/tests/diagnostics/dynamic-surface.ts index ad7cb07f6..b044ba833 100644 --- a/tests/diagnostics/dynamic-surface.ts +++ b/tests/diagnostics/dynamic-surface.ts @@ -1,15 +1,10 @@ -// The island-backed ambient surface (Math beyond the static 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 -// flag — never an ICE, never a link error. (Math.floor/abs/round and the -// ask-4 wholeness-discharge pair Math.trunc/ceil, .split(string), the -// 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 price = (19.99).toPrecision(4); const swapped = "banana".replace("an", "AN"); const ch = "hello".at(0); const n = Number.parseFloat("3.14"); +void up; +void price; +void swapped; +void ch; +void n; diff --git a/tests/harness/__snapshots__/dynamic-surface.ts.txt b/tests/harness/__snapshots__/dynamic-surface.ts.txt index 47310acd2..80a213f20 100644 --- a/tests/harness/__snapshots__/dynamic-surface.ts.txt +++ b/tests/harness/__snapshots__/dynamic-surface.ts.txt @@ -1,53 +1,43 @@ -dynamic-surface.ts:10:12 - error SC2012: 'Math.sqrt' runs in the embedded dynamic engine, which this build does not include +dynamic-surface.ts:1:12 - error SC2012: 'Math.sqrt' runs in the embedded dynamic engine, which this build does not include - 9 | // appear here.) - 10 | const up = Math.sqrt(2); - | ^~~~~~~~~~~~ - 11 | const tau = Math.PI * 2; + 1 | const up = Math.sqrt(2); + | ^~~~~~~~~~~~ + 2 | 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:11:13 - error SC2012: 'Math.PI' runs in the embedded dynamic engine, which this build does not include +dynamic-surface.ts:2:15 - error SC2012: '.toPrecision()' on numbers 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); + 1 | const up = Math.sqrt(2); + 2 | const price = (19.99).toPrecision(4); + | ^~~~~~~~~~~~~~~~~~~~~~ + 3 | const swapped = "banana".replace("an", "AN"); 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 +dynamic-surface.ts:3:17 - error SC2012: '.replace()' on strings runs in the embedded dynamic engine, which this build does not include - 11 | const tau = Math.PI * 2; - 12 | const price = (19.99).toPrecision(4); - | ^~~~~~~~~~~~~~~~~~~~~~ - 13 | const swapped = "banana".replace("an", "AN"); + 2 | const price = (19.99).toPrecision(4); + 3 | const swapped = "banana".replace("an", "AN"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 4 | const ch = "hello".at(0); 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:13:17 - error SC2012: '.replace()' on strings runs in the embedded dynamic engine, which this build does not include +dynamic-surface.ts:4:12 - error SC2012: '.at()' on strings runs in the embedded dynamic engine, which this build does not include - 12 | const price = (19.99).toPrecision(4); - 13 | const swapped = "banana".replace("an", "AN"); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 14 | const ch = "hello".at(0); + 3 | const swapped = "banana".replace("an", "AN"); + 4 | const ch = "hello".at(0); + | ^~~~~~~~~~~~~ + 5 | const n = Number.parseFloat("3.14"); 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:14:12 - error SC2012: '.at()' on strings runs in the embedded dynamic engine, which this build does not include +dynamic-surface.ts:5:11 - error SC2012: 'Number.parseFloat' runs in the embedded dynamic engine, which this build does not include - 13 | const swapped = "banana".replace("an", "AN"); - 14 | const ch = "hello".at(0); - | ^~~~~~~~~~~~~ - 15 | const n = Number.parseFloat("3.14"); - - 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:15:11 - error SC2012: 'Number.parseFloat' runs in the embedded dynamic engine, which this build does not include - - 14 | const ch = "hello".at(0); - 15 | const n = Number.parseFloat("3.14"); - | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 16 | + 4 | const ch = "hello".at(0); + 5 | const n = Number.parseFloat("3.14"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~ + 6 | void up; hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it \ No newline at end of file diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index 6866e6933..2cb851cbc 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -115,6 +115,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.E", source: "console.log(Math.E);\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" }, @@ -142,7 +144,6 @@ const PROBES: Probe[] = [ // 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",