mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Merge main
This commit is contained in:
+40
-2
@@ -6,6 +6,46 @@ All notable changes to scriptc will be documented in this file.
|
||||
|
||||
<!-- release:start -->
|
||||
|
||||
## 0.4.6
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Node islands:** programs can load Node.js in-process on first use and run npm packages that scriptc doesn't compile in Node islands.
|
||||
- Native values, closures and promises pass into islands by reference, and island handles, classes and collections can be used from compiled code.
|
||||
- Modules are shared between a program and its islands, and island modules are evaluated only when the program uses them.
|
||||
- Once Node loads, the runtime's event loop runs on Node's libuv, and programs exit cleanly with Node loaded.
|
||||
- **New options:**
|
||||
- `--exceptions=rare` unwinds through recursive functions instead of checking a status after each call.
|
||||
- `--static` builds static Linux executables (opt-in).
|
||||
- `--large-pages` moves executable code onto huge pages; `--cpu-dispatch` adds x86-64-v3 copies of hot functions.
|
||||
- Type patches replace a package's declaration files or supply missing declarations.
|
||||
- The shape overlay types JavaScript from facts the compiler proves (opt-in).
|
||||
- **Faster programs:**
|
||||
- Exceptions raise through cached unwind plans on x86-64 Linux, and every native function has an unwind table entry.
|
||||
- Ordinary objects are stored as a layout plus value slots; unions of classes are the instance pointer itself; class instances that stay in their frame use frame storage.
|
||||
- Object groups that die together are allocated in arenas, and the cycle collector does cheaper trial deletion and adapts its nursery trigger.
|
||||
- The speed runtime is optimized with a committed runtime profile, and Math functions are computed as V8 does, with libm linked only when needed.
|
||||
- Loops: short constant-count loops run together, for and while loops are versioned on the ranges their locals enter with, counted loops unroll by the period of a guard, and small leaf functions called from loops are inlined.
|
||||
- Typed arrays: header fields are read once at a speculative loop's entry, products under bitwise operators stay in 32 bits, and shared typed-array elements are read and written with plain accesses at a counted loop's counter.
|
||||
- Strings: UTF-16 indexes of long non-ASCII strings map in constant time, ASCII case converts eight bytes at a time, template literals allocate at their exact length, and strings iterate over their bytes.
|
||||
- Untyped code: class instance fields are read inline at dynamic property sites with two inline-cache entries, native arrays iterate without type-name scans, and numbers pass to directly called untyped functions as JavaScript values.
|
||||
- I/O: fs thread-pool completions and pool-thread writes make fewer syscalls; sockets, clients and agent entries register in constant time; fetch() reuses keep-alive connections; web streams, WritableStream and TransformStream run natively.
|
||||
- Large executables map their text with 2 MiB pages, and freed heap pages are returned to the system after the frontend is disposed.
|
||||
- **Faster builds:**
|
||||
- Small release and speed programs compile in balanced partitions, the entry module's top-level code gets its own partition, and unchanged partitions stay cached across edits.
|
||||
- Optimized executables that stay one partition generate code concurrently, and emission's workers stay busy.
|
||||
- The native compiler is laid out by context-sensitive profiles collected in two phases.
|
||||
- Type checking uses a shared tsc-ts server by default on Linux and macOS, and lib diagnostics are reused across options and module sets that cannot change them.
|
||||
- Rebuilds skip the helper version probe and the digests of unchanged files, and the CLI links without huge text pages, so cold builds use less memory.
|
||||
- **Node.js parity:**
|
||||
- `Error.captureStackTrace`, Node's navigator, builtin globals read through the global object, and unhandled rejections reported in programs without async work.
|
||||
- node:zlib is built on the zlib Node bundles; `Response.clone` and `Request.clone` work on typed values; fetch bodies have the members of a web ReadableStream; redirects whose chunked body has not ended are followed.
|
||||
- Classes that extend Array compile; strings spread into their code points; frozen checked records stay frozen; sloppy-mode `this` is the global object.
|
||||
- `util.format` runtime format strings, `inspect.custom` and colors, and untyped `promisify`.
|
||||
- **TypeScript:** scriptc reads TypeScript 7.1's binary AST format and API server beside 7.0's, and tsc-ts is updated to 0.0.10.
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.4.5
|
||||
|
||||
### Highlights
|
||||
@@ -40,8 +80,6 @@ All notable changes to scriptc will be documented in this file.
|
||||
- `util.inspect` of dynamic classes, functions and `%o` arguments, and `Symbol.toStringTag` fields and getters, match Node.
|
||||
- **npm packages:** more npm and CommonJS dependencies compile statically, including namespace-style packages with guarded declarations and `var` exports, and untyped Maps, Sets and arrays are typed from their uses.
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.4.4
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code.
|
||||
`Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs.
|
||||
`Object.freeze`, `Object.seal`, and `Object.preventExtensions` of a class instance held in an untyped value change the instance itself, so writes through its typed references are checked the same way.
|
||||
A typed record checked from a frozen untyped object (an object literal frozen in a JavaScript file, or a frozen `JSON.parse` result) is frozen too, and writes through its typed references are checked the same way.
|
||||
The `TypeError` for a write to a read-only property that was added to a class instance through an untyped value names `#<Object>` where Node names the class.
|
||||
Functions support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible`; the operations apply to the function's own properties (`name`, `length`, `prototype`, and the ones a program assigns).
|
||||
Class constructors are not supported.
|
||||
|
||||
@@ -5,3 +5,5 @@
|
||||
- **JavaScript parameters:** In a JavaScript file TypeScript does not check, a prototype method (`C.prototype.m = function (...) {}`) takes whatever values its callers pass; its JSDoc types document the usual values without constraining them.
|
||||
A sloppy-mode function with named parameters may read `arguments` only where the aliasing of its parameters cannot show: it reads `arguments.length`, or reads elements and never writes a parameter or an element.
|
||||
Other uses of `arguments` in such a function are refused with `SC1090`; ES modules, `'use strict'` code, classes, and functions with default, rest, or destructured parameters have no such restriction.
|
||||
A sloppy-mode function called with `undefined` or `null` as its receiver gets the global object as `this`, as in Node.
|
||||
Called with a primitive receiver, its `this` is the primitive itself, where Node passes the primitive's wrapper object (`typeof this` is `"object"` there).
|
||||
|
||||
@@ -127,6 +127,9 @@ TargetOptions targetOptions(const Triple &TargetTriple) {
|
||||
// The Android API 26 runtime pack uses the NDK's emulated TLS ABI.
|
||||
// Program globals and runtime thread instances must use the same ABI.
|
||||
Options.EmulatedTLS = TargetTriple.isAndroid();
|
||||
// Program constructors go in .init_array, which the C runtime's startup
|
||||
// files run on ELF, as runtime units' constructors do (runtime-unit.cpp).
|
||||
Options.UseInitArray = TargetTriple.isOSBinFormatELF();
|
||||
if (ProfileLayout && TargetTriple.isOSBinFormatELF()) {
|
||||
Options.FunctionSections = true;
|
||||
Options.EnableMachineFunctionSplitter = true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-arm64",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "Native scriptc compiler for darwin-arm64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-x64",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "Native scriptc compiler for darwin-x64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-gnu",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "Native scriptc compiler for linux-arm64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-musl",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "Native scriptc compiler for linux-arm64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-gnu",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "Native scriptc compiler for linux-x64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-musl",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "Native scriptc compiler for linux-x64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-win32-x64-msvc",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "Native scriptc compiler for win32-x64-msvc",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scriptc",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
+57
-8
@@ -387,9 +387,43 @@ declare module "node:fs" {
|
||||
readonly ctime: Date;
|
||||
readonly birthtime: Date;
|
||||
}
|
||||
/* Node's BigIntStats: the same snapshot under the bigint member table.
|
||||
* Every number member of Stats — including the *Ms millisecond members —
|
||||
* answers a bigint, and the *Ns nanosecond members sit beside them; the
|
||||
* four time members stay Dates. */
|
||||
export interface BigIntStats {
|
||||
isFile(): boolean;
|
||||
isDirectory(): boolean;
|
||||
isSymbolicLink(): boolean;
|
||||
isBlockDevice(): boolean;
|
||||
isCharacterDevice(): boolean;
|
||||
isFIFO(): boolean;
|
||||
isSocket(): boolean;
|
||||
readonly dev: bigint;
|
||||
readonly mode: bigint;
|
||||
readonly nlink: bigint;
|
||||
readonly uid: bigint;
|
||||
readonly gid: bigint;
|
||||
readonly rdev: bigint;
|
||||
readonly blksize: bigint;
|
||||
readonly ino: bigint;
|
||||
readonly size: bigint;
|
||||
readonly blocks: bigint;
|
||||
readonly atimeMs: bigint;
|
||||
readonly mtimeMs: bigint;
|
||||
readonly ctimeMs: bigint;
|
||||
readonly birthtimeMs: bigint;
|
||||
readonly atimeNs: bigint;
|
||||
readonly mtimeNs: bigint;
|
||||
readonly ctimeNs: bigint;
|
||||
readonly birthtimeNs: bigint;
|
||||
readonly atime: Date;
|
||||
readonly mtime: Date;
|
||||
readonly ctime: Date;
|
||||
readonly birthtime: Date;
|
||||
}
|
||||
/* throwIfNoEntry: false answers undefined for a missing entry (ENOENT or
|
||||
* ENOTDIR) instead of throwing. BigIntStats (bigint: true) has no
|
||||
* lowering. */
|
||||
* ENOTDIR) instead of throwing. bigint: true answers BigIntStats. */
|
||||
export interface StatSyncOptions {
|
||||
bigint?: boolean;
|
||||
throwIfNoEntry?: boolean;
|
||||
@@ -399,24 +433,38 @@ declare module "node:fs" {
|
||||
path: string | URL,
|
||||
options: StatSyncOptions & { bigint?: false; throwIfNoEntry: false },
|
||||
): Stats | undefined;
|
||||
export function statSync(
|
||||
path: string | URL,
|
||||
options: StatSyncOptions & { bigint: true; throwIfNoEntry: false },
|
||||
): BigIntStats | undefined;
|
||||
export function statSync(
|
||||
path: string | URL,
|
||||
options: StatSyncOptions & { bigint?: false; throwIfNoEntry?: true },
|
||||
): Stats;
|
||||
/* bigint: true type-checks so the lowering can name it as unsupported. */
|
||||
export function statSync(path: string | URL, options: StatSyncOptions & { bigint: true }): Stats;
|
||||
export function statSync(
|
||||
path: string | URL,
|
||||
options: StatSyncOptions & { bigint: true; throwIfNoEntry?: true },
|
||||
): BigIntStats;
|
||||
/* A runtime-valued `bigint` is refused at lowering, so the catch-all keeps
|
||||
* the Stats answer the other reachable forms give. */
|
||||
export function statSync(path: string | URL, options: StatSyncOptions): Stats | undefined;
|
||||
export function lstatSync(path: string | URL, options?: undefined): Stats;
|
||||
export function lstatSync(
|
||||
path: string | URL,
|
||||
options: StatSyncOptions & { bigint?: false; throwIfNoEntry: false },
|
||||
): Stats | undefined;
|
||||
export function lstatSync(
|
||||
path: string | URL,
|
||||
options: StatSyncOptions & { bigint: true; throwIfNoEntry: false },
|
||||
): BigIntStats | undefined;
|
||||
export function lstatSync(
|
||||
path: string | URL,
|
||||
options: StatSyncOptions & { bigint?: false; throwIfNoEntry?: true },
|
||||
): Stats;
|
||||
/* bigint: true type-checks so the lowering can name it as unsupported. */
|
||||
export function lstatSync(path: string | URL, options: StatSyncOptions & { bigint: true }): Stats;
|
||||
export function lstatSync(
|
||||
path: string | URL,
|
||||
options: StatSyncOptions & { bigint: true; throwIfNoEntry?: true },
|
||||
): BigIntStats;
|
||||
export function lstatSync(path: string | URL, options: StatSyncOptions): Stats | undefined;
|
||||
/* The fd pair behind spawn's fd-stdio form: openSync(path, flags) →
|
||||
* the raw fd. Numeric flags use an inline OR of the O_* constants. */
|
||||
@@ -498,8 +546,9 @@ declare module "node:fs" {
|
||||
position?: number | null,
|
||||
encoding?: "utf8" | "utf-8",
|
||||
): number;
|
||||
/* statSync over an open fd — the same Stats snapshot. */
|
||||
export function fstatSync(fd: number): Stats;
|
||||
/* statSync over an open fd — the same snapshot. */
|
||||
export function fstatSync(fd: number, options?: StatSyncOptions & { bigint?: false }): Stats;
|
||||
export function fstatSync(fd: number, options: StatSyncOptions & { bigint: true }): BigIntStats;
|
||||
/* realpath(3) — resolves symlinks, `.`/`..`, throwing Node's fs error
|
||||
* shapes for missing paths. */
|
||||
export function realpathSync(path: string | URL): string;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <poll.h>
|
||||
#include <time.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
@@ -102,7 +103,9 @@ static int child_main(int argc, char **argv) {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (strcmp(mode, "echo") != 0) return 73;
|
||||
// "slow-echo" answers each read 20 ms later: after the client's poll.
|
||||
int slow = strcmp(mode, "slow-echo") == 0;
|
||||
if (!slow && strcmp(mode, "echo") != 0) return 73;
|
||||
// Unbuffered reads let short writes make progress without waiting for a
|
||||
// complete stdio buffer. Fragment each echoed reply independently.
|
||||
uint8_t bytes[17];
|
||||
@@ -113,6 +116,14 @@ static int child_main(int argc, char **argv) {
|
||||
ssize_t count = read(STDIN_FILENO, bytes, sizeof(bytes));
|
||||
#endif
|
||||
if (count <= 0) return count < 0 ? 74 : 0;
|
||||
if (slow) {
|
||||
#ifdef _WIN32
|
||||
Sleep(20);
|
||||
#else
|
||||
struct timespec delay = {0, 20000000L};
|
||||
nanosleep(&delay, NULL);
|
||||
#endif
|
||||
}
|
||||
if (fwrite(bytes, 1, (size_t)count, stdout) != (size_t)count || fflush(stdout) != 0) return 75;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,6 +552,41 @@ static int valid_range(const uint8_t *bytes, size_t size, uint32_t offset, uint3
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
/* How long a read polls the socket before it blocks. The server answers
|
||||
* most requests within a few microseconds; a blocked reader that the reply
|
||||
* wakes takes tens of microseconds more per round trip. */
|
||||
#define TS7_REPLY_POLL_NS 200000L
|
||||
|
||||
static long monotonic_ns(void) {
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return now.tv_sec * 1000000000L + now.tv_nsec;
|
||||
}
|
||||
|
||||
/* recv on the server's socket: polls it for TS7_REPLY_POLL_NS, then waits. */
|
||||
static ssize_t ts7_recv(int socket, uint8_t *bytes, size_t length) {
|
||||
long started = -1;
|
||||
for (;;) {
|
||||
ssize_t count = recv(socket, bytes, length, MSG_DONTWAIT);
|
||||
if (count >= 0) return count;
|
||||
if (errno == EINTR) continue;
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) return -1;
|
||||
long now = monotonic_ns();
|
||||
if (started < 0) started = now;
|
||||
else if (now - started >= TS7_REPLY_POLL_NS) break;
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
__builtin_ia32_pause();
|
||||
#elif defined(__aarch64__)
|
||||
__asm__ volatile("yield");
|
||||
#endif
|
||||
}
|
||||
ssize_t count;
|
||||
do { count = recv(socket, bytes, length, 0); } while (count < 0 && errno == EINTR);
|
||||
return count;
|
||||
}
|
||||
#endif
|
||||
|
||||
int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size,
|
||||
uint32_t offset, uint32_t length) {
|
||||
Ts7Process *p = find_process(handle);
|
||||
@@ -565,8 +600,7 @@ int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size,
|
||||
if (code == ERROR_BROKEN_PIPE) return 0;
|
||||
system_error(code); return -1;
|
||||
#else
|
||||
ssize_t count;
|
||||
do { count = recv(p->socket, bytes + offset, length, 0); } while (count < 0 && errno == EINTR);
|
||||
ssize_t count = ts7_recv(p->socket, bytes + offset, length);
|
||||
if (count < 0) { system_error(errno); return -1; }
|
||||
return (int32_t)count;
|
||||
#endif
|
||||
|
||||
@@ -149,6 +149,20 @@ static void exchange_cases(void) {
|
||||
scriptc_ts7_close(newer);
|
||||
}
|
||||
|
||||
/* A reply that arrives after the read's poll window: the read waits for it. */
|
||||
static void slow_reply_cases(void) {
|
||||
uint32_t id = start("slow-echo", 0);
|
||||
assert(id != 0);
|
||||
const uint8_t bytes[3] = {1, 2, 3};
|
||||
uint8_t result[3] = {0};
|
||||
for (unsigned round = 0; round < 3; round++) {
|
||||
write_exact(id, bytes, sizeof(bytes), 0, sizeof(bytes));
|
||||
read_exact(id, result, sizeof(result));
|
||||
assert(memcmp(bytes, result, sizeof(bytes)) == 0);
|
||||
}
|
||||
scriptc_ts7_close(id);
|
||||
}
|
||||
|
||||
static void failure_cases(void) {
|
||||
uint8_t bytes[1] = {42};
|
||||
const char *saved = executable;
|
||||
@@ -289,6 +303,7 @@ static int process_main(int argc, char **argv) {
|
||||
unsigned before = resource_count();
|
||||
argument_cases();
|
||||
exchange_cases();
|
||||
slow_reply_cases();
|
||||
failure_cases();
|
||||
#ifndef _WIN32
|
||||
attach_cases();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/compiler",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -53,6 +53,7 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
emitter: features.emitter,
|
||||
symbol: features.symbol,
|
||||
bigint: features.bigint,
|
||||
...(features.bigstats ? { bigstats: true } : {}),
|
||||
searchParams: features.searchParams,
|
||||
qs: features.qs,
|
||||
utilPromisify: features.utilPromisify,
|
||||
|
||||
@@ -48,6 +48,8 @@ export async function createNativeLinkPlan(options: {
|
||||
windowsSubsystem?: WindowsSubsystem;
|
||||
profileGenerate?: boolean;
|
||||
profileLayout?: boolean;
|
||||
/** `false` keeps 4 KiB segment alignment for large program code. */
|
||||
hugeText?: boolean;
|
||||
programObjectDependencies?: readonly NativeArtifactDependency[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
resolver?: (specifier: string) => string;
|
||||
@@ -55,6 +57,9 @@ export async function createNativeLinkPlan(options: {
|
||||
if (options.ffi?.frameworks?.length && options.target.platform !== "darwin")
|
||||
throw new Error("FFI frameworks require a Darwin target");
|
||||
const runtimePack = await loadRuntimePack(options);
|
||||
const hugeText =
|
||||
options.hugeText !== false &&
|
||||
hugeTextProgram(options.target, [options.programObject, ...(options.programPartitions ?? [])]);
|
||||
return {
|
||||
target: options.target,
|
||||
darwinDebugSymbols: needsDarwinDebugSymbols(
|
||||
@@ -88,11 +93,6 @@ export async function createNativeLinkPlan(options: {
|
||||
programObjectDependencies: [...(options.programObjectDependencies ?? [])],
|
||||
runtimePack,
|
||||
...(options.staticLink === true ? { staticLink: true } : {}),
|
||||
...(hugeTextProgram(options.target, [
|
||||
options.programObject,
|
||||
...(options.programPartitions ?? []),
|
||||
])
|
||||
? { hugeText: true }
|
||||
: {}),
|
||||
...(hugeText ? { hugeText: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2889,6 +2889,21 @@ export class LlDyn {
|
||||
B.line(`call void @scr_str_release(ptr ${ek})`);
|
||||
});
|
||||
}
|
||||
// A record built from a frozen object is frozen too, as the object it
|
||||
// copies (frozen-records.ts makes such shapes freezable).
|
||||
if (shape.freezable) {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_checked_record_frozen(ptr)`);
|
||||
host.declare(`declare void @scr_record_freeze(ptr)`);
|
||||
const frozen = B.tmp();
|
||||
const lFreeze = B.newLabel("dcr.freeze");
|
||||
const lDone = B.newLabel("dcr.frozen");
|
||||
B.line(`${frozen} = call i1 @scr_dyn_checked_record_frozen(ptr %d)`);
|
||||
B.condBr(frozen, lFreeze, lDone);
|
||||
B.startBlock(lFreeze);
|
||||
B.line(`call void @scr_record_freeze(ptr %r0)`);
|
||||
B.br(lDone);
|
||||
B.startBlock(lDone);
|
||||
}
|
||||
B.terminate(`ret ptr %r0`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ import {
|
||||
} from "../../ir/inline-arrays.js";
|
||||
import { inlineArraysApply, programLifetimeClassOf } from "./inline-arrays.js";
|
||||
import { pairRowSums } from "../../ir/row-pairs.js";
|
||||
import { jamSaturatingLoops } from "../../ir/saturating-loop-jams.js";
|
||||
import {
|
||||
allocatedLiteral,
|
||||
fillAllocationLength,
|
||||
@@ -763,16 +764,18 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
pairRowSums(
|
||||
inlineArraysPass(
|
||||
options,
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
jamSaturatingLoops(
|
||||
inlineArraysPass(
|
||||
options,
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -4620,6 +4623,7 @@ export class LlEmitter {
|
||||
case "url":
|
||||
case "searchParams":
|
||||
case "stats":
|
||||
case "bigintStats":
|
||||
case "fileHandle":
|
||||
case "dir":
|
||||
case "spawnRes":
|
||||
|
||||
@@ -61,9 +61,11 @@ export function emitUnionWiden(
|
||||
/** The identity union conversion (IR unionRetype). Two tagged unions
|
||||
* share the operand's box: its +1 becomes the result, so no box is built
|
||||
* and no payload is retained. The operand is a whole-value use in every
|
||||
* lifetime analysis, so it is never a stack box. A nullable-pointer union
|
||||
* on either side, or a source union whose boxes a checked-dynamic cast has
|
||||
* cached (and refreshes in place), rebuilds each listed arm instead. */
|
||||
* lifetime analysis, so it is never a stack box. Two pointer unions of one
|
||||
* encoding share the pointer the same way. A pointer union on only one
|
||||
* side, pointer unions whose encodings differ for an arm the value may
|
||||
* hold, or a source union whose boxes a checked-dynamic cast has cached (and
|
||||
* refreshes in place), rebuild each listed arm instead. */
|
||||
export function emitUnionRetype(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "unionRetype" },
|
||||
@@ -75,14 +77,15 @@ export function emitUnionRetype(
|
||||
if (from && to) {
|
||||
// Equal encodings make the pointer itself the target value. A shape
|
||||
// union derives its record arms from the record and a class union its
|
||||
// class arms from the class id, so only the unit encodings must agree.
|
||||
// class arms from the class id, so only the unit encodings must agree,
|
||||
// and only when the value may hold one of the source's unit arms: a
|
||||
// narrowing that drops `undefined` or `null` keeps the very pointer.
|
||||
const holdsUnit = e.tags.some((tag) => tag === from.unitTag || tag === from.nullTag);
|
||||
const units = !holdsUnit || (from.unitTag === to.unitTag && from.nullTag === to.nullTag);
|
||||
const same =
|
||||
from.repr === "single" && to.repr === "single"
|
||||
? from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag
|
||||
: from.repr !== "single" &&
|
||||
from.repr === to.repr &&
|
||||
from.unitTag === to.unitTag &&
|
||||
from.nullTag === to.nullTag;
|
||||
? from.refTag === to.refTag && units
|
||||
: from.repr !== "single" && from.repr === to.repr && units;
|
||||
if (same) return host.own({ name: value.name, type: e.type });
|
||||
return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
|
||||
}
|
||||
|
||||
@@ -1170,6 +1170,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "dir":
|
||||
case "watcher":
|
||||
case "stats":
|
||||
case "bigstats":
|
||||
case "zlib":
|
||||
case "atomics":
|
||||
return host.emitFilesystemLibCall(e);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { DYN, F64, STATS_T, STRING, type IrExpr } from "../../ir/ir.js";
|
||||
import { DYN, F64, STRING, type IrExpr } from "../../ir/ir.js";
|
||||
import { mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, traceArg, vAdapters } from "./shapes.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
|
||||
@@ -551,9 +551,12 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
if (e.type.kind !== "union")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
|
||||
const def = host.unionsById.get(e.type.unionId);
|
||||
const statsTag = def ? def.arms.findIndex((a) => a.kind === "stats") : -1;
|
||||
const statsTag = def
|
||||
? def.arms.findIndex((a) => a.kind === "stats" || a.kind === "bigintStats")
|
||||
: -1;
|
||||
const statsArm = def?.arms[statsTag];
|
||||
const undefTag = undefinedArmTag(e.type, host.unionsById);
|
||||
if (statsTag < 0 || undefTag < 0)
|
||||
if (statsTag < 0 || undefTag < 0 || !statsArm)
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`);
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
const throws = host.emitExpr(e.args[1]!);
|
||||
@@ -564,7 +567,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
`${raw} = call ptr @scr_fs_stat_probe(ptr ${path.name}, i1 zeroext ${noFollow}, i1 zeroext ${throws.name})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return host.wrapNullable(raw, raw, STATS_T, statsTag, e.type, undefTag);
|
||||
return host.wrapNullable(raw, raw, statsArm, statsTag, e.type, undefTag);
|
||||
}
|
||||
if (e.fn === "fs.readdirTypesSync" || e.fn === "fsp.readdirTypes") {
|
||||
// Dirent rows assembled inline from one scandir snapshot — the C
|
||||
|
||||
@@ -383,6 +383,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"process.stdioWriteDyn": "scr_process_stdio_write_dyn",
|
||||
"process.stderrWrite": "scr_process_stderr_write",
|
||||
"process.isTTY": "scr_process_is_tty",
|
||||
"process.isTTYValue": "scr_process_is_tty_value",
|
||||
// A promise that never settles (events.once of an event a synchronous
|
||||
// stream never emits); a fresh pending promise is its owned result.
|
||||
"events.pendingForever": "scr_promise_new",
|
||||
@@ -625,6 +626,35 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"stats.mtime": "scr_stats_mtime",
|
||||
"stats.ctime": "scr_stats_ctime",
|
||||
"stats.birthtime": "scr_stats_birthtime",
|
||||
"bigstats.isFile": "scr_stats_is_file",
|
||||
"bigstats.isDirectory": "scr_stats_is_dir",
|
||||
"bigstats.isSymbolicLink": "scr_stats_is_symlink",
|
||||
"bigstats.isBlockDevice": "scr_stats_is_block_device",
|
||||
"bigstats.isCharacterDevice": "scr_stats_is_character_device",
|
||||
"bigstats.isFIFO": "scr_stats_is_fifo",
|
||||
"bigstats.isSocket": "scr_stats_is_socket",
|
||||
"bigstats.size": "scr_stats_big_size",
|
||||
"bigstats.dev": "scr_stats_big_dev",
|
||||
"bigstats.ino": "scr_stats_big_ino",
|
||||
"bigstats.blocks": "scr_stats_big_blocks",
|
||||
"bigstats.nlink": "scr_stats_big_nlink",
|
||||
"bigstats.mode": "scr_stats_big_mode",
|
||||
"bigstats.uid": "scr_stats_big_uid",
|
||||
"bigstats.gid": "scr_stats_big_gid",
|
||||
"bigstats.rdev": "scr_stats_big_rdev",
|
||||
"bigstats.blksize": "scr_stats_big_blksize",
|
||||
"bigstats.atimeMs": "scr_stats_big_atime_ms",
|
||||
"bigstats.mtimeMs": "scr_stats_big_mtime_ms",
|
||||
"bigstats.ctimeMs": "scr_stats_big_ctime_ms",
|
||||
"bigstats.birthtimeMs": "scr_stats_big_birthtime_ms",
|
||||
"bigstats.atimeNs": "scr_stats_big_atime_ns",
|
||||
"bigstats.mtimeNs": "scr_stats_big_mtime_ns",
|
||||
"bigstats.ctimeNs": "scr_stats_big_ctime_ns",
|
||||
"bigstats.birthtimeNs": "scr_stats_big_birthtime_ns",
|
||||
"bigstats.atime": "scr_stats_big_atime",
|
||||
"bigstats.mtime": "scr_stats_big_mtime",
|
||||
"bigstats.ctime": "scr_stats_big_ctime",
|
||||
"bigstats.birthtime": "scr_stats_big_birthtime",
|
||||
"text.decode": "scr_text_decode",
|
||||
"bytes.bufferSource": "scr_bytes_buffer_source",
|
||||
"bytes.copyBrand": "scr_bytes_copy_brand",
|
||||
@@ -1061,7 +1091,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"nodeIsland.defer": "scr_node_island_defer",
|
||||
"nodeIsland.import": "scr_ni_import",
|
||||
"nodeIsland.get": "scr_node_island_get",
|
||||
"nodeIsland.getKey": "scr_ni_get",
|
||||
"nodeIsland.getKey": "scr_node_island_get_key",
|
||||
"nodeIsland.set": "scr_node_island_set",
|
||||
"nodeIsland.setKey": "scr_node_island_set_key",
|
||||
"nodeIsland.invoke": "scr_node_island_invoke",
|
||||
@@ -1069,6 +1099,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"nodeIsland.applyTo": "scr_node_island_apply_to",
|
||||
"nodeIsland.construct": "scr_node_island_construct",
|
||||
"nodeIsland.instanceof": "scr_node_island_instanceof",
|
||||
"nodeIsland.isArray": "scr_ni_is_array",
|
||||
"nodeIsland.classMethods": "scr_node_island_class_methods",
|
||||
"nodeIsland.subclass": "scr_node_island_subclass",
|
||||
"nodeIsland.superConstruct": "scr_node_island_super_construct",
|
||||
|
||||
@@ -682,7 +682,11 @@ export function emitBorrowedArrayRead(
|
||||
B.startBlock(slow);
|
||||
host.declare("declare ptr @scr_arr_borrow_ref(ptr, double)");
|
||||
const value = B.tmp();
|
||||
B.line(`${value} = call ptr @scr_arr_borrow_ref(ptr ${array.name}, double ${index.name})`);
|
||||
// The fallback only reads the array (a hole or an out-of-range index
|
||||
// traps), so loops keep the array's header loads hoisted past it.
|
||||
B.line(
|
||||
`${value} = call ptr @scr_arr_borrow_ref(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
|
||||
);
|
||||
B.line(`store ptr ${value}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
|
||||
@@ -143,6 +143,7 @@ export type NullableRefArm =
|
||||
| { kind: "record"; shapeId: string }
|
||||
| { kind: "string" }
|
||||
| { kind: "stats" }
|
||||
| { kind: "bigintStats" }
|
||||
| (IrType & { kind: "array" | "bytes" | "map" | "set" | "func" });
|
||||
|
||||
/** The arm is a JS object: always truthy. */
|
||||
@@ -367,6 +368,7 @@ function nullableShape(
|
||||
return { ...base, arm: { kind: "record", shapeId: arm.shapeId } };
|
||||
if (arm.kind === "string") return { ...base, arm: { kind: "string" } };
|
||||
if (arm.kind === "stats") return { ...base, arm: { kind: "stats" } };
|
||||
if (arm.kind === "bigintStats") return { ...base, arm: { kind: "bigintStats" } };
|
||||
if (
|
||||
arm.kind === "array" ||
|
||||
arm.kind === "bytes" ||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import type { LlValue } from "./expr-context.js";
|
||||
import { guardedValue } from "./borrowed-receivers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { llvmQuoted } from "../literals.js";
|
||||
import { emitBorrowedArrayRead, emitProjectedArrayRead } from "./local-array-reads.js";
|
||||
import {
|
||||
emitBorrowedMapGet,
|
||||
@@ -459,12 +460,19 @@ export function analyzeBorrowedReturns(emitter: LlEmitter): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** The alias metadata of the module's field and memory categories. Each type
|
||||
* node is named by its category (`field:<name>`, `array:header`, ...), not
|
||||
* by its number: numbers follow the order in which the module's functions
|
||||
* first use the categories, so an edit that adds one renumbers the rest. The
|
||||
* LLVM helper renumbers each program partition's metadata, so a partition
|
||||
* whose functions are unchanged keeps the same text and its cached
|
||||
* artifacts. */
|
||||
export function fieldAliasMetadata(emitter: LlEmitter): string[] {
|
||||
if (emitter.fieldAliasTags.size === 0) return [];
|
||||
const lines = ['!0 = !{!"scriptc native fields"}'];
|
||||
for (const tag of emitter.fieldAliasTags.values()) {
|
||||
for (const [category, tag] of emitter.fieldAliasTags) {
|
||||
lines.push(
|
||||
`!${tag - 1} = !{!"field.${tag}", !0, i64 0}`,
|
||||
`!${tag - 1} = !{!${llvmQuoted(category)}, !0, i64 0}`,
|
||||
`!${tag} = !{!${tag - 1}, !${tag - 1}, i64 0}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ const LEAF_KINDS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"url",
|
||||
"searchParams",
|
||||
"stats",
|
||||
"bigintStats",
|
||||
"undefinedT",
|
||||
"nullT",
|
||||
"void",
|
||||
|
||||
@@ -94,6 +94,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"dyn.compare",
|
||||
// Reads the ambient receiver (a fresh reference to the bound value).
|
||||
"dyn.this",
|
||||
// A sloppy-mode function's receiver from it (a fresh reference).
|
||||
"error.new",
|
||||
"error.nodeThrow",
|
||||
"dyn.typedRefIs",
|
||||
@@ -119,6 +120,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"process.platform",
|
||||
"process.execPath",
|
||||
"process.isTTY",
|
||||
"process.isTTYValue",
|
||||
"worker.data",
|
||||
"worker.isMainThread",
|
||||
"worker.parentPort",
|
||||
|
||||
@@ -1304,6 +1304,7 @@ function arrUsesRef(host: ShapeHost, elem: IrType): boolean {
|
||||
elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace
|
||||
elem.kind === "netServer" ||
|
||||
elem.kind === "stats" || // fs.Stats snapshots: scr_stats_* adapters, no trace
|
||||
elem.kind === "bigintStats" || // the same snapshot handle, BigIntStats reads
|
||||
elem.kind === "dyn" || // native collection seeds/drains: traced REF
|
||||
elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside)
|
||||
elem.kind === "date" || // Date objects: scr_date_* adapters, no trace (no refs inside)
|
||||
|
||||
@@ -268,3 +268,51 @@ test("record unions retype as the record pointer itself", async () => {
|
||||
// No record of the visitor is ever boxed.
|
||||
expect(ir).not.toMatch(/call ptr @scr_union_new_ref/);
|
||||
});
|
||||
|
||||
// Children that may be absent, as IR nodes have: reading one after its
|
||||
// `=== undefined` or `=== null` test narrows a shape union with a unit arm
|
||||
// to the shape union without it.
|
||||
const OPTIONAL_CHILDREN = `
|
||||
type Num = { kind: "num"; value: number };
|
||||
type Ref = { kind: "ref"; id: string };
|
||||
type Call = { kind: "call"; callee: Expr; receiver?: Expr; args: Expr[] };
|
||||
type Init = { kind: "init"; value: Expr | null };
|
||||
type Expr = Num | Ref | Call | Init;
|
||||
function count(e: Expr): number {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
case "ref":
|
||||
return 1;
|
||||
case "call":
|
||||
return (
|
||||
1 +
|
||||
count(e.callee) +
|
||||
(e.receiver === undefined ? 0 : count(e.receiver)) +
|
||||
e.args.reduce((n, a) => n + count(a), 0)
|
||||
);
|
||||
case "init":
|
||||
return 1 + (e.value === null ? 0 : count(e.value));
|
||||
}
|
||||
}
|
||||
const tree: Expr = {
|
||||
kind: "call",
|
||||
callee: { kind: "ref", id: "f" },
|
||||
receiver: { kind: "init", value: { kind: "num", value: 1 } },
|
||||
args: [{ kind: "init", value: null }],
|
||||
};
|
||||
console.log(count(tree));
|
||||
`;
|
||||
|
||||
test("dropping a shape union's unit arm keeps the record pointer", async () => {
|
||||
const { module, ir } = await llvmFor(OPTIONAL_CHILDREN, false);
|
||||
const identities = module.functions.filter(
|
||||
(fn) =>
|
||||
fn.name.startsWith("%union.retag.") &&
|
||||
JSON.stringify(fn.body).includes('"kind":"unionRetype"'),
|
||||
);
|
||||
expect(identities.length).toBeGreaterThan(0);
|
||||
for (const helper of identities) {
|
||||
const symbol = `sc_bf__x25_union_retag_${helper.name.slice("%union.retag.".length)}`;
|
||||
expect(body(ir, symbol)).not.toMatch(/retype\.built/);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,6 +103,10 @@ export interface NativeLinkFeatures {
|
||||
emitter: boolean;
|
||||
symbol: boolean;
|
||||
bigint: boolean;
|
||||
/** BigIntStats member reads (`statSync(p, { bigint: true })` members):
|
||||
* scr_stats_bigint.c. Absent (false) otherwise, so BigInt programs that
|
||||
* never read a stat member keep their link line and size. */
|
||||
bigstats?: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
utilPromisify: boolean;
|
||||
|
||||
@@ -194,6 +194,9 @@ export interface CcOptions {
|
||||
* Symbol-free binaries keep their exact link line. */
|
||||
symbol?: boolean;
|
||||
bigint?: boolean;
|
||||
/** BigIntStats member reads: compiles scr_stats_bigint.c. Every other
|
||||
* bigint program keeps its exact link line and size. */
|
||||
bigstats?: boolean;
|
||||
/** Records URLSearchParams use in the build identity. Its runtime unit is
|
||||
* part of the base sources because checked URL handles expose searchParams. */
|
||||
searchParams?: boolean;
|
||||
|
||||
@@ -153,6 +153,11 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
),
|
||||
unit((u) => u.opts.copying, "scr_copying.c"),
|
||||
unit((u) => u.opts.fileHandle, "scr_file_handle.c"),
|
||||
// BigIntStats member reads (`statSync(p, { bigint: true })` integers and
|
||||
// `*Ns`/`*Ms` members). Gated on the reads themselves (the bigstats
|
||||
// feature), not on bigint: every bigint value or wasm conversion turns
|
||||
// bigint on, and the linked getters would move their executable.
|
||||
unit((u) => u.opts.bigstats === true, "scr_stats_bigint.c"),
|
||||
// win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
|
||||
// gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares
|
||||
// them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036,
|
||||
@@ -186,7 +191,10 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
),
|
||||
unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"),
|
||||
unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"),
|
||||
unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"),
|
||||
unit(
|
||||
(u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch || u.opts.nodeIsland === true,
|
||||
"scr_dyn_invoke.c",
|
||||
),
|
||||
// Buffer.prototype methods called on checked-dynamic values.
|
||||
unit((u) => u.opts.dynBuffer === true && u.opts.dynInvoke, "scr_bytes_dyn.c"),
|
||||
// Builtin functions read as values in JavaScript dependencies.
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
RuntimePackError,
|
||||
type RuntimePackManifest,
|
||||
} from "./runtime-pack.js";
|
||||
import { HUGE_TEXT_MIN_CODE_BYTES } from "./huge-text.js";
|
||||
import { createNativeLinkPlan } from "./link-plan.js";
|
||||
import {
|
||||
executableLinkerEnvironmentFingerprint,
|
||||
@@ -266,6 +267,34 @@ describe("runtime pack manifests", () => {
|
||||
expect(staticPlan.staticLink).toBe(true);
|
||||
});
|
||||
|
||||
test("Linux links of large program code use huge text unless it is turned off", async () => {
|
||||
const { packagePath, root } = await fixture(LINUX_X64_GNU_TARGET);
|
||||
// An ELF64 relocatable object's headers with one executable section of
|
||||
// HUGE_TEXT_MIN_CODE_BYTES; only the headers are read.
|
||||
const object = Buffer.alloc(64 + 2 * 64);
|
||||
object.writeUInt32BE(0x7f454c46, 0);
|
||||
object[4] = 2;
|
||||
object[5] = 1;
|
||||
object.writeBigUInt64LE(64n, 40);
|
||||
object.writeUInt16LE(64, 58);
|
||||
object.writeUInt16LE(2, 60);
|
||||
object.writeUInt32LE(1, 128 + 4); // SHT_PROGBITS
|
||||
object.writeBigUInt64LE(0x6n, 128 + 8); // SHF_ALLOC | SHF_EXECINSTR
|
||||
object.writeBigUInt64LE(BigInt(HUGE_TEXT_MIN_CODE_BYTES), 128 + 32);
|
||||
await writeFile(join(root, "program.o"), object);
|
||||
const options = {
|
||||
target: LINUX_X64_GNU_TARGET,
|
||||
programObject: join(root, "program.o"),
|
||||
outPath: join(root, "program"),
|
||||
features: BASE,
|
||||
ffi: null,
|
||||
optimization: "release" as const,
|
||||
resolver: () => packagePath,
|
||||
};
|
||||
expect((await createNativeLinkPlan(options)).hugeText).toBe(true);
|
||||
expect((await createNativeLinkPlan({ ...options, hugeText: false })).hugeText).toBeUndefined();
|
||||
});
|
||||
|
||||
test("selection chooses the most-specific variant and feature archive", async () => {
|
||||
const { packagePath } = await fixture();
|
||||
const resolver = () => packagePath;
|
||||
|
||||
@@ -57,6 +57,12 @@ export interface CompileBaseOptions {
|
||||
* at startup when the system allows them. The moved code stays resident.
|
||||
* Executables only; no effect on other platforms. */
|
||||
largePages?: boolean;
|
||||
/** Internal: `false` links a Linux executable with 4 KiB segment alignment
|
||||
* even when its program code is large enough for text on huge pages
|
||||
* (backend/huge-text.ts). The scriptc CLI is linked this way
|
||||
* (scripts/build-native-cli.mts): most of its runs are short, and a short
|
||||
* run maps every 2 MiB text window it touches whole. */
|
||||
hugeText?: false;
|
||||
/** How often the program throws (`--exceptions`). `rare` puts recursive
|
||||
* functions and those near a `throw` on the unwinding exception ABI too:
|
||||
* calls to them need no pending-flag test, and an exception that crosses
|
||||
|
||||
@@ -117,7 +117,9 @@ export const DIAGNOSTIC_CODES = {
|
||||
SC8010: "value crossing a Node island boundary in a form without an exact copy",
|
||||
SC8011: "native class extending a Node island class outside the supported forms",
|
||||
SC8020: "stateful module duplicated across the Node island boundary",
|
||||
SC8021: "Node island evaluation notes (deferred or at startup)",
|
||||
SC8022: "island code extends a class of a compiled module",
|
||||
SC8023: "Node island build failed; built natively with runtime fences",
|
||||
SC8030: "type patch: no range covers the installed copy",
|
||||
SC8031: "type patch: selector matches nothing",
|
||||
SC8032: "type patch: declaration already has the type",
|
||||
@@ -156,8 +158,6 @@ export const RETIRED_DIAGNOSTIC_CODES = {
|
||||
* abandoned, delete them here. */
|
||||
export const RESERVED_DIAGNOSTIC_CODES = {
|
||||
SC8012: "Node islands (ni/* branches)",
|
||||
SC8021: "Node islands (ni/* branches)",
|
||||
SC8023: "Node islands (ni/* branches)",
|
||||
} as const satisfies Record<`SC${number}`, string>;
|
||||
|
||||
export type DiagnosticCode = keyof typeof DIAGNOSTIC_CODES;
|
||||
|
||||
@@ -460,6 +460,23 @@ export function nodeIslandBoundaryDiag(what: string, loc: SrcLoc): ScrDiagnostic
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8023 — the build that ran fenced packages as Node islands failed, so
|
||||
* the program was built natively with those runtime fences (each throws
|
||||
* when reached). A warning on the native build, with the island build's
|
||||
* first error. */
|
||||
export function nodeIslandRetryFailedDiag(
|
||||
packages: readonly string[],
|
||||
first: ScrDiagnostic,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC8023",
|
||||
message: `running ${packages.map((p) => `'${p}'`).join(", ")} in Node islands failed to compile (${first.code} at ${first.loc.file}:${first.loc.start}: ${first.message}); built natively instead, where their runtime fences throw when reached`,
|
||||
loc,
|
||||
hint: "fix the island build's error, or mark the packages with --node-island=<pkg> to see every error",
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8030–SC8033 — a type patch (`scriptc.typePatches`) that did not
|
||||
* apply as written: no range covers the installed copy (SC8030), its
|
||||
* selector matches nothing (SC8031), the declaration already has the type
|
||||
@@ -1043,6 +1060,33 @@ export function noLoweringDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** A stdlib-global snapshot reached a site that needs its VALUE (an
|
||||
* argument, a return, a stored field). The declaration emitted no storage
|
||||
* — alias plumbing only serves member and call reads — and the global's
|
||||
* own value has no lowering, so the read cannot resolve. Reported at the
|
||||
* declaration, where the real cause sits; the use site carries the SC2004
|
||||
* cascade. */
|
||||
export function stdlibSnapshotValueUseDiag(
|
||||
name: string,
|
||||
alias: string,
|
||||
typeText: string | undefined,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
// Name the requested type when it has a readable spelling; a structural
|
||||
// dump of the global's own object type says less than the global itself.
|
||||
const unholdable =
|
||||
typeText === undefined
|
||||
? "the global it names is not a value scriptc can hold"
|
||||
: `a '${typeText}' is not a value scriptc can hold`;
|
||||
return {
|
||||
code: "SC2020",
|
||||
message: `the stdlib-global snapshot '${name}' ('${alias}') cannot be used as a value: the declaration lowers to alias plumbing (no storage, no code), and ${unholdable}`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: `read the members through the global directly ('${alias}.member') instead of passing '${name}' around as a value`,
|
||||
};
|
||||
}
|
||||
|
||||
/** A REACHED use of a part of the WebAssembly API scriptc does not
|
||||
* implement yet (SC2020): the streaming functions, Tag and Exception, the
|
||||
* classes as values. compile, instantiate, validate, the Module, Instance,
|
||||
@@ -1134,6 +1178,30 @@ export function unionMismatchDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** An object literal that SPREADS union-typed sources (`{ ...base,
|
||||
* ...override }` where each source is `A | B | C`) builds one field set per
|
||||
* combination of source arms. A union of records cannot hold that: each arm
|
||||
* carries its own field table, so naming one arm would drop the fields the
|
||||
* other arms contributed (Node keeps every field). The refusal names the
|
||||
* candidate arms, because the honest repair is an explicit annotation per
|
||||
* source (or one open record type), never a guessed arm. */
|
||||
export function unionLiteralMismatchDiag(
|
||||
expectedText: string,
|
||||
arms: readonly string[],
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
const named = arms.map((a) => `'${a}'`).join(", ");
|
||||
return {
|
||||
code: "SC2003",
|
||||
message:
|
||||
`a spread merge over union-typed sources builds a value that can carry fields from several arms at once, so no single arm of '${expectedText}' can represent it` +
|
||||
(arms.length > 0 ? ` (candidate arms: ${named})` : ""),
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "merge ONE arm per path (narrow every spread source to that same arm first — a discriminant switch or an assertion — so the spread builds a single record), or declare ONE open record type whose fields are all optional instead of a union of all-optional arms",
|
||||
};
|
||||
}
|
||||
|
||||
/** A use of a binding whose DECLARATION did not compile: the declaration
|
||||
* site already carries the real diagnostic (a fenced type, a blocked
|
||||
* signature, an unsupported binding form), and every later reference falls
|
||||
|
||||
@@ -234,6 +234,19 @@ export interface NodeIslandsEnvelope {
|
||||
unknownPackages: string[];
|
||||
}
|
||||
|
||||
/** Node island notices of a build that succeeds (scope-B): SC8001 and
|
||||
* SC8021 notes (a package runs in a Node island, an island evaluates at
|
||||
* boot) and the SC8020 warning (a stateful module duplicated across the
|
||||
* boundary). The envelope reports all three as warnings. */
|
||||
export function isNodeIslandNoticeCode(code: string): boolean {
|
||||
return code === "SC8001" || code === "SC8020" || code === "SC8021" || code === "SC8023";
|
||||
}
|
||||
|
||||
/** SC8001 and SC8021 are notes, not warnings. */
|
||||
export function isNodeIslandNoteCode(code: string): boolean {
|
||||
return code === "SC8001" || code === "SC8021";
|
||||
}
|
||||
|
||||
/** The divergence band: compiles, but the program can behave differently
|
||||
* from Node. Warnings, never build failures. */
|
||||
export function isDivergenceCode(code: string): boolean {
|
||||
@@ -370,6 +383,7 @@ export function locateDiagnostic(
|
||||
severity:
|
||||
isDivergenceCode(diag.code) ||
|
||||
isPerformanceCode(diag.code) ||
|
||||
isNodeIslandNoticeCode(diag.code) ||
|
||||
diag.code === "SC8004" ||
|
||||
isTypePatchCode(diag.code)
|
||||
? "warning"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* longer than FRAME_MAX_WIDTH (minified sources) shows a window of that
|
||||
* width around the diagnostic's column, with "…" where text is cut.
|
||||
*/
|
||||
import { isNodeIslandNoteCode, isNodeIslandNoticeCode } from "./envelope.js";
|
||||
import { isTypePatchCode, type ScrDiagnostic } from "./diagnostic.js";
|
||||
|
||||
export interface RenderOptions {
|
||||
@@ -102,14 +103,21 @@ export function renderDiagnostic(
|
||||
frame.push(
|
||||
c(DIM, ` ${" ".repeat(gutterWidth)} | `) +
|
||||
" ".repeat(caretCol) +
|
||||
c(isWarningCode(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)),
|
||||
c(
|
||||
isWarningCode(diag.code) || isNodeIslandNoticeCode(diag.code) ? YELLOW : RED,
|
||||
"^" + "~".repeat(spanOnLine - 1),
|
||||
),
|
||||
);
|
||||
emitLine(lineNum + 1);
|
||||
}
|
||||
|
||||
const warning = isWarningCode(diag.code);
|
||||
// Node island notices of a successful build: SC8020 warns, SC8001 and
|
||||
// SC8021 are notes.
|
||||
const note = isNodeIslandNoteCode(diag.code);
|
||||
const warning = isWarningCode(diag.code) || diag.code === "SC8020" || diag.code === "SC8023";
|
||||
const severity = note ? c(CYAN, "note") : warning ? c(YELLOW, "warning") : c(RED, "error");
|
||||
out.push(
|
||||
`${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${warning ? c(YELLOW, "warning") : c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`,
|
||||
`${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${severity} ${c(BOLD, diag.code)}: ${diag.message}`,
|
||||
);
|
||||
if (frame.length) {
|
||||
out.push("", ...frame);
|
||||
@@ -191,9 +199,11 @@ export function renderWarnings(
|
||||
sourceTextByFile: Map<string, string>,
|
||||
opts: RenderOptions = {},
|
||||
): string {
|
||||
const n = diags.length;
|
||||
const n = diags.filter((d) => !isNodeIslandNoteCode(d.code)).length;
|
||||
const rendered = renderDiagnostics([...diags], sourceTextByFile, opts);
|
||||
if (n === 0) return rendered;
|
||||
return (
|
||||
renderDiagnostics([...diags], sourceTextByFile, opts) +
|
||||
rendered +
|
||||
`\n\n${n} warning${n === 1 ? "" : "s"}: the program compiled, but ${n === 1 ? "this site" : "these sites"} can behave differently from Node.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,6 +98,8 @@ export interface EarlyExecutableNativeFeatures {
|
||||
emitter: boolean;
|
||||
symbol: boolean;
|
||||
bigint: boolean;
|
||||
/** BigIntStats member reads: scr_stats_bigint.c. Absent (false) otherwise. */
|
||||
bigstats?: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
utilPromisify: boolean;
|
||||
@@ -174,6 +176,8 @@ export interface EarlyExecutableCacheOptions {
|
||||
libnodeIdentity?: () => string | undefined;
|
||||
/** Omitted unless --large-pages is set. */
|
||||
largePages?: true;
|
||||
/** Omitted unless the build links without huge text (CompileOptions.hugeText). */
|
||||
noHugeText?: true;
|
||||
/** Omitted unless --exceptions=rare is set. */
|
||||
exceptions?: "rare";
|
||||
/** Omitted for the x86-64 baseline (`--target-cpu`). */
|
||||
@@ -345,6 +349,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
|
||||
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
|
||||
...(options.detachedExit ? ["detached-exit"] : []),
|
||||
...(options.largePages ? ["large-pages"] : []),
|
||||
...(options.noHugeText ? ["no-huge-text"] : []),
|
||||
...(options.exceptions === "rare" ? ["exceptions-rare"] : []),
|
||||
...targetCpuKeyParts(options.targetCpu),
|
||||
...cpuDispatchKeyParts(options.cpuDispatch),
|
||||
@@ -395,6 +400,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
|
||||
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
|
||||
if (options.detachedExit) hash.update("detached-exit\0");
|
||||
if (options.largePages) hash.update("large-pages\0");
|
||||
if (options.noHugeText) hash.update("no-huge-text\0");
|
||||
if (options.exceptions === "rare") hash.update("exceptions-rare\0");
|
||||
for (const part of targetCpuKeyParts(options.targetCpu)) hash.update(`${part}\0`);
|
||||
for (const part of cpuDispatchKeyParts(options.cpuDispatch)) hash.update(`${part}\0`);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { frontendDisposed } from "../frontend/dispose-hook.js";
|
||||
import {
|
||||
checkerPanicDiag,
|
||||
nodeIslandNoteDiag,
|
||||
nodeIslandRetryFailedDiag,
|
||||
npmStaticRefusalDiag,
|
||||
iceDiag,
|
||||
isCheckerPanic,
|
||||
@@ -71,9 +72,10 @@ export function prepareExecutableModule(
|
||||
timing("frontend-island-reload");
|
||||
}
|
||||
let lowered: LowerResult;
|
||||
let fenceEdges: readonly PackageEdge[] | undefined;
|
||||
let sourceTexts: Map<string, string>;
|
||||
let disposed = false;
|
||||
let fenceGrown: Map<string, IslandPackage> | null = null;
|
||||
const fenceSure = new Set<string>();
|
||||
// The frontend (and its tsgo server) is released as soon as lowering
|
||||
// ends — clang and the link never hold it open.
|
||||
try {
|
||||
@@ -166,6 +168,31 @@ export function prepareExecutableModule(
|
||||
retryFences,
|
||||
);
|
||||
}
|
||||
// A failed program whose compiled packages need the runtime or a
|
||||
// mutable object model (NI-2's report): try them as islands. If that
|
||||
// build fails too, its failure is the one reported. This build typed
|
||||
// those packages from their JavaScript, where the types they only
|
||||
// declare (interfaces, aliases such as `Static<T>`) are `any`, so its
|
||||
// diagnostics describe a program the island build does not compile:
|
||||
// there the packages' declarations type the user code.
|
||||
const marked = retryFences
|
||||
? fenceIslands(entryPath, opts, fe, lowered, lowerIslands, false)
|
||||
: null;
|
||||
if (marked !== null) {
|
||||
fe.dispose();
|
||||
frontendDisposed();
|
||||
disposed = true;
|
||||
return prepareExecutableModule(
|
||||
entryPath,
|
||||
opts,
|
||||
ffi,
|
||||
buildPlatform,
|
||||
createFrontend,
|
||||
timing,
|
||||
marked,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return fail(lowered.diagnostics);
|
||||
}
|
||||
|
||||
@@ -196,10 +223,6 @@ export function prepareExecutableModule(
|
||||
return fail([targetRefusalDiag("wasm32-wasi", unavailable.surface, unavailable.loc)]);
|
||||
}
|
||||
}
|
||||
// The package import edges island selection reads after lowering, taken
|
||||
// while the frontend is open.
|
||||
if (retryFences && lowered.runtimeFences.some(sureFence))
|
||||
fenceEdges = fe.nodeIslandScan?.({ userJs: false }).edges;
|
||||
sourceTexts = fe.sourceTexts();
|
||||
if (lowered.contracts) {
|
||||
const checked = checkContracts(lowered.module, lowered.contracts, sourceTexts);
|
||||
@@ -207,6 +230,8 @@ export function prepareExecutableModule(
|
||||
if (checked.diagnostics.length > 0) return fail(checked.diagnostics);
|
||||
if (checked.contracts) lowered.module.contracts = checked.contracts;
|
||||
}
|
||||
if (retryFences)
|
||||
fenceGrown = fenceIslands(entryPath, opts, fe, lowered, lowerIslands, true, fenceSure);
|
||||
} finally {
|
||||
if (!disposed) {
|
||||
fe.dispose();
|
||||
@@ -227,17 +252,21 @@ export function prepareExecutableModule(
|
||||
...(lowered.performance ?? []),
|
||||
],
|
||||
};
|
||||
// Runtime fences in a compiled package that surely fire: fences that need
|
||||
// the JavaScript runtime itself (R1: code from strings, computed
|
||||
// import()/require(), addons, loader hooks), and fences in top-level
|
||||
// module code, which throw when the module evaluates. Build again with
|
||||
// those packages as islands, and keep this build when that one cannot
|
||||
// compile. A gap fence inside a function body may never run, so it stays
|
||||
// a deferred refusal.
|
||||
const runtimeFences = lowered.runtimeFences.filter(sureFence);
|
||||
if (retryFences && runtimeFences.length > 0) {
|
||||
const grown = loweringIslands(entryPath, opts, fe, runtimeFences, lowerIslands, fenceEdges);
|
||||
if (grown !== null) {
|
||||
// Packages compiled with runtime fences that need the JavaScript
|
||||
// runtime or a mutable object model (fenceIslands): build again with
|
||||
// them as islands, and keep this build when that one cannot compile.
|
||||
// If that build fails, retry with only the packages whose fences fire
|
||||
// for sure (module-init or runtime fences); if that fails too, the native
|
||||
// build carries an SC8023 warning with the island build's first error,
|
||||
// never a silent fallback.
|
||||
if (fenceGrown !== null) {
|
||||
const attempts = [fenceGrown];
|
||||
const sure = new Map(
|
||||
[...fenceGrown].filter(([name]) => lowerIslands.has(name) || fenceSure.has(name)),
|
||||
);
|
||||
if (sure.size > lowerIslands.size && sure.size < fenceGrown.size) attempts.push(sure);
|
||||
let firstError: ScrDiagnostic | null = null;
|
||||
for (const islands of attempts) {
|
||||
const islanded = prepareExecutableModule(
|
||||
entryPath,
|
||||
opts,
|
||||
@@ -245,21 +274,22 @@ export function prepareExecutableModule(
|
||||
buildPlatform,
|
||||
createFrontend,
|
||||
timing,
|
||||
grown,
|
||||
islands,
|
||||
false,
|
||||
);
|
||||
if (islanded.ok) return islanded;
|
||||
firstError ??= islanded.diagnostics[0] ?? null;
|
||||
}
|
||||
if (firstError !== null) {
|
||||
const added = [...fenceGrown.keys()].filter((name) => !lowerIslands.has(name));
|
||||
prepared.warnings.push(
|
||||
nodeIslandRetryFailedDiag(added, firstError, { file: entryPath, start: 0, end: 0 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
return prepared;
|
||||
}
|
||||
|
||||
/** A run-time fence that surely fires: it needs the JavaScript runtime
|
||||
* itself, or it is in top-level module code. */
|
||||
function sureFence(d: ScrDiagnostic): boolean {
|
||||
return d.moduleInit === true || classifyDiagnostic(d, "fence").class === "runtime";
|
||||
}
|
||||
|
||||
/** The packages a build runs in Node islands, or null for none: under
|
||||
* `--node-islands=auto` (the default) every npm package the native
|
||||
* pipeline refused, plus packages marked `node`; under `explicit` only the
|
||||
@@ -482,3 +512,109 @@ function preflightIslands(
|
||||
}
|
||||
return grown.size === known.size ? null : grown;
|
||||
}
|
||||
|
||||
/** Packages compiled natively with runtime fences (refusals deferred to the
|
||||
* operation that reaches them) that should run in islands instead: those
|
||||
* with a fence that surely fires (one in top-level module code, which runs
|
||||
* when the module evaluates) or that itself needs the runtime (R1). Other
|
||||
* fences stay deferred refusals until decision 8 (D6) retires them, so
|
||||
* typed packages like jose stay native. With `requireFence` false (after a
|
||||
* failed build) the package's island class by NI-2's report decides
|
||||
* instead: runtime (R1) or object-model (R2). `--node-islands=auto` only;
|
||||
* null for none. */
|
||||
function fenceIslands(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
fe: ReturnType<FrontendFactory>,
|
||||
lowered: LowerResult,
|
||||
known: ReadonlyMap<string, IslandPackage>,
|
||||
requireFence = true,
|
||||
sure?: Set<string>,
|
||||
): Map<string, IslandPackage> | null {
|
||||
if (fe.nodeIslandScan === undefined) return null;
|
||||
if (requireFence && lowered.runtimeFences.length === 0) return null;
|
||||
const policy = nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands);
|
||||
if (policy.mode !== "auto" || known.size > 32) return null;
|
||||
// An explicit --npm-static request keeps its package native.
|
||||
const explicit = new Set(
|
||||
Array.isArray(opts.npmStatic) ? opts.npmStatic.map((n) => n.replace(/\.\.\.$/, "")) : [],
|
||||
);
|
||||
const compiled = new Set(
|
||||
fe.npmStatic
|
||||
.filter((e) => e.status === "static" && !explicit.has(e.package))
|
||||
.map((e) => e.package),
|
||||
);
|
||||
const fenced = new Map<string, boolean>();
|
||||
for (const d of lowered.runtimeFences) {
|
||||
const at = packageDirOf(d.loc.file);
|
||||
if (at === null || !compiled.has(at.name) || known.has(at.name)) continue;
|
||||
if (policy.forcedNative.has(at.name) || policy.unloaded.has(at.name)) continue;
|
||||
// A fence in top-level module code throws when the module evaluates:
|
||||
// it counts like one that needs the runtime.
|
||||
const runtime = d.moduleInit === true || classifyDiagnostic(d, "fence").class === "runtime";
|
||||
fenced.set(at.name, (fenced.get(at.name) ?? false) || runtime);
|
||||
if (runtime) sure?.add(at.name);
|
||||
}
|
||||
if (requireFence && fenced.size === 0) return null;
|
||||
const scan = fe.nodeIslandScan({ userJs: false });
|
||||
const report = buildNodeIslandReport({
|
||||
entry: entryPath,
|
||||
scan,
|
||||
npm: fe.npmStatic,
|
||||
diagnostics: lowered.diagnostics,
|
||||
fences: lowered.runtimeFences,
|
||||
policy,
|
||||
});
|
||||
const grown = new Map(known);
|
||||
for (const island of report.islands) {
|
||||
const runtimeFence = requireFence
|
||||
? fenced.get(island.package)
|
||||
: (fenced.get(island.package) ?? false);
|
||||
if (runtimeFence === undefined || island.path === null) continue;
|
||||
if (!compiled.has(island.package) || known.has(island.package)) continue;
|
||||
if (policy.forcedNative.has(island.package) || policy.unloaded.has(island.package)) continue;
|
||||
// With fences (requireFence), only fences that surely fire count: a
|
||||
// gap fence inside a function body may never run (jose's typed paths
|
||||
// stay native, DECISIONS.md 2026-10-21). After a failed build
|
||||
// (!requireFence), the package's report class decides.
|
||||
if (requireFence && !runtimeFence) continue;
|
||||
if (!runtimeFence && island.class !== "runtime" && island.class !== "object-model") continue;
|
||||
grown.set(island.package, {
|
||||
name: island.package,
|
||||
version: island.version,
|
||||
dir: island.path,
|
||||
class: island.class,
|
||||
reason: island.reason,
|
||||
});
|
||||
}
|
||||
// A native ES module that imports an island CommonJS module would read
|
||||
// its bindings from the module's exports object, where Node gives it the
|
||||
// namespace its CommonJS facade builds (the lexed names, module.exports
|
||||
// as the default): the importing package runs in the island too.
|
||||
for (let added = true; added;) {
|
||||
added = false;
|
||||
for (const edge of scan.edges) {
|
||||
const name = edge.to;
|
||||
if (!grown.has(name) || edge.fromDir === null || edge.shim) continue;
|
||||
const at = packageDirOf(edge.fromFile);
|
||||
if (at === null || grown.has(at.name) || !compiled.has(at.name)) continue;
|
||||
if (policy.forcedNative.has(at.name) || policy.unloaded.has(at.name)) continue;
|
||||
if (!isEsmFile(edge.fromFile)) continue;
|
||||
const target =
|
||||
resolveBareModule(edge.fromFile, name, "js-only") ??
|
||||
resolveBareModule(edge.fromFile, name, "js-only", "require");
|
||||
if (target === null || isEsmFile(target.typesFile)) continue;
|
||||
grown.set(at.name, {
|
||||
name: at.name,
|
||||
version: packageVersionOf(at.dir),
|
||||
dir: at.dir,
|
||||
class: "packaging",
|
||||
reason: `an ES module of the package imports the CommonJS island package '${name}'`,
|
||||
});
|
||||
// It goes wherever that island goes, the sure-fence retry included.
|
||||
if (known.has(name) || sure?.has(name) === true) sure?.add(at.name);
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
return grown.size === known.size ? null : grown;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { type IrType, typeKey } from "../../ir/ir.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Whether `actual` is `declared` with every `Map<unknown, unknown>` or
|
||||
* `Set<unknown>` (TypeScript's `any` type arguments of an untyped empty
|
||||
* collection, mapped to unknown) replaced by a map or set of a concrete
|
||||
* layout, and with nothing else different. A union `declared` matches when
|
||||
* `actual` is one of its record arms and every other arm is the residue
|
||||
* form of that arm. */
|
||||
export function anyResidueReplacedBy(lowerer: Lowerer, declared: IrType, actual: IrType): boolean {
|
||||
const residue = (t: IrType): boolean =>
|
||||
(t.kind === "map" && t.key.kind === "dyn" && t.value.kind === "dyn") ||
|
||||
(t.kind === "set" && t.elem.kind === "dyn");
|
||||
const replaces = (d: IrType, a: IrType, seen: Set<string>): boolean => {
|
||||
if (typeKey(d) === typeKey(a)) return true;
|
||||
if (residue(d)) return a.kind === d.kind && !residue(a);
|
||||
if (d.kind === "record" && a.kind === "record") {
|
||||
const pair = `${d.shapeId}>${a.shapeId}`;
|
||||
if (seen.has(pair)) return true;
|
||||
seen.add(pair);
|
||||
const from = lowerer.shapes.get(d.shapeId);
|
||||
const to = lowerer.shapes.get(a.shapeId);
|
||||
if (!from || !to || from.tuple || to.tuple || from.indexValue || to.indexValue) return false;
|
||||
if (from.fields.length !== to.fields.length) return false;
|
||||
return from.fields.every((field) => {
|
||||
const other = to.fields.find((f) => f.name === field.name);
|
||||
return other !== undefined && replaces(field.type, other.type, seen);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (declared.kind === "union" && actual.kind === "record") {
|
||||
const arms = lowerer.unions.get(declared.unionId)?.arms ?? [];
|
||||
return (
|
||||
arms.some((arm) => typeKey(arm) === typeKey(actual)) &&
|
||||
arms.every(
|
||||
(arm) =>
|
||||
typeKey(arm) === typeKey(actual) ||
|
||||
(arm.kind === "record" && replaces(arm, actual, new Set())),
|
||||
)
|
||||
);
|
||||
}
|
||||
return replaces(declared, actual, new Set());
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
funcOf,
|
||||
} from "../../ir/ir.js";
|
||||
import { helperLoc, varRef } from "../../ir/build.js";
|
||||
import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { unsupportedDiag, type ScrDiagnostic } from "../../diagnostics/diagnostic.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { mathMemberValue } from "./lower-stdlib.js";
|
||||
@@ -117,21 +117,46 @@ const WRITABLE = 1;
|
||||
const CONFIGURABLE = 4;
|
||||
|
||||
/** The namespace object the stdlib global `canonical` reads as in
|
||||
* JavaScript, or null when it is not a namespace. */
|
||||
* JavaScript, or null when it is not a namespace. The fences of its members
|
||||
* without a function value are reported unless `reportFences` is false: the
|
||||
* program's builtin lookup (global-values.ts) holds every namespace without
|
||||
* the program naming it, and its members still throw their fences when
|
||||
* called. */
|
||||
export function builtinNamespaceValue(
|
||||
lowerer: Lowerer,
|
||||
canonical: string,
|
||||
loc: SrcLoc,
|
||||
reportFences = true,
|
||||
): IrExpr | null {
|
||||
const members = namespaceMembers(canonical);
|
||||
if (members === null) return null;
|
||||
const name = `%builtin.namespace.${canonical}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name))
|
||||
defineNamespaceBuilder(lowerer, canonical, members, name, helperLoc(loc));
|
||||
if (reportFences) reportMemberFences(lowerer, canonical, members, helperLoc(loc));
|
||||
lowerer.noteEdge(name);
|
||||
return { kind: "call", callee: name, args: [], type: DYN, loc };
|
||||
}
|
||||
|
||||
/** Reports the SC1090 fence of each member of `canonical` whose function
|
||||
* throws it (fencedMember), once per program. */
|
||||
function reportMemberFences(
|
||||
lowerer: Lowerer,
|
||||
canonical: string,
|
||||
members: readonly NamespaceMember[],
|
||||
loc: SrcLoc,
|
||||
): void {
|
||||
for (const { key, length } of members) {
|
||||
if (length < 0) continue;
|
||||
const display = `${canonical}.${key}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === fenceName(display))) continue;
|
||||
const diag = fenceDiag(display, loc);
|
||||
if (lowerer.runtimeFences.some((d) => d.code === diag.code && d.message === diag.message))
|
||||
continue;
|
||||
lowerer.runtimeFences.push(diag);
|
||||
}
|
||||
}
|
||||
|
||||
/** The lifted function that builds the namespace object once and answers
|
||||
* the same object afterwards. */
|
||||
function defineNamespaceBuilder(
|
||||
@@ -253,18 +278,17 @@ function functionValue(lowerer: Lowerer, canonical: string, key: string, loc: Sr
|
||||
return fencedMember(lowerer, `${canonical}.${key}`, key, loc);
|
||||
}
|
||||
|
||||
const fenceName = (display: string): string => `%builtin.fence.${display}`;
|
||||
const fenceDiag = (display: string, loc: SrcLoc): ScrDiagnostic =>
|
||||
unsupportedDiag("SC1090", loc, `'${display}' as a function value (call it directly)`);
|
||||
|
||||
/** One function per member without a function value: calling it throws the
|
||||
* SC1090 fence, while `typeof`, identity and the property's presence stay
|
||||
* as in Node. */
|
||||
* as in Node. builtinNamespaceValue reports the fence. */
|
||||
function fencedMember(lowerer: Lowerer, display: string, key: string, loc: SrcLoc): IrExpr {
|
||||
const name = `%builtin.fence.${display}`;
|
||||
const name = fenceName(display);
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const diag = unsupportedDiag(
|
||||
"SC1090",
|
||||
loc,
|
||||
`'${display}' as a function value (call it directly)`,
|
||||
);
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const diag = fenceDiag(display, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../surfaces.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js";
|
||||
import {
|
||||
BIGINT_STATS_T,
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
DIR_T,
|
||||
@@ -1077,6 +1078,18 @@ export const STATS_TYPE_TESTS: Record<string, IrLibFn> = {
|
||||
isSocket: "stats.isSocket",
|
||||
};
|
||||
|
||||
/** The same type tests on a BigIntStats receiver (statSync with
|
||||
* `{ bigint: true }`): one more snapshot read, no bigint arithmetic. */
|
||||
const BIGINT_STATS_TYPE_TESTS: Record<string, IrLibFn> = {
|
||||
isFile: "bigstats.isFile",
|
||||
isDirectory: "bigstats.isDirectory",
|
||||
isSymbolicLink: "bigstats.isSymbolicLink",
|
||||
isBlockDevice: "bigstats.isBlockDevice",
|
||||
isCharacterDevice: "bigstats.isCharacterDevice",
|
||||
isFIFO: "bigstats.isFIFO",
|
||||
isSocket: "bigstats.isSocket",
|
||||
};
|
||||
|
||||
/** Method calls on Stats-typed receivers: the type tests are pure reads on
|
||||
* the stat snapshot (a followed statSync snapshot never answers true to
|
||||
* isSymbolicLink — take lstatSync's, Node's own split). Anything else
|
||||
@@ -1094,10 +1107,14 @@ export function lowerStatsMethodCall(
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "stats") return null;
|
||||
const receiverKind = lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind;
|
||||
if (receiverKind !== "stats" && receiverKind !== "bigintStats") return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const name = access.name.text;
|
||||
const fn = Object.hasOwn(STATS_TYPE_TESTS, name) ? STATS_TYPE_TESTS[name] : undefined;
|
||||
// BigIntStats' type tests are the same snapshot mode read; only the kind
|
||||
// of the handle differs.
|
||||
const table = receiverKind === "bigintStats" ? BIGINT_STATS_TYPE_TESTS : STATS_TYPE_TESTS;
|
||||
const fn = Object.hasOwn(table, name) ? table[name] : undefined;
|
||||
if (fn && call.arguments.nodes.length === 0) {
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
return { kind: "libCall", fn, args: [receiver], type: BOOL, loc: locOf(call) };
|
||||
@@ -1482,14 +1499,71 @@ function lowerFsReaddirNames(
|
||||
|
||||
/** fs.statSync / fs.lstatSync's documented option keys. */
|
||||
const FS_STAT_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set(["bigint", "throwIfNoEntry"]);
|
||||
/** fs.fstatSync's: an open descriptor never misses, so there is no
|
||||
* throwIfNoEntry. */
|
||||
const FS_FSTAT_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set(["bigint"]);
|
||||
|
||||
/** The stat representation a mapped result type carries: BigIntStats and
|
||||
* `BigIntStats | undefined` read the bigint member table, Stats and
|
||||
* `Stats | undefined` the number one; anything else is neither. */
|
||||
function statRepresentation(
|
||||
callType: IrType | undefined | null,
|
||||
arms: readonly IrType[] | undefined,
|
||||
): "bigint" | "number" | null {
|
||||
const kindOf = (t: IrType): "bigint" | "number" | null =>
|
||||
t.kind === "bigintStats" ? "bigint" : t.kind === "stats" ? "number" : null;
|
||||
if (callType === undefined || callType === null) return null;
|
||||
const own = kindOf(callType);
|
||||
if (own !== null) return own;
|
||||
if (arms !== undefined) {
|
||||
for (const arm of arms) {
|
||||
const repr = kindOf(arm);
|
||||
if (repr !== null) return repr;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The `bigint` option member. The literal picks the representation —
|
||||
* BigIntStats for true, Stats for false — and the call's own type must
|
||||
* already agree with it, because a `Stats | BigIntStats`-valued flag has no
|
||||
* static member table. Answers the member's lowered value when evaluating
|
||||
* it can have an effect (`bigint: (count++, true)`), which the call then
|
||||
* runs in its place among the options; null when it can be dropped. */
|
||||
function checkStatBigintOption(
|
||||
lowerer: Lowerer,
|
||||
value: ts.Expression,
|
||||
spelling: string,
|
||||
representation: "bigint" | "number" | null,
|
||||
): IrExpr | null {
|
||||
const t = lowerer.typeOf(value);
|
||||
const literal = t.flags & ts.TypeFlags.BooleanLiteral ? lowerer.checker.typeToString(t) : null;
|
||||
if (literal === null) {
|
||||
lowerer.noLowering(
|
||||
`${spelling} with a runtime-valued bigint option`,
|
||||
value,
|
||||
"pass the literal true or false: the result's representation is chosen statically",
|
||||
);
|
||||
}
|
||||
if (representation === null || (literal === "true") !== (representation === "bigint")) {
|
||||
lowerer.noLowering(
|
||||
`${spelling} with bigint: ${literal} where the result is not ${literal === "true" ? "BigIntStats" : "Stats"}`,
|
||||
value,
|
||||
"pass bigint: true to receive BigIntStats, or omit it to receive Stats",
|
||||
);
|
||||
}
|
||||
const lowered = lowerer.lowerExprExpecting(value, BOOL);
|
||||
return isSafeToDiscard(lowered) ? null : lowered;
|
||||
}
|
||||
|
||||
/** fs.statSync / fs.lstatSync(path, options). The options are an object
|
||||
* literal, where throwIfNoEntry takes any boolean and bigint must be a
|
||||
* literal false (BigIntStats has no lowering), or a stored record whose
|
||||
* only member is a boolean throwIfNoEntry. When the call's type is
|
||||
* `Stats | undefined`, the call lowers to the probe, which answers
|
||||
* undefined for a missing entry without building the error. An explicitly
|
||||
* undefined options argument is the one-argument form. */
|
||||
* literal, where throwIfNoEntry takes any boolean and bigint is the literal
|
||||
* true (BigIntStats) or false (Stats), or a stored record whose only member
|
||||
* is a boolean throwIfNoEntry. When the call's type is
|
||||
* `Stats | undefined` (or `BigIntStats | undefined`), the call lowers to the
|
||||
* probe, which answers undefined for a missing entry without building the
|
||||
* error. An explicitly undefined options argument is the one-argument
|
||||
* form. */
|
||||
export function lowerFsStatOptionsCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -1500,9 +1574,18 @@ export function lowerFsStatOptionsCall(
|
||||
const plainFn: IrLibFn = noFollow ? "fs.lstatSync" : "fs.statSync";
|
||||
const optsNode = call.arguments.nodes[1]!;
|
||||
const path = lowerFsPath(lowerer, call.arguments.nodes[0]!);
|
||||
// `{ bigint: true }` selects Node's BigIntStats: the SAME snapshot handle,
|
||||
// read through the bigint member table. The checker's answer for the call
|
||||
// already names the variant, so the mapped call type is the source of
|
||||
// truth and the option member below has to agree with it.
|
||||
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const def = callType?.kind === "union" ? lowerer.unions.get(callType.unionId) : undefined;
|
||||
const representation = statRepresentation(callType, def?.arms);
|
||||
const statsType: IrType = representation === "bigint" ? BIGINT_STATS_T : STATS_T;
|
||||
// The path binds first, then the options' effects, then the stat runs.
|
||||
const plainAfter = (effect: IrExpr | null): IrExpr => {
|
||||
if (effect === null) return { kind: "libCall", fn: plainFn, args: [path], type: STATS_T, loc };
|
||||
if (effect === null)
|
||||
return { kind: "libCall", fn: plainFn, args: [path], type: statsType, loc };
|
||||
const pathLocal = lowerer.declareHiddenLocal("%statPath", STRING);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
@@ -1514,10 +1597,10 @@ export function lowerFsStatOptionsCall(
|
||||
kind: "libCall",
|
||||
fn: plainFn,
|
||||
args: [varRef(pathLocal.id, STRING, loc)],
|
||||
type: STATS_T,
|
||||
type: statsType,
|
||||
loc,
|
||||
},
|
||||
type: STATS_T,
|
||||
type: statsType,
|
||||
loc,
|
||||
};
|
||||
};
|
||||
@@ -1579,6 +1662,10 @@ export function lowerFsStatOptionsCall(
|
||||
lowerer.noLowering(`${spelling} with this options value`, optsNode, "pass an object literal");
|
||||
}
|
||||
}
|
||||
// The members in source order, when a bigint member has an effect: the
|
||||
// throwIfNoEntry value ("throwIfNoEntry") and each effect.
|
||||
const members: (IrExpr | "throwIfNoEntry")[] = [];
|
||||
let bigintEffect = false;
|
||||
for (const p of ts.isObjectLiteralExpression(optsNode) ? optsNode.properties.nodes : []) {
|
||||
const m = optionMember(p);
|
||||
if (!m) {
|
||||
@@ -1590,14 +1677,12 @@ export function lowerFsStatOptionsCall(
|
||||
}
|
||||
if (m.name === "throwIfNoEntry") {
|
||||
throwIfNoEntry = lowerer.lowerExprExpecting(m.value, BOOL);
|
||||
members.push("throwIfNoEntry");
|
||||
} else if (m.name === "bigint") {
|
||||
const t = lowerer.typeOf(m.value);
|
||||
if (!(t.flags & ts.TypeFlags.BooleanLiteral) || lowerer.checker.typeToString(t) !== "false") {
|
||||
lowerer.noLowering(
|
||||
`${spelling} with bigint: true`,
|
||||
m.value,
|
||||
"BigIntStats has no lowering; omit bigint for Stats",
|
||||
);
|
||||
const effect = checkStatBigintOption(lowerer, m.value, spelling, representation);
|
||||
if (effect !== null) {
|
||||
members.push(effect);
|
||||
bigintEffect = true;
|
||||
}
|
||||
} else {
|
||||
fenceOrDropOptionKey(
|
||||
@@ -1606,12 +1691,31 @@ export function lowerFsStatOptionsCall(
|
||||
m.name,
|
||||
spelling,
|
||||
FS_STAT_DOCUMENTED_OPTIONS,
|
||||
"throwIfNoEntry and bigint: false are the supported options",
|
||||
"throwIfNoEntry and bigint are the supported options",
|
||||
);
|
||||
}
|
||||
}
|
||||
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (callType?.kind === "stats") {
|
||||
// With a bigint member's effect, the path binds first, then the members
|
||||
// run in source order (a runtime throwIfNoEntry into a local), then the
|
||||
// stat; without one, the call evaluates its operands in that order itself.
|
||||
const ordered = (stat: (pathArg: IrExpr, flag: IrExpr | null) => IrExpr): IrExpr => {
|
||||
if (!bigintEffect) return stat(path, throwIfNoEntry);
|
||||
const pathLocal = lowerer.declareHiddenLocal("%statPath", STRING);
|
||||
const stmts: IrStmt[] = [{ kind: "varDecl", localId: pathLocal.id, init: path, loc: path.loc }];
|
||||
let flag = throwIfNoEntry;
|
||||
for (const member of members) {
|
||||
if (member !== "throwIfNoEntry") {
|
||||
stmts.push({ kind: "exprStmt", expr: member, loc: member.loc });
|
||||
} else if (flag !== null && flag.kind !== "boolLit") {
|
||||
const flagLocal = lowerer.declareHiddenLocal("%statThrowIfNoEntry", BOOL);
|
||||
stmts.push({ kind: "varDecl", localId: flagLocal.id, init: flag, loc: flag.loc });
|
||||
flag = varRef(flagLocal.id, BOOL, flag.loc);
|
||||
}
|
||||
}
|
||||
const result = stat(varRef(pathLocal.id, STRING, loc), flag);
|
||||
return { kind: "seqExpr", stmts, result, type: result.type, loc };
|
||||
};
|
||||
if (callType?.kind === "stats" || callType?.kind === "bigintStats") {
|
||||
// A runtime flag that is false answers undefined, which a Stats result
|
||||
// cannot hold.
|
||||
if (throwIfNoEntry !== null && !(throwIfNoEntry.kind === "boolLit" && throwIfNoEntry.value)) {
|
||||
@@ -1621,13 +1725,19 @@ export function lowerFsStatOptionsCall(
|
||||
"the result must be Stats | undefined when throwIfNoEntry may be false",
|
||||
);
|
||||
}
|
||||
return plainAfter(null);
|
||||
if (!bigintEffect) return plainAfter(null);
|
||||
return ordered((pathArg) => ({
|
||||
kind: "libCall",
|
||||
fn: plainFn,
|
||||
args: [pathArg],
|
||||
type: statsType,
|
||||
loc,
|
||||
}));
|
||||
}
|
||||
const def = callType?.kind === "union" ? lowerer.unions.get(callType.unionId) : undefined;
|
||||
if (
|
||||
!def ||
|
||||
def.arms.length !== 2 ||
|
||||
!def.arms.some((a) => a.kind === "stats") ||
|
||||
!def.arms.some((a) => a.kind === "stats" || a.kind === "bigintStats") ||
|
||||
!def.arms.some((a) => a.kind === "undefinedT")
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
@@ -1636,12 +1746,107 @@ export function lowerFsStatOptionsCall(
|
||||
`${spelling}(path, { throwIfNoEntry: false }) answers Stats | undefined`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
return ordered((pathArg, flag) => ({
|
||||
kind: "libCall",
|
||||
fn: noFollow ? "fs.lstatProbeSync" : "fs.statProbeSync",
|
||||
args: [path, throwIfNoEntry ?? boolLit(true, loc)],
|
||||
args: [pathArg, flag ?? boolLit(true, loc)],
|
||||
type: callType!,
|
||||
loc,
|
||||
}));
|
||||
}
|
||||
|
||||
/** fs.fstatSync(fd, options): the descriptor form of the same snapshot.
|
||||
* Node documents only `bigint` here — an open descriptor cannot miss, so the
|
||||
* result is Stats or BigIntStats, never undefined. */
|
||||
export function lowerFsStatFdOptionsCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const spelling = "fstatSync";
|
||||
const fd = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64);
|
||||
const optsNode = call.arguments.nodes[1]!;
|
||||
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const representation = statRepresentation(callType, undefined);
|
||||
const statsType: IrType = representation === "bigint" ? BIGINT_STATS_T : STATS_T;
|
||||
const plain = (): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "fs.fstatSync",
|
||||
args: [fd],
|
||||
type: statsType,
|
||||
loc,
|
||||
});
|
||||
const undefinedOptions = lowerStaticallyUndefinedArgument(lowerer, optsNode);
|
||||
if (undefinedOptions !== null) {
|
||||
if (isSafeToDiscard(undefinedOptions)) return plain();
|
||||
// The descriptor binds first, then the options' effects, then the stat.
|
||||
const fdLocal = lowerer.declareHiddenLocal("%fstatFd", F64);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: fdLocal.id, init: fd, loc: fd.loc },
|
||||
{ kind: "exprStmt", expr: undefinedOptions, loc: undefinedOptions.loc },
|
||||
],
|
||||
result: {
|
||||
kind: "libCall",
|
||||
fn: "fs.fstatSync",
|
||||
args: [varRef(fdLocal.id, F64, loc)],
|
||||
type: statsType,
|
||||
loc,
|
||||
},
|
||||
type: statsType,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (!ts.isObjectLiteralExpression(optsNode)) {
|
||||
lowerer.noLowering(
|
||||
`${spelling} with this options value`,
|
||||
optsNode,
|
||||
`pass an object literal: ${spelling}(fd, { bigint: true })`,
|
||||
);
|
||||
}
|
||||
const effects: IrExpr[] = [];
|
||||
for (const p of optsNode.properties.nodes) {
|
||||
const m = optionMember(p);
|
||||
if (!m) {
|
||||
lowerer.noLowering(
|
||||
`${spelling} with this options shape`,
|
||||
p,
|
||||
"spreads and computed keys have no lowering — write each member inline",
|
||||
);
|
||||
}
|
||||
if (m.name === "bigint") {
|
||||
const effect = checkStatBigintOption(lowerer, m.value, spelling, representation);
|
||||
if (effect !== null) effects.push(effect);
|
||||
} else {
|
||||
fenceOrDropOptionKey(
|
||||
lowerer,
|
||||
p,
|
||||
m.name,
|
||||
spelling,
|
||||
FS_FSTAT_DOCUMENTED_OPTIONS,
|
||||
"bigint is the supported option",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (effects.length === 0) return plain();
|
||||
// The descriptor binds first, then the bigint member's effect, then the stat.
|
||||
const fdLocal = lowerer.declareHiddenLocal("%fstatFd", F64);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: fdLocal.id, init: fd, loc: fd.loc },
|
||||
...effects.map((expr): IrStmt => ({ kind: "exprStmt", expr, loc: expr.loc })),
|
||||
],
|
||||
result: {
|
||||
kind: "libCall",
|
||||
fn: "fs.fstatSync",
|
||||
args: [varRef(fdLocal.id, F64, loc)],
|
||||
type: statsType,
|
||||
loc,
|
||||
},
|
||||
type: statsType,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,12 @@ import { type ParamShape } from "../call-signatures.js";
|
||||
import { BYTES_U8, DYN, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { lowerProcessLoadEnvFile } from "./environment.js";
|
||||
import { lowerFsWatchCall, lowerFsReaddirTypesCall, lowerFsStatOptionsCall } from "./filesystem.js";
|
||||
import {
|
||||
lowerFsWatchCall,
|
||||
lowerFsReaddirTypesCall,
|
||||
lowerFsStatFdOptionsCall,
|
||||
lowerFsStatOptionsCall,
|
||||
} from "./filesystem.js";
|
||||
import { lowerZlibModuleCall } from "./compression.js";
|
||||
import { lowerForkCall } from "./child-process.js";
|
||||
import {
|
||||
@@ -162,8 +167,8 @@ export function lowerBuiltinModuleCall(
|
||||
if ((syncReaddirTypes || promiseReaddirTypes) && expr.arguments.nodes.length === 2) {
|
||||
return lowerFsReaddirTypesCall(lowerer, expr, loc, promiseReaddirTypes);
|
||||
}
|
||||
// fs.statSync/fs.lstatSync(path, options): throwIfNoEntry and bigint:
|
||||
// false, routed BEFORE the 1-arg table completion.
|
||||
// fs.statSync/fs.lstatSync(path, options) and fs.fstatSync(fd, options):
|
||||
// throwIfNoEntry and bigint, routed BEFORE the table completion.
|
||||
if (
|
||||
bi.module === "fs" &&
|
||||
(bi.member === "statSync" || bi.member === "lstatSync") &&
|
||||
@@ -171,6 +176,9 @@ export function lowerBuiltinModuleCall(
|
||||
) {
|
||||
return lowerFsStatOptionsCall(lowerer, expr, loc, bi.member === "lstatSync");
|
||||
}
|
||||
if (bi.module === "fs" && bi.member === "fstatSync" && expr.arguments.nodes.length === 2) {
|
||||
return lowerFsStatFdOptionsCall(lowerer, expr, loc);
|
||||
}
|
||||
if (bi.module === "os" && bi.member === "userInfo") {
|
||||
return lowerOsUserInfoCall(lowerer, expr, loc);
|
||||
}
|
||||
|
||||
@@ -849,6 +849,11 @@ export function lowerProcessProperty(
|
||||
"process.env as a value of this type (read one variable: process.env.NAME or process.env[name])",
|
||||
);
|
||||
}
|
||||
// process.cwd as a value: the process module's member (surfaces.ts).
|
||||
if (member === "cwd") {
|
||||
const value = lowerer.lowerBuiltinCallableValue({ module: "process", member }, loc);
|
||||
if (value) return value;
|
||||
}
|
||||
if (member === "exit" || member === "cwd" || member === "getuid" || member === "kill") {
|
||||
lowerer.unsupported("SC1090", expr, `process methods as values (call '${member}' directly)`);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { isChildSurfaceMember } from "../surfaces.js";
|
||||
import {
|
||||
BIGINT_T,
|
||||
BOOL,
|
||||
CHILDSTREAM_T,
|
||||
CHILDWRITER_T,
|
||||
@@ -49,6 +50,22 @@ const STATS_FIELDS: Record<string, IrLibFn> = {
|
||||
birthtime: "stats.birthtime",
|
||||
};
|
||||
const DATE_FIELDS = new Set(["atime", "mtime", "ctime", "birthtime"]);
|
||||
/** BigIntStats answers a `bigint` for EVERY number member of Stats — the
|
||||
* integers, the `*Ms` millisecond members (the truncated nanosecond
|
||||
* quotient, as Node's binding computes them) and the BigIntStats-only `*Ns`
|
||||
* nanosecond members. Only the four time members stay `Date`s. */
|
||||
const NANOSECOND_FIELDS: Record<string, IrLibFn> = {
|
||||
atimeNs: "bigstats.atimeNs",
|
||||
mtimeNs: "bigstats.mtimeNs",
|
||||
ctimeNs: "bigstats.ctimeNs",
|
||||
birthtimeNs: "bigstats.birthtimeNs",
|
||||
};
|
||||
const BIGINT_STATS_FIELDS: Record<string, IrLibFn> = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(STATS_FIELDS).map(([name]) => [name, `bigstats.${name}` as IrLibFn]),
|
||||
),
|
||||
...NANOSECOND_FIELDS,
|
||||
};
|
||||
export function lowerBuiltinExtraProperty(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
@@ -136,6 +153,7 @@ export function lowerBuiltinExtraProperty(
|
||||
}
|
||||
if (
|
||||
kind !== "stats" &&
|
||||
kind !== "bigintStats" &&
|
||||
kind !== "fileHandle" &&
|
||||
kind !== "dir" &&
|
||||
kind !== "spawnRes" &&
|
||||
@@ -260,11 +278,15 @@ export function lowerBuiltinExtraProperty(
|
||||
lowerer.checker.getSymbolAtLocation(expr.name),
|
||||
);
|
||||
}
|
||||
if (kind === "stats" && Object.hasOwn(STATS_FIELDS, name)) {
|
||||
const statsFields =
|
||||
kind === "stats" ? STATS_FIELDS : kind === "bigintStats" ? BIGINT_STATS_FIELDS : undefined;
|
||||
if (statsFields !== undefined && Object.hasOwn(statsFields, name)) {
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
const fn = STATS_FIELDS[name]!;
|
||||
// The Date fields are the *Ms values rounded, as Node's dateFromMs builds them.
|
||||
const type = DATE_FIELDS.has(name) ? DATE_T : F64;
|
||||
const fn = statsFields[name]!;
|
||||
// The Date fields are the millisecond value rounded, as Node's
|
||||
// dateFromMs builds them; every other Stats number is a bigint on
|
||||
// BigIntStats.
|
||||
const type = DATE_FIELDS.has(name) ? DATE_T : kind === "bigintStats" ? BIGINT_T : F64;
|
||||
return { kind: "libCall", fn, args: [receiver], type, loc };
|
||||
}
|
||||
if (kind === "spawnRes" && name === "signal") {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, isNodeEsmFile } from "../program.js";
|
||||
import { isJsSourceFile, isNodeEsmFile, locOf } from "../program.js";
|
||||
import {
|
||||
genResultRecord,
|
||||
restTupleParamTypes,
|
||||
@@ -36,7 +36,7 @@ import { isStreamUndefCallExpr } from "./lower-server.js";
|
||||
import { nsPathPrefix } from "./lower-namespaces.js";
|
||||
import { declSymbolOf } from "./lower-modules.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { restPackingAdapterFits } from "./coercions/function-adapters.js";
|
||||
import { funcSlotAdaptable, restPackingAdapterFits } from "./coercions/function-adapters.js";
|
||||
import { implicitMonoFile, implicitAnyParamSymbolsOf } from "./generic-functions.js";
|
||||
|
||||
/** How a parameter participates in CALL-SITE COMPLETION (the frontend
|
||||
@@ -668,21 +668,6 @@ export function requireExactArityValue(
|
||||
// optional/defaulted params spell their `T | undefined` slots (mapType's
|
||||
// completed-signature contract), so omitted trailing args complete with
|
||||
// the undefined arm like any direct call.
|
||||
// A nested function declaration whose only non-required parameter is a
|
||||
// typed rest: its value carries the typed-rest ABI its own type spells,
|
||||
// and every use converts it at its slot (same signature, or the rest
|
||||
// packing adapter for fixed parameters).
|
||||
if (
|
||||
contextual === null &&
|
||||
ts.isFunctionDeclaration(blame) &&
|
||||
blame.name !== undefined &&
|
||||
funcType.kind === "func" &&
|
||||
funcType.restAbi === "typed" &&
|
||||
shapes.every((s) => s.mode === "required" || s.mode === "rest")
|
||||
) {
|
||||
const own = lowerer.mapTypeOf(lowerer.typeOf(blame.name));
|
||||
if (own && typeEquals(own, funcType)) return;
|
||||
}
|
||||
const target = contextual ? lowerer.checker.getContextualType(contextual) : undefined;
|
||||
const mapped = target
|
||||
? lowerer.mapTypeOf(target)
|
||||
@@ -722,8 +707,33 @@ export function requireExactArityValue(
|
||||
const member = literalType
|
||||
? lowerer.checker.getPropertyOfType(literalType, blame.name.text)
|
||||
: undefined;
|
||||
const slot = member ? lowerer.memberSlotTypeOf(lowerer.checker.getTypeOfSymbol(member)) : null;
|
||||
let slot = member ? lowerer.memberSlotTypeOf(lowerer.checker.getTypeOfSymbol(member)) : null;
|
||||
// A literal contextualized by an INDEX SIGNATURE (`Record<string,
|
||||
// Handler>`) names no member for the method: its slot is the index
|
||||
// signature's value type.
|
||||
if (!slot && literalType) {
|
||||
const index = lowerer.checker.getIndexInfosOfType(literalType)[0];
|
||||
if (index) slot = lowerer.memberSlotTypeOf(index.type);
|
||||
}
|
||||
if (slot && typeEquals(slot, funcType)) return;
|
||||
// The slot may pass parameters the method's completed ABI cannot take
|
||||
// verbatim (a required narrower interface parameter where the method
|
||||
// declares `ctx?: Ctx`, completing to `Ctx | undefined`): the slot
|
||||
// coercion adapts the pair like any other function value.
|
||||
if (
|
||||
slot?.kind === "func" &&
|
||||
funcType.kind === "func" &&
|
||||
funcSlotAdaptable(lowerer, funcType, slot, locOf(blame))
|
||||
)
|
||||
return;
|
||||
// An UNCONTEXTUALIZED literal's own inferred type is what every later
|
||||
// read of the member spells, and it is built from the method's own
|
||||
// completed signature (mapType's completed-signature contract), so the
|
||||
// method's own completed type is its slot.
|
||||
if (!slot) {
|
||||
const own = lowerer.mapTypeOf(lowerer.typeOf(blame));
|
||||
if (own && typeEquals(own, funcType)) return;
|
||||
}
|
||||
}
|
||||
// A union-typed slot (`runner || defaultRunner` under a
|
||||
// `CommandRunner | undefined` context): the value can only inhabit
|
||||
@@ -770,6 +780,18 @@ export function requireExactArityValue(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// A signature the slot's coercion adapts without a trap-only path: an
|
||||
// extra trailing OPTIONAL parameter the slot never passes (forwarded as
|
||||
// undefined, JavaScript's surplus-argument rule) or a parameter pair the
|
||||
// adapter converts (a required narrower interface parameter against the
|
||||
// value's `Ctx | undefined` completed slot).
|
||||
if (
|
||||
mappedFn?.kind === "func" &&
|
||||
funcType.kind === "func" &&
|
||||
funcSlotAdaptable(lowerer, funcType, mappedFn, locOf(blame))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// An uncontextualized npm-static implementation function can still
|
||||
// have a declaration-backed completed ABI: the package's .d.ts
|
||||
// projection supplies the body-facing parameter/return types while
|
||||
|
||||
@@ -37,7 +37,19 @@ export class ClassProtocols {
|
||||
};
|
||||
const usesReceiver = (node: ts.Node): boolean =>
|
||||
node.kind === ts.SyntaxKind.ThisKeyword || ts.forEachChild(node, usesReceiver) === true;
|
||||
// The names any type in the program declares as a get/set accessor, and
|
||||
// every object literal in it. A data member of a literal can only
|
||||
// disagree with a target at a name in this set, and marking waits until
|
||||
// the walk ends, so the checker round trip stays off every literal whose
|
||||
// names are not accessors somewhere (this analysis runs on every
|
||||
// program, so it must stay syntax-cheap).
|
||||
const accessorNames = new Set<string>();
|
||||
const literals: ts.ObjectLiteralExpression[] = [];
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
|
||||
const name = propertyNameText(node.name);
|
||||
if (name !== null) accessorNames.add(name);
|
||||
}
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
||||
for (const clause of node.heritageClauses?.nodes ?? [])
|
||||
if (clause.token === ts.SyntaxKind.ImplementsKeyword)
|
||||
@@ -61,31 +73,107 @@ export class ClassProtocols {
|
||||
this.methods.set(ts.symbolName(property), entries);
|
||||
}
|
||||
}
|
||||
if (
|
||||
ts.isObjectLiteralExpression(node) &&
|
||||
node.properties.nodes.some(
|
||||
(property) =>
|
||||
(ts.isMethodDeclaration(property) ||
|
||||
ts.isGetAccessorDeclaration(property) ||
|
||||
ts.isSetAccessorDeclaration(property)) &&
|
||||
property.body !== undefined &&
|
||||
usesReceiver(property.body),
|
||||
)
|
||||
) {
|
||||
// Receiver-dependent methods and accessors need a live object even
|
||||
// without a class implementation. Receiver-free methods keep their
|
||||
// native callable fields, including async callbacks.
|
||||
add(this.checker.getTypeAtLocation(node));
|
||||
const contextual = this.checker.getContextualType(node);
|
||||
if (contextual) add(contextual);
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
if (
|
||||
node.properties.nodes.some(
|
||||
(property) =>
|
||||
(ts.isMethodDeclaration(property) ||
|
||||
ts.isGetAccessorDeclaration(property) ||
|
||||
ts.isSetAccessorDeclaration(property)) &&
|
||||
property.body !== undefined &&
|
||||
usesReceiver(property.body),
|
||||
)
|
||||
) {
|
||||
// Receiver-dependent methods and accessors need a live object even
|
||||
// without a class implementation. Receiver-free methods keep their
|
||||
// native callable fields, including async callbacks.
|
||||
add(this.checker.getTypeAtLocation(node));
|
||||
const contextual = this.checker.getContextualType(node);
|
||||
if (contextual) add(contextual);
|
||||
}
|
||||
// Member-kind disagreements with the literal's target type are
|
||||
// checked once the walk ends, when the accessor-name index above is
|
||||
// complete (see markMemberKindViews).
|
||||
literals.push(node);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
for (const source of this.sources) if (!source.isDeclarationFile) visit(source);
|
||||
for (const literal of literals) this.markMemberKindViews(literal, accessorNames, add);
|
||||
this.markIdentityFlows(protocols);
|
||||
return protocols;
|
||||
}
|
||||
|
||||
/** Marks the structural types a literal's member kinds disagree with: a
|
||||
* get/set accessor satisfying a declared DATA member, or a data property
|
||||
* satisfying a declared accessor member. One static record layout cannot
|
||||
* hold both member kinds at one name (a data member is a storage slot, an
|
||||
* accessor member a `%get:`/`%set:` closure pair), so the target type
|
||||
* keeps checked identity instead: every value of it is a live
|
||||
* checked-dynamic view, where the accessor stays live, `Object.keys`
|
||||
* lists it, and `JSON.stringify` calls it — Node's own model.
|
||||
*
|
||||
* Only the TARGET is marked: the literal then takes the checked-dynamic
|
||||
* literal path (`lowerDynObjectLiteral`), which defines an accessor member
|
||||
* as a live get/set property and a data member as a plain property, so
|
||||
* both directions construct live objects. */
|
||||
private markMemberKindViews(
|
||||
literal: ts.ObjectLiteralExpression,
|
||||
accessorNames: ReadonlySet<string>,
|
||||
add: (type: ts.Type) => void,
|
||||
): void {
|
||||
const members: { name: string; accessor: boolean }[] = [];
|
||||
for (const property of literal.properties.nodes) {
|
||||
if (ts.isSpreadAssignment(property) || property.name === undefined) continue;
|
||||
const name = propertyNameText(property.name);
|
||||
if (name === null) continue;
|
||||
if (ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property)) {
|
||||
members.push({ name, accessor: true });
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
||||
// A data member only disagrees with an accessor-declared target, and
|
||||
// only a type that declares an accessor somewhere can be one: the
|
||||
// name index keeps the checker round trip off the common literal.
|
||||
accessorNames.has(name)
|
||||
)
|
||||
members.push({ name, accessor: false });
|
||||
}
|
||||
if (members.length === 0) return;
|
||||
const contextual = attempt(() => this.checker.getContextualType(literal));
|
||||
if (contextual === undefined) return;
|
||||
// Only one object type can be the one the literal converts into: a union
|
||||
// with several arms picks its arm from the value itself, and an
|
||||
// intersection merges members, so neither has the single layout the
|
||||
// literal would have to disagree with. Nullish arms are ignored, so
|
||||
// `Counter | undefined` still names Counter.
|
||||
const present = contextual.isUnionType()
|
||||
? ts
|
||||
.constituentTypes(contextual)
|
||||
.filter((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) === 0)
|
||||
: [contextual];
|
||||
const target = present.length === 1 ? present[0]! : undefined;
|
||||
if (target === undefined || target.isIntersectionType()) return;
|
||||
if ((target.flags & ts.TypeFlags.TypeParameter) !== 0) return;
|
||||
if (!this.isStructuralTarget(target)) return;
|
||||
const disagrees = members.some((member) => {
|
||||
const kind = attempt(() => this.memberKind(target, member.name));
|
||||
return kind == null ? false : (kind === "accessor") !== member.accessor;
|
||||
});
|
||||
if (disagrees) add(target);
|
||||
}
|
||||
|
||||
/** The member kind `type` declares at `name` — an accessor (get/set pair)
|
||||
* or a data property — or null when it declares no member by that name. */
|
||||
private memberKind(type: ts.Type, name: string): "accessor" | "data" | null {
|
||||
const property = this.checker.getPropertyOfType(type, name);
|
||||
if (property === undefined) return null;
|
||||
return property.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)
|
||||
? "accessor"
|
||||
: "data";
|
||||
}
|
||||
|
||||
/** Structural types that receive a value with identity: a program class
|
||||
* instance, or a value of a type already marked here. Natively such a
|
||||
* type would be a record, and the conversion into it a field copy, so a
|
||||
@@ -1375,6 +1463,14 @@ function attempt<T>(query: () => T): T | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
/** The static property name a member declares, or null for a computed key
|
||||
* (its name is not a compile-time fact). */
|
||||
function propertyNameText(name: ts.PropertyName): string | null {
|
||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
||||
if (ts.isNumericLiteral(name)) return String(Number(name.text));
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `(value as T).member` and `(value as T)[key]`: the assertion is read or
|
||||
* written member-wise in place, which lowers to the instance's own fields
|
||||
* and creates no view that could outlive it. */
|
||||
|
||||
@@ -134,6 +134,57 @@ export function cleanFuncAdaptable(
|
||||
return src.ret.kind === "void" && dst.ret.kind === "dyn";
|
||||
}
|
||||
|
||||
/** Whether the slot's argument type `actual` converts into the function's
|
||||
* parameter type `expected` without a trap-only path: the pieces
|
||||
* funcCoerceAdapter's parameter loop converts (coercibleValue, the record
|
||||
* width conversion — which also selects a width-liftable arm of a union
|
||||
* destination, the required-narrower-argument shape — the partial
|
||||
* checked-dynamic extraction, and the union arm preservation). */
|
||||
function cleanParamPair(lowerer: Lowerer, actual: IrType, expected: IrType): boolean {
|
||||
if (lowerer.coercibleValue(actual, expected)) return true;
|
||||
if (lowerer.widthLiftPlan(actual, expected) !== null) return true;
|
||||
if (actual.kind === "union" && expected.kind === "dyn")
|
||||
return (
|
||||
lowerer.unions.get(actual.unionId)?.arms.some((arm) => lowerer.dynConvertible(arm)) ?? false
|
||||
);
|
||||
if (actual.kind === "union" && expected.kind === "union") {
|
||||
const target = lowerer.unions.get(expected.unionId);
|
||||
return (
|
||||
target !== undefined && target.arms.every((arm) => lowerer.armTag(actual.unionId, arm) >= 0)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether a function VALUE of `src` may inhabit a `dst` slot: the slot's
|
||||
* coercion (funcCoerceAdapter) forwards its arguments, converts them, and
|
||||
* converts the result — with no trap-only (stranded) path. This is
|
||||
* cleanFuncAdaptable extended to the source-with-MORE-parameters shape: a
|
||||
* value carrying extra trailing parameters whose completed ABI holds
|
||||
* undefined (a `(id, params, signal, onUpdate, ctx?: Ctx)` lambda in a
|
||||
* four-parameter slot). A call through the slot never passes them, so the
|
||||
* coercion forwards undefined — JavaScript's surplus-argument rule. */
|
||||
export function funcSlotAdaptable(
|
||||
lowerer: Lowerer,
|
||||
src: IrType & { kind: "func" },
|
||||
dst: IrType & { kind: "func" },
|
||||
loc: SrcLoc,
|
||||
): boolean {
|
||||
if (restPackingAdapterFits(lowerer, src, dst)) return true;
|
||||
if (src.rest === true || dst.rest === true) return false;
|
||||
for (let i = dst.params.length; i < src.params.length; i++) {
|
||||
const param = src.params[i]!;
|
||||
if (omittedArgFor(lowerer, param, loc) === null && !typeEquals(param, UNDEFINED_T))
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < src.params.length && i < dst.params.length; i++) {
|
||||
if (!cleanParamPair(lowerer, dst.params[i]!, src.params[i]!)) return false;
|
||||
}
|
||||
if (dst.ret.kind === "void") return true;
|
||||
if (lowerer.coercibleValue(src.ret, dst.ret)) return true;
|
||||
return src.ret.kind === "void" && dst.ret.kind === "dyn";
|
||||
}
|
||||
|
||||
/** Whether a typed-rest function (`(...m: T[]) => R`) can serve a slot
|
||||
* with fixed parameters (`(m: T) => void`): the slot's parameters convert
|
||||
* to the function's fixed parameters, then to the rest element type. */
|
||||
@@ -348,6 +399,7 @@ export function funcCoerceAdapter(
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (cleanParamPair(lowerer, actual, expected)) continue;
|
||||
strandParams = true;
|
||||
}
|
||||
let voidRet: "dyn" | "strand" | null = null;
|
||||
|
||||
@@ -76,6 +76,17 @@ export function lowerMapMethodCall(
|
||||
if (!probedUntyped && !lowerer.isStdlibMember(access) && name !== "has") return null;
|
||||
const loc = locOf(call);
|
||||
const value = lowerer.lowerExpr(access.expression);
|
||||
// A receiver the checker types `Map<any, any>` (inference residue of an
|
||||
// untyped `new Map()`) whose value has a concrete layout: its keys and
|
||||
// values convert into that layout.
|
||||
if (
|
||||
value.type.kind === "map" &&
|
||||
receiverIr.key.kind === "dyn" &&
|
||||
receiverIr.value.kind === "dyn" &&
|
||||
value.type.key.kind !== "dyn" &&
|
||||
value.type.value.kind !== "dyn"
|
||||
)
|
||||
receiverIr = value.type;
|
||||
const receiver =
|
||||
lowerer.runtimeOptionalPropertyReceiver(access.expression, value, receiverIr, name) ?? value;
|
||||
if (receiver.type.kind !== "map") {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
*
|
||||
* - a union element to one of its arms, or to a union of some of its
|
||||
* arms, by its tag;
|
||||
* - a union element to a record with the same fields as one of its arms,
|
||||
* through that arm;
|
||||
* - a checked-dynamic element by dynCheck;
|
||||
* - a class instance to a subclass by its class;
|
||||
* - a tuple or record to one with the same fields, field by field.
|
||||
@@ -229,10 +231,35 @@ function unionConversion(
|
||||
if (targetArms === null) return null;
|
||||
const body: IrStmt[] = [];
|
||||
let kept = 0;
|
||||
let recordArmConversions = 0;
|
||||
for (let tag = 0; tag < source.arms.length; tag++) {
|
||||
const arm = source.arms[tag]!;
|
||||
const targetTag = targetArms.findIndex((t) => typeEquals(t, arm));
|
||||
if (targetTag < 0) continue;
|
||||
if (targetTag < 0) {
|
||||
// A record target that is no arm itself but the same fields as one
|
||||
// (`Model<"a">` from the `Model<Api>` arm, whose nested union field
|
||||
// the predicate narrows): that arm converts field by field.
|
||||
if (to.kind !== "record" || arm.kind !== "record") continue;
|
||||
const narrowedArm: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag,
|
||||
value: input,
|
||||
type: arm,
|
||||
loc,
|
||||
};
|
||||
const converted = conversionBody(lowerer, narrowedArm, arm, to, typeText, loc);
|
||||
if (converted === null) continue;
|
||||
recordArmConversions++;
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: input, type: BOOL, loc },
|
||||
then: converted,
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
kept++;
|
||||
const payload: IrExpr = isUnitType(arm)
|
||||
? { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }
|
||||
@@ -249,8 +276,11 @@ function unionConversion(
|
||||
loc,
|
||||
});
|
||||
}
|
||||
// Every arm the target names must be one of the union's own arms.
|
||||
if (kept === 0 || kept !== targetArms.length) return null;
|
||||
// Every arm the target names must be one of the union's own arms, or a
|
||||
// record target converts from at least one arm. A record target that is
|
||||
// itself an arm keeps that arm's elements as they are, and the arms with
|
||||
// its fields convert.
|
||||
if (kept === 0 ? recordArmConversions === 0 : kept !== targetArms.length) return null;
|
||||
body.push(
|
||||
typeError(
|
||||
`a kept element is not a '${typeText}' (a type predicate returned true for a value of another type)`,
|
||||
|
||||
@@ -354,6 +354,31 @@ function literalDefault(node: ts.Expression): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** A fresh object literal (parentheses allowed) one of whose property
|
||||
* values, at any literal depth, is `new Map()` or `new Set()` of the
|
||||
* standard library with neither arguments nor type arguments. */
|
||||
function untypedCollectionFallback(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
let value = node;
|
||||
while (ts.isParenthesizedExpression(value)) value = value.expression;
|
||||
if (!ts.isObjectLiteralExpression(value)) return false;
|
||||
return value.properties.nodes.some((property) => {
|
||||
if (!ts.isPropertyAssignment(property)) return false;
|
||||
let inner = property.initializer;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
if (ts.isObjectLiteralExpression(inner)) return untypedCollectionFallback(lowerer, inner);
|
||||
if (
|
||||
!ts.isNewExpression(inner) ||
|
||||
!ts.isIdentifier(inner.expression) ||
|
||||
(inner.arguments?.nodes.length ?? 0) !== 0 ||
|
||||
(inner.typeArguments?.nodes.length ?? 0) !== 0
|
||||
)
|
||||
return false;
|
||||
const symbol = lowerer.resolveValueSymbol(inner.expression);
|
||||
const name = symbol ? ts.symbolName(symbol) : "";
|
||||
return symbol !== null && lowerer.isStdlibSymbol(symbol) && (name === "Map" || name === "Set");
|
||||
});
|
||||
}
|
||||
|
||||
export function lowerNullishPair(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
@@ -387,6 +412,24 @@ export function lowerNullishPair(
|
||||
// arm that has no first-class union representation.
|
||||
const fresh = rest.length === 1 ? lowerer.emptyCollectionFor(expr.right, rest[0]!) : null;
|
||||
if (fresh) return { kind: "nullish", left, right: fresh, type: fresh.type, loc };
|
||||
// `optionalRecord ?? { a: 1, items: new Map() }`: a fresh fallback literal
|
||||
// whose untyped empty collections (`new Map()`, `new Set()` without type
|
||||
// arguments, which the checker types `Map<any, any>`) leave the checker's
|
||||
// join as a second record arm. When the literal is assignable to the
|
||||
// left's record, it builds as that record, so the collections take its
|
||||
// element types and the result keeps one layout.
|
||||
if (
|
||||
rest.length === 1 &&
|
||||
rest[0]!.kind === "record" &&
|
||||
untypedCollectionFallback(lowerer, expr.right) &&
|
||||
lowerer.checker.isTypeAssignableTo(
|
||||
lowerer.typeOf(expr.right),
|
||||
lowerer.checker.getNonNullableType(lowerer.typeOf(expr.left)),
|
||||
)
|
||||
) {
|
||||
const right = lowerer.lowerExprExpecting(expr.right, rest[0]!);
|
||||
return { kind: "nullish", left, right, type: rest[0]!, loc };
|
||||
}
|
||||
let type = lowerer.irTypeOf(expr);
|
||||
// A runtime-optional read can reach the default even when declarations
|
||||
// call the left side present. Preserve a differently typed primitive
|
||||
|
||||
@@ -86,7 +86,7 @@ import {
|
||||
identifierReadSymbol,
|
||||
stdlibGlobalNameOf,
|
||||
} from "../surfaces.js";
|
||||
import { builtinConstantsObject } from "../lower-builtin-values.js";
|
||||
import { builtinConstantsObject, pathModuleValue } from "../lower-builtin-values.js";
|
||||
import { lowerTlsRootCertificates } from "../lower-server.js";
|
||||
import { builtinFunctionFenceValue } from "../builtin-function-fences.js";
|
||||
import { unsupportedModuleFeatureOf } from "../../builtin-modules.js";
|
||||
@@ -562,6 +562,16 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
}
|
||||
const c = builtinModuleConstOf(lowerer, bi.module, bi.member);
|
||||
if (c !== undefined) return builtinConstLit(c, loc);
|
||||
// `import { posix, win32 } from "node:path"` as VALUES: the platform
|
||||
// namespaces are members of the path module object, so the named
|
||||
// import reads the same checked-dynamic module value the property
|
||||
// form (path.posix) lowers to.
|
||||
if (
|
||||
(bi.module === "path" || bi.module === "path/posix" || bi.module === "path/win32") &&
|
||||
(bi.member === "posix" || bi.member === "win32")
|
||||
) {
|
||||
return pathModuleValue(lowerer, `path/${bi.member}`, loc);
|
||||
}
|
||||
// module.builtinModules — the baked Node v24 list, a fresh
|
||||
// string[] per read.
|
||||
if (bi.module === "module" && bi.member === "builtinModules") {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { dateJsonType, isDateJsonSource } from "../builtins/dates.js";
|
||||
import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { pathModuleValue } from "../lower-builtin-values.js";
|
||||
import { pathModuleSpreadSource } from "../lower-builtin-values.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
import type { IrExpr, IrLocal, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
|
||||
import { isCjsExportTableLiteral, isJsSourceFile, locOf } from "../../program.js";
|
||||
import { NARROW_FIRST } from "../surfaces.js";
|
||||
import { recordShapeMismatchDiag } from "../../../diagnostics/diagnostic.js";
|
||||
import {
|
||||
recordShapeMismatchDiag,
|
||||
unionLiteralMismatchDiag,
|
||||
} from "../../../diagnostics/diagnostic.js";
|
||||
import { PoisonError, jsFuncNameOf, neverTaintedJsType } from "../lowerer.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { lowerIndexMergeHelper } from "../containers/indexed-objects.js";
|
||||
@@ -307,7 +310,12 @@ export function lowerDynObjectLiteral(
|
||||
const builtin = lowerer.builtinNamespaceModuleOf(prop.expression);
|
||||
const raw =
|
||||
builtin === "path" || builtin === "path/posix" || builtin === "path/win32"
|
||||
? pathModuleValue(lowerer, builtin === "path" ? "path/posix" : builtin, locOf(prop))
|
||||
? pathModuleSpreadSource(
|
||||
lowerer,
|
||||
builtin === "path" ? "path/posix" : builtin,
|
||||
isNamespaceImport(lowerer, prop.expression),
|
||||
locOf(prop),
|
||||
)
|
||||
: !boxValue &&
|
||||
conditionalArms &&
|
||||
(conditionalSpreadOf(prop.expression) !== null ||
|
||||
@@ -1301,7 +1309,24 @@ export function lowerObjectLiteral(
|
||||
if (fallback) return fallback;
|
||||
lowerer.unsupported("SC1090", deferredConditionalSpread, CONDITIONAL_SPREAD_FENCE);
|
||||
}
|
||||
if (!mapped || mapped.kind !== "record") lowerer.badType(expr, tsType);
|
||||
if (!mapped || mapped.kind !== "record") {
|
||||
// A spread merge over UNION-typed sources (`{ ...base, ...override }`
|
||||
// with `base`, `override` of arm-union types) has one field set per
|
||||
// combination of source arms, so its own type is a union of records.
|
||||
// No single arm of the contextual union can hold that value without
|
||||
// dropping the fields the other arms contributed (Node keeps them
|
||||
// all), and a runtime tag would be a guess about which arm the value
|
||||
// "is". Refuse, naming the candidate arms.
|
||||
if (mapped?.kind === "union") {
|
||||
const members = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
|
||||
const arms = [...new Set(members.map((m) => lowerer.checker.typeToString(m)))];
|
||||
lowerer.pushDiag(
|
||||
unionLiteralMismatchDiag(lowerer.checker.typeToString(tsType), arms, locOf(expr)),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
lowerer.badType(expr, tsType);
|
||||
}
|
||||
let type: IrType = mapped;
|
||||
// A fresh, plain literal can construct its fields directly for a known
|
||||
// destination with the same field set. This matters for inferred
|
||||
@@ -3655,3 +3680,12 @@ function rejectThisInObjectAccessor(lowerer: Lowerer, node: ts.Node): void {
|
||||
}
|
||||
ts.forEachChild(node, (child) => rejectThisInObjectAccessor(lowerer, child));
|
||||
}
|
||||
|
||||
/** Whether a spread source is an ES module namespace binding
|
||||
* (`import * as path`); a default import, a require and a named
|
||||
* sub-namespace are module objects. */
|
||||
function isNamespaceImport(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
if (!ts.isIdentifier(expr)) return false;
|
||||
const decl = lowerer.checker.getSymbolAtLocation(expr)?.declarations[0];
|
||||
return decl !== undefined && ts.isNamespaceImport(decl);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,14 @@ import {
|
||||
localPrivateBrandClass,
|
||||
privateNameClass,
|
||||
} from "../private-brands.js";
|
||||
import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js";
|
||||
import {
|
||||
type Lowerer,
|
||||
PoisonError,
|
||||
builtinValueNameOf,
|
||||
jsFuncNameOf,
|
||||
neverTaintedJsType,
|
||||
own,
|
||||
} from "../lowerer.js";
|
||||
import {
|
||||
ARRAY_METHODS,
|
||||
MAP_METHODS,
|
||||
@@ -44,6 +51,7 @@ import {
|
||||
isParseArgsDynTypeName,
|
||||
} from "../../type-mapper.js";
|
||||
import {
|
||||
BIGINT_T,
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
DYN,
|
||||
@@ -1202,13 +1210,15 @@ export function lowerPropertyAccessExpr(
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
) {
|
||||
const fnName = jsFuncNameOf(expr.expression);
|
||||
// A builtin function value whose length is not its adapter's
|
||||
// parameter count (`Math.max.length` is 2).
|
||||
const fnLength =
|
||||
probed.kind === "closure"
|
||||
? lowerer.builtinCallableValueLengths.get(probed.fnName)
|
||||
: undefined;
|
||||
// A function is named by its definition, through const aliases: a
|
||||
// builtin module member whose Node name differs from the member
|
||||
// (`process.cwd` is `wrappedCwd`) by the module table, anything else
|
||||
// by the property or function it reads. A builtin function value
|
||||
// keeps its length when that is not its adapter's parameter count
|
||||
// (`Math.max.length` is 2).
|
||||
const fnName =
|
||||
builtinValueNameOf(expr.expression, lowerer) ?? jsFuncNameOf(expr.expression, lowerer);
|
||||
const fnLength = lowerer.builtinFunctionIdentity(probed).fnLength;
|
||||
const boxed: IrExpr = {
|
||||
kind: "dynFrom",
|
||||
value: probed,
|
||||
@@ -2097,6 +2107,7 @@ export function lowerIntrinsicProperty(
|
||||
kind !== "url" &&
|
||||
kind !== "searchParams" &&
|
||||
kind !== "stats" &&
|
||||
kind !== "bigintStats" &&
|
||||
kind !== "spawnRes" &&
|
||||
kind !== "child" &&
|
||||
kind !== "bytes" &&
|
||||
@@ -2252,18 +2263,31 @@ export function lowerIntrinsicProperty(
|
||||
lowerer.checker.getSymbolAtLocation(expr.name),
|
||||
);
|
||||
}
|
||||
if (kind === "stats") {
|
||||
if (kind === "stats" || kind === "bigintStats") {
|
||||
const bigintStats = kind === "bigintStats";
|
||||
if (name === "size") {
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
return { kind: "libCall", fn: "stats.size", args: [receiver], type: F64, loc: locOf(expr) };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: bigintStats ? "bigstats.size" : "stats.size",
|
||||
args: [receiver],
|
||||
type: bigintStats ? BIGINT_T : F64,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
if (Object.hasOwn(STATS_TYPE_TESTS, name)) {
|
||||
lowerer.unsupported("SC1090", expr, `Stats methods as values (call '${name}' directly)`);
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`${bigintStats ? "BigIntStats" : "Stats"} methods as values (call '${name}' directly)`,
|
||||
);
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`Stats.${name}`,
|
||||
`${bigintStats ? "BigIntStats" : "Stats"}.${name}`,
|
||||
expr,
|
||||
"isFile(), isDirectory(), isSymbolicLink(), isBlockDevice(), isCharacterDevice(), isFIFO(), isSocket(), dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, atimeMs, mtimeMs, ctimeMs, birthtimeMs, atime, mtime, ctime, and birthtime are the supported Stats members",
|
||||
bigintStats
|
||||
? "BigIntStats' isFile(), isDirectory(), isSymbolicLink(), isBlockDevice(), isCharacterDevice(), isFIFO(), isSocket(), dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, atimeNs, mtimeNs, ctimeNs, birthtimeNs, atimeMs, mtimeMs, ctimeMs, birthtimeMs, atime, mtime, ctime, and birthtime are the supported members"
|
||||
: "isFile(), isDirectory(), isSymbolicLink(), isBlockDevice(), isCharacterDevice(), isFIFO(), isSocket(), dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, atimeMs, mtimeMs, ctimeMs, birthtimeMs, atime, mtime, ctime, and birthtime are the supported Stats members",
|
||||
lowerer.checker.getSymbolAtLocation(expr.name),
|
||||
);
|
||||
}
|
||||
@@ -3474,6 +3498,26 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether a receiver is a value the checker ruled UNREACHABLE (`never`),
|
||||
* directly or through the parenthesized assertion chain that escapes it
|
||||
* (`(x as any)`, `(x as unknown)`, `x!`). The erasure is what hides the
|
||||
* value's stored union from the checker; the value itself still holds it. */
|
||||
function unreachableReceiver(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
|
||||
let node: ts.Expression = expr.expression;
|
||||
for (;;) {
|
||||
if ((lowerer.typeOf(node).flags & ts.TypeFlags.Never) !== 0) return true;
|
||||
if (
|
||||
ts.isParenthesizedExpression(node) ||
|
||||
ts.isNonNullExpression(node) ||
|
||||
ts.isAssertionExpression(node)
|
||||
) {
|
||||
node = node.expression;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Shared-field read `r.f` on a UNION receiver: supported exactly when
|
||||
* every arm is a record/class possessing the field with ONE shared IR
|
||||
* type — the discriminant pattern (`r.kind`, primitive) and the
|
||||
@@ -3491,14 +3535,26 @@ export function lowerUnionProperty(
|
||||
const receiverIr =
|
||||
structuralReadType(lowerer, expr.expression) ??
|
||||
lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
|
||||
if (receiverIr?.kind !== "union") return null;
|
||||
// The checker can rule the receiver UNREACHABLE and the assertion that
|
||||
// escapes it (`const x: never = event; (event as any).type` — the
|
||||
// `default:` branch of a switch that exhausts every arm) erases to
|
||||
// checked-dynamic, hiding the union the value actually holds. Node reads
|
||||
// the member off the object it is; the arms all declare it, so the read
|
||||
// is the shared-field read. `never` admits no reachable call site, so no
|
||||
// reachable program changes shape.
|
||||
const unreachable = receiverIr?.kind !== "union" && unreachableReceiver(lowerer, expr);
|
||||
if (receiverIr?.kind !== "union" && !unreachable) return null;
|
||||
// Lower the receiver first: its runtime value can still carry the full
|
||||
// binding union even when the checker has removed several variants.
|
||||
let value = lowerer.lowerExpr(expr.expression);
|
||||
// A guard can remove several variants without selecting one record.
|
||||
// Materialize that sub-union before looking for common fields; arms
|
||||
// ruled out by the guard must not veto a field shared by the survivors.
|
||||
if (value.type.kind === "union" && !typeEquals(value.type, receiverIr)) {
|
||||
if (
|
||||
receiverIr?.kind === "union" &&
|
||||
value.type.kind === "union" &&
|
||||
!typeEquals(value.type, receiverIr)
|
||||
) {
|
||||
const helper = lowerer.narrowedRetagHelper(
|
||||
expr.expression,
|
||||
value.type.unionId,
|
||||
|
||||
@@ -306,6 +306,83 @@ export function lowerUnionFieldWrite(
|
||||
return { kind: "seqExpr", stmts: body, result: dispatch, type: result.type, loc };
|
||||
}
|
||||
|
||||
/** `delete u.f` on a union receiver: each record arm declares `f` as an
|
||||
* OPTIONAL field (an undefined-armed slot), so each tag writes its own
|
||||
* slot's ABSENT state — the monomorphic optional-field delete, dispatched
|
||||
* over the arms the way union field writes are. The union value is
|
||||
* retained once, so every arm deletes the object the program passed, not a
|
||||
* projection. Null when an arm is not a record that declares `f` optional
|
||||
* (unit arms, class arms, required fields, accessors and index signatures
|
||||
* keep their own fences): a required slot has no absence to represent. */
|
||||
export function lowerUnionFieldDelete(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrStmt | null {
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (access.questionDotToken || mapped?.kind !== "union") return null;
|
||||
let receiver = lowerer.maybeNarrow(lowerer.lowerExpr(access.expression), access.expression);
|
||||
if (receiver.type.kind === "union" && !typeEquals(receiver.type, mapped))
|
||||
receiver = narrowStoredClassValue(lowerer, receiver, mapped) ?? receiver;
|
||||
if (receiver.type.kind !== "union") return null;
|
||||
const unionId = receiver.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms;
|
||||
if (!arms || arms.length < 2) return null;
|
||||
const name = access.name.text;
|
||||
const loc = locOf(access);
|
||||
const fields: { arm: IrType & { kind: "record" }; type: IrType }[] = [];
|
||||
for (const arm of arms) {
|
||||
if (arm.kind !== "record" || lowerer.shapes.get(arm.shapeId)?.tuple) return null;
|
||||
const field = unionFieldArm(lowerer, arm, name);
|
||||
if (!field || lowerer.absentFieldValue(field.type, loc) === null) return null;
|
||||
fields.push({ arm, type: field.type });
|
||||
}
|
||||
const object = lowerer.declareHiddenLocal("%unionDeleteObject", receiver.type);
|
||||
const objectRef = varRef(object.id, object.type, loc);
|
||||
const branch = (tag: number): IrStmt[] => {
|
||||
const field = fields[tag]!;
|
||||
const absent = lowerer.absentFieldValue(field.type, loc);
|
||||
if (absent === null) return [];
|
||||
const write: IrStmt = {
|
||||
kind: "recordSet",
|
||||
obj: { kind: "unionNarrow", unionId, tag, value: objectRef, type: field.arm, loc },
|
||||
shapeId: field.arm.shapeId,
|
||||
field: name,
|
||||
value: absent,
|
||||
loc,
|
||||
};
|
||||
// A sloppy-mode delete on a frozen record leaves it unchanged.
|
||||
markSloppyRecordWrites(lowerer, write, access);
|
||||
return [write];
|
||||
};
|
||||
// One tag test per arm, the last arm as the fall-through else: a delete
|
||||
// runs at most one arm's write.
|
||||
const dispatch = (tag: number): IrStmt[] =>
|
||||
tag === fields.length - 1
|
||||
? branch(tag)
|
||||
: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag,
|
||||
value: objectRef,
|
||||
negated: false,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: branch(tag),
|
||||
else_: dispatch(tag + 1),
|
||||
loc,
|
||||
},
|
||||
];
|
||||
return {
|
||||
kind: "block",
|
||||
body: [{ kind: "varDecl", localId: object.id, init: receiver, loc }, ...dispatch(0)],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Read the element at a literal index from a union of tuples whose element
|
||||
* types differ (`readonly [true, number] | readonly [false, string]`, index
|
||||
* 1): each tag reads its own tuple's slot and widens it into the union of
|
||||
|
||||
@@ -121,6 +121,8 @@ function freezableTypes(
|
||||
const shapes = new Set<string>();
|
||||
const arrays = new Set<string>();
|
||||
const live: IrType[] = [];
|
||||
const checked: IrType[] = [];
|
||||
let freezesUntyped = false;
|
||||
for (const fn of functions)
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
@@ -130,19 +132,28 @@ function freezableTypes(
|
||||
} else if (e.kind === "libCall" && e.fn === "array.freeze") {
|
||||
const target = e.args[0];
|
||||
if (target && target.type.kind === "array") arrays.add(typeKey(target.type));
|
||||
} else if (e.kind === "dynFrom") live.push(e.value.type);
|
||||
} else if (e.kind === "libCall" && e.fn === "dyn.freeze") freezesUntyped = true;
|
||||
else if (e.kind === "dynFrom") live.push(e.value.type);
|
||||
else if (e.kind === "dynCheck") checked.push(e.type);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
if (!lowerer.untypedIntegrity) return { shapes, arrays };
|
||||
// An untyped reference to a class instance, or to a record or array
|
||||
// boxed live, reaches the values its fields and elements hold as further
|
||||
// live references. Class instances also cross into untyped code through
|
||||
// compiled callbacks, so every class is a root.
|
||||
const stack = [...live];
|
||||
for (const info of lowerer.classes.values())
|
||||
for (const field of info.def.fields) stack.push(field.type);
|
||||
if (!lowerer.untypedIntegrity && !freezesUntyped) return { shapes, arrays };
|
||||
const stack: IrType[] = [];
|
||||
// A record checked from an untyped value copies it, frozen when the value
|
||||
// is (a fresh literal frozen in a JavaScript file included), so the shapes
|
||||
// a check builds are freezable in a program that freezes untyped values.
|
||||
if (freezesUntyped) stack.push(...checked);
|
||||
if (lowerer.untypedIntegrity) {
|
||||
// An untyped reference to a class instance, or to a record or array
|
||||
// boxed live, reaches the values its fields and elements hold as further
|
||||
// live references. Class instances also cross into untyped code through
|
||||
// compiled callbacks, so every class is a root.
|
||||
stack.push(...live);
|
||||
for (const info of lowerer.classes.values())
|
||||
for (const field of info.def.fields) stack.push(field.type);
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
while (stack.length > 0) {
|
||||
const type = stack.pop()!;
|
||||
|
||||
@@ -154,6 +154,22 @@ function returnsOpenJsBinding(lowerer: Lowerer, decl: ts.SignatureDeclaration):
|
||||
return open;
|
||||
}
|
||||
|
||||
/** `type` when it is the ambient Promise, or its one Promise arm when it is
|
||||
* a union with exactly one; undefined for anything else (`void`, `unknown`,
|
||||
* a PromiseLike). */
|
||||
function promiseArm(lowerer: Lowerer, type: ts.Type): ts.Type | undefined {
|
||||
const isPromise = (t: ts.Type): boolean => {
|
||||
const symbol = t.getSymbol();
|
||||
return (
|
||||
symbol !== undefined && ts.symbolName(symbol) === "Promise" && lowerer.isStdlibSymbol(symbol)
|
||||
);
|
||||
};
|
||||
if (isPromise(type)) return type;
|
||||
if (!type.isUnionType()) return undefined;
|
||||
const arms = ts.constituentTypes(type).filter(isPromise);
|
||||
return arms.length === 1 ? arms[0] : undefined;
|
||||
}
|
||||
|
||||
/** Map a declaration's return contract while retaining JavaScript
|
||||
* inference fallbacks. Diagnostics preserve the caller's blame node. */
|
||||
export function declaredReturnType(
|
||||
@@ -164,7 +180,39 @@ export function declaredReturnType(
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && ts.isGetAccessorDeclaration(decl)) return DYN;
|
||||
const sig = lowerer.checker.getSignatureFromDeclaration(decl);
|
||||
if (!sig) lowerer.unsupported("SC1090", decl, "this function form");
|
||||
const retTsType = lowerer.checker.getReturnTypeOfSignature(sig);
|
||||
let retTsType = lowerer.checker.getReturnTypeOfSignature(sig);
|
||||
// An ASYNC object-literal method with no return annotation and a
|
||||
// contextual signature takes its return from that signature, as an async
|
||||
// arrow does: the body's `return` builds at the contextually typed
|
||||
// literal, so the checker's own inferred literal (narrower than the
|
||||
// contextual record) could never hold it.
|
||||
if (
|
||||
ts.isMethodDeclaration(decl) &&
|
||||
ts.isObjectLiteralExpression(decl.parent) &&
|
||||
decl.type === undefined &&
|
||||
decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true
|
||||
) {
|
||||
const contextual = lowerer.checker.getContextualType(decl.parent);
|
||||
const slot =
|
||||
contextual === undefined || (!ts.isIdentifier(decl.name) && !ts.isStringLiteral(decl.name))
|
||||
? undefined
|
||||
: lowerer.checker.getPropertyOfType(
|
||||
lowerer.checker.getNonNullableType(contextual),
|
||||
decl.name.text,
|
||||
);
|
||||
const signatures =
|
||||
slot === undefined
|
||||
? []
|
||||
: lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(slot));
|
||||
// A slot whose return is not a promise (`() => void`, or a union such
|
||||
// as `Promise<T> | void`) contributes only its promise: an async
|
||||
// method returns one whatever the slot says.
|
||||
const promise =
|
||||
signatures.length === 1
|
||||
? promiseArm(lowerer, lowerer.checker.getReturnTypeOfSignature(signatures[0]!))
|
||||
: undefined;
|
||||
if (promise !== undefined) retTsType = promise;
|
||||
}
|
||||
// An inferred null/undefined return in JS can read a mutable checked-value
|
||||
// field. Keep the actual value instead of restricting it to the initializer.
|
||||
if (
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import { isStrictCode } from "./strict-mode.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
import { stdlibGlobalNameOf } from "./surfaces.js";
|
||||
@@ -122,11 +123,17 @@ function readsBuiltinByName(lowerer: Lowerer, node: ts.ElementAccessExpression):
|
||||
/** The value a JavaScript source reads for the declared global `name`
|
||||
* taken as a value, whatever its spelling (`name`, `globalThis.name`,
|
||||
* `const { name } = globalThis`, `globalThis[name]`), or null when it
|
||||
* reads the opaque identity token. */
|
||||
export function jsGlobalValueByName(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null {
|
||||
* reads the opaque identity token. `reportFences` (builtinNamespaceValue)
|
||||
* is false for the program's lookup. */
|
||||
export function jsGlobalValueByName(
|
||||
lowerer: Lowerer,
|
||||
name: string,
|
||||
loc: SrcLoc,
|
||||
reportFences = true,
|
||||
): IrExpr | null {
|
||||
return (
|
||||
jsSpecialGlobalValue(lowerer, name, loc) ??
|
||||
builtinNamespaceValue(lowerer, name, loc) ??
|
||||
builtinNamespaceValue(lowerer, name, loc, reportFences) ??
|
||||
builtinGlobalValue(name, loc, true)
|
||||
);
|
||||
}
|
||||
@@ -169,22 +176,150 @@ const URI_FUNCTIONS = new Set([
|
||||
"decodeURIComponent",
|
||||
]);
|
||||
|
||||
/** The builtin globals that sloppy-mode functions of the JavaScript source
|
||||
* `sf` read through their `this` (`this.Array`, `this["Array"]`) or through
|
||||
* a variable it initializes (`var root = this; root.Array`); every declared
|
||||
* global when such a read takes a computed key (`this[name]`). Called with
|
||||
* undefined or null as `this`, such a function reads them on the global
|
||||
* object, which answers them through the program's lookup. */
|
||||
export function sloppyThisGlobalReadsOf(lowerer: Lowerer, sf: ts.SourceFile): ReadonlySet<string> {
|
||||
const cached = lowerer.sloppyThisGlobalReads.get(sf);
|
||||
if (cached !== undefined) return cached;
|
||||
const reads = new Set<string>();
|
||||
if (isJsSourceFile(sf)) {
|
||||
const declared = new Set(declaredGlobalNames(lowerer, sf));
|
||||
// Element reads of `this` or of a variable, and the names of the
|
||||
// variables initialized with `this`: the checker resolves only the
|
||||
// element reads of `this` and of those variables.
|
||||
const elementReads: ts.ElementAccessExpression[] = [];
|
||||
const thisAliases = new Set<string>();
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
ts.isIdentifier(node.name) &&
|
||||
declared.has(node.name.text) &&
|
||||
readsSloppyThis(lowerer, node.expression)
|
||||
)
|
||||
reads.add(node.name.text);
|
||||
else if (ts.isElementAccessExpression(node)) {
|
||||
const target = withoutParentheses(node.expression);
|
||||
if (target.kind === ts.SyntaxKind.ThisKeyword || ts.isIdentifier(target))
|
||||
elementReads.push(node);
|
||||
} else if (
|
||||
ts.isVariableDeclaration(node) &&
|
||||
ts.isIdentifier(node.name) &&
|
||||
node.initializer !== undefined &&
|
||||
withoutParentheses(node.initializer).kind === ts.SyntaxKind.ThisKeyword
|
||||
)
|
||||
thisAliases.add(node.name.text);
|
||||
return undefined;
|
||||
});
|
||||
for (const node of elementReads) {
|
||||
const target = withoutParentheses(node.expression);
|
||||
if (ts.isIdentifier(target) && !thisAliases.has(target.text)) continue;
|
||||
if (!readsSloppyThis(lowerer, node.expression) || !readsBuiltinByName(lowerer, node))
|
||||
continue;
|
||||
const key = withoutParentheses(node.argumentExpression);
|
||||
if (ts.isStringLiteralLike(key)) {
|
||||
if (declared.has(key.text)) reads.add(key.text);
|
||||
} else if (!ts.isNumericLiteral(key)) {
|
||||
for (const name of declared) reads.add(name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lowerer.sloppyThisGlobalReads.set(sf, reads);
|
||||
return reads;
|
||||
}
|
||||
|
||||
/** Whether `expr` is the `this` of a sloppy-mode function, or a variable
|
||||
* whose only declaration initializes it with that `this`. */
|
||||
function readsSloppyThis(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
const inner = withoutParentheses(expr);
|
||||
if (inner.kind === ts.SyntaxKind.ThisKeyword) return sloppyReceiverOf(inner);
|
||||
if (!ts.isIdentifier(inner)) return false;
|
||||
const declarations = lowerer.checker.getSymbolAtLocation(inner)?.declarations ?? [];
|
||||
const declaration = declarations[0];
|
||||
if (
|
||||
declarations.length !== 1 ||
|
||||
declaration === undefined ||
|
||||
!ts.isVariableDeclaration(declaration)
|
||||
)
|
||||
return false;
|
||||
const init =
|
||||
declaration.initializer === undefined ? undefined : withoutParentheses(declaration.initializer);
|
||||
return init !== undefined && init.kind === ts.SyntaxKind.ThisKeyword && sloppyReceiverOf(init);
|
||||
}
|
||||
|
||||
function withoutParentheses(expr: ts.Expression): ts.Expression {
|
||||
let inner = expr;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
return inner;
|
||||
}
|
||||
|
||||
/** Whether the `this` at `node` is a sloppy-mode function's receiver
|
||||
* (lower-functions.ts declareCallReceiver): arrows take the enclosing one. */
|
||||
function sloppyReceiverOf(node: ts.Node): boolean {
|
||||
for (let parent = node.parent; parent !== undefined; parent = parent.parent) {
|
||||
if (ts.isArrowFunction(parent)) continue;
|
||||
if (ts.isFunctionLike(parent)) return parent.body !== undefined && !isStrictCode(parent.body);
|
||||
if (
|
||||
ts.isSourceFile(parent) ||
|
||||
ts.isClassDeclaration(parent) ||
|
||||
ts.isClassExpression(parent) ||
|
||||
ts.isClassStaticBlockDeclaration(parent)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The names of the global object's declared properties that the lookup can
|
||||
* answer. */
|
||||
function declaredGlobalNames(lowerer: Lowerer, at: ts.Node): string[] {
|
||||
const symbol = lowerer.checker.resolveName(
|
||||
"globalThis",
|
||||
ts.SymbolFlags.Value,
|
||||
ts.getSourceFileOfNode(at),
|
||||
false,
|
||||
);
|
||||
if (symbol === undefined) return [];
|
||||
return lowerer.checker
|
||||
.getPropertiesOfType(lowerer.checker.getTypeOfSymbol(symbol))
|
||||
.map((property) => ts.symbolName(property))
|
||||
.filter((name) => name !== TEMPORAL_GLOBAL && !RUNTIME_ANSWERED.has(name));
|
||||
}
|
||||
|
||||
/** The names the program's lookup answers: every declared global when a
|
||||
* module reads the global object by computed names, else the ones its
|
||||
* sloppy-mode functions read through `this`. */
|
||||
function lookupNames(lowerer: Lowerer): readonly string[] {
|
||||
if (lowerer.globalLookupNames !== undefined) return lowerer.globalLookupNames;
|
||||
const modules = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry];
|
||||
const names = modules.some((sf) => computedGlobalReadOf(lowerer, sf) !== null)
|
||||
? declaredGlobalNames(lowerer, lowerer.entry)
|
||||
: modules.flatMap((sf) => [...sloppyThisGlobalReadsOf(lowerer, sf)]);
|
||||
lowerer.globalLookupNames = [...new Set(names)].sort();
|
||||
return lowerer.globalLookupNames;
|
||||
}
|
||||
|
||||
/** Whether the JavaScript source `sf` installs the program's lookup when its
|
||||
* body starts: it reads builtin globals through the global object by
|
||||
* computed names, or through a sloppy-mode function's `this`. */
|
||||
export function installsGlobalValues(lowerer: Lowerer, sf: ts.SourceFile): boolean {
|
||||
return (
|
||||
computedGlobalReadOf(lowerer, sf) !== null || sloppyThisGlobalReadsOf(lowerer, sf).size > 0
|
||||
);
|
||||
}
|
||||
|
||||
/** The statement that installs the program's lookup, built on first use
|
||||
* from the names the checker declares on `receiver` (a global object). */
|
||||
export function globalValuesInstall(
|
||||
lowerer: Lowerer,
|
||||
receiver: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): IrStmt {
|
||||
* from the names it answers (lookupNames). */
|
||||
export function globalValuesInstall(lowerer: Lowerer, loc: SrcLoc): IrStmt {
|
||||
const fnType: IrType & { kind: "func" } = { kind: "func", params: [STRING], ret: DYN };
|
||||
if (!lowerer.liftedFnNamed(LOOKUP_FN)) {
|
||||
const names = lowerer.checker
|
||||
.getPropertiesOfType(lowerer.typeOf(receiver))
|
||||
.map((property) => ts.symbolName(property))
|
||||
.filter((name) => name !== TEMPORAL_GLOBAL && !RUNTIME_ANSWERED.has(name));
|
||||
const name: IrExpr = { kind: "varRef", localId: "name.0", type: STRING, loc };
|
||||
const body: IrStmt[] = [];
|
||||
for (const global of [...new Set(names)].sort()) {
|
||||
for (const global of lookupNames(lowerer)) {
|
||||
const webStream = (WEB_STREAM_CLASSES as readonly string[]).indexOf(global);
|
||||
const value: IrExpr | null = REFUSED.has(global)
|
||||
? { kind: "unitLit", unit: "null", type: { kind: "nullT" }, loc }
|
||||
@@ -192,7 +327,7 @@ export function globalValuesInstall(
|
||||
? programNamesWebStreamClass(lowerer)
|
||||
? webStreamClassValue(webStream, loc)
|
||||
: null
|
||||
: jsGlobalValueByName(lowerer, global, loc);
|
||||
: jsGlobalValueByName(lowerer, global, loc, false);
|
||||
if (value === null) continue;
|
||||
body.push({
|
||||
kind: "if",
|
||||
|
||||
@@ -426,21 +426,86 @@ const box = (value: IrExpr): IrExpr =>
|
||||
value.type.kind === "dyn" ? value : { kind: "dynFrom", value, type: DYN, loc: value.loc };
|
||||
|
||||
export function pathModuleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr {
|
||||
const fields: { key: IrExpr; value: IrExpr }[] = [];
|
||||
const add = (member: string, value: IrExpr): void => {
|
||||
fields.push({ key: strLit(member, loc), value: box(value) });
|
||||
// node:path's namespaces are module objects: the same handle
|
||||
// `process.getBuiltinModule("node:path")` answers. The handle caches
|
||||
// each member (so `path.join === path.join` and a mutation through one
|
||||
// spelling is visible through the other) and identifies the module, so
|
||||
// `posix === path.posix` holds — the object model the platform
|
||||
// namespaces need when they are VALUES (`const api = win32`).
|
||||
return moduleValue(lowerer, module, loc);
|
||||
}
|
||||
|
||||
/** Node's own keys of the node:path module objects (path, path.posix and
|
||||
* path.win32), in order; `_makeLong` is the same function as
|
||||
* toNamespacedPath. */
|
||||
const PATH_MODULE_KEYS = [
|
||||
"resolve",
|
||||
"normalize",
|
||||
"isAbsolute",
|
||||
"join",
|
||||
"relative",
|
||||
"toNamespacedPath",
|
||||
"dirname",
|
||||
"basename",
|
||||
"extname",
|
||||
"format",
|
||||
"parse",
|
||||
"matchesGlob",
|
||||
"sep",
|
||||
"delimiter",
|
||||
"win32",
|
||||
"posix",
|
||||
"_makeLong",
|
||||
] as const;
|
||||
|
||||
/** What a spread of a node:path namespace copies: the module object's own
|
||||
* members in Node's order, each read through the module value, so the copy
|
||||
* holds the same functions (`{ ...path }.join === path.join`). An ES module
|
||||
* namespace (`import * as path`) has its keys sorted and also `default`,
|
||||
* the module object. Members the module value does not answer (format and
|
||||
* parse have no function values) are left out, as they are of the value. */
|
||||
export function pathModuleSpreadSource(
|
||||
lowerer: Lowerer,
|
||||
module: string,
|
||||
namespace: boolean,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const local = lowerer.declareHiddenLocal("%pathModule", DYN);
|
||||
const value = varRef(local.id, DYN, loc);
|
||||
const answered = (key: string): boolean =>
|
||||
key === "default" ||
|
||||
key === "posix" ||
|
||||
key === "win32" ||
|
||||
builtinModuleConstOf(lowerer, module, key) !== undefined ||
|
||||
lowerer.lowerBuiltinCallableValue(
|
||||
{ module, member: key === "_makeLong" ? "toNamespacedPath" : key },
|
||||
loc,
|
||||
) !== null;
|
||||
const keys = (namespace ? [...PATH_MODULE_KEYS, "default"].sort() : [...PATH_MODULE_KEYS]).filter(
|
||||
answered,
|
||||
);
|
||||
const fields = keys.map((key) => ({
|
||||
key: strLit(key, loc),
|
||||
value:
|
||||
key === "default"
|
||||
? value
|
||||
: ({
|
||||
kind: "dynKeyGet",
|
||||
value,
|
||||
key: strLit(key === "_makeLong" ? "toNamespacedPath" : key, loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
} as IrExpr),
|
||||
}));
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: local.id, init: pathModuleValue(lowerer, module, loc), loc },
|
||||
],
|
||||
result: { kind: "dynObjLit", fields, type: DYN, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
for (const member of Object.keys(builtinModuleFns()[module] ?? {})) {
|
||||
const value = lowerer.lowerBuiltinCallableValue({ module, member }, loc);
|
||||
if (value) add(member, value);
|
||||
}
|
||||
for (const member of Object.keys(BUILTIN_MODULE_CONSTS[module] ?? {})) {
|
||||
const value = builtinModuleConstOf(lowerer, module, member);
|
||||
if (value !== undefined) add(member, builtinConstLit(value, loc));
|
||||
}
|
||||
add("posix", moduleValue(lowerer, "path/posix", loc));
|
||||
add("win32", moduleValue(lowerer, "path/win32", loc));
|
||||
return { kind: "dynObjLit", fields, type: DYN, loc };
|
||||
}
|
||||
|
||||
export function utilTypesModuleValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
|
||||
@@ -3707,7 +3707,17 @@ function lowerDynReceiverMethodCall(
|
||||
const binding = ts.isIdentifier(access.expression)
|
||||
? lowerer.builtinImportOf(access.expression)
|
||||
: null;
|
||||
if (binding?.module !== "worker_threads" || binding.member !== "parentPort") return null;
|
||||
const parentPort = binding?.module === "worker_threads" && binding.member === "parentPort";
|
||||
// node:path's platform namespaces resolve to ambient declarations the
|
||||
// same way, but their VALUE is the checked-dynamic module object (the
|
||||
// whole namespace), so `win32.join(...)` takes this receiver path
|
||||
// exactly like `path.win32.join(...)`.
|
||||
const pathNamespace =
|
||||
(binding?.module === "path" ||
|
||||
binding?.module === "path/posix" ||
|
||||
binding?.module === "path/win32") &&
|
||||
(binding.member === "posix" || binding.member === "win32");
|
||||
if (!parentPort && !pathNamespace) return null;
|
||||
}
|
||||
// Only checker-untyped receivers: `any`/`unknown`, or the `any[]` an
|
||||
// Array.isArray guard narrows them to (the value is STILL the checked-dynamic tree
|
||||
@@ -4919,6 +4929,9 @@ function lowerArrayIsArrayCall(
|
||||
// itself instead of folding on the extracted type (typeof's rule).
|
||||
if (arg.kind === "dynCheck" && arg.value.type.kind === "dyn") arg = arg.value;
|
||||
if (arg.type.kind === "dyn") {
|
||||
// With Node islands the value may be an island array (scope-B NI-4).
|
||||
if (lowerer.nodeIslands !== null)
|
||||
return { kind: "libCall", fn: "nodeIsland.isArray", args: [arg], type: BOOL, loc };
|
||||
return { kind: "dynTest", test: "array", value: arg, type: BOOL, loc };
|
||||
}
|
||||
if (arg.type.kind === "union") {
|
||||
@@ -5142,11 +5155,22 @@ function lowerUnionToStringCall(
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
if (access.name.text !== "toString" || call.arguments.nodes.length !== 0) return null;
|
||||
if (access.name.text !== "toString") return null;
|
||||
// `.toString("utf8")` on `string | Buffer` (a Buffer's default decode;
|
||||
// String.prototype.toString ignores its arguments): the same helper.
|
||||
const args = call.arguments.nodes;
|
||||
const utf8Arg =
|
||||
args.length === 1 && ts.isStringLiteralLike(args[0]!) && /^utf-?8$/i.test(args[0].text);
|
||||
if (args.length !== 0 && !utf8Arg) return null;
|
||||
const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (recvT?.kind !== "union") return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const def = lowerer.unions.get(recvT.unionId);
|
||||
if (
|
||||
utf8Arg &&
|
||||
!def?.arms.every((a) => a.kind === "string" || (a.kind === "bytes" && a.elem === "u8"))
|
||||
)
|
||||
return null;
|
||||
const stringable = def?.arms.every(
|
||||
(a) =>
|
||||
a.kind === "string" ||
|
||||
|
||||
@@ -133,7 +133,11 @@ import { isAssertClassNew } from "./assert-instances.js";
|
||||
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { lowerArrayConstructor } from "./containers/array-construction.js";
|
||||
import { initializedCollectionType } from "./js-collection-types.js";
|
||||
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
|
||||
import {
|
||||
lowerMapSeedNew,
|
||||
lowerSetSeedNew,
|
||||
mapFromSeedValue,
|
||||
} from "./containers/collection-methods.js";
|
||||
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
|
||||
import { knownBufEncoding, lowerBufferConstruct } from "./containers/bytes.js";
|
||||
import { lowerSearchParamsNew } from "./builtins/url.js";
|
||||
@@ -3451,7 +3455,15 @@ export function collectClassShapeInner(
|
||||
if (shape.bodyType) shape.bodyType = DYN;
|
||||
}
|
||||
ft.params = shapes.map((shape) => shape.type);
|
||||
if (ft.ret.kind !== "void") ft.ret = DYN;
|
||||
// A GENERATOR method's result is the suspended generator its spawn
|
||||
// wrapper answers, so the generator kind IS the checked-crossable
|
||||
// ABI: dynFrom boxes a generator by reference and dyn calls resume
|
||||
// it through the dyn generator ops. Replacing it with plain DYN
|
||||
// would leave the method no representation at all (the generator
|
||||
// branch below needs the channels to build its metadata), which
|
||||
// fenced every `c.m` read on a prototype-accessed class. Only the
|
||||
// ARGUMENTS cross as checked values here.
|
||||
if (ft.ret.kind !== "void" && member.asteriskToken === undefined) ft.ret = DYN;
|
||||
}
|
||||
// JS subclasses can add parameters to a method. Reserve checked
|
||||
// argument slots in the base before constructing its vtable; shorter
|
||||
@@ -11062,6 +11074,35 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
mapped = ctxMapped;
|
||||
}
|
||||
}
|
||||
// `new Map(source)` the checker types `Map<any, any>` because the
|
||||
// source's checker type joins an untyped `new Map()` with a typed map
|
||||
// (a union of map types): the copy takes the layout the source value
|
||||
// actually has.
|
||||
if (
|
||||
seedArg &&
|
||||
!entriesLit &&
|
||||
mapped?.kind === "map" &&
|
||||
mapped.key.kind === "dyn" &&
|
||||
mapped.value.kind === "dyn" &&
|
||||
(expr.typeArguments?.nodes.length ?? 0) === 0 &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(seedArg))?.kind === "union"
|
||||
) {
|
||||
const source = lowerer.lowerExpr(seedArg);
|
||||
if (
|
||||
source.type.kind === "map" &&
|
||||
source.type.key.kind !== "dyn" &&
|
||||
source.type.value.kind !== "dyn"
|
||||
) {
|
||||
const copied = mapFromSeedValue(lowerer, source, source.type);
|
||||
if (copied) return copied;
|
||||
}
|
||||
lowerer.noLowering(
|
||||
"new Map(entries)",
|
||||
expr,
|
||||
"a source whose map layouts differ (give the empty `new Map()` its type arguments)",
|
||||
);
|
||||
}
|
||||
if (seedArg && !entriesLit && mapped?.kind === "map") {
|
||||
const seeded = lowerMapSeedNew(lowerer, seedArg, mapped);
|
||||
if (seeded) return seeded;
|
||||
|
||||
@@ -2953,6 +2953,7 @@ export function typeofAnswer(arm: IrType): string | null {
|
||||
case "url":
|
||||
case "searchParams":
|
||||
case "stats":
|
||||
case "bigintStats":
|
||||
case "fileHandle":
|
||||
case "dir":
|
||||
case "spawnRes":
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
blockBodyOf,
|
||||
} from "./function-returns.js";
|
||||
import { generatorMeta, isThisParameter, type ParamShape } from "./call-signatures.js";
|
||||
import { isStrictCode } from "./strict-mode.js";
|
||||
import { lowerSloppyGlobalReceiver } from "./lower-global-value.js";
|
||||
|
||||
/** Save the call-site receiver before defaults or body effects. Escaping
|
||||
* arrows capture this binding instead of reading a later call's receiver. */
|
||||
@@ -48,16 +50,28 @@ function declareCallReceiver(
|
||||
): IrStmt {
|
||||
const receiver = lowerer.declareThis(DYN);
|
||||
const loc = locOf(node);
|
||||
const bound: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: generator ? "dyn.generatorThis" : "dyn.this",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// A sloppy-mode function's `this` is the global object when it is called
|
||||
// with undefined or null. The function's own body decides its mode.
|
||||
const sloppy = !ts.isArrowFunction(node) && node.body !== undefined && !isStrictCode(node.body);
|
||||
return {
|
||||
kind: "varDecl",
|
||||
localId: receiver.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: generator ? "dyn.generatorThis" : "dyn.this",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
init: sloppy
|
||||
? {
|
||||
kind: "nullish",
|
||||
left: bound,
|
||||
right: lowerSloppyGlobalReceiver(lowerer, node, loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: bound,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -298,9 +312,15 @@ export function lowerLambda(
|
||||
// A lambda IS a value: the completed-ABI rule applies at birth. The
|
||||
// contextual (target) type decides — `(x?: number) => void` may flow
|
||||
// into a slot annotated `(x: number | undefined) => void` (same ABI
|
||||
// signature), anything else is fenced. Nested function declarations and
|
||||
// object-literal shorthand methods aren't expressions — always fenced.
|
||||
// signature), anything else is fenced. Object-literal shorthand methods
|
||||
// aren't expressions: their slot is the literal's, judged inside.
|
||||
// A nested function DECLARATION is not an expression either: lowering it
|
||||
// builds the body, not a value, and every value use re-runs the same
|
||||
// fence at the reference (identifiers.ts) with that use's own target
|
||||
// (a call, or `const g = f` under an annotated slot). Fencing it here
|
||||
// refused declarations that are only ever CALLED.
|
||||
if (
|
||||
!ts.isFunctionDeclaration(node) &&
|
||||
!(
|
||||
ts.isMethodDeclaration(node) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(node)) &&
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { DYN, STRING, arrayOf, type IrExpr } from "../../ir/ir.js";
|
||||
import { DYN, STRING, arrayOf, type IrExpr, type SrcLoc } from "../../ir/ir.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
@@ -8,11 +8,29 @@ import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
* Known globals without a value implementation refuse at runtime; absent host
|
||||
* capabilities answer undefined. The checker supplies names, never values. */
|
||||
export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
return lowerGlobalValueOfType(lowerer, lowerer.typeOf(expr), locOf(expr));
|
||||
}
|
||||
|
||||
/** The global object a sloppy-mode function receives as `this` when it is
|
||||
* called with undefined or null: the value `globalThis` reads. */
|
||||
export function lowerSloppyGlobalReceiver(lowerer: Lowerer, node: ts.Node, loc: SrcLoc): IrExpr {
|
||||
const symbol = lowerer.checker.resolveName(
|
||||
"globalThis",
|
||||
ts.SymbolFlags.Value,
|
||||
ts.getSourceFileOfNode(node),
|
||||
false,
|
||||
);
|
||||
return lowerGlobalValueOfType(
|
||||
lowerer,
|
||||
symbol === undefined ? undefined : lowerer.checker.getTypeOfSymbol(symbol),
|
||||
loc,
|
||||
);
|
||||
}
|
||||
|
||||
function lowerGlobalValueOfType(lowerer: Lowerer, type: ts.Type | undefined, loc: SrcLoc): IrExpr {
|
||||
// The temporal global is typed for the programs that name it (TS 7's
|
||||
// lib slice) but absent in Node 24: reads answer undefined.
|
||||
const names = lowerer.checker
|
||||
.getPropertiesOfType(lowerer.typeOf(expr))
|
||||
const names = (type === undefined ? [] : lowerer.checker.getPropertiesOfType(type))
|
||||
.map((property) => ts.symbolName(property))
|
||||
.filter((name) => name !== TEMPORAL_GLOBAL);
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { isIslandPromisifyCall, noteIslandSubclass } from "./lower-node-island.js";
|
||||
import { isIslandSubclassDecl } from "../../node-islands/boundary.js";
|
||||
import { computedGlobalReadOf, globalValuesInstall } from "./global-values.js";
|
||||
import { globalValuesInstall, installsGlobalValues } from "./global-values.js";
|
||||
import type { TypeFailureSlot } from "../../diagnostics/diagnostic.js";
|
||||
import { THREADS_MODULE } from "../threads-import.js";
|
||||
import { collectIslandImport } from "./lower-node-island.js";
|
||||
import { collectIslandImport, collectIslandRequires } from "./lower-node-island.js";
|
||||
import { NODE_API_MODULE } from "../node-api-import.js";
|
||||
import { positionTag } from "./structural-positions.js";
|
||||
import { EXPERIMENTAL_MODULE } from "../experimental-import.js";
|
||||
@@ -732,7 +732,10 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const fp of parts) collectDynamicImports(lowerer, fp.sf);
|
||||
for (const fp of parts) {
|
||||
collectDynamicImports(lowerer, fp.sf);
|
||||
collectIslandRequires(lowerer, fp.sf);
|
||||
}
|
||||
}
|
||||
|
||||
/** Literal dynamic-import collection: every
|
||||
@@ -3169,10 +3172,10 @@ export function lowerFileInit(
|
||||
// (NI-6); the plan also creates their declarations' TDZ flags.
|
||||
if (sharedModulePlans(lowerer).length > 0 && sf === lowerer.entry)
|
||||
header.push(...sharedModuleRegistrations(lowerer));
|
||||
// A module that reads builtin globals through the global object by
|
||||
// computed names installs the program's lookup before its body runs.
|
||||
const computedGlobalRead = computedGlobalReadOf(lowerer, sf);
|
||||
if (computedGlobalRead) header.push(globalValuesInstall(lowerer, computedGlobalRead, loc0));
|
||||
// A module that reads builtin globals through the global object, by
|
||||
// computed names or through a sloppy-mode function's `this`, installs
|
||||
// the program's lookup before its body runs.
|
||||
if (installsGlobalValues(lowerer, sf)) header.push(globalValuesInstall(lowerer, loc0));
|
||||
// Top-level `this` of a CommonJS module: its object exists before the
|
||||
// module's first statement (lowerCjsTopLevelThis).
|
||||
const exportsThis = lowerer.cjsExportsThis.get(sf);
|
||||
|
||||
@@ -413,10 +413,32 @@ export function collectIslandImport(
|
||||
// module evaluated.
|
||||
const clause = ts.isImportDeclaration(stmt) ? stmt.importClause : undefined;
|
||||
const named = clause?.namedBindings;
|
||||
const snapshotNames: string[] = [];
|
||||
if (!binding.esm && named !== undefined && ts.isNamedImports(named)) {
|
||||
for (const el of named.elements.nodes) {
|
||||
if (el.isTypeOnly) continue;
|
||||
const name = el.propertyName?.text ?? el.name.text;
|
||||
snapshotNames.push(el.propertyName?.text ?? el.name.text);
|
||||
}
|
||||
}
|
||||
// A CommonJS namespace import's members are the same snapshot: the
|
||||
// members the file reads (`ns.name`).
|
||||
if (!binding.esm && named !== undefined && ts.isNamespaceImport(named)) {
|
||||
const nsSymbol = lowerer.checker.getSymbolAtLocation(named.name);
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
ts.isIdentifier(node.name) &&
|
||||
node.expression.text === named.name.text &&
|
||||
lowerer.checker.getSymbolAtLocation(node.expression) === nsSymbol &&
|
||||
!snapshotNames.includes(node.name.text)
|
||||
)
|
||||
snapshotNames.push(node.name.text);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
{
|
||||
for (const name of snapshotNames) {
|
||||
if (name === "default") continue;
|
||||
let g = binding.snapshots.get(name);
|
||||
if (g === undefined) {
|
||||
@@ -451,6 +473,7 @@ function islandGet(target: IrExpr, key: string, loc: SrcLoc): IrExpr {
|
||||
}
|
||||
|
||||
type IslandBinding =
|
||||
| { kind: "required"; module: IslandModuleBinding }
|
||||
| { kind: "namespace"; module: IslandModuleBinding }
|
||||
| { kind: "default"; module: IslandModuleBinding }
|
||||
| { kind: "named"; module: IslandModuleBinding; name: string };
|
||||
@@ -487,7 +510,13 @@ function islandBindingOf(lowerer: Lowerer, ident: ts.Identifier): IslandBinding
|
||||
const name = decl.propertyName?.text ?? decl.name.text;
|
||||
binding = name === "default" ? "default" : { name };
|
||||
} else {
|
||||
return null;
|
||||
// `const x = require("pkg")` (a CommonJS module of a native package
|
||||
// requiring an island package): x is module.exports.
|
||||
const spec = requireSpecOf(lowerer, decl);
|
||||
if (spec === null) return null;
|
||||
const path = islandModuleFile(islands, ts.getSourceFileOfNode(decl).fileName, spec, "require");
|
||||
const module = path === null ? undefined : lowerer.islandModules.get(path);
|
||||
return module === undefined ? null : { kind: "required", module };
|
||||
}
|
||||
const spec = moduleSpecifierOf(importDecl);
|
||||
if (spec === null || importDecl === undefined) return null;
|
||||
@@ -514,6 +543,8 @@ function islandBindingValue(
|
||||
): IrExpr {
|
||||
const ns = varRef(b.module.global.id, DYN, loc);
|
||||
switch (b.kind) {
|
||||
case "required":
|
||||
return ns;
|
||||
case "namespace":
|
||||
if (!b.module.esm)
|
||||
boundaryFail(lowerer, "the namespace object of a CommonJS island module as a value", node);
|
||||
@@ -624,6 +655,8 @@ function copiedType(lowerer: Lowerer, t: IrType): boolean {
|
||||
case "bigint":
|
||||
case "nullT":
|
||||
case "undefinedT":
|
||||
// Buffers and typed arrays share their memory with the V8 view.
|
||||
case "bytes":
|
||||
return true;
|
||||
case "union": {
|
||||
const def = lowerer.unions.get(t.unionId);
|
||||
@@ -844,11 +877,20 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
// NI-5: the runtime exports dyn objects by reference and copies only
|
||||
// primitives, strings and (for now) arrays.
|
||||
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
|
||||
case "refused":
|
||||
case "refused": {
|
||||
const type = lowerer.typeOf(node);
|
||||
// A type parameter the instance binds to dyn (a generic function
|
||||
// over island types, `<T extends TSchema>`): its values are dyn
|
||||
// values, and they cross the way `any` values do.
|
||||
if (
|
||||
(type.flags & ts.TypeFlags.TypeParameter) !== 0 &&
|
||||
lowerer.typeParamResolver(type)?.kind === "dyn"
|
||||
)
|
||||
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
|
||||
// NI-5: closures, promises, records and class instances cross by
|
||||
// reference (scr_node_island_export.c): their dyn boxes keep the
|
||||
// native referent, and the closure's thunk checks its arguments.
|
||||
if (exportedByReference(lowerer, lowerer.typeOf(node))) {
|
||||
if (exportedByReference(lowerer, type)) {
|
||||
const lowered = lowerer.lowerExpr(node);
|
||||
const value = lowerer.coerceToExpected(lowered, DYN);
|
||||
return lowered.type.kind === "object"
|
||||
@@ -857,9 +899,10 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
}
|
||||
return boundaryFail(
|
||||
lowerer,
|
||||
`a '${lowerer.checker.typeToString(lowerer.typeOf(node))}' value (a native object would cross as a copy and lose its identity)`,
|
||||
`a '${lowerer.checker.typeToString(type)}' value (a native object would cross as a copy and lose its identity)`,
|
||||
node,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1041,6 +1084,11 @@ function freshLiteral(
|
||||
node: ts.ObjectLiteralExpression | ts.ArrayLiteralExpression,
|
||||
): IrExpr {
|
||||
const loc = locOf(node);
|
||||
// Spreads, methods, accessors and computed keys: the literal lowers as
|
||||
// the program's own object, which crosses by reference (NI-5 exports);
|
||||
// nothing else holds it, so that is exact.
|
||||
if (!plainLiteral(node))
|
||||
return lowerer.coerceToExpected(lowerer.lowerExprExpecting(node, DYN), DYN);
|
||||
if (ts.isArrayLiteralExpression(node)) {
|
||||
const elems = node.elements.nodes.map((el) => {
|
||||
if (ts.isSpreadElement(el) || ts.isOmittedExpression(el))
|
||||
@@ -1324,7 +1372,7 @@ export function lowerIslandExpr(lowerer: Lowerer, expr: ts.Expression): IrExpr |
|
||||
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
|
||||
if (!isForeignExpr(lowerer, callee.expression)) return null;
|
||||
lowerer.usesNodeIsland = true;
|
||||
const target = islandReceiver(lowerer, callee.expression, callee.name.text, loc);
|
||||
const target = islandReceiver(lowerer, callee.expression, callee.name.text, loc, true);
|
||||
if (target.direct !== undefined) {
|
||||
return fromIsland(
|
||||
lowerer,
|
||||
@@ -1603,12 +1651,14 @@ function islandCompoundAssign(
|
||||
|
||||
/** The receiver of a member operation on an island value. A CommonJS
|
||||
* namespace binding's members are module.exports' (and `default` is
|
||||
* module.exports itself), answered directly. */
|
||||
* module.exports itself), answered directly; a call of a member keeps
|
||||
* module.exports as its receiver, so it goes through the method call. */
|
||||
function islandReceiver(
|
||||
lowerer: Lowerer,
|
||||
receiver: ts.Expression,
|
||||
member: string,
|
||||
loc: SrcLoc,
|
||||
call = false,
|
||||
): { value: IrExpr; direct?: IrExpr } {
|
||||
const inner = stripTypeWrappers(receiver);
|
||||
if (ts.isIdentifier(inner)) {
|
||||
@@ -1616,6 +1666,9 @@ function islandReceiver(
|
||||
if (b !== null && b.kind === "namespace" && !b.module.esm) {
|
||||
const exports = varRef(b.module.global.id, DYN, loc);
|
||||
if (member === "default") return { value: exports, direct: exports };
|
||||
const snapshot = b.module.snapshots.get(member);
|
||||
if (snapshot !== undefined && !call)
|
||||
return { value: exports, direct: varRef(snapshot.id, DYN, loc) };
|
||||
return { value: exports };
|
||||
}
|
||||
}
|
||||
@@ -1943,6 +1996,82 @@ export function lowerIslandSuperMethodCall(
|
||||
return fromIsland(lowerer, raw, call);
|
||||
}
|
||||
|
||||
/** A literal freshLiteral copies member by member: identifier or string
|
||||
* keys with values, shorthand properties, array elements without spreads
|
||||
* or holes. */
|
||||
function plainLiteral(node: ts.ObjectLiteralExpression | ts.ArrayLiteralExpression): boolean {
|
||||
if (ts.isArrayLiteralExpression(node))
|
||||
return node.elements.nodes.every(
|
||||
(el) => !ts.isSpreadElement(el) && !ts.isOmittedExpression(el),
|
||||
);
|
||||
return node.properties.nodes.every(
|
||||
(p) =>
|
||||
(ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) ||
|
||||
ts.isShorthandPropertyAssignment(p),
|
||||
);
|
||||
}
|
||||
|
||||
/** The specifier of `x = require("spec")` (a variable declaration binding
|
||||
* an identifier to a literal require), or null. */
|
||||
function requireSpecOf(lowerer: Lowerer, decl: ts.Node): string | null {
|
||||
if (
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
decl.initializer === undefined
|
||||
)
|
||||
return null;
|
||||
// Only a const keeps the module: a let or var can be reassigned.
|
||||
const list = decl.parent;
|
||||
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (
|
||||
!ts.isCallExpression(init) ||
|
||||
!ts.isIdentifier(init.expression) ||
|
||||
init.expression.text !== "require" ||
|
||||
init.arguments.nodes.length !== 1
|
||||
)
|
||||
return null;
|
||||
// The global require, not a function of the program named require.
|
||||
if (
|
||||
lowerer.checker
|
||||
.getSymbolAtLocation(init.expression)
|
||||
?.declarations.some((d) => !ts.getSourceFileOfNode(d).isDeclarationFile)
|
||||
)
|
||||
return null;
|
||||
const arg = init.arguments.nodes[0]!;
|
||||
return ts.isStringLiteralLike(arg) ? arg.text : null;
|
||||
}
|
||||
|
||||
/** Top-level `const x = require("pkg")` statements of island packages in a
|
||||
* compiled module (a native package requiring an island package): load the
|
||||
* island module in the module's %init prelude (shortcut: before the body,
|
||||
* not at the statement). */
|
||||
export function collectIslandRequires(lowerer: Lowerer, sf: ts.SourceFile): void {
|
||||
const islands = lowerer.nodeIslands;
|
||||
if (islands === null) return;
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isVariableStatement(stmt)) continue;
|
||||
for (const decl of stmt.declarationList.declarations.nodes) {
|
||||
const spec = requireSpecOf(lowerer, decl);
|
||||
if (spec === null) continue;
|
||||
const path = islandModuleFile(islands, sf.fileName, spec, "require");
|
||||
if (path === null) continue;
|
||||
lowerer.usesNodeIsland = true;
|
||||
const loc = locOf(decl);
|
||||
const binding = islandModuleGlobal(lowerer, path);
|
||||
const actions = lowerer.jsonInitActions.get(sf) ?? [];
|
||||
lowerer.jsonInitActions.set(sf, actions);
|
||||
actions.push({
|
||||
kind: "assign",
|
||||
localId: binding.global.id,
|
||||
value: loadIslandModule(lowerer, path, loc),
|
||||
loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* `x instanceof Error` for values that may be island objects (NI-5c). */
|
||||
const INTRINSIC_ERRORS = new Set([
|
||||
"Error",
|
||||
|
||||
@@ -341,6 +341,11 @@ export function statefulReason(sf: ts.SourceFile): string | null {
|
||||
)
|
||||
continue;
|
||||
if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) continue; // directives
|
||||
// CommonJS export statements (`module.exports = f`, `exports.x = v`,
|
||||
// `module.exports.x = v`) and expandos on the module's own functions
|
||||
// (`f.sync = g`) declare the module's surface; they are not effects.
|
||||
if (ts.isExpressionStatement(stmt) && isCjsDeclaringAssignment(stmt.expression, functions(sf)))
|
||||
continue;
|
||||
return "its evaluation has effects beyond declarations";
|
||||
}
|
||||
// Writes from function bodies to module-level bindings.
|
||||
@@ -600,3 +605,39 @@ export function sharedModuleRegistrations(lowerer: Lowerer): IrStmt[] {
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The names of a module's top-level function declarations. */
|
||||
function functions(sf: ts.SourceFile): Set<string> {
|
||||
const names = new Set<string>();
|
||||
for (const stmt of sf.statements.nodes)
|
||||
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) names.add(stmt.name.text);
|
||||
return names;
|
||||
}
|
||||
|
||||
/** `module.exports = …`, `exports.x = …`, `module.exports.x = …` (also
|
||||
* chained: `exports = module.exports = …`), or `f.x = …` for a top-level
|
||||
* function `f`. */
|
||||
function isCjsDeclaringAssignment(e: ts.Expression, fns: Set<string>): boolean {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return false;
|
||||
const left = e.left;
|
||||
const isModuleExports = (n: ts.Expression): boolean =>
|
||||
ts.isPropertyAccessExpression(n) &&
|
||||
ts.isIdentifier(n.expression) &&
|
||||
n.expression.text === "module" &&
|
||||
ts.isIdentifier(n.name) &&
|
||||
n.name.text === "exports";
|
||||
const target =
|
||||
(ts.isIdentifier(left) && left.text === "exports") ||
|
||||
isModuleExports(left) ||
|
||||
(ts.isPropertyAccessExpression(left) &&
|
||||
((ts.isIdentifier(left.expression) &&
|
||||
(left.expression.text === "exports" || fns.has(left.expression.text))) ||
|
||||
isModuleExports(left.expression)));
|
||||
if (!target) return false;
|
||||
// A chained assignment declares too.
|
||||
const right = e.right;
|
||||
if (ts.isBinaryExpression(right) && right.operatorToken.kind === ts.SyntaxKind.EqualsToken)
|
||||
return isCjsDeclaringAssignment(right, fns);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { lowerCollectionSpread } from "./containers/collection-methods.js";
|
||||
import { lowerEventObjectPropertyWrite } from "./lower-event-target.js";
|
||||
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
|
||||
import { importMetaBindingSource } from "./import-meta.js";
|
||||
import { anyResidueReplacedBy } from "./any-collection-residue.js";
|
||||
import { iteratorCanStep, iteratorValue } from "./iterator-consumption.js";
|
||||
import { notIterableSpelling } from "./iterable-spelling.js";
|
||||
import { isPrivateSplitConsumer } from "./split-consumers.js";
|
||||
@@ -259,7 +260,7 @@ import {
|
||||
import { keptIdentityAssertion } from "./assertion-identity.js";
|
||||
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerUnionFieldWrite } from "./expressions/union-fields.js";
|
||||
import { lowerUnionFieldDelete, lowerUnionFieldWrite } from "./expressions/union-fields.js";
|
||||
import {
|
||||
UNSUPPORTED,
|
||||
checkerPanicDiag,
|
||||
@@ -2617,7 +2618,7 @@ export function lowerDestructuringDecl(
|
||||
init.type.kind === "dyn" &&
|
||||
ts.isObjectBindingPattern(decl.name) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
startsWithRuntimeKey(lowerer, decl.name) &&
|
||||
(decl.name.elements.nodes.length === 0 || startsWithRuntimeKey(lowerer, decl.name)) &&
|
||||
destrSpellingOf(decl.initializer) !== null;
|
||||
if (
|
||||
jsRuntimeKeyCheck ||
|
||||
@@ -3181,6 +3182,39 @@ export function lowerBindingPattern(
|
||||
// every statically non-nullish kind, record or not (the assignment
|
||||
// path's rule); possibly-nullish sources would throw in JS and keep
|
||||
// a fence.
|
||||
//
|
||||
// A CHECKED-DYNAMIC source still owes that step at runtime: an empty
|
||||
// object pattern over null/undefined throws RequireObjectCoercible,
|
||||
// and an empty array pattern throws GetIterator's "is not iterable".
|
||||
// Nothing else about the pattern is observable, so the fence is the
|
||||
// whole lowering.
|
||||
if (srcType.kind === "dyn") {
|
||||
const unitLoc = locOf(pattern);
|
||||
const isObject = ts.isObjectBindingPattern(pattern);
|
||||
for (const unit of ["null", "undefined"] as const) {
|
||||
// V8 describes an argument-derived source by its type ("object
|
||||
// null"); a bare non-empty pattern names its source instead, but
|
||||
// those reads throw on their own before this check could fire.
|
||||
const described = unit === "null" ? "object null" : "undefined";
|
||||
const message = isObject
|
||||
? `Cannot destructure '${described}' as it is ${unit}.`
|
||||
: `${described} is not iterable (cannot read property Symbol(Symbol.iterator))`;
|
||||
out.push({
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: unit, value: srcRef(), type: BOOL, loc: unitLoc },
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: nodeThrowExpr(1, "", message, VOID, unitLoc),
|
||||
loc: unitLoc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc: unitLoc,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isUnitType(srcType) ||
|
||||
(srcType.kind === "union" &&
|
||||
@@ -4738,6 +4772,16 @@ function stdlibGlobalTokenDestructure(
|
||||
) {
|
||||
// A data member reads its value once, exactly like the spelled
|
||||
// `process.<member>` read the destructuring performs.
|
||||
} else if (
|
||||
globalName === "process" &&
|
||||
propName === "cwd" &&
|
||||
(value = lowerer.lowerBuiltinCallableValue(
|
||||
{ module: "process", member: propName },
|
||||
locOf(el),
|
||||
)) !== null
|
||||
) {
|
||||
// process.cwd reads the directory when it is called, so the detached
|
||||
// function is the same one every other spelling reads.
|
||||
} else {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
@@ -6426,6 +6470,19 @@ export function lowerVarDecl(
|
||||
typeInvolvesWebStream(lowerer, lowerer.typeOf(decl.name))))
|
||||
)
|
||||
type = DYN;
|
||||
// An unannotated TypeScript binding whose checker type carries `any`
|
||||
// inference residue (`Map<any, any>` from an untyped `new Map()` joined
|
||||
// into the initializer's type) takes the initializer's own layout when
|
||||
// that layout replaces only the residue: `any` admits every use, and the
|
||||
// value has that layout.
|
||||
if (
|
||||
!decl.type &&
|
||||
type !== null &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
!typeEquals(type, init.type) &&
|
||||
anyResidueReplacedBy(lowerer, type, init.type)
|
||||
)
|
||||
type = init.type;
|
||||
// A const whose initializer has another record or array layout than the
|
||||
// binding's type keeps the initializer's layout: converting would copy the
|
||||
// value, so the binding would not be the value its initializer names.
|
||||
@@ -7732,6 +7789,14 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `delete u.f` on a union of records that every arm declares `f` optional
|
||||
// on: dispatch over the arms and write each slot's ABSENT state (the
|
||||
// monomorphic optional-field delete). Required fields, class arms and
|
||||
// other receivers keep their own fences below.
|
||||
if (ts.isPropertyAccessExpression(target)) {
|
||||
const unionDelete = lowerUnionFieldDelete(lowerer, target);
|
||||
if (unionDelete) return unionDelete;
|
||||
}
|
||||
let obj = lowerer.lowerExpr(target.expression);
|
||||
// `delete this.stack` in an Error subclass that declares a `stack`
|
||||
// accessor (quickjs-wasi's JSException): V8 gives each error an own
|
||||
|
||||
@@ -288,6 +288,7 @@ import {
|
||||
noLoweringDiag,
|
||||
overloadedSignatureTypeDiag,
|
||||
recordShapeMismatchDiag,
|
||||
stdlibSnapshotValueUseDiag,
|
||||
unsupportedNativeApiDiag,
|
||||
unsupportedPackageValueDiag,
|
||||
unionMismatchDiag,
|
||||
@@ -2111,6 +2112,67 @@ function anyPiecedFuncType(lowerer: Lowerer, node: ts.Node, t: ts.Type): IrType
|
||||
* their own name, anonymous function/arrow expressions their
|
||||
* NamedEvaluation home (a variable initializer or property assignment).
|
||||
* Null when nothing names the value (the box stays anonymous). */
|
||||
/** The name Node gives the builtin module function `node` reads, through
|
||||
* const aliases (`const get = process.cwd`), when the module table names its
|
||||
* value differently from its member (BuiltinModuleFn.valueName); else null. */
|
||||
export function builtinValueNameOf(node: ts.Node, lowerer: Lowerer): string | null {
|
||||
let n: ts.Node = node;
|
||||
const seen = new Set<ts.Symbol>();
|
||||
for (;;) {
|
||||
while (ts.isParenthesizedExpression(n)) n = n.expression;
|
||||
if (!ts.isIdentifier(n)) break;
|
||||
const imported = lowerer.builtinImportOf(n);
|
||||
if (imported)
|
||||
return builtinModuleFnOf(lowerer, imported.module, imported.member)?.valueName ?? null;
|
||||
const symbol = lowerer.resolveValueSymbol(n);
|
||||
const declaration = symbol?.valueDeclaration;
|
||||
if (!symbol || seen.has(symbol) || !declaration) return null;
|
||||
seen.add(symbol);
|
||||
// A member destructured from a module or the process global
|
||||
// (`const { cwd } = process`).
|
||||
if (ts.isBindingElement(declaration)) {
|
||||
const pattern = declaration.parent;
|
||||
const holder = pattern?.parent;
|
||||
const key = declaration.propertyName ?? declaration.name;
|
||||
if (
|
||||
declaration.dotDotDotToken ||
|
||||
!pattern ||
|
||||
!ts.isObjectBindingPattern(pattern) ||
|
||||
!holder ||
|
||||
!ts.isVariableDeclaration(holder) ||
|
||||
!holder.initializer ||
|
||||
(ts.getCombinedNodeFlags(holder) & ts.NodeFlags.Const) === 0 ||
|
||||
!ts.isIdentifier(key)
|
||||
)
|
||||
return null;
|
||||
return builtinMemberValueName(lowerer, holder.initializer, key.text);
|
||||
}
|
||||
if (
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
!declaration.initializer ||
|
||||
(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return null;
|
||||
n = declaration.initializer;
|
||||
}
|
||||
if (!ts.isPropertyAccessExpression(n)) return null;
|
||||
return builtinMemberValueName(lowerer, n.expression, n.name.text);
|
||||
}
|
||||
|
||||
/** BuiltinModuleFn.valueName of `member` read from `receiver`, the process
|
||||
* global or a builtin module's namespace; null for anything else. */
|
||||
function builtinMemberValueName(
|
||||
lowerer: Lowerer,
|
||||
receiver: ts.Expression,
|
||||
member: string,
|
||||
): string | null {
|
||||
const module = lowerer.isStdlibGlobal(receiver, "process")
|
||||
? "process"
|
||||
: lowerer.builtinNamespaceModuleOf(receiver);
|
||||
if (module === null) return null;
|
||||
return builtinModuleFnOf(lowerer, module, member)?.valueName ?? null;
|
||||
}
|
||||
|
||||
export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null {
|
||||
let n: ts.Node = node;
|
||||
while (ts.isParenthesizedExpression(n)) n = n.expression;
|
||||
@@ -2138,14 +2200,18 @@ export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null {
|
||||
if (ts.isPropertyAccessExpression(n)) return n.name.text;
|
||||
if ((ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) && n.name) return n.name.text;
|
||||
if (ts.isFunctionExpression(n) || ts.isArrowFunction(n)) {
|
||||
const p = n.parent;
|
||||
if (p && ts.isVariableDeclaration(p) && p.initializer === n && ts.isIdentifier(p.name)) {
|
||||
// An anonymous function takes the name of the binding or property it
|
||||
// initializes, through parentheses (`const f = (function () {})`).
|
||||
let outer: ts.Node = n;
|
||||
while (outer.parent && ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
|
||||
const p = outer.parent;
|
||||
if (p && ts.isVariableDeclaration(p) && p.initializer === outer && ts.isIdentifier(p.name)) {
|
||||
return p.name.text;
|
||||
}
|
||||
if (p && ts.isBindingElement(p) && p.initializer === n && ts.isIdentifier(p.name)) {
|
||||
if (p && ts.isBindingElement(p) && p.initializer === outer && ts.isIdentifier(p.name)) {
|
||||
return p.name.text;
|
||||
}
|
||||
if (p && ts.isPropertyAssignment(p) && p.initializer === n && ts.isIdentifier(p.name)) {
|
||||
if (p && ts.isPropertyAssignment(p) && p.initializer === outer && ts.isIdentifier(p.name)) {
|
||||
return p.name.text;
|
||||
}
|
||||
}
|
||||
@@ -3391,6 +3457,16 @@ export class Lowerer {
|
||||
* checks resolve through this map (stdlibGlobalNameOf), declarations
|
||||
* emit nothing. */
|
||||
readonly stdlibGlobalAliases = new Map<ts.Symbol, string>();
|
||||
/** The TypeScript declarations registered in stdlibGlobalAliases: the
|
||||
* global each one names and its own name node. Their declarations emit
|
||||
* nothing, so a read that needs the VALUE reports at the declaration
|
||||
* (stdlibGlobalAliasDecl, rejectUnresolvedSymbol) instead of compiling
|
||||
* it away. An unread snapshot stays elided: reading a global has no
|
||||
* side effects. */
|
||||
readonly stdlibGlobalAliasDecls = new Map<ts.Symbol, { alias: string; name: ts.Identifier }>();
|
||||
/** Snapshots whose declaration-side refusal already reported (one
|
||||
* declaration, one report). */
|
||||
private readonly stdlibSnapshotReported = new Set<ts.Symbol>();
|
||||
/** The stdlibGlobalAliases bound through a cast of the global object
|
||||
* (`(globalThis as { Buffer?: B }).Buffer`). Their static type is the
|
||||
* cast's, so their value reads are refused (identifiers.ts). */
|
||||
@@ -3787,6 +3863,12 @@ export class Lowerer {
|
||||
/** global-values.ts's answer per JavaScript source: the receiver of its
|
||||
* first read of the global object by a computed name, or null. */
|
||||
readonly computedGlobalReads = new Map<ts.SourceFile, ts.Expression | null>();
|
||||
/** global-values.ts's answer per JavaScript source: the builtin globals
|
||||
* its sloppy-mode functions read through their `this`. */
|
||||
readonly sloppyThisGlobalReads = new Map<ts.SourceFile, ReadonlySet<string>>();
|
||||
/** global-values.ts's answer: the names the program's global lookup
|
||||
* answers; undefined until asked. */
|
||||
globalLookupNames: readonly string[] | undefined = undefined;
|
||||
/** web-streams-dyn.ts's answer: whether a JavaScript source names a web
|
||||
* stream class; undefined until asked. */
|
||||
namesWebStreamClass: boolean | undefined = undefined;
|
||||
@@ -4239,6 +4321,10 @@ export class Lowerer {
|
||||
checkedTable("methodLambdasInProgress", () => this.methodLambdasInProgress.size),
|
||||
checkedTable("classMethodValueSelections", () => this.classMethodValueSelections.size),
|
||||
checkedTable("stdlibGlobalAliases", () => this.stdlibGlobalAliases.size),
|
||||
// The snapshots' own declarations and their one-report memory: both
|
||||
// grow with stdlibGlobalAliases, one entry per registered declaration.
|
||||
checkedTable("stdlibGlobalAliasDecls", () => this.stdlibGlobalAliasDecls.size),
|
||||
checkedTable("stdlibSnapshotReported", () => this.stdlibSnapshotReported.size),
|
||||
checkedTable("cjsAccessorFns", () => this.cjsAccessorFns.size),
|
||||
checkedTable("cjsAccessorSnapshots", () => this.cjsAccessorSnapshots.size),
|
||||
checkedTable("cjsSnapshotReady", () => this.cjsSnapshotReady.size),
|
||||
@@ -4419,6 +4505,8 @@ export class Lowerer {
|
||||
"createRequireBindingTargets",
|
||||
"createRequireElementTargets",
|
||||
"computedGlobalReads",
|
||||
"sloppyThisGlobalReads",
|
||||
"globalLookupNames",
|
||||
"namesWebStreamClass",
|
||||
"timeValueDateDecisions",
|
||||
"timeValueDateWrites",
|
||||
@@ -11397,6 +11485,24 @@ export class Lowerer {
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
/** The declaration-side refusal of a stdlib-global snapshot read as a
|
||||
* VALUE (stdlibGlobalAliasDecl): report once, at the declaration's name,
|
||||
* naming the type the program asked the compiler to hold when it has a
|
||||
* short spelling. */
|
||||
reportStdlibSnapshot(symbol: ts.Symbol): void {
|
||||
const decl = this.stdlibGlobalAliasDecls.get(symbol);
|
||||
if (decl === undefined || this.stdlibSnapshotReported.has(symbol)) return;
|
||||
this.stdlibSnapshotReported.add(symbol);
|
||||
const raw = panicSafe(
|
||||
() => this.checker.typeToString(this.checker.getTypeOfSymbol(symbol)),
|
||||
"",
|
||||
);
|
||||
const typeText = raw !== "" && raw.length <= 48 ? raw : undefined;
|
||||
this.pushDiag(
|
||||
stdlibSnapshotValueUseDiag(decl.name.text, decl.alias, typeText, locOf(decl.name)),
|
||||
);
|
||||
}
|
||||
|
||||
stdlibMemberFence(access: ts.PropertyAccessExpression): void {
|
||||
return stdlibMemberFence(this, access);
|
||||
}
|
||||
@@ -12334,20 +12440,13 @@ export class Lowerer {
|
||||
liveReference(expr.type) ||
|
||||
(expr.type.kind === "union" &&
|
||||
(this.unions.get(expr.type.unionId)?.arms.some(liveReference) ?? false));
|
||||
// A builtin function's adapter boxes under the builtin's name, the
|
||||
// name Node's function carries (`[Function: randomUUID]`).
|
||||
const builtinName =
|
||||
expr.kind === "closure" ? this.builtinCallableValueNames.get(expr.fnName) : undefined;
|
||||
const builtinLength =
|
||||
expr.kind === "closure" ? this.builtinCallableValueLengths.get(expr.fnName) : undefined;
|
||||
return {
|
||||
kind: "dynFrom",
|
||||
value: expr,
|
||||
type: DYN,
|
||||
loc: expr.loc,
|
||||
...(liveRef ? { liveRef: true as const } : {}),
|
||||
...(builtinName !== undefined ? { fnName: builtinName } : {}),
|
||||
...(builtinLength !== undefined ? { fnLength: builtinLength } : {}),
|
||||
...this.builtinFunctionIdentity(expr),
|
||||
};
|
||||
}
|
||||
return expr;
|
||||
@@ -12495,7 +12594,13 @@ export class Lowerer {
|
||||
) {
|
||||
return {
|
||||
kind: "dynCheck",
|
||||
value: { kind: "dynFrom", value: expr, type: DYN, loc: expr.loc },
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: expr,
|
||||
type: DYN,
|
||||
loc: expr.loc,
|
||||
...this.builtinFunctionIdentity(expr),
|
||||
},
|
||||
type: expected,
|
||||
loc: expr.loc,
|
||||
};
|
||||
@@ -15547,6 +15652,18 @@ export class Lowerer {
|
||||
this.pushDiag(blockedBindingUseDiag(name, locOf(node)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
// A stdlib-global snapshot read as a VALUE (`return proc` for
|
||||
// `const proc = globalThis.process`): the declaration emitted no
|
||||
// storage — alias plumbing only serves member and call reads — and the
|
||||
// global's own value has no lowering. The cause is the declaration, so
|
||||
// report there (once) and cascade here, instead of the generic
|
||||
// binding-form text that names neither.
|
||||
if (symbol !== null && this.stdlibGlobalAliasDecls.has(symbol)) {
|
||||
this.reportStdlibSnapshot(symbol);
|
||||
this.blockedBindings.add(symbol);
|
||||
this.pushDiag(blockedBindingUseDiag(name, locOf(node)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
// A binding whose TYPE keeps a generic call signature and whose
|
||||
// declaration carries no initializer (`declare const o4: undefined |
|
||||
// (<T>(f: (a: T) => T) => T)` — the optional-chained ambient shape):
|
||||
@@ -16930,7 +17047,10 @@ export class Lowerer {
|
||||
if (!fnName) {
|
||||
fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key);
|
||||
this.builtinCallableValueFns.set(key, fnName);
|
||||
this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1));
|
||||
this.builtinCallableValueNames.set(
|
||||
fnName,
|
||||
fn.valueName ?? display.slice(display.lastIndexOf(".") + 1),
|
||||
);
|
||||
if (fn.valueLength !== undefined)
|
||||
this.builtinCallableValueLengths.set(fnName, fn.valueLength);
|
||||
const params = funcType.params.map((type, index) => ({
|
||||
@@ -17022,6 +17142,19 @@ export class Lowerer {
|
||||
return { kind: "closure", fnName, captures: [], type: funcType, loc };
|
||||
}
|
||||
|
||||
/** The name and length a builtin function's adapter boxes under: the ones
|
||||
* Node's function carries (`[Function: randomUUID]`). Empty for any other
|
||||
* value. */
|
||||
builtinFunctionIdentity(expr: IrExpr): { fnName?: string; fnLength?: number } {
|
||||
if (expr.kind !== "closure") return {};
|
||||
const name = this.builtinCallableValueNames.get(expr.fnName);
|
||||
const length = this.builtinCallableValueLengths.get(expr.fnName);
|
||||
return {
|
||||
...(name !== undefined ? { fnName: name } : {}),
|
||||
...(length !== undefined ? { fnLength: length } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
isBuiltinCallableValue(expr: IrExpr): boolean {
|
||||
return (
|
||||
expr.kind === "closure" &&
|
||||
|
||||
@@ -823,6 +823,9 @@ export interface BuiltinModuleFn {
|
||||
/** The function value's `length` when it is not the count of its
|
||||
* required value parameters (`Math.max`'s 2 over its rest pack). */
|
||||
valueLength?: number;
|
||||
/** The function value's `name` when it is not the member's
|
||||
* (`process.cwd` is Node's `wrappedCwd`). */
|
||||
valueName?: string;
|
||||
}
|
||||
|
||||
export type BuiltinValueParam =
|
||||
@@ -1894,6 +1897,7 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable {
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
},
|
||||
// util.debug is util.debuglog, under its name.
|
||||
debug: {
|
||||
fn: "util.debuglog",
|
||||
params: [DYN, DYN],
|
||||
@@ -1902,6 +1906,7 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable {
|
||||
{ mode: "required", type: DYN },
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
valueName: "debuglog",
|
||||
},
|
||||
},
|
||||
// The util/types spoke supplies the probe name to the shared brand check.
|
||||
@@ -1954,10 +1959,29 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable {
|
||||
transcode: { fn: "buffer.transcode", params: [DYN, DYN, DYN], result: BYTES_U8 },
|
||||
},
|
||||
tty: {
|
||||
isatty: { fn: "process.isTTY", params: [F64], result: BOOL },
|
||||
isatty: {
|
||||
fn: "process.isTTY",
|
||||
params: [F64],
|
||||
result: BOOL,
|
||||
// As a value (loggers store `tty.isatty`), the adapter takes any
|
||||
// argument: a value that is not an integral fd answers false, and is
|
||||
// never coerced.
|
||||
valueFn: "process.isTTYValue",
|
||||
valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }],
|
||||
valueLength: 1,
|
||||
},
|
||||
},
|
||||
process: {
|
||||
loadEnvFile: { fn: "process.loadEnvFile", params: [DYN], result: VOID },
|
||||
// Every spelling of process.cwd as a value is this one function, which
|
||||
// reads the directory when it is called.
|
||||
cwd: {
|
||||
fn: "process.cwd",
|
||||
params: [],
|
||||
result: STRING,
|
||||
valueParams: [],
|
||||
valueName: "wrappedCwd",
|
||||
},
|
||||
},
|
||||
// node:string_decoder's surface is the StringDecoder CLASS — new/write/
|
||||
// end are special-cased (lowerNew + lowerStringDecoderMethodCall); no
|
||||
@@ -2289,6 +2313,9 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
|
||||
randomInt: ["crypto.randomIntMax", "crypto.randomIntCb", "crypto.randomIntPromise"],
|
||||
randomUUID: ["crypto.randomUUIDOpts"],
|
||||
},
|
||||
tty: {
|
||||
isatty: ["process.isTTYValue"],
|
||||
},
|
||||
os: {
|
||||
// lowerOsUserInfoCall assembles the record from scalar libCalls.
|
||||
userInfo: ["os.userHomedir", "os.userShell"],
|
||||
@@ -2720,12 +2747,6 @@ function buildAmbientSurfaceFns(): readonly AmbientSurfaceRow[] {
|
||||
fns: ["process.release"],
|
||||
note: "the compatibility target's release metadata as one stable read-only object",
|
||||
},
|
||||
{
|
||||
id: "node-builtin.process.cwd",
|
||||
kind: "node-builtin",
|
||||
name: "process.cwd",
|
||||
fns: ["process.cwd"],
|
||||
},
|
||||
{
|
||||
id: "node-builtin.process.chdir",
|
||||
kind: "node-builtin",
|
||||
@@ -2860,7 +2881,7 @@ function buildAmbientSurfaceFns(): readonly AmbientSurfaceRow[] {
|
||||
id: "node-builtin.process.isTTY",
|
||||
kind: "node-builtin",
|
||||
name: "process.isTTY",
|
||||
fns: ["process.isTTY"],
|
||||
fns: ["process.isTTY", "process.isTTYValue"],
|
||||
note: "the isTTY read on process.stdin/stdout/stderr — one surface across the three streams",
|
||||
},
|
||||
{
|
||||
@@ -3717,6 +3738,15 @@ export function stdlibGlobalAliasDecl(
|
||||
lowerer.stdlibGlobalAliases.set(symbol, name);
|
||||
if (init !== undefined && castGlobalAliasSource(lowerer, init))
|
||||
lowerer.castGlobalAliases.add(symbol);
|
||||
// The snapshot exists for reads, and its declaration emits nothing: a
|
||||
// read that needs its VALUE (an argument, a return) cannot resolve, so
|
||||
// register the declaration to report the cause there rather than at the
|
||||
// use (rejectUnresolvedSymbol). TypeScript sources only: JavaScript
|
||||
// bindings keep their checked-dynamic value paths. A snapshot no read
|
||||
// resolves stays elided — reading a global has no side effects, so
|
||||
// eliding an unread one is observationally identical to Node.
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(nameNode)))
|
||||
lowerer.stdlibGlobalAliasDecls.set(symbol, { alias: name, name: nameNode });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6410,7 +6410,11 @@ function preflight7(load: LoadResult): {
|
||||
// optional-dependency try/require pattern), so no fence.
|
||||
// Binding forms keep the fence: their downstream reads
|
||||
// would need the module that never loads.
|
||||
if (req.decl !== null || probeNodeRequireRefusal(sf.fileName, req.spec) === null) {
|
||||
if (
|
||||
(req.decl !== null || probeNodeRequireRefusal(sf.fileName, req.spec) === null) &&
|
||||
// A Node island package: loaded in Node at the require (NI-4).
|
||||
!isFrontendIsland(packageNameOfSpecifier(req.spec))
|
||||
) {
|
||||
diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(req.spec)));
|
||||
}
|
||||
}
|
||||
@@ -6467,7 +6471,10 @@ function preflight7(load: LoadResult): {
|
||||
if (canonicalBuiltinModule(spec) === null && !processModuleAliasRequire7(spec, null)) {
|
||||
// Binding-less by construction — same require-site throw
|
||||
// channel as the statement-level form above.
|
||||
if (probeNodeRequireRefusal(sf.fileName, spec) === null) {
|
||||
if (
|
||||
probeNodeRequireRefusal(sf.fileName, spec) === null &&
|
||||
!isFrontendIsland(packageNameOfSpecifier(spec))
|
||||
) {
|
||||
diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(spec)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1087,6 +1087,8 @@ export function formatIrType(
|
||||
return append("bigint");
|
||||
case "stats":
|
||||
return append("Stats");
|
||||
case "bigintStats":
|
||||
return append("BigIntStats");
|
||||
case "fileHandle":
|
||||
return append("FileHandle");
|
||||
case "dir":
|
||||
@@ -1649,6 +1651,27 @@ export function isLegacyUrlSymbol(
|
||||
* (errno, dlopen, priority) and SignalConstants, and the fallback's twins:
|
||||
* the runtime's checked-dynamic constants object, whose members vary by
|
||||
* platform. */
|
||||
/** True for node:path's platform-specific namespaces ("path/posix",
|
||||
* "path/win32"). Their values are the path module object, so they map to
|
||||
* the checked-dynamic module value (see pathModuleValue in
|
||||
* lowering/lower-builtin-values.ts), not to a module declaration. */
|
||||
export function isPathPlatformNamespaceId(moduleId: string): boolean {
|
||||
return moduleId === "builtin:path/posix" || moduleId === "builtin:path/win32";
|
||||
}
|
||||
|
||||
/** @types/node's `PlatformPath` interface (module "path"): the declared type
|
||||
* of `path.posix`/`path.win32`. Its values are the path module object. */
|
||||
function isPlatformPathType(type: ts.Type, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
|
||||
const sym = type.getSymbol();
|
||||
if (sym === undefined || ts.symbolName(sym) !== "PlatformPath") return false;
|
||||
return sym.declarations.some(
|
||||
(decl) =>
|
||||
ts.isInterfaceDeclaration(decl) &&
|
||||
ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) &&
|
||||
isDeclaredInAmbientModule(decl, "path"),
|
||||
);
|
||||
}
|
||||
|
||||
export function isOsConstantsType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
@@ -2145,7 +2168,15 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
}
|
||||
{
|
||||
const moduleId = ctx.moduleNamespaceId?.(type) ?? null;
|
||||
if (moduleId !== null) return { kind: "moduleNs", moduleId };
|
||||
// node:path's platform namespaces (`path.posix`, `path.win32`) are
|
||||
// ordinary objects at run time: the same module object the whole
|
||||
// namespace lowers to (pathModuleValue). Typing them as a module
|
||||
// namespace declaration made every value read a static type with no
|
||||
// value lowering; the checked-dynamic module value is the honest
|
||||
// type, and a union with a plain record arm collapses onto it.
|
||||
if (moduleId !== null && !isPathPlatformNamespaceId(moduleId))
|
||||
return { kind: "moduleNs", moduleId };
|
||||
if (moduleId !== null) return DYN;
|
||||
}
|
||||
// A TypeScript object literal that spreads a checked-dynamic value
|
||||
// (`{ ...requestOptions, path, timeout }`): its keys are the source's
|
||||
@@ -2193,6 +2224,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
) {
|
||||
return { kind: "object", className: "%Error" };
|
||||
}
|
||||
// @types/node declares the platform-path namespace value as the
|
||||
// `PlatformPath` interface of module "path"; its values ARE the path
|
||||
// module object (the checked-dynamic module value).
|
||||
if (isPlatformPathType(type, ctx)) return DYN;
|
||||
if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN;
|
||||
// The WebAssembly API's values have the checked-dynamic representation, so
|
||||
// declarations that merely mention them compile; each reached use of the
|
||||
@@ -3683,6 +3718,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
) {
|
||||
return { kind: "stats" };
|
||||
}
|
||||
// fs.BigIntStats: the SAME snapshot handle under Node's bigint member
|
||||
// table (`statSync(p, { bigint: true })`). Declared by @types/node as
|
||||
// `class BigIntStats` in "fs"; the fallback declarations keep the class
|
||||
// shape too. Provenance-checked like Stats.
|
||||
if (
|
||||
psym !== undefined &&
|
||||
ts.symbolName(psym) === "BigIntStats" &&
|
||||
psym.declarations.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
ctx.isStdlibFile(ts.getSourceFileOfNode(d)),
|
||||
)
|
||||
) {
|
||||
return { kind: "bigintStats" };
|
||||
}
|
||||
// Capacity snapshots and options keep their live object representation
|
||||
// across numeric/BigInt overloads, runtime options, and callbacks.
|
||||
if (
|
||||
|
||||
@@ -633,6 +633,7 @@ async function compileExecutableNative(
|
||||
* (BuildSources): the spelling clang sees and native cache keys name. */
|
||||
llvmVisiblePath?: string,
|
||||
profileLayout = false,
|
||||
hugeText = true,
|
||||
): Promise<void> {
|
||||
const programIsObject = /\.(?:o|obj)$/.test(llvmPath);
|
||||
const runtimePackTarget = programIsObject && !sanitize ? nativeCodegenTarget() : null;
|
||||
@@ -650,6 +651,7 @@ async function compileExecutableNative(
|
||||
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
|
||||
...(profileGenerate ? { profileGenerate: true } : {}),
|
||||
...(profileLayout ? { profileLayout: true } : {}),
|
||||
...(hugeText ? {} : { hugeText: false }),
|
||||
programObjectDependencies,
|
||||
});
|
||||
const cacheableLinker =
|
||||
@@ -732,6 +734,7 @@ async function compileExecutableNative(
|
||||
emitter: features.emitter,
|
||||
symbol: features.symbol,
|
||||
bigint: features.bigint,
|
||||
...(features.bigstats ? { bigstats: true } : {}),
|
||||
searchParams: features.searchParams,
|
||||
qs: features.qs,
|
||||
utilPromisify: features.utilPromisify,
|
||||
@@ -1824,6 +1827,7 @@ async function compileTracked(
|
||||
// identity (node-islands/libnode.ts) and a hit requires it unchanged.
|
||||
libnodeIdentity: () => libnodeCacheKey(opts.libnode, process.env, dirname(entryPath)),
|
||||
...(opts.largePages === true ? { largePages: true as const } : {}),
|
||||
...(opts.hugeText === false ? { noHugeText: true as const } : {}),
|
||||
...(opts.exceptions === "rare" ? { exceptions: "rare" as const } : {}),
|
||||
...targetCpuField(opts.targetCpu),
|
||||
...(opts.cpuDispatch === true ? { cpuDispatch: true as const } : {}),
|
||||
@@ -2004,6 +2008,7 @@ async function compileTracked(
|
||||
profile?.generate !== undefined,
|
||||
hitSources.llvmPath,
|
||||
nativeProfileLayout(profile),
|
||||
opts.hugeText !== false,
|
||||
);
|
||||
if (nativeProgramObject !== null && opts.nativeProgramObject === true) {
|
||||
await rename(nativeProgramObject.linkPath, nativeProgramObject.artifactPath);
|
||||
@@ -2152,6 +2157,7 @@ async function compileTracked(
|
||||
profile?.generate !== undefined,
|
||||
llvmPath,
|
||||
nativeProfileLayout(profile),
|
||||
opts.hugeText !== false,
|
||||
);
|
||||
timing("native-link");
|
||||
if (nativeProgramObject !== null && opts.nativeProgramObject === true) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { IrType } from "./ir.js";
|
||||
import { BIGINT_T } from "./ir.js";
|
||||
import {
|
||||
arrayOf,
|
||||
BIGINT_STATS_T,
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
bytesOf,
|
||||
@@ -204,6 +205,9 @@ function buildLibFnSignatures() {
|
||||
"nodeIsland.applyTo": { argTypes: [DYN, DYN, DYN], result: DYN },
|
||||
"nodeIsland.construct": { argTypes: [DYN, DYN], result: DYN },
|
||||
"nodeIsland.instanceof": { argTypes: [DYN, DYN], result: BOOL },
|
||||
/** Array.isArray of a dyn value in a program with Node islands: island
|
||||
* arrays answer true too. */
|
||||
"nodeIsland.isArray": { argTypes: [DYN], result: BOOL },
|
||||
/** NI-5: a class instance crossing into an island, with the builder of
|
||||
* its class's method table (run once per class). Answers the value. */
|
||||
"nodeIsland.classMethods": { argTypes: [DYN, DYN], result: DYN },
|
||||
@@ -1379,6 +1383,39 @@ function buildLibFnSignatures() {
|
||||
"stats.mtime": { argTypes: [STATS_T], result: DATE_T },
|
||||
"stats.ctime": { argTypes: [STATS_T], result: DATE_T },
|
||||
"stats.birthtime": { argTypes: [STATS_T], result: DATE_T },
|
||||
/** BigIntStats (statSync with `{ bigint: true }`): the same snapshot
|
||||
* handle under Node's bigint member table — every number member of Stats
|
||||
* (the integers, `*Ms` and the BigIntStats-only `*Ns`) answers `bigint`;
|
||||
* only the four time members stay `Date`s. */
|
||||
"bigstats.isFile": { argTypes: [BIGINT_STATS_T], result: BOOL },
|
||||
"bigstats.isDirectory": { argTypes: [BIGINT_STATS_T], result: BOOL },
|
||||
"bigstats.isSymbolicLink": { argTypes: [BIGINT_STATS_T], result: BOOL },
|
||||
"bigstats.isBlockDevice": { argTypes: [BIGINT_STATS_T], result: BOOL },
|
||||
"bigstats.isCharacterDevice": { argTypes: [BIGINT_STATS_T], result: BOOL },
|
||||
"bigstats.isFIFO": { argTypes: [BIGINT_STATS_T], result: BOOL },
|
||||
"bigstats.isSocket": { argTypes: [BIGINT_STATS_T], result: BOOL },
|
||||
"bigstats.size": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.dev": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.ino": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.blocks": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.nlink": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.mode": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.uid": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.gid": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.rdev": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.blksize": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.atimeNs": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.mtimeNs": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.ctimeNs": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.birthtimeNs": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.atimeMs": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.mtimeMs": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.ctimeMs": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.birthtimeMs": { argTypes: [BIGINT_STATS_T], result: BIGINT_T },
|
||||
"bigstats.atime": { argTypes: [BIGINT_STATS_T], result: DATE_T },
|
||||
"bigstats.mtime": { argTypes: [BIGINT_STATS_T], result: DATE_T },
|
||||
"bigstats.ctime": { argTypes: [BIGINT_STATS_T], result: DATE_T },
|
||||
"bigstats.birthtime": { argTypes: [BIGINT_STATS_T], result: DATE_T },
|
||||
// The wider sync fs slice (unlink/chmod/chown/copyfile and the
|
||||
// mode-carrying write/mkdir forms).
|
||||
/** The wider sync fs slice (scr_lib.c), all throwing catchably with
|
||||
@@ -3600,6 +3637,8 @@ function buildLibFnSignatures() {
|
||||
"process.pid": { argTypes: [], result: F64 },
|
||||
"dyn.this": { argTypes: [], result: DYN },
|
||||
"dyn.generatorThis": { argTypes: [], result: DYN },
|
||||
/** A sloppy-mode function's `this` from its receiver: the global object
|
||||
* for undefined and null, else the receiver. Never throws. */
|
||||
"process.getuid": { argTypes: [], result: F64 },
|
||||
"process.getgid": { argTypes: [], result: F64 },
|
||||
/** geteuid(2) / getegid(2) / getgroups(2) (the effective gid appended
|
||||
@@ -5349,6 +5388,7 @@ function buildLibFnSignatures() {
|
||||
/** Validated isatty(3) probe. Stream property lowering maps false to
|
||||
* undefined; tty.isatty exposes the boolean result directly. */
|
||||
"process.isTTY": { argTypes: [F64], result: BOOL },
|
||||
"process.isTTYValue": { argTypes: [DYN], result: BOOL },
|
||||
/** A promise that never settles (events.once of an event that cannot
|
||||
* occur, e.g. 'drain' on a synchronous process stream). The result is
|
||||
* events.once's own Promise<any[]> type. */
|
||||
|
||||
@@ -1863,6 +1863,10 @@ export function walkTypeFields(node: IrType, walker: IrFieldWalker, skipType: bo
|
||||
walker.string(node.kind);
|
||||
return;
|
||||
}
|
||||
case "bigintStats": {
|
||||
walker.string(node.kind);
|
||||
return;
|
||||
}
|
||||
case "bool": {
|
||||
walker.string(node.kind);
|
||||
return;
|
||||
@@ -2268,6 +2272,7 @@ export type IrFieldsCurrent = [
|
||||
Unread<FieldsNotRead<Extract<IrType, { kind: "array" }>, "kind" | "elem" | "optionalElements">>,
|
||||
Unread<ValuesNotRead<Extract<IrType, { kind: "array" }>["optionalElements"]>>,
|
||||
Unread<FieldsNotRead<Extract<IrType, { kind: "bigint" }>, "kind">>,
|
||||
Unread<FieldsNotRead<Extract<IrType, { kind: "bigintStats" }>, "kind">>,
|
||||
Unread<FieldsNotRead<Extract<IrType, { kind: "bool" }>, "kind">>,
|
||||
Unread<FieldsNotRead<Extract<IrType, { kind: "bytes" }>, "kind" | "elem">>,
|
||||
Unread<FieldsNotRead<Extract<IrType, { kind: "caught" }>, "kind">>,
|
||||
|
||||
@@ -171,6 +171,12 @@ export type IrType =
|
||||
* never part of a cycle, no trace. Same container rules as url: union
|
||||
* arms fine, arrays/maps/JSON fenced. */
|
||||
| { kind: "stats" }
|
||||
/** statSync/lstatSync/fstatSync with `{ bigint: true }`: the SAME snapshot
|
||||
* handle as `stats` (scr_stats_* retain/release), with Node's BigIntStats
|
||||
* member table — the integer and `*Ns` members read as `bigint`, the `*Ms`
|
||||
* members as `number`, the four time members as `Date`. Distinct kind so
|
||||
* the member reads and the type identity differ from Stats. */
|
||||
| { kind: "bigintStats" }
|
||||
/** A node:fs/promises FileHandle: heap, refcounted, MUTABLE — aliases
|
||||
* share one descriptor slot, so close() is idempotent and every alias
|
||||
* observes fd === -1 after closure. The final release closes a still-
|
||||
@@ -401,6 +407,7 @@ export type IrType =
|
||||
|
||||
const POINTER_HANDLE_KINDS = [
|
||||
"stats",
|
||||
"bigintStats",
|
||||
"fileHandle",
|
||||
"dir",
|
||||
"spawnRes",
|
||||
@@ -485,6 +492,7 @@ export const RUNTIME_RC_STEMS: Record<IrType["kind"], string> = {
|
||||
searchParams: "scr_sp",
|
||||
symbol: "scr_sym",
|
||||
stats: "scr_stats",
|
||||
bigintStats: "scr_stats",
|
||||
fileHandle: "scr_file_handle",
|
||||
dir: "scr_dir",
|
||||
spawnRes: "scr_spawn_res",
|
||||
@@ -552,6 +560,7 @@ export const REF_TRUTHY_KINDS: ReadonlySet<string> = new Set([
|
||||
"url",
|
||||
"searchParams",
|
||||
"stats",
|
||||
"bigintStats",
|
||||
"fileHandle",
|
||||
"dir",
|
||||
"spawnRes",
|
||||
@@ -596,6 +605,7 @@ export const URL_T: IrType = { kind: "url" };
|
||||
export const SEARCH_PARAMS_T: IrType = { kind: "searchParams" };
|
||||
export const SYMBOL_T: IrType = { kind: "symbol" };
|
||||
export const STATS_T: IrType = { kind: "stats" };
|
||||
export const BIGINT_STATS_T: IrType = { kind: "bigintStats" };
|
||||
export const FILEHANDLE_T: IrType = { kind: "fileHandle" };
|
||||
export const DIR_T: IrType = { kind: "dir" };
|
||||
export const SPAWNRES_T: IrType = { kind: "spawnRes" };
|
||||
@@ -669,6 +679,7 @@ export function isSupportedArrayElem(t: IrType): boolean {
|
||||
// fs.Stats snapshots (`await Promise.all(paths.map(stat))`): immutable
|
||||
// refcounted handles with their own retain/release adapters.
|
||||
case "stats":
|
||||
case "bigintStats":
|
||||
case "symbol":
|
||||
case "classval":
|
||||
case "moduleNs":
|
||||
|
||||
@@ -42,6 +42,21 @@ describe("runtime feature snapshots", () => {
|
||||
expect(moduleUsesHttp2(mod)).toBe(false);
|
||||
});
|
||||
|
||||
test("gates the BigIntStats unit on its member reads, not on bigint", () => {
|
||||
const reads = module();
|
||||
reads.functions[0]!.body.push(call("bigstats.size"));
|
||||
expect(moduleRuntimeFeatures(reads)).toMatchObject({ bigint: true, bigstats: true });
|
||||
|
||||
const storage = module();
|
||||
storage.functions[0]!.locals.push({
|
||||
id: "value",
|
||||
name: "value",
|
||||
mutable: false,
|
||||
type: { kind: "bigint" },
|
||||
});
|
||||
expect(moduleRuntimeFeatures(storage)).toMatchObject({ bigint: true, bigstats: false });
|
||||
});
|
||||
|
||||
test("keeps legacy HTTP/2 calls and isolated CA inspection out of the HTTP/2 unit", () => {
|
||||
const mod = module();
|
||||
mod.functions[0]!.body.push(call("http2.streamNoop"));
|
||||
|
||||
@@ -852,12 +852,18 @@ const RUNTIME_FEATURE_RULES = {
|
||||
* queueMicrotask scheduling goes through the runtime's *_als entry
|
||||
* points, which carry the active stores into each callback. */
|
||||
asyncContext: rule({ fns: [...ASYNC_CONTEXT_LIB_FNS] }),
|
||||
/** scr_wasm.c converts i64 values through BigInt. */
|
||||
/** scr_wasm.c converts i64 values through BigInt, and BigIntStats reads
|
||||
* produce bigint values. */
|
||||
bigint: rule({
|
||||
fns: ["process.hrtimeValue"],
|
||||
prefixes: ["bigint.", "wasm."],
|
||||
prefixes: ["bigint.", "wasm.", "bigstats."],
|
||||
types: ["bigint"],
|
||||
}),
|
||||
/** BigIntStats member reads (`statSync(p, { bigint: true })` members) link
|
||||
* scr_stats_bigint.c. The bigint feature is broader than this unit: every
|
||||
* bigint value and wasm conversion turns it on, so gating the unit on it
|
||||
* would link the getters into programs that never name a stat member. */
|
||||
bigstats: rule({ prefixes: ["bigstats."] }),
|
||||
childProcess: rule({
|
||||
fns: [...CHILD_PROCESS_LIB_FNS],
|
||||
prefixes: CHILD_PROCESS_LIB_PREFIXES,
|
||||
@@ -1278,6 +1284,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
assert: false,
|
||||
asyncContext: false,
|
||||
bigint: false,
|
||||
bigstats: false,
|
||||
childProcess: false,
|
||||
cipher: false,
|
||||
copying: false,
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, VOID, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
|
||||
import { everyStmtList } from "./traverse.js";
|
||||
import { jamSaturatingLoops } from "./saturating-loop-jams.js";
|
||||
|
||||
const loc = { file: "saturating-loop-jams.ts", start: 0, end: 0 };
|
||||
const BYTES = { kind: "bytes", elem: "f64" } as const;
|
||||
const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 });
|
||||
const ref = (localId: string): IrExpr => ({ loc, kind: "varRef", localId, type: F64 });
|
||||
type Op = "+" | "*" | "-" | "<" | ">" | "|" | "===" | "<<";
|
||||
const bin = (op: Op, left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
loc,
|
||||
kind: "bin",
|
||||
op,
|
||||
left,
|
||||
right,
|
||||
type: op === "<" || op === ">" || op === "===" ? BOOL : F64,
|
||||
});
|
||||
const decl = (id: string, init: IrExpr): IrStmt => ({ loc, kind: "varDecl", localId: id, init });
|
||||
const set = (id: string, value: IrExpr): IrStmt => ({ loc, kind: "assign", localId: id, value });
|
||||
const stepBy = (id: string, by: number): IrStmt => set(id, bin("+", ref(id), num(by)));
|
||||
const loop = (
|
||||
id: string,
|
||||
end: IrExpr,
|
||||
by: number,
|
||||
body: IrStmt[],
|
||||
start: IrExpr = num(0),
|
||||
): IrStmt & { kind: "for" } => ({
|
||||
loc,
|
||||
kind: "for",
|
||||
init: decl(id, start),
|
||||
cond: bin("<", ref(id), end),
|
||||
update: stepBy(id, by),
|
||||
body,
|
||||
});
|
||||
const exitWhen = (acc: string, full: number): IrStmt => ({
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: bin("===", ref(acc), num(full)),
|
||||
then: [{ loc, kind: "break" }],
|
||||
else_: null,
|
||||
});
|
||||
const get = (index: IrExpr): IrExpr => ({
|
||||
loc,
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: { loc, kind: "varRef", localId: "cr", type: BYTES },
|
||||
args: [index],
|
||||
type: F64,
|
||||
invalidNaN: true,
|
||||
});
|
||||
|
||||
/** One lane of mandelbrot's escape test: `z = z * z + c`, `b |= bit` and
|
||||
* an exit once `b` is full. */
|
||||
function lane(z: string, c: string, bit: number, full = 3, extra: IrStmt[] = []): IrStmt[] {
|
||||
return [
|
||||
decl(`${z}.t`, bin("*", ref(z), ref(z))),
|
||||
set(z, bin("+", ref(`${z}.t`), ref(c))),
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: bin(">", ref(`${z}.t`), num(4)),
|
||||
then: [set("b", bin("|", ref("b"), num(bit))), exitWhen("b", full), ...extra],
|
||||
else_: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function nest(
|
||||
options: {
|
||||
full?: number;
|
||||
post?: IrStmt[];
|
||||
preExtra?: IrStmt[];
|
||||
innerExtra?: IrStmt[];
|
||||
innerStart?: IrExpr;
|
||||
innerEnd?: IrExpr;
|
||||
copies?: number;
|
||||
} = {},
|
||||
): IrStmt[] {
|
||||
const post = options.post ?? [set("res", bin("|", bin("<<", ref("res"), num(2)), ref("b")))];
|
||||
return [
|
||||
decl("res", num(0)),
|
||||
loop("i", num(2 * (options.copies ?? 4)), 2, [
|
||||
decl("c1", get(ref("i"))),
|
||||
decl("c2", get(bin("+", ref("i"), num(1)))),
|
||||
decl("z1", ref("c1")),
|
||||
decl("z2", ref("c2")),
|
||||
decl("b", num(0)),
|
||||
...(options.preExtra ?? []),
|
||||
loop(
|
||||
"j",
|
||||
options.innerEnd ?? num(50),
|
||||
1,
|
||||
[
|
||||
...lane("z1", "c1", 2, options.full),
|
||||
...lane("z2", "c2", 1, options.full),
|
||||
...(options.innerExtra ?? []),
|
||||
],
|
||||
options.innerStart,
|
||||
),
|
||||
...post,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
function moduleOf(body: IrStmt[]): IrModule {
|
||||
const ids = new Set<string>();
|
||||
everyStmtList(body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "varRef" && e.localId !== "cr") ids.add(e.localId);
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" || s.kind === "assign") ids.add(s.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const fn: IrFunction = {
|
||||
name: "render",
|
||||
params: [{ localId: "cr", name: "cr", type: BYTES }],
|
||||
locals: [
|
||||
{ id: "cr", name: "cr", type: BYTES, mutable: false },
|
||||
...[...ids].map((id) => ({ id, name: id, type: F64, mutable: true })),
|
||||
],
|
||||
body,
|
||||
returnType: VOID,
|
||||
loc,
|
||||
};
|
||||
return { irVersion: 17, sourceFile: loc.file, entry: "render", functions: [fn] } as IrModule;
|
||||
}
|
||||
|
||||
function loops(body: IrStmt[]): (IrStmt & { kind: "for" })[] {
|
||||
const found: (IrStmt & { kind: "for" })[] = [];
|
||||
everyStmtList(body, {
|
||||
expr: () => true,
|
||||
stmt: (s) => {
|
||||
if (s.kind === "for") found.push(s);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
test("jams a constant-count loop whose inner loop exits once its accumulator is full", () => {
|
||||
const mod = moduleOf(nest());
|
||||
const out = jamSaturatingLoops(mod);
|
||||
expect(out).not.toBe(mod);
|
||||
const body = out.functions[0]!.body;
|
||||
// The outer loop is gone: one inner loop runs the four iterations.
|
||||
const remaining = loops(body);
|
||||
expect(remaining.length).toBe(1);
|
||||
const inner = remaining[0]!;
|
||||
// Its only exit tests every copy's accumulator, at the end of the body.
|
||||
let breaks = 0;
|
||||
everyStmtList(inner.body, {
|
||||
expr: () => true,
|
||||
stmt: (s) => {
|
||||
if (s.kind === "break") breaks++;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
expect(breaks).toBe(1);
|
||||
const last = inner.body[inner.body.length - 1]!;
|
||||
expect(last.kind).toBe("if");
|
||||
// Each copy keeps its own accumulator and lanes; `post` runs in order.
|
||||
const added = out.functions[0]!.locals.filter((local) => local.id.startsWith("%jam."));
|
||||
expect(added.filter((local) => local.name === "%jam.b").length).toBe(4);
|
||||
const block = body[1]!;
|
||||
expect(block.kind).toBe("block");
|
||||
if (block.kind !== "block") return;
|
||||
const posts = block.body.slice(-4);
|
||||
expect(posts.every((s) => s.kind === "assign" && s.localId === "res")).toBe(true);
|
||||
});
|
||||
|
||||
test("keeps loops whose exits or values do not allow running iterations together", () => {
|
||||
const reject = (body: IrStmt[]): void => {
|
||||
const mod = moduleOf(body);
|
||||
expect(jamSaturatingLoops(mod)).toBe(mod);
|
||||
};
|
||||
// An exit before the accumulator is full: later ORs would change it.
|
||||
reject(nest({ full: 2 }));
|
||||
// `post` reads a lane value the copies keep computing past their exit.
|
||||
reject(nest({ post: [set("res", bin("+", ref("res"), ref("z1")))] }));
|
||||
// `pre` reads what `post` writes.
|
||||
reject(nest({ preExtra: [decl("seed", ref("res"))] }));
|
||||
// The inner loop writes the accumulator other than by an OR: a later
|
||||
// iteration can clear bits after an exit.
|
||||
reject(nest({ innerExtra: [set("b", num(0))] }));
|
||||
reject(nest({ innerExtra: [set("b", bin("+", ref("b"), num(4)))] }));
|
||||
// The inner loop's body advances its counter, which the copies share.
|
||||
reject(nest({ innerExtra: [stepBy("j", 1)] }));
|
||||
// The inner loop's start or bound reads the counter or a local of the
|
||||
// iteration.
|
||||
reject(nest({ innerStart: ref("i") }));
|
||||
reject(nest({ innerEnd: bin("+", num(10), ref("i")) }));
|
||||
reject(nest({ preExtra: [decl("start", num(3))], innerStart: ref("start") }));
|
||||
// One iteration, or more than eight.
|
||||
reject(nest({ copies: 1 }));
|
||||
reject(nest({ copies: 9 }));
|
||||
// A store in the inner loop is observable past an exit.
|
||||
reject([
|
||||
decl("res", num(0)),
|
||||
loop("i", num(8), 2, [
|
||||
decl("b", num(0)),
|
||||
decl("z1", num(0)),
|
||||
loop("j", num(50), 1, [
|
||||
...lane("z1", "z1", 2, 3, [
|
||||
{
|
||||
loc,
|
||||
kind: "bytesSet",
|
||||
arr: { loc, kind: "varRef", localId: "cr", type: BYTES },
|
||||
index: num(0),
|
||||
value: num(1),
|
||||
},
|
||||
]),
|
||||
]),
|
||||
set("res", bin("|", ref("res"), ref("b"))),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,443 @@
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { everyStmtList, transformStmtList } from "./traverse.js";
|
||||
|
||||
/** The most iterations of an outer loop that run jammed. */
|
||||
const MAX_COPIES = 8;
|
||||
|
||||
/** The most statement and expression nodes of one outer iteration: every
|
||||
* copy duplicates it. */
|
||||
const MAX_BODY_NODES = 160;
|
||||
|
||||
/** Binary operators of numbers and booleans that run no other code and
|
||||
* cannot throw. */
|
||||
const PURE_BINARY = new Set<string>([
|
||||
"+",
|
||||
"-",
|
||||
"*",
|
||||
"/",
|
||||
"%",
|
||||
"&",
|
||||
"|",
|
||||
"^",
|
||||
"<<",
|
||||
">>",
|
||||
">>>",
|
||||
"<",
|
||||
"<=",
|
||||
">",
|
||||
">=",
|
||||
"===",
|
||||
"!==",
|
||||
]);
|
||||
|
||||
const PURE_UNARY = new Set<string>(["-", "+", "~", "!"]);
|
||||
|
||||
interface Jam {
|
||||
counter: string;
|
||||
/** The counter's value on each iteration. */
|
||||
values: number[];
|
||||
pre: IrStmt[];
|
||||
inner: IrStmt & { kind: "for" };
|
||||
post: IrStmt[];
|
||||
/** The accumulator the inner loop's exits test, and its full value. */
|
||||
acc: string;
|
||||
full: number;
|
||||
}
|
||||
|
||||
function isRef(e: IrExpr | null | undefined, id: string): boolean {
|
||||
return e?.kind === "varRef" && e.localId === id;
|
||||
}
|
||||
|
||||
function intLit(e: IrExpr | null | undefined): number | null {
|
||||
return e?.kind === "numLit" && Number.isSafeInteger(e.value) && !Object.is(e.value, -0)
|
||||
? e.value
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `id = id + step` with a positive integer literal step. */
|
||||
function constantStep(update: IrStmt | null, id: string): number | null {
|
||||
if (
|
||||
update?.kind !== "assign" ||
|
||||
update.localId !== id ||
|
||||
update.value.kind !== "bin" ||
|
||||
update.value.op !== "+" ||
|
||||
!isRef(update.value.left, id)
|
||||
)
|
||||
return null;
|
||||
const step = intLit(update.value.right);
|
||||
return step !== null && step > 0 ? step : null;
|
||||
}
|
||||
|
||||
/** A number or boolean expression that runs no other code and cannot throw:
|
||||
* literals, reads of locals and module bindings, arithmetic, comparisons
|
||||
* and typed-array element reads that answer NaN out of range. */
|
||||
function pureExpr(e: IrExpr, locals: ReadonlyMap<string, IrLocal>): boolean {
|
||||
if (e.type.kind !== "f64" && e.type.kind !== "bool") return false;
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
case "boolLit":
|
||||
return true;
|
||||
case "varRef": {
|
||||
const local = locals.get(e.localId);
|
||||
if (local) return !local.boxed && !local.tdz;
|
||||
return e.localId.startsWith("%g.");
|
||||
}
|
||||
case "bin":
|
||||
return PURE_BINARY.has(e.op) && pureExpr(e.left, locals) && pureExpr(e.right, locals);
|
||||
case "unary":
|
||||
return PURE_UNARY.has(e.op) && pureExpr(e.operand, locals);
|
||||
case "bytesIntrinsic":
|
||||
return (
|
||||
e.method === "get" &&
|
||||
e.invalidNaN === true &&
|
||||
e.receiver.kind === "varRef" &&
|
||||
e.args.length === 1 &&
|
||||
pureExpr(e.args[0]!, locals)
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** A local written by `s` (a declaration or assignment), or null. */
|
||||
function writtenLocal(s: IrStmt): string | null {
|
||||
return s.kind === "varDecl" || s.kind === "assign" ? s.localId : null;
|
||||
}
|
||||
|
||||
/** Straight-line declarations and assignments of pure values. */
|
||||
function pureStatements(list: readonly IrStmt[], locals: ReadonlyMap<string, IrLocal>): boolean {
|
||||
return list.every(
|
||||
(s) =>
|
||||
(s.kind === "varDecl" && s.init !== null && pureExpr(s.init, locals)) ||
|
||||
(s.kind === "assign" && locals.has(s.localId) && pureExpr(s.value, locals)),
|
||||
);
|
||||
}
|
||||
|
||||
/** The bits `C` of `id = id | C`, with `C` a non-negative int32 literal, or
|
||||
* null. */
|
||||
function orBits(s: IrStmt): number | null {
|
||||
if (
|
||||
s.kind !== "assign" ||
|
||||
s.value.kind !== "bin" ||
|
||||
s.value.op !== "|" ||
|
||||
!isRef(s.value.left, s.localId)
|
||||
)
|
||||
return null;
|
||||
const bits = intLit(s.value.right);
|
||||
return bits !== null && bits >= 0 && bits <= 0x7fffffff ? bits : null;
|
||||
}
|
||||
|
||||
/** The statements of the inner loop's body: pure declarations and
|
||||
* assignments, `if` statements over them, `acc = acc | C`, and
|
||||
* `if (acc === full) break` (collected into `breaks`). */
|
||||
function innerStatements(
|
||||
list: readonly IrStmt[],
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
acc: { id: string | null; bits: number; breaks: number[] },
|
||||
): boolean {
|
||||
return list.every((s) => {
|
||||
if (s.kind === "if") {
|
||||
// `if (acc === full) break`
|
||||
if (
|
||||
s.then.length === 1 &&
|
||||
s.then[0]!.kind === "break" &&
|
||||
s.then[0]!.label === undefined &&
|
||||
(s.else_ === null || s.else_.length === 0) &&
|
||||
s.cond.kind === "bin" &&
|
||||
s.cond.op === "===" &&
|
||||
s.cond.left.kind === "varRef" &&
|
||||
intLit(s.cond.right) !== null
|
||||
) {
|
||||
if (acc.id !== null && acc.id !== s.cond.left.localId) return false;
|
||||
acc.id = s.cond.left.localId;
|
||||
acc.breaks.push(intLit(s.cond.right)!);
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
pureExpr(s.cond, locals) &&
|
||||
innerStatements(s.then, locals, acc) &&
|
||||
innerStatements(s.else_ ?? [], locals, acc)
|
||||
);
|
||||
}
|
||||
const bits = orBits(s);
|
||||
if (s.kind === "assign" && bits !== null) {
|
||||
if (acc.id !== null && acc.id !== s.localId) return false;
|
||||
acc.id = s.localId;
|
||||
acc.bits |= bits;
|
||||
return true;
|
||||
}
|
||||
return pureStatements([s], locals);
|
||||
});
|
||||
}
|
||||
|
||||
function countNodes(list: readonly IrStmt[]): number {
|
||||
let n = 0;
|
||||
everyStmtList(list as IrStmt[], {
|
||||
expr: () => ++n <= MAX_BODY_NODES,
|
||||
stmt: () => ++n <= MAX_BODY_NODES,
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Reads of local `id` anywhere in `list`. */
|
||||
function reads(list: readonly IrStmt[], id: string): boolean {
|
||||
return !everyStmtList(list as IrStmt[], { expr: (e) => !isRef(e, id), stmt: () => true });
|
||||
}
|
||||
|
||||
/** Locals written anywhere in `list`. */
|
||||
function writes(list: readonly IrStmt[]): Set<string> {
|
||||
const out = new Set<string>();
|
||||
everyStmtList(list as IrStmt[], {
|
||||
expr: () => true,
|
||||
stmt: (s) => {
|
||||
const id = writtenLocal(s);
|
||||
if (id !== null) out.add(id);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Locals declared anywhere in `list`. */
|
||||
function declared(list: readonly IrStmt[]): Set<string> {
|
||||
const out = new Set<string>();
|
||||
everyStmtList(list as IrStmt[], {
|
||||
expr: () => true,
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl") out.add(s.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The jam of `loop`, or null. */
|
||||
function matchJam(
|
||||
loop: IrStmt & { kind: "for" },
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
): Jam | null {
|
||||
if (loop.labels?.length || loop.init?.kind !== "varDecl") return null;
|
||||
const counter = loop.init.localId;
|
||||
const local = locals.get(counter);
|
||||
if (local?.type.kind !== "f64" || local.boxed || local.tdz) return null;
|
||||
const start = intLit(loop.init.init);
|
||||
const step = constantStep(loop.update, counter);
|
||||
const cond = loop.cond;
|
||||
if (
|
||||
start === null ||
|
||||
step === null ||
|
||||
cond?.kind !== "bin" ||
|
||||
cond.op !== "<" ||
|
||||
!isRef(cond.left, counter)
|
||||
)
|
||||
return null;
|
||||
const end = intLit(cond.right);
|
||||
if (end === null) return null;
|
||||
const values: number[] = [];
|
||||
for (let v = start; v < end && values.length <= MAX_COPIES; v += step) values.push(v);
|
||||
if (values.length < 2 || values.length > MAX_COPIES) return null;
|
||||
// One inner loop, with straight-line code before and after it.
|
||||
const at = loop.body.findIndex((s) => s.kind === "for");
|
||||
if (at < 0) return null;
|
||||
const inner = loop.body[at] as IrStmt & { kind: "for" };
|
||||
const pre = loop.body.slice(0, at),
|
||||
post = loop.body.slice(at + 1);
|
||||
if (!pureStatements(pre, locals) || !pureStatements(post, locals)) return null;
|
||||
if (countNodes(loop.body) > MAX_BODY_NODES) return null;
|
||||
if (inner.labels?.length || inner.init?.kind !== "varDecl") return null;
|
||||
const j = inner.init.localId;
|
||||
const jLocal = locals.get(j);
|
||||
if (jLocal?.type.kind !== "f64" || jLocal.boxed || jLocal.tdz) return null;
|
||||
if (inner.init.init === null || !pureExpr(inner.init.init, locals)) return null;
|
||||
if (inner.cond === null || !pureExpr(inner.cond, locals) || constantStep(inner.update, j) !== 1)
|
||||
return null;
|
||||
const acc = { id: null as string | null, bits: 0, breaks: [] as number[] };
|
||||
if (!innerStatements(inner.body, locals, acc) || acc.id === null) return null;
|
||||
const full = acc.bits;
|
||||
// Every exit tests the accumulator against the value every OR leaves
|
||||
// unchanged once it is reached.
|
||||
if (acc.breaks.length === 0 || acc.breaks.some((value) => value !== full)) return null;
|
||||
const accId = acc.id;
|
||||
// The accumulator starts at a literal in `pre`, and the inner loop changes
|
||||
// it only by its ORs.
|
||||
const accDecl = pre.find((s) => s.kind === "varDecl" && s.localId === accId);
|
||||
if (accDecl?.kind !== "varDecl" || intLit(accDecl.init) === null) return null;
|
||||
const onlyOrs = everyStmtList(inner.body, {
|
||||
expr: () => true,
|
||||
stmt: (s) => writtenLocal(s) !== accId || orBits(s) !== null,
|
||||
});
|
||||
if (!onlyOrs) return null;
|
||||
// Every copy shares the inner counter: only the inner loop's update writes
|
||||
// it.
|
||||
if (writes(inner.body).has(j)) return null;
|
||||
// The counter, the inner bound and every value the inner loop reads from
|
||||
// outside the iteration are the same for every copy: only `pre` and the
|
||||
// counter vary.
|
||||
const own = declared([...pre, inner]);
|
||||
own.delete(counter);
|
||||
const innerWrites = writes([inner]);
|
||||
innerWrites.delete(j);
|
||||
for (const id of innerWrites) if (!own.has(id) && id !== accId) return null;
|
||||
if (writes([inner]).has(counter) || writes(pre).has(counter) || writes(post).has(counter))
|
||||
return null;
|
||||
// Copies run `pre` and the inner loop of later iterations before `post`
|
||||
// of earlier ones: `pre` and the inner loop must not read what `post`
|
||||
// writes, and `pre` writes only its own declarations.
|
||||
const postWrites = writes(post);
|
||||
for (const id of postWrites) if (reads([...pre, inner], id) || own.has(id)) return null;
|
||||
for (const id of writes(pre)) if (!own.has(id)) return null;
|
||||
// After the inner loop, iterations past a copy's exit are dead: `post`
|
||||
// reads no value the inner loop writes besides the accumulator.
|
||||
for (const id of innerWrites) if (id !== accId && reads(post, id)) return null;
|
||||
// The copies share the inner loop's start and bound: they read neither the
|
||||
// counter nor a local of the iteration.
|
||||
const controls: IrStmt[] = [
|
||||
{ kind: "exprStmt", expr: inner.init.init, loc: inner.loc },
|
||||
{ kind: "exprStmt", expr: inner.cond, loc: inner.loc },
|
||||
];
|
||||
if (reads(controls, counter)) return null;
|
||||
for (const id of own) if (id !== j && reads(controls, id)) return null;
|
||||
return { counter, values, pre, inner, post, acc: accId, full };
|
||||
}
|
||||
|
||||
/** Runs the iterations of a short constant-count loop together when its body
|
||||
* is an inner loop that exits once an accumulator reaches the value its ORs
|
||||
* saturate at (mandelbrot's pairs of pixels per output byte):
|
||||
*
|
||||
* for (let i = 0; i < 8; i += 2) {
|
||||
* <pre: pure declarations, including `let b = 0`>
|
||||
* for (let j = 0; j < M; j++) {
|
||||
* <pure arithmetic>; if (c) { b |= 2; if (b === 3) break; } ...
|
||||
* }
|
||||
* <post: pure assignments reading b>
|
||||
* }
|
||||
*
|
||||
* becomes every iteration's `pre` (with `i` a literal), one inner loop
|
||||
* running every iteration's body in turn that exits when every accumulator
|
||||
* is full, and every iteration's `post` in order. Once an accumulator is
|
||||
* full its ORs leave it full, so the iterations a copy runs past its own
|
||||
* exit leave its accumulator as its own loop left it, and every other value
|
||||
* they compute is dead: the bodies only compute numbers, read typed-array
|
||||
* elements that answer NaN out of range, and write their own locals. `pre`
|
||||
* and the inner loop read nothing `post` writes, so running later `pre`s
|
||||
* and inner loops first changes no value. Independent iterations then
|
||||
* overlap their dependence chains. */
|
||||
export function jamSaturatingLoops(mod: IrModule): IrModule {
|
||||
let changed = false;
|
||||
const functions = mod.functions.map((fn): IrFunction => {
|
||||
if (fn.async || fn.generator) return fn;
|
||||
const locals = new Map(fn.locals.map((local) => [local.id, local]));
|
||||
let found = false;
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => !found,
|
||||
stmt: (s) => {
|
||||
if (s.kind === "for" && matchJam(s, locals) !== null) found = true;
|
||||
return !found;
|
||||
},
|
||||
});
|
||||
if (!found) return fn;
|
||||
const added: IrLocal[] = [];
|
||||
const ids = new Set(fn.locals.map((local) => local.id));
|
||||
const fresh = (of: IrLocal, copy: number): string => {
|
||||
let id = `%jam.${copy}.${of.id}`;
|
||||
while (ids.has(id)) id += "_";
|
||||
ids.add(id);
|
||||
const { source: _source, ...rest } = of;
|
||||
added.push({ ...rest, id, name: `%jam.${of.name}` });
|
||||
return id;
|
||||
};
|
||||
const body = transformStmtList(fn.body, {
|
||||
expr: (e) => e,
|
||||
stmt: (s) => {
|
||||
if (s.kind !== "for") return s;
|
||||
const jam = matchJam(s, locals);
|
||||
return jam === null ? s : emitJam(jam, locals, fresh);
|
||||
},
|
||||
});
|
||||
changed = true;
|
||||
return { ...fn, locals: [...fn.locals, ...added], body };
|
||||
});
|
||||
return changed ? { ...mod, functions } : mod;
|
||||
}
|
||||
|
||||
function emitJam(
|
||||
jam: Jam,
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
fresh: (of: IrLocal, copy: number) => string,
|
||||
): IrStmt {
|
||||
const { inner, pre, post, values, counter } = jam;
|
||||
const loc = inner.loc;
|
||||
const own = declared([...pre, ...inner.body]);
|
||||
const j = (inner.init as IrStmt & { kind: "varDecl" }).localId;
|
||||
own.delete(j);
|
||||
const copies = values.map((value, copy) => {
|
||||
const rename = new Map<string, string>();
|
||||
for (const id of own) rename.set(id, fresh(locals.get(id)!, copy));
|
||||
const rewrite = (list: readonly IrStmt[]): IrStmt[] =>
|
||||
transformStmtList(list as IrStmt[], {
|
||||
expr: (e) => {
|
||||
if (e.kind !== "varRef") return e;
|
||||
if (e.localId === counter) return { kind: "numLit", value, type: F64, loc: e.loc };
|
||||
const to = rename.get(e.localId);
|
||||
return to === undefined ? e : { ...e, localId: to };
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" || s.kind === "assign") {
|
||||
const to = rename.get(s.localId);
|
||||
if (to !== undefined) return { ...s, localId: to };
|
||||
}
|
||||
return s;
|
||||
},
|
||||
});
|
||||
// The copy's exits become the shared test at the end of the iteration.
|
||||
const withoutBreaks = (list: readonly IrStmt[]): IrStmt[] =>
|
||||
list.flatMap((s): IrStmt[] => {
|
||||
if (s.kind !== "if") return [s];
|
||||
if (s.then.length === 1 && s.then[0]!.kind === "break") return [];
|
||||
return [
|
||||
{ ...s, then: withoutBreaks(s.then), else_: s.else_ ? withoutBreaks(s.else_) : null },
|
||||
];
|
||||
});
|
||||
return {
|
||||
pre: rewrite(pre),
|
||||
body: rewrite(withoutBreaks(inner.body)),
|
||||
post: rewrite(post),
|
||||
acc: rename.get(jam.acc)!,
|
||||
};
|
||||
});
|
||||
let allFull: IrExpr | null = null;
|
||||
for (const copy of copies) {
|
||||
const full: IrExpr = {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: { kind: "varRef", localId: copy.acc, type: F64, loc },
|
||||
right: { kind: "numLit", value: jam.full, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
allFull =
|
||||
allFull === null
|
||||
? full
|
||||
: { kind: "logical", op: "&&", left: allFull, right: full, type: BOOL, loc };
|
||||
}
|
||||
const jammed: IrStmt = {
|
||||
...inner,
|
||||
body: [
|
||||
...copies.flatMap((copy) => copy.body),
|
||||
{ kind: "if", cond: allFull!, then: [{ kind: "break", loc }], else_: null, loc },
|
||||
],
|
||||
};
|
||||
return {
|
||||
kind: "block",
|
||||
body: [...copies.flatMap((copy) => copy.pre), jammed, ...copies.flatMap((copy) => copy.post)],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -64,6 +64,7 @@ export function everyTypeChild(node: IrType, visit: (node: IrType) => boolean):
|
||||
case "searchParams":
|
||||
case "symbol":
|
||||
case "stats":
|
||||
case "bigintStats":
|
||||
case "fileHandle":
|
||||
case "dir":
|
||||
case "spawnRes":
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
import {
|
||||
arrayOf,
|
||||
iterableArmsElem,
|
||||
BIGINT_STATS_T,
|
||||
BYTES_ELEMENT_NAME,
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
@@ -51,6 +52,7 @@ import {
|
||||
RUNTIME_ERROR_CLASSES,
|
||||
RUNTIME_STREAM_CLASSES,
|
||||
shapeHasAccessorSlots,
|
||||
STATS_T,
|
||||
STRING,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
@@ -4239,7 +4241,14 @@ function validateFunction(
|
||||
}
|
||||
}
|
||||
if (!typeEquals(e.type, sig.result)) {
|
||||
err(`libCall ${e.fn} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
|
||||
// The stat calls answer the SAME snapshot under either member table:
|
||||
// their `bigint` option picks BigIntStats' reads over Stats'.
|
||||
const statsVariant =
|
||||
(e.fn === "fs.statSync" || e.fn === "fs.lstatSync" || e.fn === "fs.fstatSync") &&
|
||||
typeEquals(sig.result, STATS_T) &&
|
||||
typeEquals(e.type, BIGINT_STATS_T);
|
||||
if (!statsVariant)
|
||||
err(`libCall ${e.fn} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4922,7 +4931,7 @@ function validateFunction(
|
||||
const ok =
|
||||
def &&
|
||||
def.arms.length === 2 &&
|
||||
def.arms.some((a) => a.kind === "stats") &&
|
||||
def.arms.some((a) => a.kind === "stats" || a.kind === "bigintStats") &&
|
||||
def.arms.some((a) => a.kind === "undefinedT");
|
||||
if (!ok) {
|
||||
err(`libCall ${e.fn} must return the 'Stats | undefined' union`, e.loc);
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
export const NODE_ISLAND_MIN_NODE = "24.13.1";
|
||||
|
||||
/** The bridge ABI this compiler's runtime speaks (scr_node_bridge.h). */
|
||||
export const NODE_BRIDGE_ABI = 8;
|
||||
export const NODE_BRIDGE_ABI = 9;
|
||||
|
||||
/** The prefix of a recorded path relative to the executable's directory. */
|
||||
export const ORIGIN = "$ORIGIN/";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"compilerVersion": "0.4.5",
|
||||
"compilerVersion": "0.4.6",
|
||||
"coverage": [
|
||||
"Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.",
|
||||
"Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.",
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
|
||||
async function emit(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-alias-names-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
return emitLlvmModule(deserializeModule(await readFile(output, "utf8")));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The alias type names that a function's tagged loads and stores use, in order. */
|
||||
function aliasNames(llvm: string, symbol: string): string[] {
|
||||
const found = new RegExp(`^define [^\\n]*@${symbol}\\([^]*?^}`, "m").exec(llvm);
|
||||
expect(found, symbol).not.toBeNull();
|
||||
const node = (id: string) => new RegExp(`^!${id} = !\\{(.*)\\}$`, "m").exec(llvm)?.[1] ?? "";
|
||||
return [...found![0].matchAll(/!tbaa !(\d+)/g)].map((tag) => {
|
||||
const type = /^!(\d+),/.exec(node(tag[1]!))?.[1] ?? "";
|
||||
return /^!"([^"]*)"/.exec(node(type))?.[1] ?? "";
|
||||
});
|
||||
}
|
||||
|
||||
const points = `
|
||||
class Point {
|
||||
x: number;
|
||||
y: number;
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
function sum(points: Point[]): number {
|
||||
let total = 0;
|
||||
for (const p of points) total += p.x * p.y;
|
||||
return total;
|
||||
}
|
||||
`;
|
||||
|
||||
test("alias type nodes are named by category, so a field added earlier leaves the others' names", async () => {
|
||||
const before = await emit(
|
||||
`${points}const all = [new Point(1, 2), new Point(3, 4)];\nconsole.log(sum(all));\n`,
|
||||
);
|
||||
// A class whose field the module uses before Point's.
|
||||
const after = await emit(`
|
||||
class Weight {
|
||||
grams: number;
|
||||
constructor(grams: number) {
|
||||
this.grams = grams;
|
||||
}
|
||||
}
|
||||
function heaviest(weights: Weight[]): number {
|
||||
let most = 0;
|
||||
for (const w of weights) most = Math.max(most, w.grams);
|
||||
return most;
|
||||
}
|
||||
console.log(heaviest([new Weight(5), new Weight(7)]));
|
||||
${points}const all = [new Point(1, 2), new Point(3, 4)];
|
||||
console.log(sum(all));
|
||||
`);
|
||||
const names = aliasNames(before, "sc_b?f_sum");
|
||||
expect(names.length).toBeGreaterThan(0);
|
||||
expect(names.every((name) => name !== "" && !/^field\.\d+$/.test(name))).toBe(true);
|
||||
expect(aliasNames(after, "sc_b?f_sum")).toEqual(names);
|
||||
});
|
||||
@@ -30,3 +30,58 @@ console.log(read(new Value()));`,
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("an accessor satisfying a declared data member keeps the target live", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "scriptc-accessor-data-member-"));
|
||||
try {
|
||||
const entry = join(directory, "main.ts");
|
||||
writeFileSync(
|
||||
entry,
|
||||
`interface Counter { size: number; label: string; }
|
||||
function makeCounter(): Counter {
|
||||
let current = 1;
|
||||
return {
|
||||
get size(): number { return current; },
|
||||
set size(next: number) { current = next; },
|
||||
label: "counter",
|
||||
};
|
||||
}
|
||||
const counter = makeCounter();
|
||||
counter.size = 10;
|
||||
console.log(counter.size, counter.label, Object.keys(counter).join(","));`,
|
||||
);
|
||||
const result = await compile(entry, {
|
||||
outDir: directory,
|
||||
outPath: join(directory, "program.ir"),
|
||||
outputKind: "ir",
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
expect(result.ok).toBe(true);
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a data property satisfying a declared accessor keeps the target live", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "scriptc-data-accessor-member-"));
|
||||
try {
|
||||
const entry = join(directory, "main.ts");
|
||||
writeFileSync(
|
||||
entry,
|
||||
`interface Live { readonly label: string; get size(): number; }
|
||||
function makeLive(): Live { return { label: "counter", size: 3 }; }
|
||||
console.log(makeLive().size);`,
|
||||
);
|
||||
const result = await compile(entry, {
|
||||
outDir: directory,
|
||||
outPath: join(directory, "program.ir"),
|
||||
outputKind: "ir",
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
expect(result.ok).toBe(true);
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { nativeCodegenTarget, nativeHelperForTarget } from "../src/backend/targets.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const target = nativeCodegenTarget();
|
||||
const helperSpec = target === null ? null : nativeHelperForTarget(target);
|
||||
const helper = (() => {
|
||||
if (helperSpec === null || process.platform === "win32") return null;
|
||||
try {
|
||||
const packageJson = createRequire(import.meta.url).resolve(
|
||||
`${helperSpec.packageName}/package.json`,
|
||||
);
|
||||
return join(dirname(packageJson), "bin", "scriptc-llvm-codegen");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
const dirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
/** A module whose constructor sets the value main returns. Functions after
|
||||
* the constructor give split code generation and partitions something to
|
||||
* divide. */
|
||||
function program(): string {
|
||||
const lines = [
|
||||
"@value = internal global i32 0",
|
||||
"@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @init, ptr null }]",
|
||||
"define internal void @init() {",
|
||||
// Volatile, so the optimizer cannot evaluate the constructor at compile time.
|
||||
" store volatile i32 42, ptr @value",
|
||||
" ret void",
|
||||
"}",
|
||||
];
|
||||
for (let index = 0; index < 8; index++)
|
||||
lines.push(
|
||||
`define internal i32 @step${index}(i32 %v) noinline {`,
|
||||
` %r = add i32 %v, ${index}`,
|
||||
" ret i32 %r",
|
||||
"}",
|
||||
);
|
||||
lines.push(
|
||||
"define i32 @main() {",
|
||||
" %value = load i32, ptr @value",
|
||||
...Array.from({ length: 8 }, (_, index) => ` %s${index} = call i32 @step${index}(i32 0)`),
|
||||
" ret i32 %value",
|
||||
"}",
|
||||
);
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
/** The objects' section names, on ELF targets (null elsewhere). */
|
||||
async function sectionNames(objects: string[]): Promise<string[] | null> {
|
||||
if (process.platform !== "linux") return null;
|
||||
const names: string[] = [];
|
||||
for (const object of objects) {
|
||||
const { stdout } = await execFileAsync("readelf", ["-S", "-W", object]);
|
||||
for (const match of stdout.matchAll(/^\s*\[\s*\d+\]\s+(\S+)/gm)) names.push(match[1]!);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Links with lld when the C compiler driver can use it, as scriptc's Linux
|
||||
* links do: unlike the BFD linker's default script, lld does not run .ctors
|
||||
* entries from .init_array. */
|
||||
async function link(objects: string[], executable: string): Promise<void> {
|
||||
const cc = process.env["CC"] ?? "cc";
|
||||
try {
|
||||
await execFileAsync(cc, ["-fuse-ld=lld", ...objects, "-o", executable]);
|
||||
} catch {
|
||||
await execFileAsync(cc, [...objects, "-o", executable]);
|
||||
}
|
||||
}
|
||||
|
||||
async function exitCode(options: {
|
||||
outputs: number;
|
||||
split?: boolean;
|
||||
name: string;
|
||||
}): Promise<{ code: number; sections: string[] | null }> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-constructors-"));
|
||||
dirs.push(dir);
|
||||
const input = join(dir, `${options.name}.ll`);
|
||||
await writeFile(input, program());
|
||||
const outputs = Array.from({ length: options.outputs }, (_, index) =>
|
||||
join(dir, `${options.name}.${index}.o`),
|
||||
);
|
||||
await execFileAsync(helper!, [
|
||||
"emit",
|
||||
"--input",
|
||||
input,
|
||||
...outputs.flatMap((output) => ["--output", output]),
|
||||
"--filetype",
|
||||
"obj",
|
||||
"--target",
|
||||
target!.llvmTriple,
|
||||
"--opt-level",
|
||||
"2",
|
||||
"--relocation-model",
|
||||
"pic",
|
||||
"--diagnostic-format",
|
||||
"json",
|
||||
"--source-path",
|
||||
"/src/program.ts",
|
||||
...(options.split === true ? ["--split", "codegen"] : []),
|
||||
]);
|
||||
const sections = await sectionNames(outputs);
|
||||
const executable = join(dir, options.name);
|
||||
await link(outputs, executable);
|
||||
try {
|
||||
await execFileAsync(executable, []);
|
||||
return { code: 0, sections };
|
||||
} catch (error) {
|
||||
return { code: (error as { code: number }).code, sections };
|
||||
}
|
||||
}
|
||||
|
||||
/** The constructor ran, and on ELF it is in .init_array, never .ctors. */
|
||||
function expectConstructorRan(result: { code: number; sections: string[] | null }): void {
|
||||
expect(result.code).toBe(42);
|
||||
if (result.sections === null) return;
|
||||
expect(result.sections).toContain(".init_array");
|
||||
expect(result.sections).not.toContain(".ctors");
|
||||
}
|
||||
|
||||
describe.runIf(helper !== null)("program constructors", () => {
|
||||
test("a constructor runs before main in a single object", async () => {
|
||||
expectConstructorRan(await exitCode({ outputs: 1, name: "single" }));
|
||||
});
|
||||
|
||||
test("a constructor runs before main with split code generation", async () => {
|
||||
expectConstructorRan(await exitCode({ outputs: 3, split: true, name: "split" }));
|
||||
});
|
||||
|
||||
test("a constructor runs before main across program partitions", async () => {
|
||||
expectConstructorRan(await exitCode({ outputs: 3, name: "partitions" }));
|
||||
});
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6818-node-island-mixed/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6819-node-island-fenced/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6825-node-island-init-fence/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6826-node-island-undefined-args/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6829-node-island-declared-types/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6870-node-island-server-fibers/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7015-path-platform-namespace-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7016-union-field-delete.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7017-nonnullable-union.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user