mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Merge main
This commit is contained in:
@@ -245,7 +245,7 @@ $ scriptc build scale.ts
|
||||
1 error.
|
||||
```
|
||||
|
||||
A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of callbacks are not followed, so an array built by `map` or `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).
|
||||
A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of a callback are followed only when scriptc copies it into the loop that runs the array method: a callback written at a `map` or `filter` call whose captured variables are not reassigned, such as `xs.map((x) => (x * 2) | 0)`. Other callback results are not followed, so for example an array built by `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).
|
||||
|
||||
## Immutable
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Contract types](/docs/contracts) are checked only where they are written as the annotation of a parameter, variable, class instance field or function result, `Acyclic` only on classes, interfaces and object type aliases, and `ProgramLifetime` only in the `implements` clause of a class declaration; other aliases, interface members and nested type positions are reported.
|
||||
In programs that use worker threads, a surrounding `try`/`catch` does not discharge `NoThrow`.
|
||||
`Int32` refuses parameters of functions used as values and fields of classes whose instances are stored in `any` or `unknown` values.
|
||||
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, does not follow callback results (such as `map` results), and checks a copy made by `slice` or a spread as if it were its source array.
|
||||
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, follows callback results only for callbacks written at a `map` or `filter` call that scriptc runs inline, and checks a copy made by `slice` or a spread as if it were its source array.
|
||||
Element reads that find a value are int32s, but a scalar `Int32` slot still needs `a[i] | 0`, because a read can miss and produce `undefined`.
|
||||
`Immutable` refuses values stored in `any` or `unknown` or that another reference can still change.
|
||||
`ProgramLifetime` applies to root classes of executables only; instances the program drops are never reclaimed unless they belong to a released [lifetime region](/docs/experimental#lifetimeregion).
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v43");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v44");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,8 +64,11 @@ static int child_main(int argc, char **argv) {
|
||||
#endif
|
||||
int listening = argc >= 9 && strcmp(argv[5], "--listen") == 0 && strcmp(argv[7], "--idle-exit") == 0;
|
||||
int fixed = listening ? 9 : 5;
|
||||
if (argc < fixed || argc > fixed + 1 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
|
||||
strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == fixed + 1 && strcmp(argv[fixed], "--timing") != 0))
|
||||
int at = fixed, timing = 0, libraries = 0;
|
||||
if (at < argc && strcmp(argv[at], "--timing") == 0) { timing = 1; at++; }
|
||||
if (at < argc && strcmp(argv[at], "--default-libraries-from-disk") == 0) { libraries = 1; at++; }
|
||||
if (argc < fixed || at != argc || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
|
||||
strncmp(argv[4], "--callbacks=", 12) != 0)
|
||||
return 71;
|
||||
const char *mode = argv[3];
|
||||
#ifndef _WIN32
|
||||
@@ -76,7 +79,8 @@ static int child_main(int argc, char **argv) {
|
||||
fputc('\n', stdout);
|
||||
fputs(argv[4], stdout);
|
||||
fputc('\n', stdout);
|
||||
fputs(argc == 6 ? "timing\n" : "plain\n", stdout);
|
||||
fputs(timing ? "timing\n" : "plain\n", stdout);
|
||||
if (libraries) fputs("libraries from disk\n", stdout);
|
||||
return ferror(stdout) ? 72 : 0;
|
||||
}
|
||||
if (strcmp(mode, "exit") == 0) return 0;
|
||||
|
||||
@@ -161,7 +161,7 @@ static wchar_t *command_line(wchar_t **arguments, size_t count) {
|
||||
}
|
||||
|
||||
static int start_process(Ts7Process *p, char **args, size_t argc) {
|
||||
wchar_t *wide[6] = {0};
|
||||
wchar_t *wide[7] = {0};
|
||||
wchar_t *line = NULL;
|
||||
HANDLE child_input = NULL, child_output = NULL, child_error = NULL;
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST attributes = NULL;
|
||||
@@ -330,7 +330,7 @@ static void stop_process(Ts7Process *p) {
|
||||
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing) {
|
||||
uint8_t timing, uint8_t libraries_from_disk) {
|
||||
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
|
||||
char *exe = copy_argument(executable, executable_size, "");
|
||||
if (exe == NULL) return 0;
|
||||
@@ -346,8 +346,12 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
}
|
||||
exit_registered = 1;
|
||||
}
|
||||
char *args[] = {exe, "--api", "--cwd", directory, callback_flag, timing ? "--timing" : NULL, NULL};
|
||||
int started = start_process(p, args, timing ? 6 : 5);
|
||||
char *args[8] = {exe, "--api", "--cwd", directory, callback_flag};
|
||||
size_t argc = 5;
|
||||
if (timing) args[argc++] = "--timing";
|
||||
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
|
||||
args[argc] = NULL;
|
||||
int started = start_process(p, args, argc);
|
||||
free(exe); free(directory); free(callback_flag);
|
||||
if (!started) { free(p); return 0; }
|
||||
p->id = next_id++;
|
||||
@@ -361,9 +365,10 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint32_t idle_exit, uint32_t memory) {
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory) {
|
||||
(void)name; (void)name_size; (void)executable; (void)executable_size; (void)cwd; (void)cwd_size;
|
||||
(void)callbacks; (void)callbacks_size; (void)timing; (void)idle_exit; (void)memory;
|
||||
(void)callbacks; (void)callbacks_size; (void)timing; (void)libraries_from_disk; (void)idle_exit; (void)memory;
|
||||
error_text("a shared checker server needs Unix sockets");
|
||||
return 0;
|
||||
}
|
||||
@@ -455,7 +460,8 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint32_t idle_exit, uint32_t memory) {
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory) {
|
||||
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
|
||||
char *server = copy_argument(name, name_size, "");
|
||||
if (server == NULL) return 0;
|
||||
@@ -491,8 +497,11 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
(void)snprintf(limit, sizeof(limit), "TSC_TS_LISTEN_MEMORY=%lu", (unsigned long)memory);
|
||||
pid_t pid = -1;
|
||||
if (callback_flag != NULL) {
|
||||
char *args[] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle,
|
||||
timing ? "--timing" : NULL, NULL};
|
||||
char *args[12] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle};
|
||||
size_t argc = 9;
|
||||
if (timing) args[argc++] = "--timing";
|
||||
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
|
||||
args[argc] = NULL;
|
||||
pid = start_server(args, limit);
|
||||
}
|
||||
free(exe); free(dir); free(callback_flag);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"ffi_format": 6,
|
||||
"functions": [
|
||||
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool"], "returns": "u32" },
|
||||
{ "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "u32", "u32"], "returns": "u32" },
|
||||
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool", "bool"], "returns": "u32" },
|
||||
{ "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "bool", "u32", "u32"], "returns": "u32" },
|
||||
{ "name": "ts7NativeRead", "symbol": "scriptc_ts7_read", "params": ["u32", "mutable-bytes", "u32", "u32"], "returns": "i32" },
|
||||
{ "name": "ts7NativeWrite", "symbol": "scriptc_ts7_write", "params": ["u32", "bytes", "u32", "u32"], "returns": "i32" },
|
||||
{ "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" },
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing);
|
||||
uint8_t timing, uint8_t libraries_from_disk);
|
||||
/* Connects to the shared checker server `name` (`--api --listen`) in this
|
||||
* user's private server directory, starting it in its own process group
|
||||
* with `--idle-exit idle_exit` and a limit of `memory` megabytes
|
||||
@@ -25,7 +25,8 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint32_t idle_exit, uint32_t memory);
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory);
|
||||
int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size,
|
||||
uint32_t offset, uint32_t length);
|
||||
int32_t scriptc_ts7_write(uint32_t handle, const uint8_t *bytes, size_t size,
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
static const char *executable;
|
||||
static const char *callbacks = "readFile,fileExists";
|
||||
|
||||
static uint32_t start(const char *mode, int timing) {
|
||||
static uint32_t start_with(const char *mode, int timing, int libraries) {
|
||||
return scriptc_ts7_open((const uint8_t *)executable, strlen(executable),
|
||||
(const uint8_t *)mode, strlen(mode),
|
||||
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing);
|
||||
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing, (uint8_t)libraries);
|
||||
}
|
||||
|
||||
static uint32_t start(const char *mode, int timing) { return start_with(mode, timing, 0); }
|
||||
|
||||
static void fail_message(void) {
|
||||
uint8_t message[1024];
|
||||
int32_t size = scriptc_ts7_error(message, sizeof(message));
|
||||
@@ -83,11 +85,13 @@ static void argument_cases(void) {
|
||||
"", "a b", "double\"quote", "end\\", "many\\\\\"quotes\\\\", "a;&$()`'b", "h\xc3\xa9llo \xf0\x9f\x8c\x8d",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
||||
for (int timing = 0; timing <= 1; timing++) {
|
||||
for (int flags = 0; flags < 4; flags++) {
|
||||
int timing = flags & 1, libraries = flags >> 1;
|
||||
char mode[256], expected[512];
|
||||
snprintf(mode, sizeof(mode), "argv:%s", values[i]);
|
||||
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n", values[i], callbacks, timing ? "timing" : "plain");
|
||||
uint32_t id = start(mode, timing);
|
||||
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n%s", values[i], callbacks,
|
||||
timing ? "timing" : "plain", libraries ? "libraries from disk\n" : "");
|
||||
uint32_t id = start_with(mode, timing, libraries);
|
||||
assert(id != 0);
|
||||
uint8_t actual[512];
|
||||
read_exact(id, actual, strlen(expected));
|
||||
@@ -147,10 +151,10 @@ static void failure_cases(void) {
|
||||
executable = "/scriptc/does-not-exist/tsgo";
|
||||
for (int i = 0; i < 30; i++) { assert(start("echo", 0) == 0); fail_message(); }
|
||||
executable = saved;
|
||||
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0, 0) == 0);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
uint32_t id = start("exit", 0);
|
||||
assert(id != 0);
|
||||
@@ -177,7 +181,7 @@ static void failure_cases(void) {
|
||||
static uint32_t attach(const char *name, const char *mode, uint32_t memory) {
|
||||
return scriptc_ts7_attach((const uint8_t *)name, strlen(name), (const uint8_t *)executable, strlen(executable),
|
||||
(const uint8_t *)mode, strlen(mode), (const uint8_t *)callbacks, strlen(callbacks),
|
||||
0, 1, memory);
|
||||
0, 0, 1, memory);
|
||||
}
|
||||
|
||||
static void expect_line(uint32_t id, const char *expected) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* one directly or through a binding read from it. The choice never changes
|
||||
* behavior. */
|
||||
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const APPENDING_METHODS: ReadonlySet<string> = new Set([
|
||||
@@ -120,3 +120,36 @@ export function findAppendedArrayLiterals(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `const xs = []` directly followed by a capacity hint for `xs` (an
|
||||
* `arraySetLength` with `reserve`) whose length reads only literals and
|
||||
* other locals: the literal can be constructed with that capacity, and the
|
||||
* hint then has nothing left to do. */
|
||||
export function reservedArrayLength(
|
||||
stmts: readonly IrStmt[],
|
||||
i: number,
|
||||
): { literal: IrExpr; hint: IrStmt & { kind: "arraySetLength" } } | null {
|
||||
const decl = stmts[i];
|
||||
const next = stmts[i + 1];
|
||||
if (decl?.kind !== "varDecl" || decl.init === null || !isEmptyLiteral(decl.init)) return null;
|
||||
if (next?.kind !== "arraySetLength" || next.reserve !== true) return null;
|
||||
if (next.arr.kind !== "varRef" || next.arr.localId !== decl.localId) return null;
|
||||
return plainLength(next.length, decl.localId) ? { literal: decl.init, hint: next } : null;
|
||||
}
|
||||
|
||||
function plainLength(e: IrExpr, array: string): boolean {
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
return true;
|
||||
case "varRef":
|
||||
return e.localId !== array;
|
||||
case "bin":
|
||||
return plainLength(e.left, array) && plainLength(e.right, array);
|
||||
case "ternary":
|
||||
return (
|
||||
plainLength(e.cond, array) && plainLength(e.then, array) && plainLength(e.else_, array)
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,37 @@ import {
|
||||
import { SourceLocations } from "../source-locations.js";
|
||||
import { mangleFunction } from "../mangle.js";
|
||||
|
||||
/** DWARF describes source bindings at their actual native storage locations. */
|
||||
/** A request a range emitter made of its debug info that added nodes
|
||||
* (LlvmDebugInfo.record), with the ids it answered. */
|
||||
export type DebugRequest =
|
||||
| { readonly op: "function"; readonly fn: string; readonly result: string }
|
||||
| {
|
||||
readonly op: "local";
|
||||
readonly local: IrLocal;
|
||||
readonly fn: string;
|
||||
readonly arg: number;
|
||||
readonly captured: boolean;
|
||||
readonly result: { variable: string; location: string };
|
||||
}
|
||||
| {
|
||||
readonly op: "location";
|
||||
readonly loc: SrcLoc;
|
||||
readonly fn: string;
|
||||
readonly result: string;
|
||||
};
|
||||
|
||||
/** DWARF describes source bindings at their actual native storage locations.
|
||||
*
|
||||
* Nodes are numbered in request order. A range emitter (emit-module.ts)
|
||||
* records the requests that added nodes; the emitter that merges the range
|
||||
* replays them in order, so every node gets the number serial emission
|
||||
* gives it, and renames the range's text from the ids the range answered to
|
||||
* its own. A request that added no node answered an id that existed before
|
||||
* the range started or that an earlier recorded request created. */
|
||||
export class LlvmDebugInfo {
|
||||
private readonly nodes: string[] = [];
|
||||
/** The requests that added nodes, while recording (record). */
|
||||
private requests: DebugRequest[] | null = null;
|
||||
private readonly files = new Map<string, string>();
|
||||
private readonly locations = new Map<string, string>();
|
||||
private readonly scopes = new Map<string, string>();
|
||||
@@ -73,7 +101,53 @@ export class LlvmDebugInfo {
|
||||
return unit;
|
||||
}
|
||||
|
||||
/** Records the requests that add nodes from here on. */
|
||||
record(): void {
|
||||
this.requests = [];
|
||||
}
|
||||
|
||||
/** The requests recorded since record. */
|
||||
recorded(): readonly DebugRequest[] {
|
||||
return this.requests ?? [];
|
||||
}
|
||||
|
||||
/** Replays a range's recorded requests in order and adds to `ids` the
|
||||
* rename from the ids the range answered to this debug info's. */
|
||||
replay(
|
||||
requests: readonly DebugRequest[],
|
||||
functionNamed: (name: string) => IrFunction,
|
||||
ids: Map<string, string>,
|
||||
): void {
|
||||
const own = (id: string, to: string | null | undefined): void => {
|
||||
if (to === null || to === undefined)
|
||||
throw new Error(`llvm emitter bug: debug request for ${id} added no node on replay`);
|
||||
if (to !== id) ids.set(id, to);
|
||||
};
|
||||
for (const request of requests) {
|
||||
if (request.op === "function") {
|
||||
own(request.result, this.function(functionNamed(request.fn)));
|
||||
continue;
|
||||
}
|
||||
const fn = ids.get(request.fn) ?? request.fn;
|
||||
if (request.op === "location") {
|
||||
own(request.result, this.location(request.loc, fn));
|
||||
continue;
|
||||
}
|
||||
const local = this.local(request.local, fn, request.arg, request.captured);
|
||||
own(request.result.variable, local?.variable);
|
||||
own(request.result.location, local?.location);
|
||||
}
|
||||
}
|
||||
|
||||
function(fn: IrFunction): string | null {
|
||||
const before = this.nodes.length;
|
||||
const id = this.functionNode(fn);
|
||||
if (this.requests !== null && id !== null && this.nodes.length > before)
|
||||
this.requests.push({ op: "function", fn: fn.name, result: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
private functionNode(fn: IrFunction): string | null {
|
||||
const pos = this.source.position(fn.loc);
|
||||
if (pos === null) return null;
|
||||
const file = this.file(pos.file);
|
||||
@@ -230,6 +304,26 @@ export class LlvmDebugInfo {
|
||||
fn: string | null,
|
||||
arg = 0,
|
||||
captured = false,
|
||||
): { variable: string; expression: string; location: string } | null {
|
||||
const before = this.nodes.length;
|
||||
const result = this.localNodes(local, fn, arg, captured);
|
||||
if (this.requests !== null && fn !== null && result !== null && this.nodes.length > before)
|
||||
this.requests.push({
|
||||
op: "local",
|
||||
local,
|
||||
fn,
|
||||
arg,
|
||||
captured,
|
||||
result: { variable: result.variable, location: result.location },
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private localNodes(
|
||||
local: IrLocal,
|
||||
fn: string | null,
|
||||
arg: number,
|
||||
captured: boolean,
|
||||
): { variable: string; expression: string; location: string } | null {
|
||||
if (fn === null || local.source === undefined) return null;
|
||||
const pos = this.source.position(local.source.loc);
|
||||
@@ -279,6 +373,14 @@ export class LlvmDebugInfo {
|
||||
}
|
||||
|
||||
location(loc: SrcLoc, fn: string | null): string | null {
|
||||
const before = this.nodes.length;
|
||||
const id = this.locationNode(loc, fn);
|
||||
if (this.requests !== null && fn !== null && id !== null && this.nodes.length > before)
|
||||
this.requests.push({ op: "location", loc, fn, result: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
private locationNode(loc: SrcLoc, fn: string | null): string | null {
|
||||
if (fn === null) return null;
|
||||
const pos = this.source.position(loc);
|
||||
if (pos === null) return null;
|
||||
|
||||
@@ -66,6 +66,25 @@ import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js
|
||||
import type { WalkerHost } from "./walkers.js";
|
||||
import type { LlStreamTypedRefAdapter } from "./expr-context.js";
|
||||
|
||||
/** typeKey with `?` after every `(T | undefined)[]` (optionalElements),
|
||||
* at any depth of arrays, Maps, Sets and promises: checked conversions to
|
||||
* `number[][]` and `(number | undefined)[][]` need separate helpers, though
|
||||
* the two types share a typeKey. */
|
||||
function checkedConversionKey(t: IrType): string {
|
||||
switch (t.kind) {
|
||||
case "array":
|
||||
return `array<${checkedConversionKey(t.elem)}>${t.optionalElements ? "?" : ""}`;
|
||||
case "map":
|
||||
return `map<${checkedConversionKey(t.key)},${checkedConversionKey(t.value)}>`;
|
||||
case "set":
|
||||
return `set<${checkedConversionKey(t.elem)}>`;
|
||||
case "promise":
|
||||
return `promise<${checkedConversionKey(t.inner)}>`;
|
||||
default:
|
||||
return typeKey(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether to-dyn of a record shape keeps the record itself: an empty shape
|
||||
* and an event-listener record (a `handleEvent` function field) box their
|
||||
* identity. Every other shape, tuples included, copies its fields into a
|
||||
@@ -759,6 +778,13 @@ export class LlDyn {
|
||||
/* ── dyn node plumbing ─────────────────────────────────────────────── */
|
||||
|
||||
/** Loads a dyn node's kind tag (after the size_t rc word). */
|
||||
/** i1: the checked-dynamic value `d` is undefined. */
|
||||
isUndefined(B: BlockBuilder, d: string): string {
|
||||
const is = B.tmp();
|
||||
B.line(`${is} = icmp eq i32 ${this.kindOf(B, d)}, ${DYN_KIND.UNDEF}`);
|
||||
return is;
|
||||
}
|
||||
|
||||
private kindOf(B: BlockBuilder, d: string): string {
|
||||
const p = B.tmp();
|
||||
const k = B.tmp();
|
||||
@@ -1145,7 +1171,7 @@ export class LlDyn {
|
||||
|
||||
/** `sc_dm_<n>(ptr d) -> i1` — does this dyn fit T? Never throws. */
|
||||
dynMatchHelper(t: IrType, preserveRefs = false): string {
|
||||
const key = typeKey(t);
|
||||
const key = checkedConversionKey(t);
|
||||
const matcherKey = preserveRefs ? `identity:${key}` : key;
|
||||
const existing = this.dynMatchers.get(matcherKey);
|
||||
if (existing) return existing;
|
||||
@@ -1460,7 +1486,7 @@ export class LlDyn {
|
||||
B.startBlock(l0);
|
||||
const n = this.lenOf(B, "%d");
|
||||
const items = this.itemsOf(B, "%d");
|
||||
this.i64Loop(B, "dm.el", n, (i, brNext) => {
|
||||
this.i64Loop(B, "dm.el", n, (i, brNext, next) => {
|
||||
this.host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
|
||||
@@ -1471,6 +1497,15 @@ export class LlDyn {
|
||||
brNext();
|
||||
B.startBlock(valueLabel);
|
||||
const e = this.itemAt(B, items, i);
|
||||
if (t.optionalElements) {
|
||||
// A `(T | undefined)[]` stored as T elements takes undefined
|
||||
// elements as well.
|
||||
const undef = B.tmp();
|
||||
const checkLabel = B.newLabel("dm.check");
|
||||
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
||||
B.condBr(undef, next, checkLabel);
|
||||
B.startBlock(checkLabel);
|
||||
}
|
||||
const ok = B.tmp();
|
||||
B.line(`${ok} = call zeroext i1 @${m}(ptr ${e})`);
|
||||
const lOk = B.newLabel("dm.eo");
|
||||
@@ -1946,7 +1981,8 @@ export class LlDyn {
|
||||
* BUILD the typed value (+1), or throw the catchable path-annotated
|
||||
* TypeError and return a dummy with the pending flag set. */
|
||||
dynCheckHelper(t: IrType, preserveRefs = false): string {
|
||||
const key = typeKey(t);
|
||||
// A `(T | undefined)[]` stored as T elements also takes undefined ones.
|
||||
const key = checkedConversionKey(t);
|
||||
const builderKey = preserveRefs ? `identity:${key}` : key;
|
||||
const existing = this.dynBuilders.get(builderKey);
|
||||
if (existing) return existing;
|
||||
@@ -2753,6 +2789,21 @@ export class LlDyn {
|
||||
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
|
||||
B.line(`store ${host.sizeType} ${i}, ptr ${ip}`);
|
||||
const e = this.itemAt(B, items, i);
|
||||
if (t.optionalElements) {
|
||||
// An undefined element is a present undefined element.
|
||||
const undef = B.tmp();
|
||||
const storeUndefined = B.newLabel("dca.undef");
|
||||
const checkLabel = B.newLabel("dca.check");
|
||||
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
||||
B.condBr(undef, storeUndefined, checkLabel);
|
||||
B.startBlock(storeUndefined);
|
||||
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
|
||||
const at = B.tmp();
|
||||
B.line(`${at} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
|
||||
B.line(`call void @scr_arr_set_undefined(ptr ${a}, double ${at})`);
|
||||
B.br(doneLabel);
|
||||
B.startBlock(checkLabel);
|
||||
}
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = call ${this.valTy(elem)} @${c}(ptr ${e}, ptr ${pathSlot})`);
|
||||
this.pendingBail(
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* replay calls the same entry point with the same arguments, so every
|
||||
* helper gets the number and the definition the serial emission gives it,
|
||||
* and the range's text is renamed to those numbers. */
|
||||
import type { DebugRequest } from "./debug-info.js";
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
|
||||
/** One event of a range's emission, in order. */
|
||||
@@ -97,6 +98,8 @@ export class EmitJournal {
|
||||
* worker process can write it. */
|
||||
export interface ChunkResult {
|
||||
readonly entries: readonly JournalEntry[];
|
||||
/** The requests that added debug metadata nodes (LlvmDebugInfo.record). */
|
||||
readonly debugRequests: readonly DebugRequest[];
|
||||
readonly flags: {
|
||||
readonly usesCoroutines: boolean;
|
||||
readonly usesTimers: boolean;
|
||||
@@ -152,23 +155,83 @@ export function replayJournal(
|
||||
}
|
||||
|
||||
/** The rename of a range's text: numbered names (`names`, without the
|
||||
* sigil) and field alias tags (`tags`). One scan for each kind of name:
|
||||
* a numbered name starts `sc_` after a character that cannot be part of a
|
||||
* name, and ends at the last digit after its counter prefix. */
|
||||
* sigil), field alias tags (`tags`) and debug metadata ids (`debugIds`,
|
||||
* `!N`). One scan for each kind of name: a numbered name starts `sc_`
|
||||
* after a character that cannot be part of a name, and ends at the last
|
||||
* digit after its counter prefix. */
|
||||
export function renameChunk(
|
||||
names: ReadonlyMap<string, string>,
|
||||
tags: ReadonlyMap<number, number>,
|
||||
debugIds: ReadonlyMap<string, string> = new Map(),
|
||||
): (text: string) => string {
|
||||
const prefixes = new Set<string>();
|
||||
for (const name of names.keys()) prefixes.add(name.replace(/\d+$/, ""));
|
||||
if (prefixes.size === 0 && tags.size === 0) return (text) => text;
|
||||
if (prefixes.size === 0 && tags.size === 0 && debugIds.size === 0) return (text) => text;
|
||||
return (text) => {
|
||||
let out = prefixes.size === 0 ? text : renameNames(text, names, prefixes);
|
||||
if (tags.size > 0) out = renameTags(out, tags);
|
||||
if (debugIds.size > 0) out = renameDebugIds(out, debugIds);
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
/** Renames metadata references `!N` outside string literals and comments.
|
||||
* Debug builds attach no alias tags, so every such reference in a
|
||||
* function's text is a debug metadata id. A string literal or a comment
|
||||
* (`;` outside a string literal) ends at the end of its line; a quote
|
||||
* inside a string literal is escaped (`\22`). */
|
||||
function renameDebugIds(text: string, ids: ReadonlyMap<string, string>): string {
|
||||
const parts: string[] = [];
|
||||
let copied = 0;
|
||||
// The line of the reference being looked at, and whether the reference
|
||||
// is inside a string literal or a comment there. The next quote and
|
||||
// semicolon positions only move forward, so each is found once.
|
||||
let lineEnd = -1;
|
||||
let quoted = false;
|
||||
let comment = false;
|
||||
let quote = text.indexOf('"');
|
||||
let semicolon = text.indexOf(";");
|
||||
for (let at = text.indexOf("!"); at >= 0; at = text.indexOf("!", at + 1)) {
|
||||
if (at > lineEnd) {
|
||||
lineEnd = text.indexOf("\n", at);
|
||||
if (lineEnd < 0) lineEnd = text.length;
|
||||
quoted = false;
|
||||
comment = false;
|
||||
// Quotes and semicolons of earlier lines end with them.
|
||||
if ((quote >= 0 && quote < at) || (semicolon >= 0 && semicolon < at)) {
|
||||
const lineStart = text.lastIndexOf("\n", at) + 1;
|
||||
while (quote >= 0 && quote < lineStart) quote = text.indexOf('"', quote + 1);
|
||||
while (semicolon >= 0 && semicolon < lineStart)
|
||||
semicolon = text.indexOf(";", semicolon + 1);
|
||||
}
|
||||
}
|
||||
while (!comment) {
|
||||
const nextQuote = quote >= 0 && quote < at ? quote : -1;
|
||||
const nextSemicolon = semicolon >= 0 && semicolon < at ? semicolon : -1;
|
||||
if (nextQuote < 0 && nextSemicolon < 0) break;
|
||||
if (nextQuote >= 0 && (nextSemicolon < 0 || nextQuote < nextSemicolon)) {
|
||||
quoted = !quoted;
|
||||
quote = text.indexOf('"', quote + 1);
|
||||
} else {
|
||||
if (!quoted) comment = true;
|
||||
semicolon = text.indexOf(";", semicolon + 1);
|
||||
}
|
||||
}
|
||||
if (quoted || comment) continue;
|
||||
if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue;
|
||||
let end = at + 1;
|
||||
while (end < text.length && isDigit(text.charCodeAt(end))) end++;
|
||||
if (end === at + 1 || (end < text.length && isNameChar(text.charCodeAt(end)))) continue;
|
||||
const own = ids.get(text.slice(at, end));
|
||||
if (own === undefined) continue;
|
||||
parts.push(text.slice(copied, at), own);
|
||||
copied = end;
|
||||
}
|
||||
if (copied === 0) return text;
|
||||
parts.push(text.slice(copied));
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
function renameNames(
|
||||
text: string,
|
||||
names: ReadonlyMap<string, string>,
|
||||
|
||||
@@ -134,6 +134,7 @@ function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] |
|
||||
let code = 1;
|
||||
try {
|
||||
emitter.journal = new EmitJournal();
|
||||
emitter.debug?.record();
|
||||
const texts: string[] = [];
|
||||
for (const fn of functions.slice(range * size, (range + 1) * size)) {
|
||||
texts.push(emitter.emitFunction(fn));
|
||||
@@ -230,13 +231,10 @@ const PARTITION_GROUP_END = "; scriptc-group";
|
||||
export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
// Debug metadata is numbered per emitter and not journaled, so a debug
|
||||
// build emits every body here.
|
||||
const ranges = emitter.debug === null;
|
||||
const chunks = emitter.targetOptions.functionChunks ?? 1;
|
||||
const fnDefs =
|
||||
(ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
|
||||
(ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ??
|
||||
(chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
|
||||
emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) ??
|
||||
[];
|
||||
if (fnDefs.length === 0)
|
||||
for (const fn of emitter.mod.functions) {
|
||||
@@ -1677,8 +1675,8 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
// order; other differing words decide by their first differing byte,
|
||||
// and strings shorter than a word by their first byte. Otherwise a
|
||||
// proven-ASCII string on either side orders by bytes (scr_str_cmp), and
|
||||
// other pairs call scr_str_cmp_u16_bytes, which only reads the two
|
||||
// strings, so LLVM can merge repeated comparisons of one pair. Callers
|
||||
// other pairs call scr_str_cmp_u16_bytes. Both only read the two strings,
|
||||
// so LLVM can merge repeated comparisons of one pair. Callers
|
||||
// only test the sign of the result.
|
||||
if (emitter.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) {
|
||||
const fn = "scr_str_cmp_u16";
|
||||
@@ -1808,7 +1806,7 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
` %anyascii = icmp slt i32 %cws, 0`,
|
||||
` br i1 %anyascii, label %memorder, label %units`,
|
||||
`memorder:`,
|
||||
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`,
|
||||
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
|
||||
` ret i32 %m`,
|
||||
`units:`,
|
||||
` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
|
||||
|
||||
@@ -478,6 +478,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
emitter.publishedTypes?.containerGuarded(s.arr.type) !== true
|
||||
)
|
||||
break;
|
||||
// The literal it follows was constructed with this capacity
|
||||
// (reservedArrayLength).
|
||||
if (emitter.consumedReserves.delete(s)) break;
|
||||
const arr = emitBorrowedInput(emitter, s.arr);
|
||||
const length = emitter.emitExpr(s.length);
|
||||
if (s.arr.type.kind !== "array")
|
||||
|
||||
@@ -202,7 +202,7 @@ import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { ReferenceWrites, type Writes } from "./reference-writes.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { StackCallbacks } from "./stack-callbacks.js";
|
||||
import { findAppendedArrayLiterals } from "./appended-array-literals.js";
|
||||
import { findAppendedArrayLiterals, reservedArrayLength } from "./appended-array-literals.js";
|
||||
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
|
||||
import {
|
||||
findLentRecordFields,
|
||||
@@ -476,14 +476,13 @@ export interface LlvmTargetOptions {
|
||||
* earlier emission found breaking its rules (UnwindAbiViolation). */
|
||||
unwindExcluded?: ReadonlySet<string>;
|
||||
/** Emit the function bodies in this many contiguous ranges, each by its
|
||||
* own emitter, and merge them in function order (emit-journal.ts). The
|
||||
* output is the same as one emitter's. Debug builds (`debugSources`)
|
||||
* emit in one emitter. */
|
||||
* own emitter, and merge them in function order (emit-journal.ts, and
|
||||
* debug-info.ts for debug metadata). The output is the same as one
|
||||
* emitter's. */
|
||||
functionChunks?: number;
|
||||
/** Emit the function bodies of a large module on this many forked worker
|
||||
* processes at a time (emit-module.ts), forked with `forkPrimitives`. The
|
||||
* output is the same as one emitter's. Debug builds (`debugSources`)
|
||||
* emit in one emitter. */
|
||||
* output is the same as one emitter's. */
|
||||
functionWorkers?: number;
|
||||
/** The host's process primitives, where it can fork (the native
|
||||
* compiler's). */
|
||||
@@ -1479,6 +1478,11 @@ export class LlEmitter {
|
||||
/** Locals whose array literal emitStmts left for the `await Promise.all`
|
||||
* that reads them (deferredPromiseAllLocal). */
|
||||
private readonly deferredPromiseAllEntries = new Map<string, ExprOf<"arrayLit">>();
|
||||
/** Empty array literals that a capacity hint follows directly
|
||||
* (reservedArrayLength), with that hint. Constructing the literal with
|
||||
* the hinted capacity consumes the hint, which then emits nothing. */
|
||||
readonly reservedArrayLengths = new Map<IrExpr, IrStmt & { kind: "arraySetLength" }>();
|
||||
readonly consumedReserves = new Set<IrStmt>();
|
||||
/** The promise a `return` emitted past its finally regions already
|
||||
* produced for its adopting await (borrowed from the return's parked
|
||||
* slot); that await takes it instead of evaluating its operand. */
|
||||
@@ -1884,9 +1888,7 @@ export class LlEmitter {
|
||||
this.journal = journal;
|
||||
this.journaled =
|
||||
journal !== null ||
|
||||
((options.functionWorkers ?? 1) > 1 &&
|
||||
options.forkPrimitives !== undefined &&
|
||||
options.debugSources === undefined);
|
||||
((options.functionWorkers ?? 1) > 1 && options.forkPrimitives !== undefined);
|
||||
// One walk of every body, which the analyses below read instead of
|
||||
// walking the bodies themselves.
|
||||
this.nodeIndex = int32Analysis?.nodeIndex ?? new IrNodeIndex(mod);
|
||||
@@ -5259,6 +5261,7 @@ export class LlEmitter {
|
||||
chunkResult(): ChunkResult {
|
||||
return {
|
||||
entries: this.journal!.entries,
|
||||
debugRequests: this.debug?.recorded() ?? [],
|
||||
flags: {
|
||||
usesCoroutines: this.usesCoroutines,
|
||||
usesTimers: this.usesTimers,
|
||||
@@ -5294,6 +5297,8 @@ export class LlEmitter {
|
||||
// emission left them (a helper set guards the declarations its first
|
||||
// request adds).
|
||||
replayJournal(this, chunk.entries, names, tags);
|
||||
const debugIds = new Map<string, string>();
|
||||
this.debug?.replay(chunk.debugRequests, (name) => this.functionNamed(name), debugIds);
|
||||
const flags = chunk.flags;
|
||||
this.usesCoroutines ||= flags.usesCoroutines;
|
||||
this.usesTimers ||= flags.usesTimers;
|
||||
@@ -5314,7 +5319,13 @@ export class LlEmitter {
|
||||
for (const name of chunk.adoptCallees) this.adoptCallees.add(name);
|
||||
for (const name of chunk.directAwaitCallees) this.directAwaitCallees.add(name);
|
||||
for (const name of chunk.inlineAwaitCallees) this.inlineAwaitCallees.add(name);
|
||||
return renameChunk(names, tags);
|
||||
return renameChunk(names, tags, debugIds);
|
||||
}
|
||||
|
||||
private functionNamed(name: string): IrFunction {
|
||||
const fn = this.fnByName.get(name);
|
||||
if (fn === undefined) throw new InternalCompilerError(`llvm emitter bug: no function ${name}`);
|
||||
return fn;
|
||||
}
|
||||
|
||||
/** Adds a content-named constant a chunk interned. Its symbol is this
|
||||
@@ -5442,7 +5453,13 @@ export class LlEmitter {
|
||||
/** An emitter for a range of this module's functions: the same facts,
|
||||
* empty module-level tables, and a journal of what the range adds. */
|
||||
chunkEmitter(): LlEmitter {
|
||||
return new LlEmitter(this.mod, { ...this.targetOptions, functionChunks: 1 }, new EmitJournal());
|
||||
const chunk = new LlEmitter(
|
||||
this.mod,
|
||||
{ ...this.targetOptions, functionChunks: 1 },
|
||||
new EmitJournal(),
|
||||
);
|
||||
chunk.debug?.record();
|
||||
return chunk;
|
||||
}
|
||||
|
||||
/** The double-ABI entry of a function with an i32 entry: every caller
|
||||
@@ -6044,6 +6061,8 @@ export class LlEmitter {
|
||||
if (this.B.isTerminated()) return;
|
||||
if (cursor) cursor.index = i;
|
||||
this.stmtCursor = cursor;
|
||||
const reserve = reservedArrayLength(stmts, i);
|
||||
if (reserve !== null) this.reservedArrayLengths.set(reserve.literal, reserve.hint);
|
||||
const deferred = this.deferredPromiseAllLocal(stmts, i);
|
||||
if (deferred !== null) {
|
||||
this.deferredPromiseAllEntries.set((stmts[i] as { localId: string }).localId, deferred);
|
||||
|
||||
@@ -946,6 +946,34 @@ export function emitStringExpr(
|
||||
}
|
||||
}
|
||||
|
||||
/** scr_arr_reserve_len reserves dense storage only up to this many slots
|
||||
* (SCR_ARR_DENSE_LIMIT, scr_array.c). */
|
||||
const RESERVE_LIMIT = 2 ** 20;
|
||||
|
||||
/** Elements the inline slots of a small array construction
|
||||
* (scr_arr_new_small, scr_arr_new_ops_small) hold at least
|
||||
* (SCR_ARR_SMALL_SLOTS, scr_array.c). */
|
||||
const SMALL_ARRAY_SLOTS = 3;
|
||||
|
||||
/** The capacity scr_arr_reserve_len would reserve for `length` on an empty
|
||||
* array: the length truncated when it is at least 1 and at most
|
||||
* RESERVE_LIMIT, else 0. */
|
||||
function reservedCapacity(host: LlvmEmitterContext, length: IrExpr): string {
|
||||
const B = host.B;
|
||||
const n = host.emitExpr(length);
|
||||
const atLeastOne = B.tmp(),
|
||||
withinLimit = B.tmp(),
|
||||
reserves = B.tmp(),
|
||||
slots = B.tmp(),
|
||||
capacity = B.tmp();
|
||||
B.line(`${atLeastOne} = fcmp oge double ${n.name}, ${f64Lit(1)}`);
|
||||
B.line(`${withinLimit} = fcmp ole double ${n.name}, ${f64Lit(RESERVE_LIMIT)}`);
|
||||
B.line(`${reserves} = and i1 ${atLeastOne}, ${withinLimit}`);
|
||||
B.line(`${slots} = select i1 ${reserves}, double ${n.name}, double ${f64Lit(0)}`);
|
||||
B.line(`${capacity} = fptoui double ${slots} to ${host.sizeType}`);
|
||||
return capacity;
|
||||
}
|
||||
|
||||
export function emitContainerExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<
|
||||
@@ -1017,7 +1045,34 @@ export function emitContainerExpr(
|
||||
? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})`
|
||||
: `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
} else {
|
||||
const hint = host.reservedArrayLengths.get(e);
|
||||
if (hint !== undefined) {
|
||||
host.reservedArrayLengths.delete(e);
|
||||
host.consumedReserves.add(hint);
|
||||
const capacity = reservedCapacity(host, hint.length);
|
||||
// The small construction's inline slots hold at least
|
||||
// SMALL_ARRAY_SLOTS elements; larger capacities are sized.
|
||||
const fits = B.tmp(),
|
||||
smallArr = B.tmp(),
|
||||
sizedArr = B.tmp();
|
||||
const smallLabel = B.newLabel("arr.cap.small"),
|
||||
sizedLabel = B.newLabel("arr.cap.sized"),
|
||||
joined = B.newLabel("arr.cap.j");
|
||||
B.line(`${fits} = icmp ule ${host.sizeType} ${capacity}, ${SMALL_ARRAY_SLOTS}`);
|
||||
B.condBr(fits, smallLabel, sizedLabel);
|
||||
B.startBlock(smallLabel);
|
||||
B.line(`${smallArr} = ${arrNewCall(host.shapeHost, elem, "0", true)}`);
|
||||
B.br(joined);
|
||||
B.startBlock(sizedLabel);
|
||||
B.line(`${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity)}`);
|
||||
B.br(joined);
|
||||
B.startBlock(joined);
|
||||
B.line(
|
||||
`${arr} = phi ptr [ ${smallArr}, %${smallLabel} ], [ ${sizedArr}, %${sizedLabel} ]`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
}
|
||||
const out = host.own({ name: arr, type: e.type });
|
||||
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
|
||||
let pending: LlValue[] = [];
|
||||
|
||||
@@ -792,13 +792,16 @@ export function streamTypedRefMaterializeAdapter(
|
||||
t: IrType,
|
||||
): LlStreamTypedRefAdapter {
|
||||
const key = typeKey(t);
|
||||
const existing = host.liveDynRefAdapters.get(key);
|
||||
// A `(T | undefined)[]` stored as T elements shares T[]'s capsule type,
|
||||
// but its adapters also store undefined elements.
|
||||
const adapterKey = t.kind === "array" && t.optionalElements ? `${key}?` : key;
|
||||
const existing = host.liveDynRefAdapters.get(adapterKey);
|
||||
if (existing) return existing;
|
||||
// The converter depends only on the static type. Intern the complete
|
||||
// graph across roots and union arms so recursive types emit once.
|
||||
const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`;
|
||||
const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" };
|
||||
host.liveDynRefAdapters.set(key, adapter);
|
||||
host.liveDynRefAdapters.set(adapterKey, adapter);
|
||||
host.journal?.allocated(
|
||||
snapshot.replace(/_materialize$/, ""),
|
||||
"streamTypedRefMaterializeAdapter",
|
||||
@@ -1312,6 +1315,18 @@ function liveArrayOps(
|
||||
G.terminate(`ret ptr ${boxed}`);
|
||||
|
||||
const S = new BlockBuilder();
|
||||
if (t.optionalElements) {
|
||||
// A `(T | undefined)[]` stores undefined as a present undefined.
|
||||
const undef = host.dyn.isUndefined(S, "%v");
|
||||
const storeUndefined = S.newLabel("live.array.undefined");
|
||||
const other = S.newLabel("live.array.value");
|
||||
S.condBr(undef, storeUndefined, other);
|
||||
S.startBlock(storeUndefined);
|
||||
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
|
||||
S.line(`call void @scr_arr_set_undefined(ptr %p, double %i)`);
|
||||
S.terminate(`ret i1 true`);
|
||||
S.startBlock(other);
|
||||
}
|
||||
const fits = S.tmp();
|
||||
S.line(`${fits} = call zeroext i1 @${host.dyn.dynMatchHelper(elem)}(ptr %v)`);
|
||||
const store = S.newLabel("live.array.store");
|
||||
|
||||
@@ -478,7 +478,9 @@ test("string comparisons decide identity, lengths and leading bytes before the r
|
||||
expect(units).toContain("%below = icmp ult i8 %bx, %by");
|
||||
expect(units).toContain("%tailable = and i1 %samelen, %short");
|
||||
expect(units).toContain("%anyascii = icmp slt i32 %cws, 0");
|
||||
expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)");
|
||||
expect(units).toContain(
|
||||
"call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)",
|
||||
);
|
||||
expect(units).toContain(
|
||||
"call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)",
|
||||
);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 43 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v43" as const;
|
||||
export const RUNTIME_ABI_VERSION = 44 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v44" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -129,7 +129,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,14 +39,14 @@ import { InternalCompilerError } from "../errors.js";
|
||||
* byte-deterministic: entries sort by id, keys are emitted in one fixed
|
||||
* order, and the output carries no timestamps or absolute paths. */
|
||||
import { FENCE_CODES, UNSUPPORTED, webAssemblyDiag } from "../diagnostics/diagnostic.js";
|
||||
import { NODE24_FETCH_COMPAT_PROFILE } from "../compat/fetch-profile.js";
|
||||
import { node24FetchCompatProfile } from "../compat/fetch-profile.js";
|
||||
import { SUPPORTED_BUILTIN_MODULES, SUPPORTED_NODE_MODULES } from "../frontend/builtin-modules.js";
|
||||
import {
|
||||
AMBIENT_SURFACE_FNS,
|
||||
ambientSurfaceFns,
|
||||
ARRAY_METHODS,
|
||||
BUILTIN_MODULE_CONSTS,
|
||||
BUILTIN_MODULE_FENCE_HINTS,
|
||||
BUILTIN_MODULE_FNS,
|
||||
builtinModuleFns,
|
||||
COLLECTION_ITERATOR_METHODS,
|
||||
COMPOUND_ASSIGN_OPS,
|
||||
STDLIB_SURFACE,
|
||||
@@ -88,18 +88,21 @@ export const MANIFEST_SCHEMA_VERSION = 2;
|
||||
/** The manifest's own scope statement: what the projection covers and —
|
||||
* just as load-bearing — what it deliberately leaves out, so absence is
|
||||
* never read as a support claim in either direction. */
|
||||
const COVERAGE_NOTES: string[] = [
|
||||
"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.",
|
||||
"The ambient-nondeterminism and ambient-authority surfaces the library sidecar's determinism attestation scans ARE projected even where they lower through dedicated code paths — the Date compositions (stdlib.date.*), perf_hooks' performance.now, and the process global's ambient reads and authority calls (node-builtin.process.*) — so a determinism fence can name every surface the attestation demotes on.",
|
||||
"stdlib and node-builtin member entries name surface whose LOWERED call forms are constrained (arity, argument shapes); declared call forms outside the lowered set are refused per site, with code SC2020 for standard-library and node-builtin surface.",
|
||||
"Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.",
|
||||
`The engine-free fetch projection targets Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} with bundled Undici ${NODE24_FETCH_COMPAT_PROFILE.target.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`,
|
||||
"The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.",
|
||||
"Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
|
||||
"Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.",
|
||||
"No scheduling metadata is published; entry ids are the stable diff keys across releases.",
|
||||
];
|
||||
function coverageNotes(): string[] {
|
||||
const fetchTarget = node24FetchCompatProfile().target;
|
||||
return [
|
||||
"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.",
|
||||
"The ambient-nondeterminism and ambient-authority surfaces the library sidecar's determinism attestation scans ARE projected even where they lower through dedicated code paths — the Date compositions (stdlib.date.*), perf_hooks' performance.now, and the process global's ambient reads and authority calls (node-builtin.process.*) — so a determinism fence can name every surface the attestation demotes on.",
|
||||
"stdlib and node-builtin member entries name surface whose LOWERED call forms are constrained (arity, argument shapes); declared call forms outside the lowered set are refused per site, with code SC2020 for standard-library and node-builtin surface.",
|
||||
"Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.",
|
||||
`The engine-free fetch projection targets Node ${fetchTarget.node} with bundled Undici ${fetchTarget.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`,
|
||||
"The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.",
|
||||
"Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
|
||||
"Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.",
|
||||
"No scheduling metadata is published; entry ids are the stable diff keys across releases.",
|
||||
];
|
||||
}
|
||||
|
||||
/** Stable kebab id segment from prose: quotes dropped, the parenthetical
|
||||
* tail dropped, non-alphanumeric runs collapsed to one dash. */
|
||||
@@ -410,7 +413,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
: "recognized module (bare and node:-prefixed specifiers)",
|
||||
});
|
||||
}
|
||||
for (const [mod, members] of Object.entries(BUILTIN_MODULE_FNS)) {
|
||||
for (const [mod, members] of Object.entries(builtinModuleFns())) {
|
||||
for (const member of Object.keys(members!)) {
|
||||
add({
|
||||
id: `${moduleId(mod)}.${member}`,
|
||||
@@ -459,10 +462,9 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
}
|
||||
|
||||
// ── fetch/Web platform: one explicit, versioned compatibility profile ─
|
||||
const fetchTarget =
|
||||
`Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} / ` +
|
||||
`Undici ${NODE24_FETCH_COMPAT_PROFILE.target.undici}`;
|
||||
for (const operation of NODE24_FETCH_COMPAT_PROFILE.operations) {
|
||||
const fetchProfile = node24FetchCompatProfile();
|
||||
const fetchTarget = `Node ${fetchProfile.target.node} / Undici ${fetchProfile.target.undici}`;
|
||||
for (const operation of fetchProfile.operations) {
|
||||
const evidence = operation.evidence.map((item) =>
|
||||
item.generated !== undefined ? `generated:${item.generated}` : `fixture:${item.fixture!}`,
|
||||
);
|
||||
@@ -478,10 +480,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
`differential evidence: ${evidence.join(", ")}`,
|
||||
});
|
||||
}
|
||||
for (const options of [
|
||||
NODE24_FETCH_COMPAT_PROFILE.requestInit,
|
||||
NODE24_FETCH_COMPAT_PROFILE.responseInit,
|
||||
]) {
|
||||
for (const options of [fetchProfile.requestInit, fetchProfile.responseInit]) {
|
||||
for (const option of options) {
|
||||
const evidence = option.evidence.map((item) =>
|
||||
item.generated !== undefined ? `generated:${item.generated}` : `fixture:${item.fixture!}`,
|
||||
@@ -497,7 +496,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const row of NODE24_FETCH_COMPAT_PROFILE.inventory.entries) {
|
||||
for (const row of fetchProfile.inventory.entries) {
|
||||
if (row.status !== "unsupported") continue;
|
||||
if (row.code === undefined || row.reason === undefined) {
|
||||
throw new InternalCompilerError(`non-static fetch inventory row '${row.id}' is incomplete`);
|
||||
@@ -524,7 +523,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
// Date/perf_hooks/process rows from the one ambient table — each entry
|
||||
// is fenceable by construction (its row carries the reach-witnessing
|
||||
// libCall spellings the fence detector uses).
|
||||
for (const row of AMBIENT_SURFACE_FNS) {
|
||||
for (const row of ambientSurfaceFns()) {
|
||||
add({
|
||||
id: row.id,
|
||||
kind: row.kind,
|
||||
@@ -578,7 +577,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
return {
|
||||
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
||||
compilerVersion,
|
||||
coverage: COVERAGE_NOTES,
|
||||
coverage: coverageNotes(),
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,3 +65,56 @@ test("fork discovery batches only candidate bindings and preserves aliases and s
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
/** forkTargetPaths over the entry of a program made of `files`, and
|
||||
* whether it asked the checker for any binding. */
|
||||
function discover(files: Record<string, string>): { targets: string[]; queried: boolean } {
|
||||
const directory = mkdtempSync(join(tmpdir(), "scriptc-fork-gate-"));
|
||||
for (const [name, text] of Object.entries(files)) writeFileSync(join(directory, name), text);
|
||||
const entry = join(directory, "main.ts");
|
||||
const host = new Ts7Host((options) => new Ts7Api(options), { cwd: directory });
|
||||
const program = host.createProgram([entry, fallbackDtsPath()], {
|
||||
noLib: true,
|
||||
types: [],
|
||||
module: ModuleKind.ESNext,
|
||||
moduleResolution: ModuleResolutionKind.Bundler,
|
||||
});
|
||||
const batch = vi.spyOn(program.getTypeChecker(), "prefetchSymbolNodesExact");
|
||||
try {
|
||||
const targets = forkTargetPaths(program, [program.getSourceFile(entry)!]).map((path) =>
|
||||
path.slice(directory.length + 1),
|
||||
);
|
||||
return { targets, queried: batch.mock.calls.length > 0 };
|
||||
} finally {
|
||||
batch.mockRestore();
|
||||
program.dispose();
|
||||
host.close();
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("fork discovery asks the checker nothing when no file names node:child_process", () => {
|
||||
expect(
|
||||
discover({
|
||||
"main.ts":
|
||||
'function fork(url: URL) { return url; }\nfork(new URL("./child.ts", import.meta.url));\n',
|
||||
}),
|
||||
).toEqual({ targets: [], queried: false });
|
||||
});
|
||||
|
||||
test("fork discovery reads a module specifier spelled with escapes", () => {
|
||||
expect(
|
||||
discover({
|
||||
"main.ts":
|
||||
'import { fork } from "node:child\\u005fprocess";\nfork(new URL("./child.ts", import.meta.url));\n',
|
||||
}),
|
||||
).toEqual({ targets: ["child.ts"], queried: true });
|
||||
});
|
||||
|
||||
test("fork discovery reads a package imports entry that maps node:child_process", () => {
|
||||
const result = discover({
|
||||
"package.json": JSON.stringify({ type: "module", imports: { "#cp": "node:child_process" } }),
|
||||
"main.ts": 'import { fork } from "#cp";\nfork(new URL("./child.ts", import.meta.url));\n',
|
||||
});
|
||||
expect(result.queried).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import { fallbackDtsPath, isNodeTypesPath, tsgoPath } from "./dts-paths.js";
|
||||
import {
|
||||
ambientDtsPath,
|
||||
fallbackDtsPath,
|
||||
isNodeTypesPath,
|
||||
overridesDtsPath,
|
||||
tsgoPath,
|
||||
} from "./dts-paths.js";
|
||||
import { nearestPackageImports } from "./resolve.js";
|
||||
import { canonicalBuiltinModule } from "./builtin-modules.js";
|
||||
import { moduleSourceCandidates } from "./module-source-candidates.js";
|
||||
|
||||
@@ -292,9 +299,77 @@ export function forkCallModulePath(program: ts.Program, call: ts.CallExpression)
|
||||
return staticForkModulePath(program, call.arguments.nodes[0]);
|
||||
}
|
||||
|
||||
/** The ways a string or template literal can spell one character of a
|
||||
* module name: the character, \xHH, \uHHHH, \u{H…}, a legacy octal escape,
|
||||
* or the character after a backslash where that is not another escape. */
|
||||
function spelledCharacter(ch: string): string {
|
||||
const code = ch.charCodeAt(0);
|
||||
const hex = code.toString(16);
|
||||
const forms = [
|
||||
ch,
|
||||
"\\\\x" + hex.padStart(2, "0"),
|
||||
"\\\\u" + hex.padStart(4, "0"),
|
||||
"\\\\u\\{0*" + hex + "\\}",
|
||||
"\\\\" + code.toString(8),
|
||||
];
|
||||
if (!"bfnrtvux0123456789".includes(ch)) forms.push("\\\\" + ch);
|
||||
return "(?:" + forms.join("|") + ")";
|
||||
}
|
||||
|
||||
/** "child_process" with escapes and line continuations in any of its
|
||||
* characters (case-insensitive, a superset). */
|
||||
const SPELLED_CHILD_PROCESS = new RegExp(
|
||||
[..."child_process"].map(spelledCharacter).join("(?:\\\\(?:\\r\\n|[\\n\\r\\u2028\\u2029]))*"),
|
||||
"i",
|
||||
);
|
||||
|
||||
/** Whether a call in the program can reach node:child_process's fork. A
|
||||
* fork binding is an import or alias whose chain names the module in a
|
||||
* module specifier (a string), or a '#' specifier the nearest package.json
|
||||
* "imports" field maps. Node's type declarations and scriptc's own declare
|
||||
* the module, and alias it only as node:child_process, so they are not
|
||||
* read; any other file that names the module in a string holds
|
||||
* "child_process" in its text, or an escape that spells it. */
|
||||
function namesChildProcess(program: ts.Program): boolean {
|
||||
const known = program.analysis.namesChildProcess;
|
||||
if (known !== null) return known;
|
||||
const own = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]);
|
||||
// Implementation files are already fetched; any other file is read as
|
||||
// the host serves it, without fetching its syntax tree.
|
||||
const texts = new Map<string, string>();
|
||||
for (const sourceFile of program.getImplementationSourceFiles())
|
||||
texts.set(tsgoPath(sourceFile.fileName), sourceFile.text);
|
||||
const scopes = new Set<unknown>();
|
||||
let names = false;
|
||||
for (const fileName of program.getSourceFileNames()) {
|
||||
const path = tsgoPath(fileName);
|
||||
if (isNodeTypesPath(fileName) || own.has(path)) continue;
|
||||
const text = texts.get(path) ?? program.getSourceText(fileName) ?? "";
|
||||
if (
|
||||
text.includes("child_process") ||
|
||||
(text.includes("\\") && SPELLED_CHILD_PROCESS.test(text))
|
||||
) {
|
||||
names = true;
|
||||
break;
|
||||
}
|
||||
const imports = nearestPackageImports(fileName);
|
||||
if (imports === undefined || scopes.has(imports)) continue;
|
||||
scopes.add(imports);
|
||||
if (JSON.stringify(imports).includes("child_process")) {
|
||||
names = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
program.analysis.namesChildProcess = names;
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Source-order static fork targets in the supplied files, deduplicated by
|
||||
* resolved path. */
|
||||
export function forkTargetPaths(program: ts.Program, files: readonly ts.SourceFile[]): string[] {
|
||||
// Without a binding of node:child_process no call is a fork, and the
|
||||
// checker queries below are skipped.
|
||||
if (!namesChildProcess(program)) return [];
|
||||
const targets: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const sourceFile of files) {
|
||||
|
||||
@@ -269,6 +269,140 @@ function readsMutableLocal(lowerer: Lowerer, e: IrExpr): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
/** A number, string or boolean element type. Arrays of it hold a present
|
||||
* undefined in the element's state, so `(T | undefined)[]` stores T
|
||||
* elements (the type mapper) and a T[] seen as one stays the same array. */
|
||||
export function primitiveElement(elem: IrType): boolean {
|
||||
return elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool";
|
||||
}
|
||||
|
||||
/** `value` is the element type with undefined, stored into an array of a
|
||||
* primitive element type: a present undefined or the element. */
|
||||
export function optionalElementValue(lowerer: Lowerer, value: IrExpr, elem: IrType): boolean {
|
||||
if (!primitiveElement(elem)) return false;
|
||||
if (value.type.kind === "undefinedT") return true;
|
||||
return (
|
||||
value.type.kind === "union" &&
|
||||
lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
typeEquals(lowerer.stripUndefinedArm(value.type), elem)
|
||||
);
|
||||
}
|
||||
|
||||
/** A value of a primitive element type that holds a slot until the value
|
||||
* stored there replaces it (insertedValues), or stands for a slot that is
|
||||
* not read. */
|
||||
export function elementPlaceholder(elem: IrType, loc: SrcLoc): IrExpr {
|
||||
if (elem.kind === "f64") return { kind: "numLit", value: 0, type: F64, loc };
|
||||
if (elem.kind === "bool") return { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
return { kind: "strLit", value: "", type: STRING, loc };
|
||||
}
|
||||
|
||||
/** Values inserted into an array (push, unshift, splice, a literal), in
|
||||
* source order. A value that can be undefined (optionalElementValue) is
|
||||
* kept in a temporary: the insertion takes a placeholder for it, and
|
||||
* `fill` then stores the value at the placeholder's index. */
|
||||
export function insertedValues(
|
||||
lowerer: Lowerer,
|
||||
values: readonly IrExpr[],
|
||||
elem: IrType,
|
||||
coerce: (value: IrExpr, index: number) => IrExpr,
|
||||
loc: SrcLoc,
|
||||
): {
|
||||
stmts: IrStmt[];
|
||||
args: IrExpr[];
|
||||
optional: boolean;
|
||||
fill: (arr: IrExpr, first: IrExpr) => IrStmt[];
|
||||
} {
|
||||
if (!values.some((value) => optionalElementValue(lowerer, value, elem)))
|
||||
return { stmts: [], args: values.map(coerce), optional: false, fill: () => [] };
|
||||
const stmts: IrStmt[] = [];
|
||||
const args: IrExpr[] = [];
|
||||
const held: { offset: number; value: IrExpr }[] = [];
|
||||
values.forEach((value, offset) => {
|
||||
// Every value is evaluated in order before the insertion.
|
||||
if (value.type.kind === "undefinedT") {
|
||||
if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc });
|
||||
args.push(elementPlaceholder(elem, loc));
|
||||
held.push({ offset, value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } });
|
||||
return;
|
||||
}
|
||||
const temp = lowerer.declareHiddenLocal("%inserted", value.type);
|
||||
stmts.push({ kind: "varDecl", localId: temp.id, init: value, loc });
|
||||
const stable = varRef(temp.id, temp.type, loc);
|
||||
if (optionalElementValue(lowerer, value, elem)) {
|
||||
args.push(elementPlaceholder(elem, loc));
|
||||
held.push({ offset, value: stable });
|
||||
} else {
|
||||
args.push(coerce(stable, offset));
|
||||
}
|
||||
});
|
||||
const fill = (arr: IrExpr, first: IrExpr): IrStmt[] =>
|
||||
held.map(({ offset, value }) =>
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
arr,
|
||||
offset === 0
|
||||
? first
|
||||
: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: first,
|
||||
right: { kind: "numLit", value: offset, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
value,
|
||||
elem,
|
||||
loc,
|
||||
),
|
||||
);
|
||||
return { stmts, args, optional: true, fill };
|
||||
}
|
||||
|
||||
/** Statements appending `value` to `arr` inside a lifted helper. A value
|
||||
* that can be undefined in a primitive array (optionalElementValue) is held
|
||||
* in the helper's local `slot` (which the caller declares with the value's
|
||||
* type) and stored as a present undefined or the element; any other value
|
||||
* converts to the element type. */
|
||||
export function appendElement(
|
||||
lowerer: Lowerer,
|
||||
arr: IrExpr,
|
||||
value: IrExpr,
|
||||
elem: IrType,
|
||||
slot: string,
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] {
|
||||
if (optionalElementValue(lowerer, value, elem) && value.type.kind !== "undefinedT") {
|
||||
const held = varRef(slot, value.type, loc);
|
||||
const length: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: arr,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
{ kind: "varDecl", localId: slot, init: value, loc },
|
||||
arrayValueStore(lowerer, arr, length, held, elem, loc),
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: arr,
|
||||
args: [lowerer.coerceToExpected(value, elem)],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Store a stabilized value without changing the array's element ABI. */
|
||||
export function arrayValueStore(
|
||||
lowerer: Lowerer,
|
||||
@@ -279,6 +413,12 @@ export function arrayValueStore(
|
||||
loc: SrcLoc,
|
||||
): IrStmt {
|
||||
if (typeEquals(value.type, elem)) return { kind: "arraySet", arr, index, value, loc };
|
||||
// An undefined value of a `(T | undefined)[]` (primitiveElement).
|
||||
if (value.type.kind === "undefinedT" && primitiveElement(elem)) {
|
||||
const write: IrStmt = { kind: "arraySetUndefined", arr, index, loc };
|
||||
if (value.kind === "unitLit") return write;
|
||||
return { kind: "block", body: [{ kind: "exprStmt", expr: value, loc }, write], loc };
|
||||
}
|
||||
if (value.type.kind === "union" && lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0) {
|
||||
// The tag test and the VALUE arm extraction both consume the value. Keep
|
||||
// an arbitrary expression (especially an array read or call) single
|
||||
|
||||
@@ -3,7 +3,8 @@ import { checkedPromiseAll, thenReader } from "../checked-promise-all.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { countedFor, varRef } from "../../../ir/build.js";
|
||||
import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js";
|
||||
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
arrayOf,
|
||||
@@ -435,6 +437,28 @@ export function lowerPromiseStaticCall(
|
||||
inner.kind === "void"
|
||||
? { kind: "promise", inner: VOID }
|
||||
: lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
// Promise<T | undefined>[] combines into a `(T | undefined)[]`, which
|
||||
// stores T elements (primitiveElement): collect the values, then copy
|
||||
// them in with their present undefined values.
|
||||
if (
|
||||
resultT?.kind === "promise" &&
|
||||
resultT.inner.kind === "array" &&
|
||||
primitiveElement(resultT.inner.elem) &&
|
||||
optionalElementValue(
|
||||
lowerer,
|
||||
{ kind: "unitLit", unit: "undefined", type: inner, loc },
|
||||
resultT.inner.elem,
|
||||
)
|
||||
) {
|
||||
const joined = optionalPromiseAll(
|
||||
lowerer,
|
||||
entries,
|
||||
inner,
|
||||
resultT as IrType & { kind: "promise" },
|
||||
loc,
|
||||
);
|
||||
if (joined) return joined;
|
||||
}
|
||||
if (
|
||||
resultT?.kind !== "promise" ||
|
||||
(inner.kind !== "void" &&
|
||||
@@ -954,3 +978,139 @@ export function lowerAwaitedUniformPromiseAll(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Promise.all(ps)` over `Promise<T | undefined>[]` for a number, string or
|
||||
* boolean T, whose result is a `(T | undefined)[]` (stored as T elements).
|
||||
* The combinator joins the entries (promise.allTuple), and its builder
|
||||
* reads each fulfilled entry into the result with its present undefined,
|
||||
* inside the combinator's settlement, so the result settles on Node's
|
||||
* turn. Entries of another type keep the general combinator. */
|
||||
function optionalPromiseAll(
|
||||
lowerer: Lowerer,
|
||||
entries: IrExpr,
|
||||
inner: IrType,
|
||||
resultT: IrType & { kind: "promise" },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const entriesT = entries.type;
|
||||
if (entriesT.kind !== "array" || entriesT.elem.kind !== "promise") return null;
|
||||
if (!typeEquals(entriesT.elem.inner, inner)) return null;
|
||||
const outT = resultT.inner as IrType & { kind: "array" };
|
||||
const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID };
|
||||
const joinedT = arrayOf(voidPromise);
|
||||
const key = `promise.allOptional:${typeKey(entriesT)}:${typeKey(outT)}`;
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = keyedHelperName(lowerer, "%promise.allOptional", key);
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const builder = keyedHelperName(lowerer, "%promise.allOptionalValues", key);
|
||||
const length = (arr: IrExpr): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: arr,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const joined = varRef("joined.0", joinedT, loc);
|
||||
const out = varRef("out.0", outT, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name: builder,
|
||||
params: [{ localId: "joined.0", name: "joined", type: joinedT }],
|
||||
returnType: outT,
|
||||
locals: [
|
||||
{ id: "joined.0", name: "joined", type: joinedT, mutable: false },
|
||||
{ id: "out.0", name: "out", type: outT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
{ id: "v.0", name: "v", type: inner, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: outT, loc },
|
||||
loc,
|
||||
},
|
||||
countedFor(loc, length(joined), (index) => [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "v.0",
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "promise.fulfilledValue",
|
||||
args: [{ kind: "arrayGet", arr: joined, index, type: voidPromise, loc }],
|
||||
type: inner,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
arrayValueStore(lowerer, out, index, varRef("v.0", inner, loc), outT.elem, loc),
|
||||
]),
|
||||
{ kind: "return", value: out, loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
const ps = varRef("ps.0", entriesT, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "ps.0", name: "ps", type: entriesT }],
|
||||
returnType: resultT,
|
||||
locals: [
|
||||
{ id: "ps.0", name: "ps", type: entriesT, mutable: false },
|
||||
{ id: "joined.0", name: "joined", type: joinedT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "joined.0",
|
||||
init: { kind: "arrayLit", elems: [], type: joinedT, loc },
|
||||
loc,
|
||||
},
|
||||
countedFor(loc, length(ps), (index) => [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: joined,
|
||||
args: [
|
||||
{
|
||||
kind: "promiseVoidWiden",
|
||||
value: { kind: "arrayGet", arr: ps, index, type: entriesT.elem, loc },
|
||||
type: voidPromise,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]),
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "promise.allTuple",
|
||||
args: [
|
||||
joined,
|
||||
{
|
||||
kind: "closure",
|
||||
fnName: builder,
|
||||
captures: [],
|
||||
type: funcOf([joinedT], outT),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: resultT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [entries], type: resultT, loc };
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js";
|
||||
import { type ParamShape } from "./call-signatures.js";
|
||||
import { arrayValueRead, arrayValueType } from "./array-values.js";
|
||||
import { buildArrayConversion } from "./coercions/builders.js";
|
||||
import { lowerArraySpreadItems, lowerArrayValueItems } from "./containers/array-construction.js";
|
||||
import { restTupleElementTypes, typeKey } from "../type-mapper.js";
|
||||
|
||||
function fixedTupleSpreadInfo(
|
||||
@@ -403,6 +404,51 @@ export function completeArgs(
|
||||
}
|
||||
}
|
||||
if (restType.kind !== "array") lowerer.unsupported("SC1090", blame, "this call form");
|
||||
// A `(T | undefined)[]` rest (stored as T elements) packs undefined
|
||||
// arguments as present undefined elements.
|
||||
const restNodes = sources.slice(restAt);
|
||||
// Plain arguments and spreads, also together with spread tuple fields
|
||||
// (pre-lowered): each item, in argument order.
|
||||
if (
|
||||
restType.optionalElements === true &&
|
||||
(!restNodes.some(isIr) || restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a)))
|
||||
) {
|
||||
const items: (ts.Expression | IrExpr)[] = [];
|
||||
for (const a of restNodes) {
|
||||
if (isIr(a)) items.push(a.ir);
|
||||
else items.push(a);
|
||||
}
|
||||
out.push(lowerArraySpreadItems(lowerer, items, restType.elem, restType, loc));
|
||||
return out;
|
||||
}
|
||||
// Tuple-spread values (pre-lowered) and plain arguments pack the same
|
||||
// way. Each value evaluates once, in argument order.
|
||||
if (
|
||||
restType.optionalElements === true &&
|
||||
!restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a))
|
||||
) {
|
||||
const stmts: IrStmt[] = [];
|
||||
const values = restNodes.map((a): IrExpr => {
|
||||
// A tuple field is read where its spread appears: a later
|
||||
// argument may write the tuple.
|
||||
let value: IrExpr;
|
||||
if (isIr(a)) {
|
||||
value = a.ir;
|
||||
if (value.kind === "seqExpr") {
|
||||
stmts.push(...value.stmts);
|
||||
value = value.result;
|
||||
}
|
||||
} else value = lowerer.lowerExpr(a as ts.Expression);
|
||||
const saved = lowerer.declareHiddenLocal("%callArg", value.type);
|
||||
stmts.push({ kind: "varDecl", localId: saved.id, init: value, loc: value.loc });
|
||||
return { kind: "varRef", localId: saved.id, type: value.type, loc: value.loc };
|
||||
});
|
||||
const items = lowerArrayValueItems(lowerer, values, restType.elem, restType, loc);
|
||||
out.push(
|
||||
stmts.length === 0 ? items : { kind: "seqExpr", stmts, result: items, type: restType, loc },
|
||||
);
|
||||
return out;
|
||||
}
|
||||
// The rest pack is a fresh array per call; surplus SPREADS copy
|
||||
// their elements in (JS-exact — `f(a, ...xs, b, ...ys)` packs in
|
||||
// order, sources untouched).
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { constructedValue } from "../constructor-returns.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import { countedFor, dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import type { FieldLift } from "./structural-plans.js";
|
||||
import { buildArrayConversion } from "./builders.js";
|
||||
import { arrayValueRead, arrayValueStore, primitiveElement } from "../array-values.js";
|
||||
import type { WidthLift } from "../width-lift.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
|
||||
import { BOOL, DYN, F64, STRING, UNDEFINED_T } from "../../../ir/ir.js";
|
||||
import { BOOL, DYN, F64, STRING, UNDEFINED_T, typeEquals } from "../../../ir/ir.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { findStaticOn, findGenericStaticOn } from "../lower-classes.js";
|
||||
import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js";
|
||||
@@ -54,6 +55,9 @@ export function widthCoerce(lowerer: Lowerer, expr: IrExpr, expected: IrType): I
|
||||
return lowerer.classStaticsProjection(expr.type.className, expected.shapeId, expr.loc);
|
||||
}
|
||||
if (expected.kind === "array" && expr.type.kind === "array") {
|
||||
const optional = optionalArrayHelper(lowerer, expr.type, expected, expr.loc);
|
||||
if (optional)
|
||||
return { kind: "call", callee: optional, args: [expr], type: expected, loc: expr.loc };
|
||||
const helper = lowerer.arrayWidthHelper(expr.type, expected, expr.loc);
|
||||
if (helper)
|
||||
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
|
||||
@@ -518,6 +522,69 @@ export function emptyArrayLiftHelper(
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Copy an array of `T | undefined` unions (a collection's values, an
|
||||
* iterator drain) into a `(T | undefined)[]` of a number, string or
|
||||
* boolean T, which stores T elements: undefined values become present
|
||||
* undefined elements. Null for other element types. */
|
||||
function optionalArrayHelper(
|
||||
lowerer: Lowerer,
|
||||
fromT: IrType & { kind: "array" },
|
||||
toT: IrType & { kind: "array" },
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
const from = fromT.elem;
|
||||
const to = toT.elem;
|
||||
if (
|
||||
!primitiveElement(to) ||
|
||||
from.kind !== "union" ||
|
||||
lowerer.armTag(from.unionId, UNDEFINED_T) < 0 ||
|
||||
!typeEquals(lowerer.stripUndefinedArm(from), to)
|
||||
)
|
||||
return null;
|
||||
const key = `arrOptional:${typeKey(from)}:${typeKey(to)}`;
|
||||
const existing = lowerer.valueHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%arr.width", key);
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const input = varRef("a.0", fromT, loc);
|
||||
const out = varRef("out.0", toT, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "a.0", name: "a", type: fromT }],
|
||||
returnType: toT,
|
||||
locals: [
|
||||
{ id: "a.0", name: "a", type: fromT, mutable: true },
|
||||
{ id: "out.0", name: "out", type: toT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
{ id: "v.0", name: "v", type: from, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: toT, loc },
|
||||
loc,
|
||||
},
|
||||
countedFor(
|
||||
loc,
|
||||
{ kind: "arrIntrinsic", method: "length", receiver: input, args: [], type: F64, loc },
|
||||
(index) => [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "v.0",
|
||||
init: arrayValueRead(lowerer, input, index, from, loc),
|
||||
loc,
|
||||
},
|
||||
arrayValueStore(lowerer, out, index, varRef("v.0", from, loc), to, loc),
|
||||
],
|
||||
),
|
||||
{ kind: "return", value: out, loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Copy an array through its validated element conversion. Intern before
|
||||
* building the body so recursive element conversions can reuse this helper. */
|
||||
export function arrayWidthHelper(
|
||||
@@ -542,14 +609,37 @@ export function arrayWidthHelper(
|
||||
name,
|
||||
arrT,
|
||||
{ kind: "arrayLit", elems: [], type: outT, loc },
|
||||
(element, _index, result) => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: result,
|
||||
args: [lowerer.applyWidthLift(elemLift, element, toElem, loc)],
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
(element, index, result) => {
|
||||
let value = lowerer.applyWidthLift(elemLift, element, toElem, loc);
|
||||
// A number, string or boolean array can hold present undefined
|
||||
// elements (`(T | undefined)[]` stores T): they, and holes, copy
|
||||
// as the target's undefined.
|
||||
const missing = primitiveElement(fromElem) ? lowerer.wrappedUndefined(toElem, loc) : null;
|
||||
if (missing !== null)
|
||||
value = {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: { kind: "arrayState", arr: varRef("a.0", arrT, loc), index, type: F64, loc },
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: value,
|
||||
else_: missing,
|
||||
type: toElem,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: result,
|
||||
args: [value],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
},
|
||||
loc,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
arrayValueStore,
|
||||
arrayValueType,
|
||||
currentArrayIndexPresent,
|
||||
primitiveElement,
|
||||
} from "../array-values.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import {
|
||||
@@ -827,15 +828,16 @@ function buildArrayHofFn(
|
||||
);
|
||||
returnType = outT;
|
||||
body = [
|
||||
readLen,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: outT, loc },
|
||||
loc,
|
||||
},
|
||||
readLen,
|
||||
// map's result has the receiver's length, holes where the receiver
|
||||
// has them. Storage for it is reserved first and the length is set
|
||||
// has them. Storage for it is reserved first (with the result's
|
||||
// construction, which directly precedes it) and the length is set
|
||||
// after the loop, so a result without holes is filled by appends and
|
||||
// gets no state bytes.
|
||||
...(method === "map"
|
||||
@@ -1689,6 +1691,8 @@ function flatMapUnionPlan(
|
||||
}
|
||||
const converts = (t: IrType): boolean => {
|
||||
if (typeEquals(t, outElem)) return true;
|
||||
// A `(T | undefined)[]` result stores T elements (primitiveElement).
|
||||
if (t.kind === "undefinedT" && primitiveElement(outElem)) return true;
|
||||
if (outElem.kind !== "union") return false;
|
||||
return lowerer.armTag(outElem.unionId, t) >= 0;
|
||||
};
|
||||
@@ -1754,24 +1758,21 @@ function flatMapUnionHelper(
|
||||
};
|
||||
if (plan.arms[tag] === "empty") return [];
|
||||
if (plan.arms[tag] === "value") {
|
||||
// A unit arm's value is the unit itself, which has no narrowed
|
||||
// representation.
|
||||
const value: IrExpr = isUnitType(arm)
|
||||
? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc }
|
||||
: narrowed;
|
||||
return [
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
outRef(),
|
||||
outLength(),
|
||||
lowerer.coerceToExpected(
|
||||
// A unit arm's value is the unit itself, which has no
|
||||
// narrowed representation.
|
||||
isUnitType(arm)
|
||||
? {
|
||||
kind: "unitLit",
|
||||
unit: arm.kind === "nullT" ? "null" : "undefined",
|
||||
type: arm,
|
||||
loc,
|
||||
}
|
||||
: narrowed,
|
||||
plan.outElem,
|
||||
),
|
||||
// An undefined value is a present undefined element of a
|
||||
// primitive array (arrayValueStore).
|
||||
arm.kind === "undefinedT" && primitiveElement(plan.outElem)
|
||||
? value
|
||||
: lowerer.coerceToExpected(value, plan.outElem),
|
||||
plan.outElem,
|
||||
loc,
|
||||
),
|
||||
@@ -2347,6 +2348,43 @@ function reduceHelper(
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The comparator of a sort over a `(T | undefined)[]`, which stores T
|
||||
* elements. The comparator only receives present elements (undefined
|
||||
* elements sort last without a call), so the unannotated parameters of an
|
||||
* inline comparator take the element type, and the runtime calls it
|
||||
* without an adapter. */
|
||||
function lowerOptionalElementComparator(
|
||||
lowerer: Lowerer,
|
||||
argNode: ts.Expression,
|
||||
comparator: ts.Expression,
|
||||
elem: IrType,
|
||||
arrT: IrType & { kind: "array" },
|
||||
): IrExpr {
|
||||
if (
|
||||
arrT.optionalElements !== true ||
|
||||
!(ts.isArrowFunction(comparator) || ts.isFunctionExpression(comparator)) ||
|
||||
isJsSourceFile(ts.getSourceFileOfNode(comparator))
|
||||
)
|
||||
return lowerer.lowerExpr(argNode);
|
||||
const rebound: { symbol: ts.Symbol; previous: IrType | undefined }[] = [];
|
||||
for (const param of comparator.parameters.nodes.slice(0, 2)) {
|
||||
if (param.type || param.initializer || param.dotDotDotToken || !ts.isIdentifier(param.name))
|
||||
continue;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (!symbol) continue;
|
||||
rebound.push({ symbol, previous: lowerer.runtimeOptionalBindingTypes.get(symbol) });
|
||||
lowerer.runtimeOptionalBindingTypes.set(symbol, elem);
|
||||
}
|
||||
try {
|
||||
return lowerer.lowerExpr(argNode);
|
||||
} finally {
|
||||
for (const { symbol, previous } of rebound) {
|
||||
if (previous === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol);
|
||||
else lowerer.runtimeOptionalBindingTypes.set(symbol, previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** `a.sort(cmp)` / `a.toSorted(cmp)`, desugared to an interned synthetic
|
||||
* function like the other array HOFs. sort mutates and returns the receiver;
|
||||
* toSorted takes its shallow snapshot INSIDE the helper, after the receiver
|
||||
@@ -2435,7 +2473,8 @@ export function lowerArraySortCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
let fnArg = undefinedArg ?? lowerer.lowerExpr(argNode);
|
||||
let fnArg =
|
||||
undefinedArg ?? lowerOptionalElementComparator(lowerer, argNode, unwrappedArg, elem, arrT);
|
||||
// Reusable JS comparators often accept checked values. Adapt the array's
|
||||
// element ABI just as other array callbacks do before sorting.
|
||||
if (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { generatorDrain } from "../iterator-adapters.js";
|
||||
import { identityPreservingWidening } from "../coercions/identity.js";
|
||||
import { liftedFreshSpread } from "../coercions/fresh-spread.js";
|
||||
import { checkedIterableSpread } from "../checked-iterable-spread.js";
|
||||
import { AstNode } from "../../ts7/ast-node.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import {
|
||||
dynUndefinedExpr,
|
||||
@@ -27,7 +28,9 @@ import {
|
||||
type IrType,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
arrayOf,
|
||||
isUnitType,
|
||||
canConvertToDyn,
|
||||
funcOf,
|
||||
isRefCounted,
|
||||
@@ -35,7 +38,13 @@ import {
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { arrayValueRead, arrayValueStore, arrayValueType } from "../array-values.js";
|
||||
import {
|
||||
arrayValueRead,
|
||||
arrayValueStore,
|
||||
arrayValueType,
|
||||
optionalElementValue,
|
||||
primitiveElement,
|
||||
} from "../array-values.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { newFnCtx } from "../lowerer.js";
|
||||
import { iteratorCanStep, iteratorValue } from "../iterator-consumption.js";
|
||||
@@ -51,6 +60,34 @@ import {
|
||||
} from "./callback-arguments.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
|
||||
/** A value whose type admits only null and undefined, stored into an array
|
||||
* that keeps undefined elements: the checker admits it there only when its
|
||||
* type is `undefined` (a tuple element typed `undefined` maps to such a
|
||||
* union), so it is evaluated and stored as undefined. */
|
||||
function undefinedForNullish(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
arrType: IrType & { kind: "array" },
|
||||
): IrExpr {
|
||||
if (arrType.optionalElements !== true || value.type.kind !== "union") return value;
|
||||
const arms = lowerer.unions.get(value.type.unionId)?.arms ?? [];
|
||||
if (arms.length === 0 || !arms.every(isUnitType)) return value;
|
||||
const loc = value.loc;
|
||||
const optional: IrType = {
|
||||
kind: "union",
|
||||
unionId: lowerer.unions.intern([arrType.elem, UNDEFINED_T]),
|
||||
};
|
||||
const missing = lowerer.wrappedUndefined(optional, loc);
|
||||
if (!missing) return value;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: value, loc }],
|
||||
result: missing,
|
||||
type: optional,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Build the argument array for a mutating/copying operation while keeping
|
||||
* the receiver's scalar payload ABI. A missing read is represented as the
|
||||
* present UNDEFINED state in this temporary, so push/unshift and toSpliced
|
||||
@@ -86,9 +123,11 @@ export function lowerArrayValueItems(
|
||||
loc,
|
||||
},
|
||||
];
|
||||
for (const value of values) {
|
||||
for (const item of values) {
|
||||
const value = undefinedForNullish(lowerer, item, arrType);
|
||||
const stored =
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, value, elem)
|
||||
? value
|
||||
: lowerer.coerceToExpected(value, elem);
|
||||
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
|
||||
@@ -96,16 +135,21 @@ export function lowerArrayValueItems(
|
||||
return { kind: "seqExpr", stmts: body, result: outRef, type: arrType, loc };
|
||||
}
|
||||
|
||||
/** The items of an array built from argument nodes (a spread copies its
|
||||
* elements in) and values already lowered (a spread tuple's fields), in
|
||||
* order. Each value is stored when its turn comes. */
|
||||
export function lowerArraySpreadItems(
|
||||
lowerer: Lowerer,
|
||||
nodes: readonly ts.Expression[],
|
||||
nodes: readonly (ts.Expression | IrExpr)[],
|
||||
elem: IrType,
|
||||
arrType: IrType & { kind: "array" },
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const out = lowerer.declareHiddenLocal("%arrayItems", arrType);
|
||||
const outRef = varRef(out.id, arrType, loc);
|
||||
const plainItems = nodes.filter((node) => !ts.isSpreadElement(node)).length;
|
||||
const plainItems = nodes.filter(
|
||||
(node) => !(node instanceof AstNode) || !ts.isSpreadElement(node),
|
||||
).length;
|
||||
const length = (): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
@@ -129,7 +173,23 @@ export function lowerArraySpreadItems(
|
||||
},
|
||||
];
|
||||
for (const node of nodes) {
|
||||
if (ts.isSpreadElement(node)) {
|
||||
if (!(node instanceof AstNode)) {
|
||||
// A spread tuple's first field declares the tuple's storage, which
|
||||
// the later fields read: its statements join this sequence, whose
|
||||
// frame lasts until every item is stored.
|
||||
let item = node;
|
||||
if (item.kind === "seqExpr") {
|
||||
body.push(...item.stmts);
|
||||
item = item.result;
|
||||
}
|
||||
const value = undefinedForNullish(lowerer, item, arrType);
|
||||
const stored =
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, value, elem)
|
||||
? value
|
||||
: lowerer.coerceToExpected(value, elem);
|
||||
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
|
||||
} else if (ts.isSpreadElement(node)) {
|
||||
let source = lowerer.lowerExpr(node.expression);
|
||||
if (
|
||||
source.type.kind === "set" &&
|
||||
@@ -212,7 +272,8 @@ export function lowerArraySpreadItems(
|
||||
} else {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
const stored =
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, value, elem)
|
||||
? value
|
||||
: lowerer.coerceInto(node, value, elem);
|
||||
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
|
||||
@@ -518,6 +579,20 @@ export function lowerArrayOfCall(
|
||||
if (call.arguments.nodes.some(ts.isSpreadElement)) {
|
||||
return lowerArraySpreadItems(lowerer, call.arguments.nodes, result.elem, result, locOf(call));
|
||||
}
|
||||
// A `(T | undefined)[]` (primitiveElement) stores undefined arguments as
|
||||
// present undefined elements.
|
||||
if (result.optionalElements === true) {
|
||||
const nodes = call.arguments.nodes;
|
||||
const values = nodes.map((arg) => lowerer.lowerExpr(arg));
|
||||
if (values.some((value) => optionalElementValue(lowerer, value, result.elem)))
|
||||
return lowerArrayValueItems(lowerer, values, result.elem, result, locOf(call));
|
||||
return {
|
||||
kind: "arrayLit",
|
||||
elems: values.map((value, i) => lowerer.coerceInto(nodes[i]!, value, result.elem)),
|
||||
type: result,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "arrayLit",
|
||||
elems: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)),
|
||||
@@ -959,6 +1034,31 @@ export function lowerArrayFromCall(
|
||||
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (arrT?.kind !== "array") lowerer.badType(call, lowerer.typeOf(call));
|
||||
const elem = arrT.elem;
|
||||
// A `(T | undefined)[]` (primitiveElement): n present undefined
|
||||
// elements.
|
||||
const elemTs = lowerer.checker.isArrayType(lowerer.typeOf(call))
|
||||
? lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0]
|
||||
: undefined;
|
||||
if (
|
||||
primitiveElement(elem) &&
|
||||
elemTs !== undefined &&
|
||||
(elemTs.isUnionType() ? ts.constituentTypes(elemTs) : [elemTs]).some(
|
||||
(part) => (part.flags & ts.TypeFlags.Undefined) !== 0,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
kind: "arrIntrinsic",
|
||||
method: "fillUndefined",
|
||||
receiver: { kind: "arrayNewLen", length: n, type: arrT, loc },
|
||||
// To the end: fill clamps an infinite end to the length.
|
||||
args: [
|
||||
numLit(0, loc),
|
||||
{ kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc },
|
||||
],
|
||||
type: arrT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const absent =
|
||||
elem.kind === "union"
|
||||
? lowerer.wrappedUndefined(elem, loc) !== null ||
|
||||
@@ -1085,7 +1185,10 @@ export function lowerArrayFromCall(
|
||||
// A generator (or an iterator adapted into one) drains in order.
|
||||
if (src.type.kind === "generator" && args.length === 1) {
|
||||
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const drained = arrT?.kind === "array" ? generatorDrain(lowerer, src, arrT.elem, loc) : null;
|
||||
const drained =
|
||||
arrT?.kind === "array"
|
||||
? generatorDrain(lowerer, src, arrT.elem, loc, arrT.optionalElements === true)
|
||||
: null;
|
||||
if (drained) return drained;
|
||||
}
|
||||
lowerer.noLowering(
|
||||
@@ -1159,6 +1262,11 @@ function buildArrayFromArrayFn(
|
||||
type: fnRet,
|
||||
loc,
|
||||
};
|
||||
// A result of another type than the element (`T | undefined` into a
|
||||
// `(T | undefined)[]`, which stores T elements) is held in a local, so
|
||||
// the store tests and converts one value.
|
||||
const held = !typeEquals(fnRet, outElem);
|
||||
const value: IrExpr = held ? varRef("v.0", fnRet, loc) : mapped;
|
||||
return {
|
||||
name,
|
||||
params: [
|
||||
@@ -1171,6 +1279,7 @@ function buildArrayFromArrayFn(
|
||||
{ id: "f.0", name: "f", type: fnT, mutable: true },
|
||||
{ id: "out.0", name: "out", type: outT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
...(held ? [{ id: "v.0", name: "v", type: fnRet, mutable: false }] : []),
|
||||
],
|
||||
body: [
|
||||
{
|
||||
@@ -1191,7 +1300,8 @@ function buildArrayFromArrayFn(
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
arrayValueStore(lowerer, out, i, mapped, outElem, loc),
|
||||
...(held ? [{ kind: "varDecl", localId: "v.0", init: mapped, loc } as const] : []),
|
||||
arrayValueStore(lowerer, out, i, value, outElem, loc),
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "i.0",
|
||||
|
||||
@@ -32,6 +32,10 @@ import {
|
||||
arrayValueRead,
|
||||
arrayValueStore,
|
||||
arrayValueType,
|
||||
elementPlaceholder,
|
||||
insertedValues,
|
||||
optionalElementValue,
|
||||
primitiveElement,
|
||||
} from "../array-values.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import {
|
||||
@@ -125,6 +129,14 @@ function lowerOptionalArrayPush(
|
||||
return { kind: "seqExpr", stmts, result: length(), type: F64, loc };
|
||||
}
|
||||
|
||||
/** The checker type of `node` admits undefined. */
|
||||
function checkerTypeAdmitsUndefined(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const type = lowerer.typeOf(node);
|
||||
return (type.isUnionType() ? ts.constituentTypes(type) : [type]).some(
|
||||
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
|
||||
);
|
||||
}
|
||||
|
||||
/** `a.splice(start, count, v)` where v may hold undefined at run time.
|
||||
* Receiver, start, count, and v evaluate once, in order; a present v is
|
||||
* inserted as a plain element literal (which a statement-position splice
|
||||
@@ -658,7 +670,10 @@ export function lowerArrayMethodCall(
|
||||
const hasSpread = itemNodes.some(ts.isSpreadElement);
|
||||
const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg));
|
||||
const statefulItems = itemProbes.some(
|
||||
(probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null,
|
||||
(probe) =>
|
||||
probe !== null &&
|
||||
(lowerer.runtimeOptionalWidening(probe.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, probe, elem)),
|
||||
);
|
||||
const items: IrExpr = hasSpread
|
||||
? lowerArraySpreadItems(lowerer, itemNodes, elem, receiverIr, loc)
|
||||
@@ -875,6 +890,65 @@ export function lowerArrayMethodCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A `(T | undefined)[]` (primitiveElement) takes undefined values as
|
||||
// present undefined elements, stored after the insertion.
|
||||
if (
|
||||
(name === "push" || name === "unshift") &&
|
||||
primitiveElement(elem) &&
|
||||
call.arguments.nodes.some((a) => checkerTypeAdmitsUndefined(lowerer, a))
|
||||
) {
|
||||
const nodes = call.arguments.nodes;
|
||||
const values = nodes.map((a) => lowerer.lowerExpr(a));
|
||||
const inserted = insertedValues(
|
||||
lowerer,
|
||||
values,
|
||||
elem,
|
||||
(value, i) => lowerer.coerceInto(nodes[i]!, value, elem),
|
||||
loc,
|
||||
);
|
||||
if (inserted.optional) {
|
||||
const array = lowerer.declareHiddenLocal("%insertInto", receiver.type);
|
||||
const arr = varRef(array.id, array.type, loc);
|
||||
const length = lowerer.declareHiddenLocal("%insertLength", F64);
|
||||
const count: IrExpr = numLit(values.length, loc);
|
||||
const first: IrExpr =
|
||||
name === "push"
|
||||
? {
|
||||
kind: "bin",
|
||||
op: "-",
|
||||
left: varRef(length.id, F64, loc),
|
||||
right: count,
|
||||
type: F64,
|
||||
loc,
|
||||
}
|
||||
: numLit(0, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: array.id, init: receiver, loc },
|
||||
...inserted.stmts,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: length.id,
|
||||
init: {
|
||||
kind: "arrIntrinsic",
|
||||
method: name,
|
||||
receiver: arr,
|
||||
args: inserted.args,
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
...inserted.fill(arr, first),
|
||||
],
|
||||
result: varRef(length.id, F64, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "arrIntrinsic", method: name, receiver, args: inserted.args, type: F64, loc };
|
||||
}
|
||||
// Inserted values flow into the element slot like an assignment would:
|
||||
// union-element arrays wrap plain arm values (coerceInto is inert
|
||||
// when the types already agree).
|
||||
@@ -996,9 +1070,12 @@ export function lowerArrayMethodCall(
|
||||
const hasSpread = itemNodes.some(ts.isSpreadElement);
|
||||
const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg));
|
||||
const statefulItems = itemProbes.some(
|
||||
(probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null,
|
||||
(probe) =>
|
||||
probe !== null &&
|
||||
(lowerer.runtimeOptionalWidening(probe.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, probe, elem)),
|
||||
);
|
||||
if (statefulItems && itemNodes.length === 1) {
|
||||
if (statefulItems && itemNodes.length === 1 && itemProbes[0]?.type.kind === "union") {
|
||||
return lowerOptionalSpliceItem(
|
||||
lowerer,
|
||||
receiver,
|
||||
@@ -1734,7 +1811,28 @@ function arraySearchHelper(
|
||||
type: valueT,
|
||||
loc,
|
||||
};
|
||||
const value = directNeedle ? rawRead : optionalValue;
|
||||
// A direct needle matches only a present value (`match` tests the state
|
||||
// first); a present undefined of a `(T | undefined)[]` (primitiveElement)
|
||||
// must not be read as T.
|
||||
const value = !directNeedle
|
||||
? optionalValue
|
||||
: primitiveElement(elem)
|
||||
? {
|
||||
kind: "ternary" as const,
|
||||
cond: {
|
||||
kind: "bin" as const,
|
||||
op: "===" as const,
|
||||
left: stateRef,
|
||||
right: numLit(1, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: rawRead,
|
||||
else_: elementPlaceholder(elem, loc),
|
||||
type: elem,
|
||||
loc,
|
||||
}
|
||||
: rawRead;
|
||||
const readT = directNeedle ? elem : valueT;
|
||||
const equal = arraySearchEquality(
|
||||
lowerer,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { CONTRACT_TYPES_MODULE } from "../../contract-types.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { writtenLocals } from "../../../ir/callback-specialization.js";
|
||||
import { AmbientReceiverReaders } from "../../../ir/receiver-readers.js";
|
||||
import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
VOID,
|
||||
type IrBindingSource,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrGlobal,
|
||||
@@ -16,9 +21,12 @@ import {
|
||||
typeKey,
|
||||
} from "../../../ir/ir.js";
|
||||
import {
|
||||
everyExpr,
|
||||
everyExprChild,
|
||||
everyStmt,
|
||||
everyStmtChild,
|
||||
everyStmtList,
|
||||
transformExpr,
|
||||
transformStmtList,
|
||||
} from "../../../ir/traverse.js";
|
||||
import {
|
||||
@@ -537,6 +545,65 @@ function matchChain(lowerer: Lowerer, e: IrExpr): Chain | null {
|
||||
};
|
||||
}
|
||||
|
||||
/** The outermost array-HOF call of `e` on its own, over its receiver. */
|
||||
function loneChain(lowerer: Lowerer, e: IrExpr): Chain | null {
|
||||
const top = stageOf(lowerer, e);
|
||||
if (!top) return null;
|
||||
const { stage, receiver } = top;
|
||||
if (receiver.type.kind !== "array") return null;
|
||||
const stages: Stage[] = [];
|
||||
let terminal: Terminal;
|
||||
if (TERMINALS.has(stage.info.method)) {
|
||||
const init =
|
||||
stage.info.method === "reduce" && stage.info.hasInit && e.kind === "call" ? e.args[2]! : null;
|
||||
terminal = { kind: "call", stage, init };
|
||||
} else if (PRODUCERS.has(stage.info.method)) {
|
||||
terminal = { kind: "collect" };
|
||||
stages.push(stage);
|
||||
} else return null;
|
||||
return {
|
||||
source: receiver,
|
||||
sourceType: receiver.type,
|
||||
slice: null,
|
||||
stages,
|
||||
terminal,
|
||||
type: e.type,
|
||||
loc: e.loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The callbacks of `chain` in order, or null when its stages cannot run
|
||||
* in one loop: element types that do not line up, an array argument past
|
||||
* the first stage, reduce without a seed, or findIndex over holes. */
|
||||
function chainCallbacks(chain: Chain): Stage[] | null {
|
||||
const { stages, terminal } = chain;
|
||||
const terminalStage = terminal.kind === "call" ? terminal.stage : null;
|
||||
const callbacks = terminalStage ? [...stages, terminalStage] : stages;
|
||||
// Element types must line up stage to stage.
|
||||
let elem = chain.sourceType.elem;
|
||||
for (const stage of callbacks) {
|
||||
if (!typeEquals(stage.info.elem, elem)) return null;
|
||||
if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem))
|
||||
return null;
|
||||
if (
|
||||
(stage.info.method === "map" || stage.info.method === "flatMap") &&
|
||||
!typeEquals(stage.info.outElem, stage.info.fnRet)
|
||||
)
|
||||
return null;
|
||||
elem = stage.info.outElem;
|
||||
}
|
||||
// Index arguments are reproduced; an array argument only names the source.
|
||||
for (let k = 0; k < callbacks.length; k++) {
|
||||
const stage = callbacks[k]!;
|
||||
const lead = stage.info.method === "reduce" ? 1 : 0;
|
||||
if (stage.info.arity > lead + 3) return null;
|
||||
if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null;
|
||||
}
|
||||
if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null;
|
||||
if (terminalStage?.info.method === "findIndex" && holeCarrying(chain)) return null;
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
/* ── the fusion pass ─────────────────────────────────────────────────── */
|
||||
|
||||
/** The values a fused loop hands from one stage to the next: a local of a
|
||||
@@ -550,6 +617,10 @@ interface Plan {
|
||||
writes: Set<string>;
|
||||
/** Some callback may throw or loop; at most one does. */
|
||||
risky: boolean;
|
||||
/** How the loop inlines callbacks, when not by their effect summaries. */
|
||||
inline?: InlineTarget;
|
||||
/** Stack frame name of the loop: the inlined callback's, for a lone call. */
|
||||
frame?: string;
|
||||
}
|
||||
|
||||
/** Fuse chains of array-HOF helper calls into one loop.
|
||||
@@ -596,12 +667,35 @@ class PipelineFusion {
|
||||
readonly added: IrFunction[] = [];
|
||||
private readonly effects: CallbackEffects;
|
||||
private readonly helpers = new Map<string, string>();
|
||||
/** Bindings each function, or a closure it creates, writes after their
|
||||
* declaration. */
|
||||
private readonly written: Map<string, Set<string>>;
|
||||
/** classCaptured's answers by function name. */
|
||||
private readonly classCaptures = new Map<string, ReadonlySet<string>>();
|
||||
/** Whether a module imports or re-exports the contract types. */
|
||||
private readonly contractTypes: boolean;
|
||||
/** Bindings of the function being fused that inlined callbacks captured. */
|
||||
private inlinedCaptures = new Set<string>();
|
||||
/** The helper calls each callback with an undefined receiver; a copied
|
||||
* body runs under the receiver around the array call instead. */
|
||||
private readonly receivers: AmbientReceiverReaders;
|
||||
|
||||
constructor(
|
||||
private readonly lowerer: Lowerer,
|
||||
functions: readonly IrFunction[],
|
||||
) {
|
||||
this.effects = new CallbackEffects(functions, lowerer.globalsList);
|
||||
this.written = writtenLocals({ functions });
|
||||
this.receivers = new AmbientReceiverReaders(new Map(functions.map((fn) => [fn.name, fn])));
|
||||
this.contractTypes = lowerer.moduleOrder.some((sf) =>
|
||||
sf.statements.nodes.some(
|
||||
(st) =>
|
||||
(ts.isImportDeclaration(st) || ts.isExportDeclaration(st)) &&
|
||||
st.moduleSpecifier !== undefined &&
|
||||
ts.isStringLiteral(st.moduleSpecifier) &&
|
||||
st.moduleSpecifier.text === CONTRACT_TYPES_MODULE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
run(fn: IrFunction): void {
|
||||
@@ -612,6 +706,7 @@ class PipelineFusion {
|
||||
});
|
||||
if (!candidate) return;
|
||||
const removed = new Set<string>();
|
||||
this.inlinedCaptures = new Set();
|
||||
let body = this.lists(fn.body, fn, removed);
|
||||
body = transformStmtList(body, {
|
||||
stmt: (s) => s,
|
||||
@@ -619,6 +714,7 @@ class PipelineFusion {
|
||||
});
|
||||
fn.body = body;
|
||||
if (removed.size > 0) fn.locals = fn.locals.filter((l) => !removed.has(l.id));
|
||||
if (this.inlinedCaptures.size > 0) unboxUncaptured(fn, this.inlinedCaptures);
|
||||
}
|
||||
|
||||
/* ── statement lists: fold single-use chain bindings ── */
|
||||
@@ -733,40 +829,51 @@ class PipelineFusion {
|
||||
/* ── expressions ── */
|
||||
|
||||
private fuse(e: IrExpr, fn: IrFunction): IrExpr | null {
|
||||
const plan = this.plan(e, fn);
|
||||
const plan = this.plan(e, fn) ?? this.lonePlan(e, fn);
|
||||
return plan ? this.emit(plan) : null;
|
||||
}
|
||||
|
||||
/** One array-HOF call on its own (or the last call of a chain that does
|
||||
* not fuse), when every callback is a closure literal whose body can be
|
||||
* copied into the loop. A single stage reorders nothing, so the
|
||||
* callbacks' effects need no model; their captured bindings must keep
|
||||
* one value while the loop runs (stableTarget), since the loop reads
|
||||
* them once, when it starts. The loop keeps the helper's order: the
|
||||
* length read once, each element read when it is visited, holes
|
||||
* skipped (and kept by map). */
|
||||
private lonePlan(e: IrExpr, fn: IrFunction): Plan | null {
|
||||
const chain = loneChain(this.lowerer, e);
|
||||
if (!chain) return null;
|
||||
const callbacks = chainCallbacks(chain);
|
||||
if (!callbacks) return null;
|
||||
const inline = this.stableTarget(fn);
|
||||
let frame: string | undefined;
|
||||
for (const stage of callbacks) {
|
||||
for (const arg of [stage.slow, stage.fast]) {
|
||||
if (!arg) continue;
|
||||
const target = inline(arg);
|
||||
if (!target) return null;
|
||||
frame ??= target.sourceName;
|
||||
}
|
||||
}
|
||||
return {
|
||||
chain,
|
||||
callbacks,
|
||||
writes: new Set(),
|
||||
risky: true,
|
||||
inline,
|
||||
...(frame === undefined ? {} : { frame }),
|
||||
};
|
||||
}
|
||||
|
||||
/** Decide whether `e` is a fusable chain; null when it is not. */
|
||||
private plan(e: IrExpr, fn: IrFunction): Plan | null {
|
||||
const chain = matchChain(this.lowerer, e);
|
||||
if (!chain) return null;
|
||||
const callbacks = chainCallbacks(chain);
|
||||
if (!callbacks) return null;
|
||||
const { stages, terminal } = chain;
|
||||
const terminalStage = terminal.kind === "call" ? terminal.stage : null;
|
||||
const callbacks = terminalStage ? [...stages, terminalStage] : stages;
|
||||
// Element types must line up stage to stage.
|
||||
let elem = chain.sourceType.elem;
|
||||
for (const stage of callbacks) {
|
||||
if (!typeEquals(stage.info.elem, elem)) return null;
|
||||
if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem))
|
||||
return null;
|
||||
if (
|
||||
(stage.info.method === "map" || stage.info.method === "flatMap") &&
|
||||
!typeEquals(stage.info.outElem, stage.info.fnRet)
|
||||
)
|
||||
return null;
|
||||
elem = stage.info.outElem;
|
||||
}
|
||||
// Index arguments are reproduced; an array argument only names the source.
|
||||
for (let k = 0; k < callbacks.length; k++) {
|
||||
const stage = callbacks[k]!;
|
||||
const lead = stage.info.method === "reduce" ? 1 : 0;
|
||||
if (stage.info.arity > lead + 3) return null;
|
||||
if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null;
|
||||
}
|
||||
if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null;
|
||||
const holes = holeCarrying(chain);
|
||||
if (terminalStage?.info.method === "findIndex" && holes) return null;
|
||||
// Effects of each callback, in chain order.
|
||||
const scope = this.effects.scope(fn, true);
|
||||
const summaries: Effects[] = [];
|
||||
@@ -831,30 +938,89 @@ class PipelineFusion {
|
||||
* are passed in by value, read when the fused call starts; no callback
|
||||
* in the chain assigns a binding another one reads). */
|
||||
private readonly inlineTarget: InlineTarget = (arg) => {
|
||||
const fn = this.closureTarget(arg);
|
||||
if (!fn) return null;
|
||||
const captures = new Set((fn.captures ?? []).map((c) => c.localId));
|
||||
const effects = this.effects.ofFunction(fn);
|
||||
if (!effects || intersects(effects.writes, captures)) return null;
|
||||
return inlinableBody(fn, captures) ? fn : null;
|
||||
};
|
||||
|
||||
/** inlineTarget for a callback created in `fn` whose captured bindings
|
||||
* nothing assigns after their declaration: `fn`'s own locals and
|
||||
* parameters, written neither by `fn` nor by any closure that shares
|
||||
* them, and not by the callback body. A binding a local class captures
|
||||
* counts as written, since its methods may assign it; so does a binding
|
||||
* a method reads through its class's captures. The callback must
|
||||
* not observe the ambient receiver (AmbientReceiverReaders). */
|
||||
private stableTarget(fn: IrFunction): InlineTarget {
|
||||
const locals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
const inherited = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
const written = this.written.get(fn.name);
|
||||
const classCaptured = this.classCaptured(fn);
|
||||
return (arg) => {
|
||||
const target = this.closureTarget(arg);
|
||||
if (!target || !written || arg.kind !== "closure") return null;
|
||||
if (!this.receivers.ignores(target)) return null;
|
||||
for (const id of arg.captures) {
|
||||
const local = locals.get(id);
|
||||
if (!local || local.tdz || inherited.has(id) || written.has(id) || classCaptured.has(id))
|
||||
return null;
|
||||
}
|
||||
const captures = new Set((target.captures ?? []).map((c) => c.localId));
|
||||
const assigns = !everyStmtList(target.body, {
|
||||
stmt: (st) => !((st.kind === "assign" || st.kind === "forOf") && captures.has(st.localId)),
|
||||
expr: (ex) =>
|
||||
!((ex.kind === "assignExpr" || ex.kind === "incDec") && captures.has(ex.localId)),
|
||||
});
|
||||
return !assigns && inlinableBody(target, captures) ? target : null;
|
||||
};
|
||||
}
|
||||
|
||||
/** The bindings of `fn` that a local class it creates captures. */
|
||||
private classCaptured(fn: IrFunction): ReadonlySet<string> {
|
||||
const known = this.classCaptures.get(fn.name);
|
||||
if (known) return known;
|
||||
const ids = new Set<string>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "classRef") for (const id of e.captures ?? []) ids.add(id);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
this.classCaptures.set(fn.name, ids);
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** The function behind a closure-literal callback the loop can inline. */
|
||||
private closureTarget(arg: IrExpr): IrFunction | null {
|
||||
if (arg.kind !== "closure" || arg.adapts) return null;
|
||||
const fn = this.effects.fn(arg.fnName);
|
||||
if (!fn || fn.async || fn.generator || fn.classCaptures?.length) return null;
|
||||
const captures = new Set((fn.captures ?? []).map((c) => c.localId));
|
||||
if (captures.size !== arg.captures.length) return null;
|
||||
const effects = this.effects.ofFunction(fn);
|
||||
if (!effects || intersects(effects.writes, captures)) return null;
|
||||
if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return null;
|
||||
let size = 0;
|
||||
const small = everyStmtList(fn.body, {
|
||||
expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef",
|
||||
stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch",
|
||||
});
|
||||
return small ? fn : null;
|
||||
};
|
||||
if ((fn.captures ?? []).length !== arg.captures.length) return null;
|
||||
return fn;
|
||||
}
|
||||
|
||||
private emit(plan: Plan): IrExpr | null {
|
||||
const built = new FusedLoop(this.lowerer, plan.chain, this.inlineTarget).build();
|
||||
const built = new FusedLoop(
|
||||
this.lowerer,
|
||||
plan.chain,
|
||||
plan.inline ?? this.inlineTarget,
|
||||
this.contractTypes,
|
||||
).build();
|
||||
if (!built) return null;
|
||||
for (const id of built.inlinedCaptures) this.inlinedCaptures.add(id);
|
||||
let name = this.helpers.get(built.key);
|
||||
if (!name) {
|
||||
name = keyedHelperName(this.lowerer, "%arr.fused", built.key);
|
||||
name = keyedHelperName(this.lowerer, plan.inline ? "%arr.loop" : "%arr.fused", built.key);
|
||||
this.helpers.set(built.key, name);
|
||||
this.added.push({ ...built.fn, name });
|
||||
const helper: IrFunction = { ...built.fn, name };
|
||||
// A lone call's loop stands in the inlined callback's stack frame.
|
||||
if (plan.frame !== undefined) helper.sourceName = plan.frame;
|
||||
this.added.push(helper);
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
@@ -866,6 +1032,48 @@ class PipelineFusion {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a statement inside `e` (a sequence expression's) returns. */
|
||||
function returns(e: IrExpr): boolean {
|
||||
return !everyExpr(e, { expr: () => true, stmt: (s) => s.kind !== "return" });
|
||||
}
|
||||
|
||||
/** Drops the box of each binding in `inlined` that no closure or class
|
||||
* created in `fn` captures any more: the callbacks that captured it were
|
||||
* copied into loops, which receive its value. A dead-zone binding keeps the
|
||||
* box that represents its uninitialized state, and bindings `fn` itself
|
||||
* captures keep theirs. */
|
||||
function unboxUncaptured(fn: IrFunction, inlined: ReadonlySet<string>): void {
|
||||
const captured = new Set<string>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "closure" || e.kind === "classRef")
|
||||
for (const id of e.captures ?? []) captured.add(id);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
const inherited = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
fn.locals = fn.locals.map((local): IrLocal => {
|
||||
if (local.boxed !== true || local.tdz === true || !inlined.has(local.id)) return local;
|
||||
if (captured.has(local.id) || inherited.has(local.id)) return local;
|
||||
const { boxed: _boxed, ...plain } = local;
|
||||
return plain;
|
||||
});
|
||||
}
|
||||
|
||||
/** A callback body the loop can copy: small, no nested closures, recursion
|
||||
* or try statements, and no boxed locals besides its captures. */
|
||||
function inlinableBody(fn: IrFunction, captures: ReadonlySet<string>): boolean {
|
||||
if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return false;
|
||||
let size = 0;
|
||||
return everyStmtList(fn.body, {
|
||||
expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef",
|
||||
stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch",
|
||||
});
|
||||
}
|
||||
|
||||
function intersects(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
||||
for (const id of a) if (b.has(id)) return true;
|
||||
return false;
|
||||
@@ -988,10 +1196,13 @@ class FusedLoop {
|
||||
private next = 0;
|
||||
private failed = false;
|
||||
|
||||
/** `contractTypes`: the program writes contract types, which the
|
||||
* contract checker binds to source bindings. */
|
||||
constructor(
|
||||
private readonly lowerer: Lowerer,
|
||||
private readonly chain: Chain,
|
||||
private readonly inlineTarget: InlineTarget,
|
||||
private readonly contractTypes: boolean,
|
||||
) {
|
||||
this.loc = chain.loc;
|
||||
}
|
||||
@@ -1000,9 +1211,9 @@ class FusedLoop {
|
||||
return `${name}.${this.next++}`;
|
||||
}
|
||||
|
||||
private local(name: string, type: IrType, mutable = false): string {
|
||||
private local(name: string, type: IrType, mutable = false, source?: IrBindingSource): string {
|
||||
const id = this.fresh(name);
|
||||
this.locals.push({ id, name, type, mutable });
|
||||
this.locals.push(source ? { id, name, type, mutable, source } : { id, name, type, mutable });
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1069,21 +1280,50 @@ class FusedLoop {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
// Copies keep their source bindings, so contract types written on
|
||||
// them are checked against what the loop stores. In a program that
|
||||
// writes contract types, a parameter with a source binding keeps its
|
||||
// own local for the same reason.
|
||||
const sources = new Map(fn.locals.map((l) => [l.id, l.source]));
|
||||
const aliased = new Set<string>();
|
||||
fn.params.forEach((p, index) => {
|
||||
const arg = args[index]!;
|
||||
if (arg.kind === "varRef" && !assigned.has(p.localId) && typeEquals(arg.type, p.type)) {
|
||||
if (
|
||||
arg.kind === "varRef" &&
|
||||
!assigned.has(p.localId) &&
|
||||
typeEquals(arg.type, p.type) &&
|
||||
!(this.contractTypes && sources.get(p.localId) !== undefined)
|
||||
) {
|
||||
renamed.set(p.localId, arg.localId);
|
||||
aliased.add(p.localId);
|
||||
}
|
||||
});
|
||||
for (const l of fn.locals) {
|
||||
if (renamed.has(l.id)) continue;
|
||||
const id = this.local(l.name, l.type, true);
|
||||
const id = this.local(l.name, l.type, true, l.source);
|
||||
renamed.set(l.id, id);
|
||||
}
|
||||
const rename = (id: string): string => renamed.get(id) ?? id;
|
||||
const renameExpr = (e: IrExpr): IrExpr => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
return { ...e, localId: rename(e.localId) };
|
||||
default:
|
||||
return e;
|
||||
}
|
||||
};
|
||||
// A bound parameter's value is an element or index with no source text
|
||||
// of its own; it is located at the parameter, without a span.
|
||||
const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] => {
|
||||
if (aliased.has(p.localId)) return [];
|
||||
const source = sources.get(p.localId);
|
||||
const arg = args[index]!;
|
||||
const init = source ? { ...arg, loc: { ...source.loc, end: source.loc.start } } : arg;
|
||||
return [{ kind: "varDecl", localId: rename(p.localId), init, loc }];
|
||||
});
|
||||
const labels = new Map<string, string>();
|
||||
const exit = this.fresh("%fused.exit");
|
||||
const label = (name: string): string => {
|
||||
let id = labels.get(name);
|
||||
if (!id) {
|
||||
@@ -1092,19 +1332,53 @@ class FusedLoop {
|
||||
}
|
||||
return id;
|
||||
};
|
||||
const result = fn.returnType.kind === "void" ? null : this.local("ret", fn.returnType, true);
|
||||
const rename = (id: string): string => renamed.get(id) ?? id;
|
||||
const body = transformStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
return { ...e, localId: rename(e.localId) };
|
||||
default:
|
||||
return e;
|
||||
/** Locals and labels of a copied statement; returns are the caller's. */
|
||||
const renameStmt = (st: IrStmt): IrStmt => {
|
||||
switch (st.kind) {
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
return { ...st, localId: rename(st.localId) };
|
||||
case "break":
|
||||
case "continue":
|
||||
return st.label ? { ...st, label: label(st.label) } : st;
|
||||
case "forOf": {
|
||||
const renamedFor = { ...st, localId: rename(st.localId) };
|
||||
return st.labels ? { ...renamedFor, labels: st.labels.map(label) } : renamedFor;
|
||||
}
|
||||
},
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "switch":
|
||||
case "block":
|
||||
return st.labels ? { ...st, labels: st.labels.map(label) } : st;
|
||||
default:
|
||||
return st;
|
||||
}
|
||||
};
|
||||
// A body whose only return is its last statement (`(x) => x * k`,
|
||||
// `{ const y = x * 2; return y > 4; }`) needs no exit block: its other
|
||||
// statements run after the bindings, and the call's value is the
|
||||
// returned expression.
|
||||
const last = fn.body[fn.body.length - 1];
|
||||
const prefix = fn.body.slice(0, -1);
|
||||
if (
|
||||
last !== undefined &&
|
||||
last.kind === "return" &&
|
||||
last.value !== null &&
|
||||
typeEquals(last.value.type, fn.returnType) &&
|
||||
!returns(last.value) &&
|
||||
prefix.every((st) => everyStmt(st, { expr: () => true, stmt: (x) => x.kind !== "return" }))
|
||||
) {
|
||||
const copy = { expr: renameExpr, stmt: renameStmt };
|
||||
return {
|
||||
pre: [...bind, ...transformStmtList(prefix, copy)],
|
||||
value: transformExpr(last.value, copy),
|
||||
};
|
||||
}
|
||||
const exit = this.fresh("%fused.exit");
|
||||
const result = fn.returnType.kind === "void" ? null : this.local("ret", fn.returnType, true);
|
||||
const body = transformStmtList(fn.body, {
|
||||
expr: renameExpr,
|
||||
stmt: (s): IrStmt => {
|
||||
switch (s.kind) {
|
||||
case "return": {
|
||||
@@ -1124,27 +1398,14 @@ class FusedLoop {
|
||||
loc: s.loc,
|
||||
};
|
||||
}
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
return { ...s, localId: rename(s.localId) };
|
||||
case "break":
|
||||
case "continue":
|
||||
return s.label && s.label !== exit ? { ...s, label: label(s.label) } : s;
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "block":
|
||||
return s.labels ? { ...s, labels: s.labels.map(label) } : s;
|
||||
default:
|
||||
return s;
|
||||
return renameStmt(s);
|
||||
}
|
||||
},
|
||||
});
|
||||
const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] =>
|
||||
aliased.has(p.localId)
|
||||
? []
|
||||
: [{ kind: "varDecl", localId: rename(p.localId), init: args[index]!, loc }],
|
||||
);
|
||||
const pre: IrStmt[] = [];
|
||||
if (result !== null) pre.push({ kind: "varDecl", localId: result, init: null, loc });
|
||||
pre.push({ kind: "block", labels: [exit], body: [...bind, ...body], loc });
|
||||
@@ -1157,7 +1418,13 @@ class FusedLoop {
|
||||
};
|
||||
}
|
||||
|
||||
build(): { fn: Omit<IrFunction, "name">; args: IrExpr[]; key: string } | null {
|
||||
build(): {
|
||||
fn: Omit<IrFunction, "name">;
|
||||
args: IrExpr[];
|
||||
key: string;
|
||||
/** Bindings the callbacks copied into the loop captured at this site. */
|
||||
inlinedCaptures: string[];
|
||||
} | null {
|
||||
const { chain, loc } = this;
|
||||
const arrT = chain.sourceType;
|
||||
const a = this.param("a", chain.source, "src");
|
||||
@@ -1642,7 +1909,10 @@ class FusedLoop {
|
||||
};
|
||||
foldKnownUnions(fn);
|
||||
scalarizeRecordTemporaries(this.lowerer, fn);
|
||||
return { fn, args: this.args, key: this.keyParts.join("|") };
|
||||
const inlinedCaptures = this.callees.flatMap((c) =>
|
||||
c.inline && c.arg.kind === "closure" ? c.arg.captures : [],
|
||||
);
|
||||
return { fn, args: this.args, key: this.keyParts.join("|"), inlinedCaptures };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1741,8 +2011,11 @@ function foldKnownUnions(fn: Omit<IrFunction, "name">): void {
|
||||
case "for":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "forOf":
|
||||
case "block":
|
||||
return { ...s, body: prune(s.body) };
|
||||
case "switch":
|
||||
return { ...s, cases: s.cases.map((c) => ({ ...c, body: prune(c.body) })) };
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
@@ -1930,6 +2203,19 @@ function scalarizeRecordTemporaries(lowerer: Lowerer, fn: Omit<IrFunction, "name
|
||||
body: rewriteList(s.body),
|
||||
},
|
||||
];
|
||||
case "forOf":
|
||||
return [{ ...s, iterable: rewriteExpr(s.iterable), body: rewriteList(s.body) }];
|
||||
case "switch":
|
||||
return [
|
||||
{
|
||||
...s,
|
||||
disc: rewriteExpr(s.disc),
|
||||
cases: s.cases.map((c) => ({
|
||||
test: c.test === null ? null : rewriteExpr(c.test),
|
||||
body: rewriteList(c.body),
|
||||
})),
|
||||
},
|
||||
];
|
||||
case "block":
|
||||
return [{ ...s, body: rewriteList(s.body) }];
|
||||
default: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { keyedHelperName } from "../stable-names.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { primitiveElement } from "../array-values.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import {
|
||||
@@ -144,12 +145,22 @@ export function lowerArrayCallback(
|
||||
const untyped = (checkerType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
|
||||
const raw = untyped ? null : lowerer.mapTypeOf(checkerType);
|
||||
const mapped = raw ? lowerer.runtimeOptionalBindingType(param.name, raw) : null;
|
||||
// A helper that passes present number, string or boolean elements
|
||||
// (a `(T | undefined)[]` stores T elements) binds a `T | undefined`
|
||||
// parameter to T, so the callback takes them without an adapter.
|
||||
const presentElement =
|
||||
mapped !== null &&
|
||||
primitiveElement(expected) &&
|
||||
mapped.kind === "union" &&
|
||||
lowerer.armTag(mapped.unionId, UNDEFINED_T) >= 0 &&
|
||||
typeEquals(lowerer.stripUndefinedArm(mapped), expected);
|
||||
if (
|
||||
untyped
|
||||
? !bindUntyped
|
||||
: mapped === null ||
|
||||
(lowerer.coercibleValue(expected, mapped) &&
|
||||
!runtimeOptionalPairVariant(lowerer, expected, mapped))
|
||||
!runtimeOptionalPairVariant(lowerer, expected, mapped) &&
|
||||
!presentElement)
|
||||
)
|
||||
return;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { primitiveElement } from "../array-values.js";
|
||||
import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
@@ -1137,6 +1138,17 @@ function setFromSeedValue(
|
||||
seed = strCharsCall(lowerer, source, loc);
|
||||
} else if (source.type.kind === "array" && typeEquals(source.type.elem, setT.elem)) {
|
||||
seed = source;
|
||||
} else if (
|
||||
source.type.kind === "array" &&
|
||||
primitiveElement(source.type.elem) &&
|
||||
setT.elem.kind === "union" &&
|
||||
lowerer.armTag(setT.elem.unionId, UNDEFINED_T) >= 0 &&
|
||||
typeEquals(lowerer.stripUndefinedArm(setT.elem), source.type.elem)
|
||||
) {
|
||||
// A `(T | undefined)[]` stores T elements: its present undefined
|
||||
// elements copy as the Set's undefined.
|
||||
seed = lowerer.coerceToExpected(source, arrayOf(setT.elem));
|
||||
if (!typeEquals(seed.type, arrayOf(setT.elem))) return null;
|
||||
} else if (source.type.kind === "record") {
|
||||
const shape = lowerer.shapes.get(source.type.shapeId);
|
||||
if (!shape?.tuple || !shape.fields.every((field) => typeEquals(field.type, setT.elem)))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js";
|
||||
import { countedFor, numLit, varRef } from "../../../ir/build.js";
|
||||
import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js";
|
||||
import { objectEnumerationReceiver } from "../object-enumeration-receiver.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
@@ -156,7 +157,14 @@ export function objectIterOverIndexShape(
|
||||
// also holds numbers): each overflow value checks into the element type,
|
||||
// so a value of a dropped arm throws instead of mistyping.
|
||||
let ivNarrows = false;
|
||||
if (valueT) {
|
||||
// `T | undefined` values into a `(T | undefined)[]`, which stores T
|
||||
// elements (primitiveElement): each value is stored with its present
|
||||
// undefined.
|
||||
const optionalValues =
|
||||
member === "values" &&
|
||||
valueT !== null &&
|
||||
optionalElementValue(lowerer, { kind: "unitLit", unit: "undefined", type: iv, loc }, valueT);
|
||||
if (valueT && !optionalValues) {
|
||||
const ivSurfaces =
|
||||
typeEquals(iv, valueT) ||
|
||||
(valueT.kind === "union" && lowerer.armTag(valueT.unionId, iv) >= 0) ||
|
||||
@@ -350,7 +358,25 @@ export function objectIterOverIndexShape(
|
||||
loc,
|
||||
},
|
||||
then: [fieldStmt],
|
||||
else_: undefinedElement ? [push(pushedOf(undefinedElement))] : null,
|
||||
else_: undefinedElement
|
||||
? [push(pushedOf(undefinedElement))]
|
||||
: member === "values" && valueT && primitiveElement(valueT)
|
||||
? [
|
||||
{
|
||||
kind: "arraySetUndefined",
|
||||
arr: outRef,
|
||||
index: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: outRef,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: null,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -374,35 +400,45 @@ export function objectIterOverIndexShape(
|
||||
const kRef = varRef("k.0", STRING, loc);
|
||||
const readValue: IrExpr | null = !valueT
|
||||
? null
|
||||
: ivNarrows
|
||||
: optionalValues
|
||||
? {
|
||||
kind: "dynCheck",
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "recordKeyGet",
|
||||
obj: rRef,
|
||||
shapeId: argIr.shapeId,
|
||||
key: kRef,
|
||||
overflowOnly: true,
|
||||
type: iv,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: valueT,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "recordKeyGet",
|
||||
obj: rRef,
|
||||
shapeId: argIr.shapeId,
|
||||
key: kRef,
|
||||
overflowOnly: true,
|
||||
type: valueT,
|
||||
type: iv,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
: ivNarrows
|
||||
? {
|
||||
kind: "dynCheck",
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "recordKeyGet",
|
||||
obj: rRef,
|
||||
shapeId: argIr.shapeId,
|
||||
key: kRef,
|
||||
overflowOnly: true,
|
||||
type: iv,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: valueT,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "recordKeyGet",
|
||||
obj: rRef,
|
||||
shapeId: argIr.shapeId,
|
||||
key: kRef,
|
||||
overflowOnly: true,
|
||||
type: valueT,
|
||||
loc,
|
||||
};
|
||||
const loopPushed: IrExpr =
|
||||
member === "keys"
|
||||
? kRef
|
||||
@@ -440,7 +476,26 @@ export function objectIterOverIndexShape(
|
||||
},
|
||||
loc,
|
||||
},
|
||||
push(loopPushed),
|
||||
...(optionalValues
|
||||
? [
|
||||
{ kind: "varDecl" as const, localId: "v.0", init: readValue!, loc },
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
outRef,
|
||||
{
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: outRef,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
varRef("v.0", iv, loc),
|
||||
valueT!,
|
||||
loc,
|
||||
),
|
||||
]
|
||||
: [push(loopPushed)]),
|
||||
],
|
||||
),
|
||||
{ kind: "return", value: outRef, loc },
|
||||
@@ -457,6 +512,7 @@ export function objectIterOverIndexShape(
|
||||
{ id: "ks.0", name: "ks", type: ksT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
{ id: "k.0", name: "k", type: STRING, mutable: false },
|
||||
...(optionalValues ? [{ id: "v.0", name: "v", type: iv, mutable: false }] : []),
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
|
||||
@@ -31,7 +31,7 @@ import { strCharsCall } from "../containers/array-construction.js";
|
||||
import { generatorDrain } from "../iterator-adapters.js";
|
||||
import { isSearchParamsIterable } from "../builtins/url.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import { arrayValueStore } from "../array-values.js";
|
||||
import { arrayValueStore, primitiveElement } from "../array-values.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
import { iteratorCanStep } from "../iterator-consumption.js";
|
||||
|
||||
@@ -103,6 +103,13 @@ function emptyNeverLiteral(lowerer: Lowerer, expr: ts.ArrayLiteralExpression): b
|
||||
* tsc's API doesn't surface it (ternary arms under an array context —
|
||||
* tsc accepts the arm covariantly, but a tagged element representation
|
||||
* must be BUILT as the slot's element type). */
|
||||
/** The checker type of `node` admits undefined. */
|
||||
function mayHoldUndefined(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const type = lowerer.typeOf(node);
|
||||
const parts = type.isUnionType() ? ts.constituentTypes(type) : [type];
|
||||
return parts.some((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0);
|
||||
}
|
||||
|
||||
export function lowerArrayLiteral(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.ArrayLiteralExpression,
|
||||
@@ -524,7 +531,13 @@ export function lowerArrayLiteral(
|
||||
// `[...it]` over a generator (or an iterator adapted into one):
|
||||
// drain it once, in order, into a fresh element array.
|
||||
if (src.type.kind === "generator") {
|
||||
const drained = generatorDrain(lowerer, src, type.elem, locOf(el));
|
||||
const drained = generatorDrain(
|
||||
lowerer,
|
||||
src,
|
||||
type.elem,
|
||||
locOf(el),
|
||||
type.optionalElements === true,
|
||||
);
|
||||
if (drained) src = drained;
|
||||
}
|
||||
// `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its
|
||||
@@ -615,6 +628,9 @@ export function lowerArrayLiteral(
|
||||
let lowered: IrExpr;
|
||||
const directOptionalCandidate =
|
||||
runtimeOptionalElements.has(i) ||
|
||||
// An element that can be undefined in a `(T | undefined)[]`
|
||||
// (primitiveElement) is stored as a present undefined.
|
||||
(primitiveElement(type.elem) && mayHoldUndefined(lowerer, el)) ||
|
||||
ts.isElementAccessExpression(el) ||
|
||||
(ts.isCallExpression(el) &&
|
||||
ts.isPropertyAccessExpression(el.expression) &&
|
||||
@@ -692,7 +708,8 @@ export function lowerArrayLiteral(
|
||||
continue;
|
||||
}
|
||||
let value = el;
|
||||
if (!typeEquals(value.type, type.elem)) {
|
||||
// An undefined element is stored directly (arrayValueStore).
|
||||
if (!typeEquals(value.type, type.elem) && value.type.kind !== "undefinedT") {
|
||||
const temp = lowerer.declareHiddenLocal("%arrayValue", value.type);
|
||||
body.push({ kind: "varDecl", localId: temp.id, init: value, loc });
|
||||
value = varRef(temp.id, temp.type, loc);
|
||||
|
||||
@@ -61,6 +61,8 @@ import {
|
||||
arrayValueType,
|
||||
lowerSafeBytesRead,
|
||||
lowerSafeIndexRead,
|
||||
primitiveElement,
|
||||
unionArrayValueRead,
|
||||
} from "../array-values.js";
|
||||
import { lowerOptionalStringIndex, lowerUnionStringIndex } from "../string-index.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
@@ -594,6 +596,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
}
|
||||
}
|
||||
if (key.type.kind === "f64") {
|
||||
const optional = optionalArmIndexRead(lowerer, value, key, locOf(expr));
|
||||
if (optional) return lowerer.maybeNarrow(optional, expr);
|
||||
const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key);
|
||||
if (safe) return safe;
|
||||
}
|
||||
@@ -884,9 +888,14 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
// must be the element type (library ABI checks and the catchable
|
||||
// RangeError contract for an out-of-bounds assert). Where undefined is
|
||||
// representable, the assertion keeps JavaScript's unchecked read.
|
||||
// A declared `(T | undefined)[]` element (stored as T) keeps its
|
||||
// optional read: the assertion then narrows it with a catchable
|
||||
// TypeError, as for a union element.
|
||||
const declaredOptional = arr.type.optionalElements === true;
|
||||
const provenPresent =
|
||||
ts.isNonNullExpression(expr.parent) &&
|
||||
expr.parent.expression === expr &&
|
||||
!declaredOptional &&
|
||||
!assertedElementKeepsUndefined(lowerer, expr.parent);
|
||||
if (provenPresent) {
|
||||
return lowerer.maybeNarrow(
|
||||
@@ -2437,3 +2446,46 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx
|
||||
return { kind: "libCall", fn: "dyn.toStringCoerce", args: [key], type: STRING, loc: key.loc };
|
||||
return lowerRecordPropertyKey(lowerer, key, node);
|
||||
}
|
||||
|
||||
/** A number-keyed read of a union of arrays with a `(T | undefined)[]` arm
|
||||
* (stored as T elements, primitiveElement) whose element facts admit
|
||||
* missing values: each arm reads its element by its state
|
||||
* (unionArrayValueRead), where the keyed read would trap on a present
|
||||
* undefined. The receiver and key evaluate once. Null for other unions. */
|
||||
function optionalArmIndexRead(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
key: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "union") return null;
|
||||
const arms = lowerer.unions.get(value.type.unionId)?.arms ?? [];
|
||||
if (
|
||||
!arms.some(
|
||||
(arm) =>
|
||||
arm.kind === "array" &&
|
||||
primitiveElement(arm.elem) &&
|
||||
arrayValueType(lowerer, arm.elem).kind === "union",
|
||||
)
|
||||
)
|
||||
return null;
|
||||
if (unionArrayValueRead(lowerer, value, key, loc) === null) return null;
|
||||
const stmts: IrStmt[] = [];
|
||||
const stable = (e: IrExpr, name: string, keep: boolean): IrExpr => {
|
||||
if (keep) return e;
|
||||
const local = lowerer.declareHiddenLocal(name, e.type);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: e, loc });
|
||||
return varRef(local.id, e.type, loc);
|
||||
};
|
||||
// A receiver local stays a direct read only when the key cannot assign
|
||||
// it (`a[(a = other, 0)]` reads the array `a` held before the key).
|
||||
const plainKey = key.kind === "varRef" || key.kind === "numLit";
|
||||
const receiver = stable(
|
||||
value,
|
||||
"%indexReceiver",
|
||||
value.kind === "numLit" || (value.kind === "varRef" && plainKey),
|
||||
);
|
||||
const index = stable(key, "%indexKey", plainKey);
|
||||
const read = unionArrayValueRead(lowerer, receiver, index, loc)!;
|
||||
return stmts.length === 0 ? read : { kind: "seqExpr", stmts, result: read, type: read.type, loc };
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { appendElement, optionalElementValue, primitiveElement } from "./array-values.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
|
||||
import {
|
||||
@@ -964,16 +965,27 @@ function generatorRetypeAdapter(
|
||||
return { kind: "call", callee: name, args: [expr], type: expected, loc };
|
||||
}
|
||||
|
||||
/** A checked-dynamic yielded value for a `(T | undefined)[]` of a
|
||||
* number, string or boolean T: converted to `T | undefined`, which the
|
||||
* drain stores as a T element or a present undefined. */
|
||||
function optionalYield(lowerer: Lowerer, value: IrExpr, elem: IrType, optional: boolean): IrExpr {
|
||||
if (!optional || value.type.kind !== "dyn" || !primitiveElement(elem)) return value;
|
||||
return lowerer.coerceToExpected(value, lowerer.withUndefinedArm(elem));
|
||||
}
|
||||
|
||||
/** Drain a synchronous generator into a fresh `elem[]` — the eager
|
||||
* consumers of an iterator (`[...it]`, `Array.from(it)`). Each step resumes
|
||||
* the generator once and appends its value; completion stops the drain
|
||||
* without appending the return value, as in JS. Null when the yielded
|
||||
* values cannot be extracted into the element type. */
|
||||
* values cannot be extracted into the element type. `optional`: the
|
||||
* result is a `(T | undefined)[]`, which stores T elements, so a
|
||||
* checked-dynamic value may also be undefined. */
|
||||
export function generatorDrain(
|
||||
lowerer: Lowerer,
|
||||
source: IrExpr,
|
||||
elem: IrType,
|
||||
loc: SrcLoc,
|
||||
optional = false,
|
||||
): IrExpr | null {
|
||||
const gen = source.type;
|
||||
if (gen.kind !== "generator" || gen.async || gen.yieldT.kind === "void") return null;
|
||||
@@ -982,14 +994,16 @@ export function generatorDrain(
|
||||
const valueT = shape?.fields.find((field) => field.name === "value")?.type;
|
||||
if (!resultType || !valueT) return null;
|
||||
const arrT: IrType = { kind: "array", elem };
|
||||
const key = `genDrain:${typeKey(gen)}:${typeKey(elem)}`;
|
||||
const key = `genDrain:${typeKey(gen)}:${typeKey(elem)}${optional ? "?" : ""}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name && stepDrivable(gen)) {
|
||||
// The record-free drive: test done, then take the yielded value.
|
||||
const genRef = varRef("gen.0", gen, loc) as IrExpr & { type: typeof gen };
|
||||
const outRef = varRef("out.0", arrT, loc);
|
||||
const value = lowerer.coerceToExpected(genValue(genRef, loc), elem);
|
||||
if (!typeEquals(value.type, elem)) return null;
|
||||
const yielded = optionalYield(lowerer, genValue(genRef, loc), elem, optional);
|
||||
const present = optionalElementValue(lowerer, yielded, elem);
|
||||
const value = present ? yielded : lowerer.coerceToExpected(yielded, elem);
|
||||
if (!present && !typeEquals(value.type, elem)) return null;
|
||||
name = keyedHelperName(lowerer, "%gen.drain", key);
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
@@ -999,6 +1013,7 @@ export function generatorDrain(
|
||||
locals: [
|
||||
{ id: "gen.0", name: "gen", type: gen, mutable: false },
|
||||
{ id: "out.0", name: "out", type: arrT, mutable: false },
|
||||
...(present ? [{ id: "v.0", name: "v", type: value.type, mutable: false }] : []),
|
||||
],
|
||||
body: [
|
||||
{
|
||||
@@ -1010,20 +1025,7 @@ export function generatorDrain(
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "unary", op: "!", operand: genStep(genRef, loc), type: BOOL, loc },
|
||||
body: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: outRef,
|
||||
args: [value],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
body: appendElement(lowerer, outRef, value, elem, "v.0", loc),
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: outRef, loc },
|
||||
@@ -1050,8 +1052,10 @@ export function generatorDrain(
|
||||
loc,
|
||||
);
|
||||
if (!raw) return null;
|
||||
const value = lowerer.coerceToExpected(raw, elem);
|
||||
if (!typeEquals(value.type, elem)) return null;
|
||||
const yielded = optionalYield(lowerer, raw, elem, optional);
|
||||
const present = optionalElementValue(lowerer, yielded, elem);
|
||||
const value = present ? yielded : lowerer.coerceToExpected(yielded, elem);
|
||||
if (!present && !typeEquals(value.type, elem)) return null;
|
||||
name = keyedHelperName(lowerer, "%gen.drain", key);
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
@@ -1062,6 +1066,7 @@ export function generatorDrain(
|
||||
{ id: "gen.0", name: "gen", type: gen, mutable: false },
|
||||
{ id: "out.0", name: "out", type: arrT, mutable: false },
|
||||
{ id: "step.0", name: "step", type: resultType, mutable: false },
|
||||
...(present ? [{ id: "v.0", name: "v", type: value.type, mutable: false }] : []),
|
||||
],
|
||||
body: [
|
||||
{
|
||||
@@ -1101,18 +1106,7 @@ export function generatorDrain(
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: outRef,
|
||||
args: [value],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
...appendElement(lowerer, outRef, value, elem, "v.0", loc),
|
||||
],
|
||||
loc,
|
||||
},
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { LIB_FN_SIGS } from "../../ir/builtin-signatures.js";
|
||||
import { libFnSignatures } from "../../ir/builtin-signatures.js";
|
||||
import { REGEX_INTRINSIC_SIGS, STR_INTRINSIC_SIGS } from "../../ir/intrinsic-signatures.js";
|
||||
import { unionMismatchDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
|
||||
@@ -162,7 +162,7 @@ function enforceExprBoundary(lowerer: Lowerer, node: IrExpr): void {
|
||||
const kind = node.kind;
|
||||
if (kind === "libCall") {
|
||||
const e = node;
|
||||
const sig = LIB_FN_SIGS[e.fn];
|
||||
const sig = libFnSignatures()[e.fn];
|
||||
if (!sig) return;
|
||||
e.args.forEach((a, i) => {
|
||||
const want = sig.argTypes[i];
|
||||
|
||||
@@ -16,7 +16,7 @@ import { strLit, varRef } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
BUILTIN_MODULE_CONSTS,
|
||||
BUILTIN_MODULE_FNS,
|
||||
builtinModuleFns,
|
||||
OBJECT_CALLABLE_VALUES,
|
||||
builtinConstLit,
|
||||
builtinModuleConstOf,
|
||||
@@ -430,7 +430,7 @@ export function pathModuleValue(lowerer: Lowerer, module: string, loc: SrcLoc):
|
||||
const add = (member: string, value: IrExpr): void => {
|
||||
fields.push({ key: strLit(member, loc), value: box(value) });
|
||||
};
|
||||
for (const member of Object.keys(BUILTIN_MODULE_FNS[module] ?? {})) {
|
||||
for (const member of Object.keys(builtinModuleFns()[module] ?? {})) {
|
||||
const value = lowerer.lowerBuiltinCallableValue({ module, member }, loc);
|
||||
if (value) add(member, value);
|
||||
}
|
||||
@@ -512,7 +512,7 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr {
|
||||
loc,
|
||||
});
|
||||
};
|
||||
for (const member of Object.keys(BUILTIN_MODULE_FNS[module] ?? {})) {
|
||||
for (const member of Object.keys(builtinModuleFns()[module] ?? {})) {
|
||||
const callable = lowerer.lowerBuiltinCallableValue({ module, member }, loc);
|
||||
if (callable) add(member, callable);
|
||||
}
|
||||
|
||||
@@ -1117,7 +1117,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
if (!argNode || (ts.isIdentifier(argNode) && argNode.text === "undefined")) {
|
||||
return { kind: "libCall", fn: "sym.newAnon", args: [], type: SYMBOL_T, loc };
|
||||
}
|
||||
const desc = lowerer.lowerExpr(argNode);
|
||||
let desc = lowerer.lowerExpr(argNode);
|
||||
// A number or boolean description is its ToString.
|
||||
if (desc.type.kind === "f64" || desc.type.kind === "bool")
|
||||
desc = lowerer.ensureString(desc, argNode);
|
||||
if (desc.type.kind !== "string") {
|
||||
lowerer.noLowering(
|
||||
`Symbol with a '${lowerer.fmt(desc.type)}' description`,
|
||||
|
||||
@@ -35,14 +35,7 @@ import { esmNamedImportLinkCrash, isCjsJsFile, locOf, npmPackageNameOf } from ".
|
||||
import { foldedStringKeyOf, lowerDynObjectLiteral } from "./expressions/object-literals.js";
|
||||
import { noteEventListeners } from "./lower-event-target.js";
|
||||
import { PoisonError, newFnCtx, own } from "./lowerer.js";
|
||||
import {
|
||||
NODE24_FETCH_COMPAT_PROFILE,
|
||||
STATIC_HEADERS_CALLS,
|
||||
STATIC_READABLE_STREAM_CALLS,
|
||||
STATIC_READABLE_STREAM_READS,
|
||||
STATIC_RESPONSE_CALLS,
|
||||
STATIC_RESPONSE_READS,
|
||||
} from "../../compat/fetch-profile.js";
|
||||
import { node24FetchCompatProfile, staticFetchMembers } from "../../compat/fetch-profile.js";
|
||||
import { classGeneratorIterable } from "./class-iterables.js";
|
||||
import { generatorDrain } from "./iterator-adapters.js";
|
||||
|
||||
@@ -434,7 +427,7 @@ function fenceRequestInitObject(
|
||||
: contextual
|
||||
? [contextual]
|
||||
: [];
|
||||
const rows = NODE24_FETCH_COMPAT_PROFILE.inventory.entries.filter(
|
||||
const rows = node24FetchCompatProfile().inventory.entries.filter(
|
||||
(entry) => entry.owner === "RequestInit" && entry.placement === "dictionary",
|
||||
);
|
||||
const shadowed = new Set<string>();
|
||||
@@ -837,7 +830,7 @@ function fetchInventoryStatus(
|
||||
placement: "static" | "prototype" | "prototype-symbol",
|
||||
): "static" | "unsupported" | "out-of-scope" | null {
|
||||
return (
|
||||
NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find(
|
||||
node24FetchCompatProfile().inventory.entries.find(
|
||||
(entry) => entry.owner === owner && entry.member === member && entry.placement === placement,
|
||||
)?.status ?? null
|
||||
);
|
||||
@@ -848,7 +841,7 @@ function fetchInventoryReason(
|
||||
member: string,
|
||||
placement: "static" | "prototype" | "prototype-symbol",
|
||||
): string | undefined {
|
||||
return NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find(
|
||||
return node24FetchCompatProfile().inventory.entries.find(
|
||||
(entry) => entry.owner === owner && entry.member === member && entry.placement === placement,
|
||||
)?.reason;
|
||||
}
|
||||
@@ -951,8 +944,9 @@ export function fenceStaticResponseMember(
|
||||
member.startsWith("[Symbol.") ? "prototype-symbol" : "prototype",
|
||||
);
|
||||
|
||||
const projected = staticFetchMembers();
|
||||
const supported = (member: string) =>
|
||||
use === "call" ? STATIC_RESPONSE_CALLS.has(member) : STATIC_RESPONSE_READS.has(member);
|
||||
use === "call" ? projected.responseCalls.has(member) : projected.responseReads.has(member);
|
||||
if (members.every(supported)) return null;
|
||||
const member = members.find((candidate) => !supported(candidate))!;
|
||||
if (status(member) === "unsupported") {
|
||||
@@ -993,12 +987,9 @@ export function fenceStaticHeadersMember(
|
||||
member.startsWith("[Symbol.") ? "prototype-symbol" : "prototype",
|
||||
);
|
||||
|
||||
if (use === "call" && members.every((member) => STATIC_HEADERS_CALLS.has(member))) {
|
||||
return null;
|
||||
}
|
||||
const member = members.find(
|
||||
(candidate) => use !== "call" || !STATIC_HEADERS_CALLS.has(candidate),
|
||||
)!;
|
||||
const headersCalls = staticFetchMembers().headersCalls;
|
||||
if (use === "call" && members.every((member) => headersCalls.has(member))) return null;
|
||||
const member = members.find((candidate) => use !== "call" || !headersCalls.has(candidate))!;
|
||||
if (status(member) === "unsupported") {
|
||||
lowerer.noLowering(
|
||||
`Headers.${member}`,
|
||||
@@ -1108,7 +1099,7 @@ function fetchInterfaceInventoryOwner(
|
||||
const symbol = [type.getSymbol(), type.getAliasSymbol()].find(
|
||||
(candidate) =>
|
||||
candidate !== undefined &&
|
||||
NODE24_FETCH_COMPAT_PROFILE.inventory.interfaces.includes(ts.symbolName(candidate)) &&
|
||||
node24FetchCompatProfile().inventory.interfaces.includes(ts.symbolName(candidate)) &&
|
||||
lowerer.isStdlibSymbol(candidate),
|
||||
);
|
||||
return symbol ? { owner: ts.symbolName(symbol), symbol } : null;
|
||||
@@ -1123,7 +1114,7 @@ function fenceFetchObjectMembers(
|
||||
if (!resolved) return;
|
||||
const { owner, symbol } = resolved;
|
||||
for (const { member, node } of members) {
|
||||
const row = NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find(
|
||||
const row = node24FetchCompatProfile().inventory.entries.find(
|
||||
(entry) =>
|
||||
entry.owner === owner &&
|
||||
entry.member === member &&
|
||||
@@ -1136,7 +1127,7 @@ function fenceFetchObjectMembers(
|
||||
// Static data properties retain the ordinary checked-dynamic keyed-read
|
||||
// lowering. Only a method extraction loses the receiver that its direct
|
||||
// call bridge would preserve; non-static rows keep their inventory fence.
|
||||
const operation = NODE24_FETCH_COMPAT_PROFILE.operations.find(
|
||||
const operation = node24FetchCompatProfile().operations.find(
|
||||
(candidate) => candidate.id === row.id,
|
||||
);
|
||||
if (row.status === "static" && operation?.kind === "property") {
|
||||
@@ -1235,7 +1226,7 @@ export function lowerFetchElementMethodCall(
|
||||
return null;
|
||||
}
|
||||
const rows = members.map((member) =>
|
||||
NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find(
|
||||
node24FetchCompatProfile().inventory.entries.find(
|
||||
(entry) =>
|
||||
entry.owner === resolved.owner &&
|
||||
entry.member === member &&
|
||||
@@ -1397,10 +1388,11 @@ export function fenceStaticReadableStreamMember(
|
||||
member.startsWith("[Symbol.") ? "prototype-symbol" : "prototype",
|
||||
);
|
||||
|
||||
const projected = staticFetchMembers();
|
||||
const supported = (member: string) =>
|
||||
use === "call"
|
||||
? STATIC_READABLE_STREAM_CALLS.has(member)
|
||||
: STATIC_READABLE_STREAM_READS.has(member);
|
||||
? projected.readableStreamCalls.has(member)
|
||||
: projected.readableStreamReads.has(member);
|
||||
if (members.every(supported)) return null;
|
||||
const member = members.find((candidate) => !supported(candidate))!;
|
||||
if (status(member) === "unsupported") {
|
||||
|
||||
@@ -6585,6 +6585,17 @@ export class Lowerer {
|
||||
mayBeUndefined: (node) => {
|
||||
if (explicitlyNonNull(node)) return assertedReadMayBeUndefined(node);
|
||||
if (mayBeOptional(node)) return true;
|
||||
// A value the checker types as possibly undefined: `(T | undefined)[]`
|
||||
// stores T elements (the type mapper), so its undefined values are
|
||||
// facts of T's arrays.
|
||||
const checked = typeOrNull(node);
|
||||
if (
|
||||
checked !== null &&
|
||||
(checked.isUnionType() ? ts.constituentTypes(checked) : [checked]).some(
|
||||
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
|
||||
)
|
||||
)
|
||||
return true;
|
||||
// `undefined as unknown as T` keeps the undefined value.
|
||||
for (
|
||||
let inner: ts.Expression = node;
|
||||
@@ -6603,7 +6614,49 @@ export class Lowerer {
|
||||
const symbol = symbolOf(e);
|
||||
return symbol !== null && iterationSymbols.has(symbol) && !provenPresent(e, symbol);
|
||||
},
|
||||
callbackReturnsUndefined: (node) => callbackReturnsOptional(node),
|
||||
iterationMayYieldUndefined: (node) => {
|
||||
const type = typeOrNull(node);
|
||||
if (type === null || this.checker.isArrayType(type)) return false;
|
||||
// A tuple yields its elements, in order.
|
||||
if (this.checker.isTupleType(type))
|
||||
return panicSafe(
|
||||
() =>
|
||||
this.checker
|
||||
.getTypeArguments(type as ts.TypeReference)
|
||||
.some((element) =>
|
||||
(element.isUnionType() ? ts.constituentTypes(element) : [element]).some(
|
||||
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
|
||||
),
|
||||
),
|
||||
true,
|
||||
);
|
||||
// The first type argument of Set<T>, Map and Set iterators,
|
||||
// generators and iterables is the value they yield.
|
||||
const element =
|
||||
(type.flags & ts.TypeFlags.Object) !== 0
|
||||
? panicSafe(() => this.checker.getTypeArguments(type as ts.TypeReference)[0], undefined)
|
||||
: undefined;
|
||||
if (element === undefined) return false;
|
||||
return (element.isUnionType() ? ts.constituentTypes(element) : [element]).some(
|
||||
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
|
||||
);
|
||||
},
|
||||
callbackReturnsUndefined: (node) => {
|
||||
if (callbackReturnsOptional(node)) return true;
|
||||
// A callback the checker types as possibly returning undefined
|
||||
// (`(m) => map.get(m)`): its results are present undefined
|
||||
// elements of a `(T | undefined)[]`, which stores T elements.
|
||||
const type = typeOrNull(node);
|
||||
return (
|
||||
type !== null &&
|
||||
this.checker.getCallSignatures(type).some((signature) => {
|
||||
const result = this.checker.getReturnTypeOfSignature(signature);
|
||||
return (result.isUnionType() ? ts.constituentTypes(result) : [result]).some(
|
||||
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
isGlobal: (node, name) => this.isStdlibGlobal(node, name),
|
||||
isString: (node) => {
|
||||
const type = typeOrNull(node);
|
||||
@@ -6630,6 +6683,14 @@ export class Lowerer {
|
||||
const rest = sig.params.find((shape) => shape.mode === "rest");
|
||||
return rest?.type.kind === "array" ? [typeKey(rest.type.elem)] : [];
|
||||
},
|
||||
restStart: (call) => {
|
||||
const callee = peel(call.expression);
|
||||
if (!ts.isIdentifier(callee) && !ts.isPropertyAccessExpression(callee)) return null;
|
||||
const symbol = callableSymbolOf(callee);
|
||||
const sig = symbol ? signatureBySymbol.get(symbol) : undefined;
|
||||
const at = sig ? sig.params.findIndex((shape) => shape.mode === "rest") : -1;
|
||||
return at >= 0 ? at : null;
|
||||
},
|
||||
markIterationBinding: (node, known) => {
|
||||
const symbol = symbolOf(node);
|
||||
if (!symbol) return false;
|
||||
@@ -6664,6 +6725,7 @@ export class Lowerer {
|
||||
const cached = scanSites.get(sf);
|
||||
if (cached !== undefined) return cached;
|
||||
const nodes: ts.Node[] = [];
|
||||
const values: ts.Node[] = [];
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
ts.isVariableDeclaration(node) ||
|
||||
@@ -6679,7 +6741,21 @@ export class Lowerer {
|
||||
ts.isPostfixUnaryExpression(node)
|
||||
)
|
||||
nodes.push(node);
|
||||
// The values these sites store, whose checker types mayBeUndefined
|
||||
// reads: batched here, since dead bodies are scanned too.
|
||||
if (ts.isArrayLiteralExpression(node))
|
||||
for (const element of node.elements.nodes)
|
||||
values.push(ts.isSpreadElement(element) ? element.expression : element);
|
||||
else if (ts.isCallExpression(node) || ts.isNewExpression(node))
|
||||
for (const arg of node.arguments?.nodes ?? [])
|
||||
values.push(ts.isSpreadElement(arg) ? arg.expression : arg);
|
||||
else if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
||||
)
|
||||
values.push(node.right);
|
||||
});
|
||||
this.checker.prefetchCollectionTypes(values);
|
||||
scanSites.set(sf, nodes);
|
||||
return nodes;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,9 @@ export interface ElementStateHost {
|
||||
elementAdmitsUndefined(key: string): boolean;
|
||||
/** The value stored by `node` can be `undefined` at runtime. */
|
||||
mayBeUndefined(node: ts.Expression): boolean;
|
||||
/** Iterating `node`, an iterable other than an array (a Set, a Map's
|
||||
* values, a generator), can yield `undefined`. */
|
||||
iterationMayYieldUndefined?(node: ts.Expression): boolean;
|
||||
/** A callback argument can return `undefined` at runtime. */
|
||||
callbackReturnsUndefined(node: ts.Expression): boolean;
|
||||
/** The value may be a plain array-like object, whose missing indices
|
||||
@@ -28,6 +31,9 @@ export interface ElementStateHost {
|
||||
/** Element keys of the rest parameter that the spread arguments of
|
||||
* `call` fill: `[]` when the callee has none, null when it is unknown. */
|
||||
restKeys(call: ts.CallExpression | ts.NewExpression): readonly string[] | null;
|
||||
/** The argument index where the callee's rest parameter starts, or null
|
||||
* when the callee has none or is unknown. */
|
||||
restStart?(call: ts.CallExpression | ts.NewExpression): number | null;
|
||||
/** Record that iterating `node`'s binding can observe `undefined`;
|
||||
* `known` when the iterated array's own facts (not only those shared by
|
||||
* its element ABI) say so. Returns true when the binding's record grew. */
|
||||
@@ -353,7 +359,11 @@ export class ArrayElementStates {
|
||||
|
||||
/** One stored value, or the elements a spread copies. */
|
||||
private valueMayBeUndefined(value: ts.Expression): boolean {
|
||||
if (ts.isSpreadElement(value)) return this.mayYieldUndefined(value.expression);
|
||||
if (ts.isSpreadElement(value))
|
||||
return (
|
||||
this.mayYieldUndefined(value.expression) ||
|
||||
(this.host.iterationMayYieldUndefined?.(value.expression) ?? false)
|
||||
);
|
||||
return this.host.mayBeUndefined(value);
|
||||
}
|
||||
|
||||
@@ -511,14 +521,26 @@ export class ArrayElementStates {
|
||||
private scanCall(node: ts.CallExpression | ts.NewExpression): boolean {
|
||||
let changed = false;
|
||||
const args = node.arguments?.nodes ?? [];
|
||||
// Spreading a sparse array into a rest parameter fills the holes.
|
||||
// Spreading a sparse array, or a tuple with undefined elements, into a
|
||||
// rest parameter fills the holes.
|
||||
if (args.some((arg) => ts.isSpreadElement(arg))) {
|
||||
for (const arg of args) {
|
||||
if (!ts.isSpreadElement(arg) || !this.mayYieldUndefined(arg.expression)) continue;
|
||||
if (!ts.isSpreadElement(arg) || !this.valueMayBeUndefined(arg)) continue;
|
||||
const rest = this.host.restKeys(node) ?? this.host.arrayKeys(arg.expression);
|
||||
if (this.addUndefined(rest)) changed = true;
|
||||
}
|
||||
}
|
||||
// An argument in a rest position that may be undefined is a present
|
||||
// undefined element of the rest array.
|
||||
const restStart = this.host.restStart?.(node) ?? null;
|
||||
if (restStart !== null)
|
||||
for (const arg of args.slice(restStart))
|
||||
if (
|
||||
!ts.isSpreadElement(arg) &&
|
||||
this.host.mayBeUndefined(arg) &&
|
||||
this.addUndefined(this.host.restKeys(node))
|
||||
)
|
||||
changed = true;
|
||||
if (this.record(node)) changed = true;
|
||||
const callee = peel(node.expression);
|
||||
if (!ts.isPropertyAccessExpression(callee)) return changed;
|
||||
|
||||
@@ -16,6 +16,8 @@ export function sanitizeUnregisteredClassTypes(
|
||||
case "object":
|
||||
return hasClass(type.className) ? type : F64;
|
||||
case "array":
|
||||
type.elem = rewrite(type.elem);
|
||||
break;
|
||||
case "set":
|
||||
type.elem = rewrite(type.elem);
|
||||
break;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,8 @@ export class ProgramAnalysis {
|
||||
/** The modules of every import cycle the exact initialization rule admitted. */
|
||||
readonly exactCycleMembers = new Set<SourceFile>();
|
||||
cycleEarlyBindings: EarlyBindingsMemo | null = null;
|
||||
/** Whether some program file can name node:child_process (namesChildProcess). */
|
||||
namesChildProcess: boolean | null = null;
|
||||
moduleEarlyBindings: EarlyBindingsMemo | null = null;
|
||||
/** Top-level require declarations: the first reference to their bindings
|
||||
* that code running before the require can reach (requireEarlyReference). */
|
||||
@@ -46,6 +48,7 @@ export class ProgramAnalysis {
|
||||
this.moduleSourceCandidates.clear();
|
||||
this.exactCycleMembers.clear();
|
||||
this.cycleEarlyBindings = null;
|
||||
this.namesChildProcess = null;
|
||||
this.moduleEarlyBindings = null;
|
||||
this.requireEarlyReferences.clear();
|
||||
this.requireFirstRunnable.clear();
|
||||
|
||||
@@ -562,6 +562,12 @@ export function nearestPkgJsonPath(fromFile: string): string | null {
|
||||
return dir === null ? null : join(dir, "package.json");
|
||||
}
|
||||
|
||||
/** The nearest package scope's "imports" field (undefined without one). */
|
||||
export function nearestPackageImports(fromFile: string): unknown {
|
||||
const dir = nearestPkgDir(dirname(resolve(fromFile)));
|
||||
return dir === null ? undefined : pkgJsonOf(dir)?.imports;
|
||||
}
|
||||
|
||||
/** The target the nearest package scope's "imports" field maps a '#'
|
||||
* specifier to under Node's ESM runtime conditions: a './' path inside the
|
||||
* package or another package's specifier. Null when no entry maps it. */
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, afterEach, expect, test } from "vitest";
|
||||
import { checkerReadsLibrariesFromDisk, setCheckerFeatureStore } from "./checker-features.js";
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-checker-features-"));
|
||||
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
||||
afterEach(() => setCheckerFeatureStore(null));
|
||||
|
||||
/** A stand-in checker that records its arguments and exits with `status`. */
|
||||
function checker(name: string, status: number): { path: string; log: string } {
|
||||
const path = join(dir, name);
|
||||
const log = path + ".log";
|
||||
writeFileSync(path, `#!/bin/sh\necho "$@" >> '${log}'\nexit ${status}\n`);
|
||||
chmodSync(path, 0o755);
|
||||
return { path, log };
|
||||
}
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"the checker is asked once whether it reads its library files from the disk, and the answer is stored",
|
||||
async () => {
|
||||
const { readFileSync, existsSync } = await import("node:fs");
|
||||
const entries = new Map<string, string>();
|
||||
setCheckerFeatureStore({
|
||||
read: (key) => entries.get(key) ?? null,
|
||||
write: (key, text) => void entries.set(key, text),
|
||||
});
|
||||
const current = checker("current", 0);
|
||||
const released = checker("released", 2);
|
||||
expect(checkerReadsLibrariesFromDisk(current.path, {})).toBe(true);
|
||||
expect(checkerReadsLibrariesFromDisk(released.path, {})).toBe(false);
|
||||
expect(readFileSync(current.log, "utf8")).toBe("--api --default-libraries-from-disk\n");
|
||||
expect([...entries.values()].sort()).toEqual(["0", "1"]);
|
||||
// Later asks use the answers: the checkers do not run again.
|
||||
expect(checkerReadsLibrariesFromDisk(current.path, {})).toBe(true);
|
||||
expect(checkerReadsLibrariesFromDisk(released.path, {})).toBe(false);
|
||||
expect(readFileSync(current.log, "utf8").split("\n").length).toBe(2);
|
||||
expect(readFileSync(released.log, "utf8").split("\n").length).toBe(2);
|
||||
// tsgo is never asked.
|
||||
const tsgo = checker("tsgo", 0);
|
||||
expect(checkerReadsLibrariesFromDisk(tsgo.path, { SCRIPTC_CHECKER: "tsgo" })).toBe(false);
|
||||
expect(existsSync(tsgo.log)).toBe(false);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { checkerBackend } from "./checker-backend.js";
|
||||
import { checkerExecutableIdentity } from "./lib-diagnostics.js";
|
||||
|
||||
/** Text entries kept between builds (the native compiler's cache). */
|
||||
export interface CheckerFeatureStore {
|
||||
read(key: string): string | null;
|
||||
write(key: string, text: string): void;
|
||||
}
|
||||
|
||||
let featureStore: CheckerFeatureStore | null = null;
|
||||
const answers = new Map<string, boolean>();
|
||||
|
||||
/** Keeps the answers of checkerReadsLibrariesFromDisk between builds. */
|
||||
export function setCheckerFeatureStore(store: CheckerFeatureStore | null): void {
|
||||
featureStore = store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the tsc-ts at `executable` takes `--api --default-libraries-from-disk`, which has it read the files
|
||||
* of its default library directory from the disk instead of through the file system callbacks. tsc-ts releases
|
||||
* before it reject the flag, so it is asked once per executable identity: the stored answer, or else
|
||||
* `tsc-ts --api --default-libraries-from-disk` with an empty input, which exits with 0 when the flag is known
|
||||
* and with a usage error when it is not. tsgo is never asked.
|
||||
*/
|
||||
export function checkerReadsLibrariesFromDisk(
|
||||
executable: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
if (checkerBackend(env) !== "tsc-ts") return false;
|
||||
const key = createHash("sha256")
|
||||
.update(JSON.stringify(["default-libraries-from-disk", checkerExecutableIdentity(executable)]))
|
||||
.digest("hex");
|
||||
const known = answers.get(key);
|
||||
if (known !== undefined) return known;
|
||||
const stored = featureStore?.read(key) ?? null;
|
||||
let answer: boolean;
|
||||
if (stored === "1" || stored === "0") answer = stored === "1";
|
||||
else {
|
||||
const probe = spawnSync(executable, ["--api", "--default-libraries-from-disk"], {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
timeout: 10_000,
|
||||
});
|
||||
answer = probe.error === undefined && probe.status === 0;
|
||||
// A probe that could not run says nothing about the flag: ask again next time.
|
||||
if (probe.error === undefined && probe.status !== null)
|
||||
featureStore?.write(key, answer ? "1" : "0");
|
||||
}
|
||||
answers.set(key, answer);
|
||||
return answer;
|
||||
}
|
||||
@@ -19,14 +19,16 @@ export const CHECKER_SERVER_MEMORY = 2048;
|
||||
* answers, and the server keeps the files it parsed for the next build.
|
||||
* Returns the server's name, a digest of everything a server's answers
|
||||
* depend on besides the files it reads through the build: the tsc-ts
|
||||
* executable, the working directory, the file system callbacks, timing, and
|
||||
* the `TSC_TS_*` environment. Returns null when the shared server is not used.
|
||||
* executable, the working directory, the file system callbacks, timing,
|
||||
* whether it reads its library files from the disk, and the `TSC_TS_*`
|
||||
* environment. Returns null when the shared server is not used.
|
||||
*/
|
||||
export function checkerServerName(
|
||||
executable: string,
|
||||
cwd: string,
|
||||
timing: boolean,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
librariesFromDisk = false,
|
||||
): string | null {
|
||||
if (env["SCRIPTC_CHECKER_SERVER"] !== "1") return null;
|
||||
if (process.platform !== "linux" && process.platform !== "darwin") return null;
|
||||
@@ -42,6 +44,7 @@ export function checkerServerName(
|
||||
cwd,
|
||||
TS7_FILE_SYSTEM_CALLBACKS,
|
||||
timing,
|
||||
librariesFromDisk,
|
||||
tscTsEnv,
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Ts7Api } from "./api.js";
|
||||
import { checkerReadsLibrariesFromDisk } from "./checker-features.js";
|
||||
import { checkerServerName } from "./checker-server.js";
|
||||
import { attachNativeTs7Process, spawnNativeTs7Wire } from "./native-process.js";
|
||||
import { Ts7RpcClient } from "./rpc-client.js";
|
||||
@@ -15,12 +16,17 @@ export function createNativeTs7Api(options: {
|
||||
shared?: boolean;
|
||||
}): Ts7Api {
|
||||
const timing = options.collectTiming ?? false;
|
||||
// The checker reads its own library files when it can: they are part of the
|
||||
// installed tsc-ts, so no build serves them differently.
|
||||
const libraries = checkerReadsLibrariesFromDisk(options.executable);
|
||||
const server =
|
||||
options.shared === false ? null : checkerServerName(options.executable, options.cwd, timing);
|
||||
options.shared === false
|
||||
? null
|
||||
: checkerServerName(options.executable, options.cwd, timing, process.env, libraries);
|
||||
const shared =
|
||||
server === null
|
||||
? null
|
||||
: attachNativeTs7Process(server, options.executable, options.cwd, timing);
|
||||
: attachNativeTs7Process(server, options.executable, options.cwd, timing, libraries);
|
||||
if (shared !== null) {
|
||||
const rpc = new Ts7RpcClient(new Ts7Wire(shared));
|
||||
try {
|
||||
@@ -32,7 +38,7 @@ export function createNativeTs7Api(options: {
|
||||
}
|
||||
}
|
||||
return new Ts7Api(
|
||||
new Ts7RpcClient(spawnNativeTs7Wire(options.executable, options.cwd, timing)),
|
||||
new Ts7RpcClient(spawnNativeTs7Wire(options.executable, options.cwd, timing, libraries)),
|
||||
options.fs,
|
||||
timing,
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ declare function ts7NativeOpen(
|
||||
cwd: string,
|
||||
callbacks: string,
|
||||
timing: boolean,
|
||||
librariesFromDisk: boolean,
|
||||
): number;
|
||||
declare function ts7NativeAttach(
|
||||
name: string,
|
||||
@@ -17,6 +18,7 @@ declare function ts7NativeAttach(
|
||||
cwd: string,
|
||||
callbacks: string,
|
||||
timing: boolean,
|
||||
librariesFromDisk: boolean,
|
||||
idleExit: number,
|
||||
memory: number,
|
||||
): number;
|
||||
@@ -63,12 +65,26 @@ export class NativeTs7Process implements Ts7WireIo {
|
||||
private handle: number;
|
||||
readonly pid: number;
|
||||
|
||||
/** With `handle`, a connection to a shared checker server (attachNativeTs7Process). */
|
||||
constructor(executable: string, cwd: string, collectTiming = false, handle = 0) {
|
||||
/** With `handle`, a connection to a shared checker server (attachNativeTs7Process). With
|
||||
* `librariesFromDisk`, the server reads its default library files from the disk
|
||||
* (checkerReadsLibrariesFromDisk). */
|
||||
constructor(
|
||||
executable: string,
|
||||
cwd: string,
|
||||
collectTiming = false,
|
||||
handle = 0,
|
||||
librariesFromDisk = false,
|
||||
) {
|
||||
this.handle =
|
||||
handle !== 0
|
||||
? handle
|
||||
: ts7NativeOpen(executable, cwd, TS7_FILE_SYSTEM_CALLBACKS, collectTiming);
|
||||
: ts7NativeOpen(
|
||||
executable,
|
||||
cwd,
|
||||
TS7_FILE_SYSTEM_CALLBACKS,
|
||||
collectTiming,
|
||||
librariesFromDisk,
|
||||
);
|
||||
if (this.handle === 0) throw nativeError("startup");
|
||||
this.pid = ts7NativePid(this.handle);
|
||||
}
|
||||
@@ -125,6 +141,7 @@ export function attachNativeTs7Process(
|
||||
executable: string,
|
||||
cwd: string,
|
||||
collectTiming = false,
|
||||
librariesFromDisk = false,
|
||||
): NativeTs7Process | null {
|
||||
const handle = ts7NativeAttach(
|
||||
name,
|
||||
@@ -132,6 +149,7 @@ export function attachNativeTs7Process(
|
||||
cwd,
|
||||
TS7_FILE_SYSTEM_CALLBACKS,
|
||||
collectTiming,
|
||||
librariesFromDisk,
|
||||
CHECKER_SERVER_IDLE_EXIT,
|
||||
CHECKER_SERVER_MEMORY,
|
||||
);
|
||||
@@ -142,6 +160,7 @@ export function spawnNativeTs7Wire(
|
||||
executable: string,
|
||||
cwd: string,
|
||||
collectTiming = false,
|
||||
librariesFromDisk = false,
|
||||
): Ts7Wire {
|
||||
return new Ts7Wire(new NativeTs7Process(executable, cwd, collectTiming));
|
||||
return new Ts7Wire(new NativeTs7Process(executable, cwd, collectTiming, 0, librariesFromDisk));
|
||||
}
|
||||
|
||||
@@ -152,33 +152,37 @@ export class Ts7Host {
|
||||
private readonly virtualChanges = new Map<string, "created" | "changed">();
|
||||
private readonly closingProjects = new Set<string>();
|
||||
private readonly api: Ts7Api;
|
||||
/** The text the server reads for a file (its readFile hook). */
|
||||
private readonly readSource: (fileName: string) => string | null;
|
||||
private closed = false;
|
||||
|
||||
constructor(createApi: Ts7ApiFactory, options?: Ts7HostOptions) {
|
||||
const virtualFiles = this.virtualFiles;
|
||||
const shadow = options?.fsShadow ?? null;
|
||||
// A virtual file, a shadow's replacement, a package shim's declaration
|
||||
// entry or the file on disk; null when shadowed out of existence or
|
||||
// missing.
|
||||
this.readSource = (fileName) => {
|
||||
const virtual = virtualFiles.get(tsgoPath(fileName));
|
||||
if (virtual !== undefined) return stripSourceBom(virtual);
|
||||
if (shadow !== null) {
|
||||
if (shadow.hideFile(fileName)) return null;
|
||||
const replacement = shadow.readFile(fileName);
|
||||
if (replacement !== undefined) return stripSourceBom(replacement);
|
||||
}
|
||||
// A package scriptc implements itself (package-shims.ts): its
|
||||
// installed declaration entry re-exports the shim module, in every
|
||||
// program the host creates.
|
||||
const shim = packageShimDeclarationText(fileName);
|
||||
if (shim !== undefined) return shim;
|
||||
const source = trackedReadFile(fileName);
|
||||
return source === null ? null : stripSourceBom(source);
|
||||
};
|
||||
this.api = createApi({
|
||||
cwd: options?.cwd ?? process.cwd(),
|
||||
collectTiming: options?.collectTiming ?? false,
|
||||
fs: {
|
||||
// string => virtual (or shadowed) hit; null => shadowed out of
|
||||
// existence; undefined => real-FS fallthrough.
|
||||
readFile: (fileName) => {
|
||||
const virtual = virtualFiles.get(tsgoPath(fileName));
|
||||
if (virtual !== undefined) return stripSourceBom(virtual);
|
||||
if (shadow !== null) {
|
||||
if (shadow.hideFile(fileName)) return null;
|
||||
const replacement = shadow.readFile(fileName);
|
||||
if (replacement !== undefined) return stripSourceBom(replacement);
|
||||
}
|
||||
// A package scriptc implements itself (package-shims.ts): its
|
||||
// installed declaration entry re-exports the shim module, in every
|
||||
// program the host creates.
|
||||
const shim = packageShimDeclarationText(fileName);
|
||||
if (shim !== undefined) return shim;
|
||||
const source = trackedReadFile(fileName);
|
||||
return source === null ? null : stripSourceBom(source);
|
||||
},
|
||||
readFile: this.readSource,
|
||||
fileExists: (fileName) => {
|
||||
if (virtualFiles.has(tsgoPath(fileName))) return true;
|
||||
if (shadow !== null && shadow.hideFile(fileName)) return false;
|
||||
@@ -193,6 +197,12 @@ export class Ts7Host {
|
||||
});
|
||||
}
|
||||
|
||||
/** The text this host serves for a file (null when it serves none),
|
||||
* without asking the server for its syntax tree. */
|
||||
sourceText(fileName: string): string | null {
|
||||
return this.readSource(fileName);
|
||||
}
|
||||
|
||||
/** Registers an in-memory file served to tsgo by the virtual-FS hooks. */
|
||||
addVirtualFile(path: string, content: string): void {
|
||||
this.ensureOpen();
|
||||
@@ -315,6 +325,12 @@ export class Ts7Program {
|
||||
return this.project.program.getSourceFile(resolve(fileName));
|
||||
}
|
||||
|
||||
/** A file's text as the host serves it, without fetching its syntax
|
||||
* tree (Ts7Host.sourceText). */
|
||||
getSourceText(fileName: string): string | null {
|
||||
return this.host.sourceText(fileName);
|
||||
}
|
||||
|
||||
getSourceFileNames(): readonly string[] {
|
||||
this.snapshot.ensureActive();
|
||||
return (this.sourceFileNamesCache ??= this.project.program.getSourceFileNames());
|
||||
|
||||
@@ -2349,6 +2349,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
elem = unitOnlyUnion(unions);
|
||||
|
||||
if (!elem) return null;
|
||||
// `(T | undefined)[]` of a number, string or boolean T stores T
|
||||
// elements: an array slot holds a present undefined by its own state,
|
||||
// so a T[] seen as `(T | undefined)[]` stays the same array.
|
||||
let optionalElements = false;
|
||||
if (elem.kind === "union") {
|
||||
const arms = unions.get(elem.unionId)?.arms;
|
||||
const present =
|
||||
arms?.length === 2 ? arms.find((arm) => arm.kind !== "undefinedT") : undefined;
|
||||
if (
|
||||
present !== undefined &&
|
||||
arms!.some((arm) => arm.kind === "undefinedT") &&
|
||||
(present.kind === "f64" || present.kind === "string" || present.kind === "bool")
|
||||
) {
|
||||
elem = present;
|
||||
optionalElements = true;
|
||||
}
|
||||
}
|
||||
// A dyn ELEMENT makes the WHOLE array the checked-dynamic value:
|
||||
// `unknown[]`, `object[]`, and the collapsed `(string | object)[]`
|
||||
// (the plugins-slot shape) — the checked-dynamic tree has real arrays, so length/
|
||||
@@ -2370,7 +2387,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// drain of the auxiliary-server registries): handles are ordinary
|
||||
// refcounted REF elements (their `_v` adapters, no trace — both drop
|
||||
// their closures at their terminal event).
|
||||
return arrayOf(elem);
|
||||
return optionalElements ? { kind: "array", elem, optionalElements: true } : arrayOf(elem);
|
||||
}
|
||||
// Tuples: `[string, string]` maps to an interned RECORD shape flagged
|
||||
// `tuple`, with one field per position named by its index ("0", "1", ...)
|
||||
|
||||
@@ -46,6 +46,8 @@ export type {
|
||||
AnalyzeResult,
|
||||
} from "./compile-types.js";
|
||||
import { compilePackedLibrary } from "./backend/library-pack.js";
|
||||
import { node24FetchCompatProfile } from "./compat/fetch-profile.js";
|
||||
import { libFnSignatures } from "./ir/builtin-signatures.js";
|
||||
import { InternalCompilerError } from "./errors.js";
|
||||
import { frontendProcessExecArgv } from "./frontend-process-args.js";
|
||||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
@@ -249,7 +251,7 @@ export {
|
||||
type SurfaceManifestEntry,
|
||||
} from "./coverage/surface-manifest.js";
|
||||
export {
|
||||
NODE24_FETCH_COMPAT_PROFILE,
|
||||
node24FetchCompatProfile,
|
||||
type FetchCompatEvidence,
|
||||
type FetchCompatFacet,
|
||||
type FetchCompatInventory,
|
||||
@@ -261,7 +263,13 @@ export {
|
||||
type FetchCompatOption,
|
||||
type FetchCompatProfile,
|
||||
} from "./compat/fetch-profile.js";
|
||||
export { LIB_FN_SIGS } from "./ir/builtin-signatures.js";
|
||||
export { libFnSignatures } from "./ir/builtin-signatures.js";
|
||||
/** @deprecated Use `libFnSignatures()`. Built when this module loads; the
|
||||
* native compiler does not load this module. */
|
||||
export const LIB_FN_SIGS = libFnSignatures();
|
||||
/** @deprecated Use `node24FetchCompatProfile()`. Built when this module
|
||||
* loads; the native compiler does not load this module. */
|
||||
export const NODE24_FETCH_COMPAT_PROFILE = node24FetchCompatProfile();
|
||||
export { validateModule } from "./ir/validate.js";
|
||||
export {
|
||||
assignLegacyTiers,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,10 @@ const COPIED_NODE_FRACTION = 0.25;
|
||||
* closure it creates writes through its environment (and transitively the
|
||||
* closures those create). A parameter or capture absent from its function's
|
||||
* set holds the value it entered with for the whole call. */
|
||||
export function writtenLocals(mod: IrModule, nodes?: IrNodeIndex): Map<string, Set<string>> {
|
||||
export function writtenLocals(
|
||||
mod: { readonly functions: readonly IrFunction[] },
|
||||
nodes?: IrNodeIndex,
|
||||
): Map<string, Set<string>> {
|
||||
return rebindings(mod, nodes).written;
|
||||
}
|
||||
|
||||
@@ -31,7 +34,7 @@ const REBINDING_KINDS: readonly IrNodeKind[] = [
|
||||
];
|
||||
|
||||
function rebindings(
|
||||
mod: IrModule,
|
||||
mod: { readonly functions: readonly IrFunction[] },
|
||||
nodes?: IrNodeIndex,
|
||||
): {
|
||||
written: Map<string, Set<string>>;
|
||||
|
||||
@@ -87,7 +87,16 @@ export type IrType =
|
||||
| { kind: "date" }
|
||||
| { kind: "string" } // heap, refcounted, UTF-8
|
||||
| { kind: "bool" }
|
||||
| { kind: "array"; elem: IrType } // heap, refcounted, monomorphic elements
|
||||
| {
|
||||
kind: "array";
|
||||
elem: IrType;
|
||||
/** A `(T | undefined)[]` of a number, string or boolean T, stored as
|
||||
* T elements (which hold a present undefined by their state). Not
|
||||
* part of the type's identity (typeEquals, typeKey): it only lets a
|
||||
* checked conversion from a dynamic value accept undefined
|
||||
* elements. */
|
||||
optionalElements?: true;
|
||||
} // heap, refcounted, monomorphic elements
|
||||
/** ES `Map<K, V>` — heap, refcounted, insertion-ordered hash map with ONE
|
||||
* runtime representation (ScrMap) and type-directed key/value handling,
|
||||
* exactly the array pattern (never per-instantiation structs). Keys use
|
||||
@@ -3940,7 +3949,7 @@ export type IrExpr =
|
||||
}
|
||||
/** Standard-library call (`process` members, node:fs functions). `fn` is a
|
||||
* closed union; arg/result types are fixed per member (validated against
|
||||
* LIB_FN_SIGS). Property READS (`process.argv`, `process.platform`) are
|
||||
* libFnSignatures()). Property READS (`process.argv`, `process.platform`) are
|
||||
* zero-arg libCalls. Args are BORROWED by the operation (frame temps
|
||||
* release at statement end); refcounted results come back owned (+1) —
|
||||
* `process.argv` returns +1 on ONE interned array (JS identity:
|
||||
|
||||
@@ -66,7 +66,7 @@ import {
|
||||
import { unionWideningTags } from "./analysis.js";
|
||||
import { alwaysReturns } from "./control-flow.js";
|
||||
import { dynamicOperationOf, functionTier } from "./tiers.js";
|
||||
import { LIB_FN_SIGS, pipelinePumpedRuns } from "./builtin-signatures.js";
|
||||
import { libFnSignatures, pipelinePumpedRuns } from "./builtin-signatures.js";
|
||||
import { ALWAYS_THROW_LIB_FNS } from "./builtin-effects.js";
|
||||
|
||||
/** The promisified crypto calls' value kinds besides checked-dynamic. */
|
||||
@@ -2072,12 +2072,18 @@ function validateFunction(
|
||||
}
|
||||
case "arrayNewLen": {
|
||||
// Mapper-less Array.from({length: n}): absent-slot fill exists
|
||||
// only for refcounted element kinds (frontend fences scalars).
|
||||
// only for refcounted element kinds. Number and boolean slots are
|
||||
// filled with present undefined values right after
|
||||
// (`(T | undefined)[]`); the frontend fences other scalar uses.
|
||||
if (e.type.kind !== "array") {
|
||||
err(`arrayNewLen must be array-typed, got ${e.type.kind}`, e.loc);
|
||||
break;
|
||||
}
|
||||
if (!isRefCounted(e.type.elem)) {
|
||||
if (
|
||||
!isRefCounted(e.type.elem) &&
|
||||
e.type.elem.kind !== "f64" &&
|
||||
e.type.elem.kind !== "bool"
|
||||
) {
|
||||
err(
|
||||
`arrayNewLen with non-refcounted ${e.type.elem.kind} elements (no absent value)`,
|
||||
e.loc,
|
||||
@@ -4104,7 +4110,7 @@ function validateFunction(
|
||||
}
|
||||
if (e.fn === "worker.new" && !workers)
|
||||
errors.push({ message: "Worker construction requires worker execution mode", loc: e.loc });
|
||||
const sig = LIB_FN_SIGS[e.fn];
|
||||
const sig = libFnSignatures()[e.fn];
|
||||
if (!sig) {
|
||||
err(`libCall of unknown library function "${e.fn as string}"`, e.loc);
|
||||
return;
|
||||
|
||||
@@ -56,9 +56,9 @@ import {
|
||||
} from "../coverage/surface-manifest.js";
|
||||
import { libFenceDiag, type ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import {
|
||||
AMBIENT_SURFACE_FNS,
|
||||
ambientSurfaceFns,
|
||||
BUILTIN_MODULE_CONSTS,
|
||||
BUILTIN_MODULE_FNS,
|
||||
builtinModuleFns,
|
||||
BUILTIN_MODULE_FN_ALIASES,
|
||||
STATIC_MATH_FNS,
|
||||
STATIC_MATH_PROPS,
|
||||
@@ -125,7 +125,7 @@ interface FenceTaxonomy {
|
||||
/** node-builtin constant ids: reads fold to literals at compile time. */
|
||||
foldedIds: Set<string>;
|
||||
/** Ambient dedicated-path surfaces (Date/perf_hooks/process): manifest
|
||||
* id → the libCall spellings witnessing reach (AMBIENT_SURFACE_FNS —
|
||||
* id → the libCall spellings witnessing reach (ambientSurfaceFns() —
|
||||
* the same rows the manifest projects the entries from). */
|
||||
ambientFns: Map<string, readonly string[]>;
|
||||
}
|
||||
@@ -148,7 +148,7 @@ function fenceTaxonomy(): FenceTaxonomy {
|
||||
const builtinFns = new Map<string, readonly string[]>();
|
||||
const builtinRoots = new Map<string, readonly string[]>();
|
||||
const foldedIds = new Set<string>();
|
||||
for (const [mod, members] of Object.entries(BUILTIN_MODULE_FNS)) {
|
||||
for (const [mod, members] of Object.entries(builtinModuleFns())) {
|
||||
const root = new Set<string>();
|
||||
for (const [member, entry] of Object.entries(members!)) {
|
||||
if (entry === undefined) continue;
|
||||
@@ -156,7 +156,7 @@ function fenceTaxonomy(): FenceTaxonomy {
|
||||
// The bare-path and url tables rebind per TARGET platform
|
||||
// (builtinModuleFnsOf): a fence on the bare id covers both flavors.
|
||||
if (mod === "path") {
|
||||
const w = BUILTIN_MODULE_FNS["path/win32"]?.[member];
|
||||
const w = builtinModuleFns()["path/win32"]?.[member];
|
||||
if (w !== undefined) fns.add(w.fn);
|
||||
}
|
||||
if (mod === "url" && member === "pathToFileURL") fns.add("url.pathToFileURLWin32");
|
||||
@@ -173,7 +173,7 @@ function fenceTaxonomy(): FenceTaxonomy {
|
||||
}
|
||||
for (const member of Object.keys(STATIC_MATH_PROPS)) foldedIds.add(`stdlib.math.${member}`);
|
||||
const ambientFns = new Map(
|
||||
AMBIENT_SURFACE_FNS.map((row) => [row.id, row.fns as readonly string[]]),
|
||||
ambientSurfaceFns().map((row) => [row.id, row.fns as readonly string[]]),
|
||||
);
|
||||
taxonomy = {
|
||||
byId,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
checkerExecutableIdentity,
|
||||
setLibDiagnosticsStore,
|
||||
} from "../frontend/ts7/lib-diagnostics.js";
|
||||
import { setCheckerFeatureStore } from "../frontend/ts7/checker-features.js";
|
||||
import { loadNativeToolchain } from "./toolchain.js";
|
||||
import { buildSanitizedRuntime } from "./sanitizer.js";
|
||||
import { runCompilerTask } from "./task.js";
|
||||
@@ -67,6 +68,11 @@ async function main(): Promise<number> {
|
||||
},
|
||||
checkerExecutableIdentity(toolchain.ts7Executable),
|
||||
);
|
||||
setCheckerFeatureStore({
|
||||
read: (key) =>
|
||||
store.read("checker-features", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null,
|
||||
write: (key, text) => store.write("checker-features", key, text),
|
||||
});
|
||||
}
|
||||
compiler = new NativeCompiler(toolchain, cache);
|
||||
return compiler;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import {
|
||||
LINUX_ARM64_GNU_TARGET,
|
||||
nativeCodegenTarget,
|
||||
@@ -23,6 +23,7 @@ function text(input: ReturnType<ReturnType<typeof fixture>["prepare"]>) {
|
||||
|
||||
const scratch: string[] = [];
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
for (const path of scratch.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -197,6 +198,30 @@ test("structurally invalid cached features cause a rebuild even with a valid dig
|
||||
expect(f.loads()).toBe(2);
|
||||
});
|
||||
|
||||
test("timing, cache placement and emission worker variables keep the frontend output", () => {
|
||||
for (const name of [
|
||||
"SCRIPTC_CACHE_DIR",
|
||||
"SCRIPTC_CACHE_MAX_MB",
|
||||
"SCRIPTC_EMIT_WORKERS",
|
||||
"SCRIPTC_EMIT_WORKER_FUNCTIONS",
|
||||
"SCRIPTC_TIMING",
|
||||
"SCRIPTC_TEST_FRONTEND_KEY",
|
||||
])
|
||||
vi.stubEnv(name, undefined);
|
||||
const f = fixture();
|
||||
expect(f.prepare().ok).toBe(true);
|
||||
vi.stubEnv("SCRIPTC_CACHE_DIR", f.cache.root);
|
||||
vi.stubEnv("SCRIPTC_CACHE_MAX_MB", "8192");
|
||||
vi.stubEnv("SCRIPTC_EMIT_WORKERS", "1");
|
||||
vi.stubEnv("SCRIPTC_EMIT_WORKER_FUNCTIONS", "100");
|
||||
vi.stubEnv("SCRIPTC_TIMING", "0");
|
||||
expect(f.prepare().ok).toBe(true);
|
||||
expect(f.loads()).toBe(1);
|
||||
vi.stubEnv("SCRIPTC_TEST_FRONTEND_KEY", "1");
|
||||
expect(f.prepare().ok).toBe(true);
|
||||
expect(f.loads()).toBe(2);
|
||||
});
|
||||
|
||||
test("serialized IR travels as bounded parts through the frontend cache", () => {
|
||||
const f = fixture();
|
||||
const first = f.prepare({ emitIr: true });
|
||||
|
||||
@@ -211,6 +211,24 @@ function implementationIdentity(toolchain: NativeToolchain): string {
|
||||
);
|
||||
}
|
||||
|
||||
/** SCRIPTC_* variables that report on a build, place its cache or set
|
||||
* backend emission parallelism. The frontend's output does not depend on
|
||||
* them, so they are left out of its cache key. */
|
||||
const FRONTEND_INDEPENDENT_VARIABLES = new Set([
|
||||
"SCRIPTC_CACHE_DIR",
|
||||
"SCRIPTC_CACHE_MAX_MB",
|
||||
"SCRIPTC_EMIT_WORKERS",
|
||||
"SCRIPTC_EMIT_WORKER_FUNCTIONS",
|
||||
"SCRIPTC_TIMING",
|
||||
]);
|
||||
|
||||
/** The SCRIPTC_* variables a frontend cache entry depends on, sorted by name. */
|
||||
function frontendEnvironment(): [string, string | undefined][] {
|
||||
return Object.entries(process.env)
|
||||
.filter(([name]) => name.startsWith("SCRIPTC_") && !FRONTEND_INDEPENDENT_VARIABLES.has(name))
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function validFeatures(value: unknown): value is NativeLinkFeatures {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const features = value as Record<string, unknown>;
|
||||
@@ -363,9 +381,7 @@ export function prepareNativeExecutable(
|
||||
implementation: implementationIdentity(toolchain),
|
||||
target: toolchain.target.name,
|
||||
declarations: toolchain.declarationsRoot ?? null,
|
||||
environment: Object.entries(process.env)
|
||||
.filter(([name]) => name.startsWith("SCRIPTC_"))
|
||||
.sort(([a], [b]) => a.localeCompare(b)),
|
||||
environment: frontendEnvironment(),
|
||||
}),
|
||||
);
|
||||
const cached = readFrontendCache(cache, key, exclusions);
|
||||
@@ -493,9 +509,7 @@ export function prepareNativeLibrary(
|
||||
implementation: implementationIdentity(toolchain),
|
||||
target: toolchain.target.name,
|
||||
declarations: toolchain.declarationsRoot ?? null,
|
||||
environment: Object.entries(process.env)
|
||||
.filter(([name]) => name.startsWith("SCRIPTC_"))
|
||||
.sort(([a], [b]) => a.localeCompare(b)),
|
||||
environment: frontendEnvironment(),
|
||||
}),
|
||||
);
|
||||
const cached = readFrontendCache(cache, key, exclusions);
|
||||
|
||||
@@ -108,5 +108,161 @@ const kept = rows.filter((r) => r.value > 3);
|
||||
console.log(kept.map((r) => r.id).length, kept.length);
|
||||
${postlude}`);
|
||||
expect(init(mod).locals.some((l) => l.name === "kept")).toBe(true);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.fused"]);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.fused", "%arr.loop"]);
|
||||
});
|
||||
|
||||
test("a lone call with a stable closure-literal callback runs as a loop", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
const scale = 3;
|
||||
const scaled = rows.map((r) => r.value * scale);
|
||||
let total = 0;
|
||||
rows.forEach((r) => { total += r.value; });
|
||||
const big = rows.filter((r) => r.value > scale);
|
||||
const sum = scaled.reduce((acc, v) => acc + v, 0);
|
||||
console.log(scaled.length, total, big.length, sum, rows.some((r) => r.id === scale));
|
||||
${postlude}`);
|
||||
// forEach assigns \`total\`, a binding of \`run\`: it keeps its helper.
|
||||
expect(helperCalls(init(mod))).toEqual([
|
||||
"%arr.forEach",
|
||||
"%arr.loop",
|
||||
"%arr.loop",
|
||||
"%arr.loop",
|
||||
"%arr.loop",
|
||||
]);
|
||||
const loops = mod.functions.filter((f) => f.name.startsWith("%arr.loop."));
|
||||
expect(loops).toHaveLength(4);
|
||||
// The callback bodies are copied into the loops: no closure value is
|
||||
// created or called, and the captured \`scale\` is a plain parameter.
|
||||
for (const fn of loops) {
|
||||
expect(count(fn, "callValue")).toBe(0);
|
||||
expect(count(fn, "closure")).toBe(0);
|
||||
}
|
||||
expect(count(init(mod), "closure")).toBe(1);
|
||||
// No closure captures \`scale\` any more, so it needs no box.
|
||||
expect(init(mod).locals.find((l) => l.name === "scale")?.boxed).toBeUndefined();
|
||||
});
|
||||
|
||||
test("a lone call keeps its helper when a capture can change while it runs", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
let limit = 2;
|
||||
const bump = (): void => { limit++; };
|
||||
const a = rows.filter((r) => { bump(); return r.id < limit; });
|
||||
let step = 1;
|
||||
step = 2;
|
||||
const b = rows.map((r) => r.id * step);
|
||||
console.log(a.length, b.length, limit);
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.map"]);
|
||||
});
|
||||
|
||||
test("a lone call keeps its helper when a local class's method can assign a capture", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
let k = 1;
|
||||
class Bump {
|
||||
go(): void { k = 10; }
|
||||
}
|
||||
const b = new Bump();
|
||||
const out = rows.map((r) => { if (r.id === 2) b.go(); return r.id * k; });
|
||||
console.log(out.join(","));
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.map"]);
|
||||
});
|
||||
|
||||
test("a copied callback keeps its switch and for-of labels and bindings", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
const out = rows.map((r) => {
|
||||
let n = 0;
|
||||
outer: switch (r.id % 3) {
|
||||
case 0:
|
||||
n = 10;
|
||||
break outer;
|
||||
case 1:
|
||||
inner: for (const v of [1, 2, 3]) {
|
||||
n += v;
|
||||
if (v === 2) break outer;
|
||||
if (v === 3) continue inner;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
n = r.value;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
console.log(out.join(","));
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.loop"]);
|
||||
});
|
||||
|
||||
test("a copied callback's switch-local records and known unions are rewritten in its cases", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
type Shape = { kind: "sq"; side: number } | { kind: "rect"; w: number; h: number };
|
||||
const out = rows.map((r) => {
|
||||
switch (r.id % 3) {
|
||||
case 0:
|
||||
const p = { a: r.id, b: 2 };
|
||||
return p.a + p.b;
|
||||
case 1:
|
||||
const sq = { kind: "sq" as const, side: r.value };
|
||||
const s: Shape = sq;
|
||||
return s.kind === "sq" ? s.side * 10 : 0;
|
||||
default:
|
||||
return r.value;
|
||||
}
|
||||
});
|
||||
console.log(out.join(","));
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.loop"]);
|
||||
});
|
||||
|
||||
test("a copied callback with early returns keeps its switch labels", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
const out = rows.map((r) => {
|
||||
let n = 0;
|
||||
outer: switch (r.id % 4) {
|
||||
case 0:
|
||||
return -1;
|
||||
case 1:
|
||||
n = 10;
|
||||
break outer;
|
||||
default:
|
||||
inner: switch (r.value % 2) {
|
||||
case 0:
|
||||
n = r.value;
|
||||
break outer;
|
||||
default:
|
||||
break inner;
|
||||
}
|
||||
n = 1;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
const hit = rows.some((r) => {
|
||||
pick: switch (r.id) {
|
||||
case 3:
|
||||
break pick;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
console.log(out.join(","), hit);
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.loop", "%arr.loop"]);
|
||||
});
|
||||
|
||||
test("a lone call in a local class's method keeps its helper for the class's captures", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
let k = 1;
|
||||
class Box {
|
||||
set(v: number): void { k = v; }
|
||||
go(): number[] {
|
||||
const b = new Box();
|
||||
return rows.map((r) => { b.set(r.id); return r.value + k; });
|
||||
}
|
||||
}
|
||||
console.log(new Box().go().join(","));
|
||||
${postlude}`);
|
||||
const go = mod.functions.find((f) => f.name.endsWith(".Box.go"));
|
||||
expect(go).toBeDefined();
|
||||
expect(helperCalls(go!)).toEqual(["%arr.map"]);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
// A specialized array helper calls its known callback directly after
|
||||
// checking the closure's entry. A callback that cannot throw skips the
|
||||
// pending test on that path, and the fallback through the closure's own
|
||||
// entry keeps it; a callback that can throw keeps it on both paths.
|
||||
// entry keeps it; a callback that can throw keeps it on both paths. The
|
||||
// callbacks are passed through bindings: a closure literal at the call
|
||||
// would run as a loop with its body copied in (array-pipelines.ts).
|
||||
test("known callbacks outside the may-throw set test the pending flag only on the fallback", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-callback-pending-"));
|
||||
try {
|
||||
@@ -18,13 +20,16 @@ test("known callbacks outside the may-throw set test the pending flag only on th
|
||||
writeFileSync(
|
||||
entry,
|
||||
`const values = [1, 2, 3, 4];
|
||||
console.log(values.every((v) => v > 0));
|
||||
console.log(values.map((v) => v * 2).join(","));
|
||||
const positive = (v: number): boolean => v > 0;
|
||||
const double = (v: number): number => v * 2;
|
||||
const stopAtThree = (v: number): boolean => {
|
||||
if (v > 2) throw new Error("stop at " + v);
|
||||
return true;
|
||||
};
|
||||
console.log(values.every(positive));
|
||||
console.log(values.map(double).join(","));
|
||||
try {
|
||||
console.log(values.every((v) => {
|
||||
if (v > 2) throw new Error("stop at " + v);
|
||||
return true;
|
||||
}));
|
||||
console.log(values.every(stopAtThree));
|
||||
} catch (error) {
|
||||
console.log("caught", (error as Error).message);
|
||||
}
|
||||
|
||||
@@ -103,16 +103,106 @@ test("a Promise.all tuple adapter requested in a range is defined", async () =>
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
// Debug metadata is numbered per emitter (debug-info.ts) and not journaled:
|
||||
// a debug build emits its function bodies in one emitter. Its one fork
|
||||
// computes the int32 slot analysis; when the fork fails, the emitter
|
||||
// computes it itself.
|
||||
test("debug builds emit function bodies serially, with every metadata reference defined", async () => {
|
||||
// Debug builds journal their metadata requests (debug-info.ts): ranges
|
||||
// merged in function order number every node as one emitter does.
|
||||
const DEBUG_PROGRAMS: { entry: string; sources: string[] }[] = [
|
||||
{
|
||||
entry: "3066-source-debugging/main.ts",
|
||||
sources: ["3066-source-debugging/main.ts", "3066-source-debugging/helper.ts"],
|
||||
},
|
||||
...[
|
||||
"1020-async-basics.ts",
|
||||
"7103-fiberless-plain-calls.ts",
|
||||
"7110-als-completion-callbacks.ts",
|
||||
"1301-errors-subclass.ts",
|
||||
"1529-class-with-statics.ts",
|
||||
"1591-js-closures.js",
|
||||
"2010-generators-basics.ts",
|
||||
"2013-generators-sent-values.ts",
|
||||
"4451-int32-field-storage.ts",
|
||||
].map((program) => ({ entry: program, sources: [program] })),
|
||||
];
|
||||
|
||||
test("debug builds emitted in ranges merge into the text one emitter gives", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-debug-"));
|
||||
const minFunctions = process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"];
|
||||
try {
|
||||
const corpus = join(import.meta.dirname, "../../../tests/corpus/3066-source-debugging");
|
||||
const entry = join(corpus, "main.ts");
|
||||
const corpus = join(import.meta.dirname, "../../../tests/corpus");
|
||||
for (const program of DEBUG_PROGRAMS) {
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
const result = await compile(join(corpus, program.entry), {
|
||||
outDir: dir,
|
||||
outPath,
|
||||
outputKind: "ir",
|
||||
optimization: "dev",
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const ir = await readFile(outPath, "utf8");
|
||||
const debugSources = new Map<string, string>();
|
||||
for (const name of program.sources)
|
||||
debugSources.set(join(corpus, name), await readFile(join(corpus, name), "utf8"));
|
||||
const options: LlvmTargetOptions = { debugSources };
|
||||
const serial = text(emitLlvmModuleSource(deserializeModule(ir), options));
|
||||
expect(serial, program.entry).toContain("!DILocation");
|
||||
|
||||
// A fork that fails leaves every range to the merging emitter.
|
||||
process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = "1";
|
||||
const forkPrimitives = {
|
||||
fork: (): number => -1,
|
||||
exit: (): void => {},
|
||||
wait: (): number => -1,
|
||||
};
|
||||
for (const variant of [
|
||||
{ functionChunks: 2 },
|
||||
{ functionChunks: 5 },
|
||||
{ functionWorkers: 4, forkPrimitives },
|
||||
] satisfies LlvmTargetOptions[]) {
|
||||
const emitted = text(
|
||||
emitLlvmModuleSource(deserializeModule(ir), { ...options, ...variant }),
|
||||
);
|
||||
expect(emitted === serial, `${program.entry} ${JSON.stringify(Object.keys(variant))}`).toBe(
|
||||
true,
|
||||
);
|
||||
const defined = new Set([...emitted.matchAll(/^(![0-9]+) = /gm)].map((m) => m[1]));
|
||||
const referenced = new Set([...emitted.matchAll(/(![0-9]+)\b/g)].map((m) => m[1]));
|
||||
expect([...referenced].filter((id) => !defined.has(id))).toEqual([]);
|
||||
}
|
||||
if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"];
|
||||
else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions;
|
||||
}
|
||||
} finally {
|
||||
if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"];
|
||||
else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions;
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}, 300_000);
|
||||
|
||||
// A method name holding a double quote is written into a comment after the
|
||||
// function's `!dbg` attachment; the rename of the range's debug ids reads
|
||||
// comments and string literals as ending at the end of their line.
|
||||
test("debug ids after a comment that holds a quote are renamed", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-debug-quote-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const source = [
|
||||
"class Greeter {",
|
||||
" ['say \"hi'](n: number): number {",
|
||||
" return n + 1;",
|
||||
" }",
|
||||
"}",
|
||||
"const greeter = new Greeter();",
|
||||
"function twice(x: number): number {",
|
||||
" return x * 2;",
|
||||
"}",
|
||||
"function report(label: string, value: number): void {",
|
||||
" console.log(label, value);",
|
||||
"}",
|
||||
'report("method", greeter[\'say "hi\'](1));',
|
||||
'report("twice", twice(3));',
|
||||
"",
|
||||
].join("\n");
|
||||
await writeFile(entry, source);
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
const result = await compile(entry, {
|
||||
outDir: dir,
|
||||
@@ -123,37 +213,16 @@ test("debug builds emit function bodies serially, with every metadata reference
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const ir = await readFile(outPath, "utf8");
|
||||
const debugSources = new Map<string, string>();
|
||||
for (const name of ["main.ts", "helper.ts"])
|
||||
debugSources.set(join(corpus, name), await readFile(join(corpus, name), "utf8"));
|
||||
const options: LlvmTargetOptions = { debugSources };
|
||||
const options: LlvmTargetOptions = { debugSources: new Map([[entry, source]]) };
|
||||
const serial = text(emitLlvmModuleSource(deserializeModule(ir), options));
|
||||
expect(serial).toContain("!DILocation");
|
||||
|
||||
let forks = 0;
|
||||
process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = "1";
|
||||
const forkPrimitives = {
|
||||
fork: (): number => {
|
||||
forks++;
|
||||
return -1;
|
||||
},
|
||||
exit: (): void => {},
|
||||
wait: (): number => -1,
|
||||
};
|
||||
for (const variant of [
|
||||
{ functionChunks: 3 },
|
||||
{ functionWorkers: 4, forkPrimitives },
|
||||
] satisfies LlvmTargetOptions[]) {
|
||||
const emitted = text(emitLlvmModuleSource(deserializeModule(ir), { ...options, ...variant }));
|
||||
expect(emitted === serial, JSON.stringify(Object.keys(variant))).toBe(true);
|
||||
const defined = new Set([...emitted.matchAll(/^(![0-9]+) = /gm)].map((m) => m[1]));
|
||||
const referenced = new Set([...emitted.matchAll(/(![0-9]+)\b/g)].map((m) => m[1]));
|
||||
expect([...referenced].filter((id) => !defined.has(id))).toEqual([]);
|
||||
expect(serial).toMatch(/!dbg ![0-9]+ \{ ;[^\n]*"/);
|
||||
for (const functionChunks of [2, 3]) {
|
||||
const chunked = text(
|
||||
emitLlvmModuleSource(deserializeModule(ir), { ...options, functionChunks }),
|
||||
);
|
||||
expect(chunked === serial, `${functionChunks} chunks`).toBe(true);
|
||||
}
|
||||
expect(forks).toBe(1);
|
||||
} finally {
|
||||
if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"];
|
||||
else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions;
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}, 120_000);
|
||||
}, 60_000);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
LIB_FN_SIGS,
|
||||
libFnSignatures,
|
||||
NODE24_FETCH_COMPAT_PROFILE,
|
||||
node24FetchCompatProfile,
|
||||
} from "../src/index.js";
|
||||
|
||||
test("the table constants name the tables their functions build", () => {
|
||||
expect(LIB_FN_SIGS).toBe(libFnSignatures());
|
||||
expect(NODE24_FETCH_COMPAT_PROFILE).toBe(node24FetchCompatProfile());
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
import { everyStmtList } from "../src/ir/traverse.js";
|
||||
|
||||
test("util.format %f lowers a boolean argument once", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-format-float-"));
|
||||
@@ -22,14 +23,20 @@ test("util.format %f lowers a boolean argument once", async () => {
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
// One lifted helper per argument: the evaluated-for-effect statement
|
||||
// reuses the lowered value instead of lowering the argument again.
|
||||
const helpers = mod.functions
|
||||
.map((f) => f.name)
|
||||
.filter((name) => name.startsWith("%arr."))
|
||||
.map((name) => name.replace(/\.[0-9a-f]+$/, ""))
|
||||
.sort();
|
||||
expect(helpers).toEqual(["%arr.every", "%arr.some"]);
|
||||
// One lifted helper call per argument: the evaluated-for-effect
|
||||
// statement reuses the lowered value instead of lowering the argument
|
||||
// again. Each call runs its callback as a loop (array-pipelines.ts).
|
||||
const helpers: string[] = [];
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "call" && e.callee.startsWith("%arr."))
|
||||
helpers.push(e.callee.replace(/\.[0-9a-f]+$/, ""));
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
expect(helpers).toEqual(["%arr.loop", "%arr.loop"]);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -355,12 +355,13 @@ describe("LLVM backend declares match scr_runtime.h prototypes", () => {
|
||||
#include "scr_runtime.h"
|
||||
|
||||
/* LLVM emits: %ScrBytes = type { i64, i64, i32, ptr, ptr, i8, i8, i8, i8, ptr }.
|
||||
* Keep every ABI fact used by its field-index GEPs explicit here. */
|
||||
* Keep every ABI fact used by its field-index GEPs explicit here. The C
|
||||
* struct continues with fields only the runtime reads. */
|
||||
_Static_assert(sizeof(size_t) == 8, "LLVM ScrBytes expects 64-bit size_t");
|
||||
_Static_assert(sizeof(void *) == 8, "LLVM ScrBytes expects 64-bit pointers");
|
||||
_Static_assert(sizeof(ScrBytesElem) == 4, "LLVM ScrBytes elem field is i32");
|
||||
_Static_assert(_Alignof(ScrBytes) == 8, "LLVM ScrBytes alignment changed");
|
||||
_Static_assert(sizeof(ScrBytes) == 56, "LLVM ScrBytes size changed");
|
||||
_Static_assert(sizeof(ScrBytes) == 64, "ScrBytes size changed");
|
||||
_Static_assert(offsetof(ScrBytes, rc) == 0, "LLVM ScrBytes.rc offset changed");
|
||||
_Static_assert(offsetof(ScrBytes, len) == 8, "LLVM ScrBytes.len offset changed");
|
||||
_Static_assert(offsetof(ScrBytes, elem) == 16, "LLVM ScrBytes.elem offset changed");
|
||||
@@ -370,7 +371,9 @@ _Static_assert(offsetof(ScrBytes, is_buffer) == 40, "LLVM ScrBytes.is_buffer off
|
||||
_Static_assert(offsetof(ScrBytes, is_data_view) == 41, "LLVM ScrBytes.is_data_view offset changed");
|
||||
_Static_assert(offsetof(ScrBytes, external) == 42, "LLVM ScrBytes.external offset changed");
|
||||
_Static_assert(offsetof(ScrBytes, untransferable) == 43, "LLVM ScrBytes.untransferable offset changed");
|
||||
_Static_assert(offsetof(ScrBytes, properties_traced) == 44, "ScrBytes.properties_traced must stay in padding");
|
||||
_Static_assert(offsetof(ScrBytes, shared) == 48, "LLVM ScrBytes.shared offset changed");
|
||||
_Static_assert(offsetof(ScrBytes, properties) == 56, "ScrBytes.properties must follow the LLVM prefix");
|
||||
`,
|
||||
);
|
||||
const driver = resolveCc();
|
||||
|
||||
@@ -378,7 +378,7 @@ done:
|
||||
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v43");
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v44");
|
||||
|
||||
const clangExe = join(dir, "clang-program");
|
||||
const linkDriver = join(dir, "clang-driver.c");
|
||||
@@ -712,7 +712,7 @@ done:
|
||||
.split("\n");
|
||||
expect(undefinedSymbols).toContain("_sf_scale");
|
||||
expect(undefinedSymbols).toContain("_sf_callback_mix");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v43");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v44");
|
||||
});
|
||||
|
||||
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { NODE24_FETCH_COMPAT_PROFILE } from "../src/compat/fetch-profile.js";
|
||||
import { node24FetchCompatProfile } from "../src/compat/fetch-profile.js";
|
||||
import { NODE_COMPAT_VERSION } from "../src/frontend/lowering/builtins/process.js";
|
||||
import { CRYPTO_CONSTANTS } from "../src/frontend/lowering/crypto-tables.js";
|
||||
import { ZLIB_CONSTANTS } from "../src/frontend/lowering/zlib-constants.js";
|
||||
@@ -23,7 +23,7 @@ test("the Node pins agree", () => {
|
||||
expect(compat.tag).toBe(`v${pinned}`);
|
||||
expect(read("Dockerfile.sandbox")).toMatch(new RegExp(`^ARG NODE_VERSION=${pinned}$`, "m"));
|
||||
expect(NODE_COMPAT_VERSION).toBe(pinned);
|
||||
expect(NODE24_FETCH_COMPAT_PROFILE.target.node).toBe(pinned);
|
||||
expect(node24FetchCompatProfile().target.node).toBe(pinned);
|
||||
expect(read("packages/runtime/src/scr_lib.c")).toContain(
|
||||
`#define SCR_NODE_COMPAT_VERSION "${pinned}"`,
|
||||
);
|
||||
|
||||
@@ -158,7 +158,9 @@ test("an unchanged captured parameter is borrowed and boxed only where a closure
|
||||
const module = await lower(`${ITEM}
|
||||
function heavier(item: Item, limits: number[]): boolean {
|
||||
if (limits.length === 0) return false;
|
||||
return limits.some((limit) => weigh(item) > limit);
|
||||
// A closure value: a closure literal at the call would run as a loop.
|
||||
const over = (limit: number): boolean => weigh(item) > limit;
|
||||
return limits.some(over);
|
||||
}
|
||||
function replaced(item: Item, other: Item): number {
|
||||
const read = () => item.weight;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6310-small-number-map-direct-index.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7753-typed-array-named-properties.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8000-optional-element-arrays-share-storage.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8001-optional-element-writers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8004-optional-element-conversions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8007-buffer-fill-swap-utf16le.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8321-typed-array-constructor-kind.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -366,7 +366,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: RUNTIME_PACK_MATRIX.target,
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -469,7 +469,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: matrix.target,
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -190,6 +190,8 @@ ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len) {
|
||||
b->external = false;
|
||||
b->untransferable = false;
|
||||
b->shared = NULL;
|
||||
b->properties = NULL;
|
||||
b->properties_traced = false;
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_live_bytes++;
|
||||
#endif
|
||||
@@ -304,6 +306,7 @@ void scr_bytes_release(ScrBytes *b) {
|
||||
if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
|
||||
if (--b->rc == 0) {
|
||||
scr_weak_dispose_inline(b);
|
||||
if (b->properties) scr_dyn_release(b->properties);
|
||||
if (b->backing) {
|
||||
scr_bytes_release(b->backing); /* a view: data points into the owner */
|
||||
} else if (b->shared) {
|
||||
@@ -740,6 +743,8 @@ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) {
|
||||
v->external = false;
|
||||
v->untransferable = false;
|
||||
v->shared = owner->shared;
|
||||
v->properties = NULL;
|
||||
v->properties_traced = false;
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_live_bytes++;
|
||||
#endif
|
||||
@@ -883,7 +888,9 @@ ScrBytes *scr_bytes_from_dyn(ScrBytesElem elem, const ScrDyn *value, bool from)
|
||||
if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(elem, value->v.bytes);
|
||||
if (!from && scr_buffer_storage_is(value)) return scr_array_buffer_view(elem, value, scr_dyn_undefined(), scr_dyn_undefined());
|
||||
if (!from && (value->kind == SCR_DYN_NUM || value->kind == SCR_DYN_BOOL ||
|
||||
value->kind == SCR_DYN_STR || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) {
|
||||
value->kind == SCR_DYN_STR || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF ||
|
||||
value->kind == SCR_DYN_SYMBOL || value->kind == SCR_DYN_BIGINT)) {
|
||||
/* ToIndex: a Symbol or BigInt length throws ToNumber's TypeError. */
|
||||
double number;
|
||||
if (!scr_dyn_number_coerce_js(value, &number)) return NULL;
|
||||
return scr_bytes_new(elem, number);
|
||||
@@ -1009,6 +1016,8 @@ ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double
|
||||
v->external = false;
|
||||
v->untransferable = false;
|
||||
v->shared = owner->shared;
|
||||
v->properties = NULL;
|
||||
v->properties_traced = false;
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_live_bytes++;
|
||||
#endif
|
||||
@@ -2486,14 +2495,57 @@ static uint32_t scr_bytes_utf16le_cp(const uint8_t *in, size_t units, size_t *u)
|
||||
return cu;
|
||||
}
|
||||
|
||||
/* Four units below 0x80 copy as four bytes (one word holds them), and
|
||||
* other units outside the surrogate range are written inline; surrogates
|
||||
* take scr_bytes_utf16le_cp. The string has one UTF-16 unit per input
|
||||
* unit, which is recorded so its length needs no walk. */
|
||||
static ScrStr *scr_bytes_decode_utf16le(const uint8_t *in, size_t n) {
|
||||
size_t units = n / 2;
|
||||
if (units > SIZE_MAX / 3) scr_bytes_oom();
|
||||
ScrStr *s = scr_str_alloc_raw(0, units * 3);
|
||||
size_t o = 0;
|
||||
for (size_t u = 0; u < units;) o = scr_bytes_put_cp(s->data, o, scr_bytes_utf16le_cp(in, units, &u));
|
||||
uint8_t *out = (uint8_t *)s->data;
|
||||
size_t o = 0, u = 0;
|
||||
while (u < units) {
|
||||
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
if (units - u >= 4) {
|
||||
uint64_t w;
|
||||
memcpy(&w, in + u * 2, 8);
|
||||
if (!(w & UINT64_C(0xff80ff80ff80ff80))) {
|
||||
out[o] = (uint8_t)w;
|
||||
out[o + 1] = (uint8_t)(w >> 16);
|
||||
out[o + 2] = (uint8_t)(w >> 32);
|
||||
out[o + 3] = (uint8_t)(w >> 48);
|
||||
o += 4;
|
||||
u += 4;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
uint32_t cu = (uint32_t)in[u * 2] | ((uint32_t)in[u * 2 + 1] << 8);
|
||||
if (cu >= 0x800 && (cu < 0xd800 || cu > 0xdfff)) {
|
||||
/* Three bytes in one four-byte store; the next write (or the
|
||||
* terminator) replaces the fourth. */
|
||||
uint8_t seq[4] = {(uint8_t)(0xe0 | (cu >> 12)), (uint8_t)(0x80 | ((cu >> 6) & 0x3f)),
|
||||
(uint8_t)(0x80 | (cu & 0x3f)), 0};
|
||||
memcpy(out + o, seq, 4);
|
||||
o += 3;
|
||||
u++;
|
||||
} else if (cu < 0x80) {
|
||||
out[o++] = (uint8_t)cu;
|
||||
u++;
|
||||
} else if (cu < 0x800) {
|
||||
out[o] = (uint8_t)(0xc0 | (cu >> 6));
|
||||
out[o + 1] = (uint8_t)(0x80 | (cu & 0x3f));
|
||||
o += 2;
|
||||
u++;
|
||||
} else {
|
||||
o = scr_bytes_put_cp(s->data, o, scr_bytes_utf16le_cp(in, units, &u));
|
||||
}
|
||||
}
|
||||
s->len = o;
|
||||
s->data[o] = '\0';
|
||||
if (o == units) scr_str_note_ascii(s);
|
||||
else scr_str_note_utf16_len(s, units);
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -2948,6 +3000,8 @@ static ScrBytes *scr_pool_take(size_t len) {
|
||||
v->external = false;
|
||||
v->untransferable = false;
|
||||
v->shared = NULL;
|
||||
v->properties = NULL;
|
||||
v->properties_traced = false;
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_live_bytes++;
|
||||
#endif
|
||||
@@ -3468,7 +3522,15 @@ static ScrBytes *scr_bytes_fill_core(ScrBytes *b, const uint8_t *pat, size_t pat
|
||||
} else if (patn == 1) {
|
||||
memset(dst, pat[0], e - o);
|
||||
} else {
|
||||
for (size_t i = o; i < e; i++) dst[i - o] = pat[(i - o) % patn];
|
||||
/* Node's fill: the pattern's first bytes (memmove: the pattern may
|
||||
* be a view of this buffer), then the filled prefix doubled. */
|
||||
size_t len = e - o, done = patn < len ? patn : len;
|
||||
memmove(dst, pat, done);
|
||||
while (done < len) {
|
||||
size_t step = done < len - done ? done : len - done;
|
||||
memcpy(dst + done, dst, step);
|
||||
done += step;
|
||||
}
|
||||
}
|
||||
if (b->shared) {
|
||||
scr_shared_store(b->data + o, dst, e - o);
|
||||
@@ -3547,11 +3609,29 @@ ScrBytes *scr_bytes_swap(ScrBytes *b, double width) {
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_INVALID_BUFFER_SIZE");
|
||||
return NULL;
|
||||
}
|
||||
for (size_t g = 0; g < b->len; g += w) {
|
||||
for (size_t i = 0; i < w / 2; i++) {
|
||||
uint8_t t = b->data[g + i];
|
||||
b->data[g + i] = b->data[g + w - 1 - i];
|
||||
b->data[g + w - 1 - i] = t;
|
||||
/* One load, byte swap and store per group. */
|
||||
uint8_t *p = b->data;
|
||||
size_t len = b->len;
|
||||
if (w == 2) {
|
||||
for (size_t g = 0; g < len; g += 2) {
|
||||
uint16_t v;
|
||||
memcpy(&v, p + g, 2);
|
||||
v = __builtin_bswap16(v);
|
||||
memcpy(p + g, &v, 2);
|
||||
}
|
||||
} else if (w == 4) {
|
||||
for (size_t g = 0; g < len; g += 4) {
|
||||
uint32_t v;
|
||||
memcpy(&v, p + g, 4);
|
||||
v = __builtin_bswap32(v);
|
||||
memcpy(p + g, &v, 4);
|
||||
}
|
||||
} else {
|
||||
for (size_t g = 0; g < len; g += 8) {
|
||||
uint64_t v;
|
||||
memcpy(&v, p + g, 8);
|
||||
v = __builtin_bswap64(v);
|
||||
memcpy(p + g, &v, 8);
|
||||
}
|
||||
}
|
||||
return scr_bytes_retain(b);
|
||||
|
||||
@@ -65,7 +65,15 @@ typedef struct ScrBytes {
|
||||
bool is_data_view; /* DataView is distinct from a numeric typed array. */
|
||||
bool external; /* Root aliases foreign memory; releasing it never frees data. */
|
||||
bool untransferable; /* A Buffer pool slab: its ArrayBuffer cannot be transferred. */
|
||||
/* The cycle collector's last trace of `properties` from the one checked
|
||||
* value that held this view (see scr_dyn_trace_v). In padding, so the
|
||||
* offsets compiled code uses hold. */
|
||||
bool properties_traced;
|
||||
ScrSharedBytes *shared; /* Neutral backing, borrowed by views from their local owner. */
|
||||
/* Named properties a program adds to the view (`ta.foo = 1`,
|
||||
* Object.defineProperty): a checked object, NULL until the first one.
|
||||
* Runtime-only and last, so compiled code's field indices hold. */
|
||||
struct ScrDyn *properties;
|
||||
} ScrBytes;
|
||||
|
||||
/* Copies out of and into shared storage, made of relaxed atomic accesses
|
||||
|
||||
@@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
void scr_runtime_abi_v43(void) {}
|
||||
void scr_runtime_abi_v44(void) {}
|
||||
|
||||
void scr_init(void) {
|
||||
#ifdef SCR_WORKERS
|
||||
|
||||
@@ -81,7 +81,7 @@ void scr_init(void);
|
||||
/* Program objects emitted by the bundled LLVM helper reference this symbol.
|
||||
* Its versioned spelling makes a mismatched manual runtime link fail before
|
||||
* the program can start. */
|
||||
void scr_runtime_abi_v43(void);
|
||||
void scr_runtime_abi_v44(void);
|
||||
void scr_runtime_workers_v8(void);
|
||||
|
||||
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
|
||||
|
||||
@@ -480,7 +480,12 @@ struct ScrDyn {
|
||||
* exits, and those clear it before release; reuse of a freed object
|
||||
* resets it (scr_dyn_alloc). */
|
||||
const char *class_name;
|
||||
struct ScrDynRegion *region; /* see ScrDynRegion */
|
||||
union {
|
||||
struct ScrDynRegion *region; /* see ScrDynRegion */
|
||||
/* While the node is pooled: the key bytes it keeps for reuse,
|
||||
* counted in the pool's key budget (scr_dyn_obj_drop_keys). */
|
||||
size_t cached_key_bytes;
|
||||
};
|
||||
/* The dynamic tier's hidden class (scr_js_object.c): the transition
|
||||
* tree node naming this object's key sequence, or NULL while it has
|
||||
* none. Only programs with dynamic-tier code read or maintain it:
|
||||
@@ -959,6 +964,9 @@ void scr_dyn_install_iterator_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_has_instance_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_async_iterator_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_async_dispose_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_species_symbol(ScrDyn *key);
|
||||
/* Records each well-known symbol the symbol unit creates. */
|
||||
void scr_dyn_install_well_known_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_to_string_tag_symbol(ScrDyn *key);
|
||||
/* True when key is Symbol.asyncDispose (registered by the symbol unit when
|
||||
* the program names it); handle symbol_get ops outside that unit use it. */
|
||||
@@ -1046,6 +1054,9 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS
|
||||
extern SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args,
|
||||
ScrDyn **result);
|
||||
ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key);
|
||||
/* A typed array's, Buffer's or DataView's own named property (+1), or NULL
|
||||
* without an exception when it has none. */
|
||||
ScrDyn *scr_bytes_named_get(const ScrDyn *view, const char *key, size_t key_len);
|
||||
/* The Buffer-flavored twin (stream chunks): string coercion/toString
|
||||
* decode utf8 instead of joining elements. */
|
||||
ScrDyn *scr_dyn_new_buffer(const ScrBytes *b);
|
||||
|
||||
@@ -1712,6 +1712,20 @@ static ScrDyn *scr_dyn_invoke_impl(
|
||||
|
||||
if (recv->kind == SCR_DYN_BYTES) {
|
||||
ScrBytes *bytes = recv->v.bytes;
|
||||
/* An own property of the view shadows the prototype's method. */
|
||||
ScrDyn *member = bytes->properties ? scr_bytes_named_get(recv, method, strlen(method)) : NULL;
|
||||
if (!member && scr_exc_pending()) return NULL;
|
||||
if (member) {
|
||||
ScrDyn *result = NULL;
|
||||
if (!scr_dyn_is_callable(member)) dyn_throw_not_fn(what);
|
||||
else {
|
||||
scr_dyn_this_push_dyn(recv);
|
||||
result = scr_dyn_call(member, args, argc, what);
|
||||
scr_dyn_this_pop();
|
||||
}
|
||||
scr_dyn_release(member);
|
||||
return result;
|
||||
}
|
||||
if (bytes->is_data_view) {
|
||||
static const char *const kinds[] = { "Uint8", "Int8", "Uint16", "Int16", "Uint32", "Int32", "Float32", "Float64", "BigUint64", "BigInt64", "Float16" };
|
||||
const bool get = !strncmp(method, "get", 3);
|
||||
|
||||
@@ -794,6 +794,52 @@ ScrStr *scr_insp_regex(ScrRegex *re) {
|
||||
return ib_take(&out);
|
||||
}
|
||||
|
||||
/* Named properties of a typed array, Buffer or DataView (its
|
||||
* ScrBytes.properties table): slot k is the k-th string-keyed entry, then
|
||||
* the k-th symbol key. */
|
||||
static size_t insp_view_property_slots(const ScrDyn *props) {
|
||||
if (!props) return 0;
|
||||
return props->v.obj.len + (props->symbol_keys ? props->symbol_keys->v.arr.len : 0);
|
||||
}
|
||||
|
||||
static ScrStr *insp_accessor_tag(const ScrDynEntry *ent);
|
||||
static void insp_key_into(InspBuf *b, const char *key, size_t len);
|
||||
|
||||
/* Slot k's `key: value` entry when it is enumerable, else NULL. */
|
||||
static ScrStr *insp_view_property(ScrDyn *props, size_t k, double recurse, double depth) {
|
||||
const ScrDynEntry *ent;
|
||||
ScrStr *key = NULL;
|
||||
if (k < props->v.obj.len) {
|
||||
ent = &props->v.obj.entries[k];
|
||||
} else {
|
||||
ScrDyn *symbol = props->symbol_keys->v.arr.items[k - props->v.obj.len];
|
||||
ent = scr_dyn_own_symbol_entry(props, symbol);
|
||||
if (ent && ent->enumerable) key = scr_insp_dyn(symbol, recurse, depth);
|
||||
}
|
||||
if (!ent || !ent->enumerable) return NULL;
|
||||
ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse, depth);
|
||||
InspBuf eb = {0};
|
||||
if (key) ib_bytes(&eb, key->data, key->len);
|
||||
else insp_key_into(&eb, ent->key, ent->key_len);
|
||||
ib_cstr(&eb, ": ");
|
||||
ib_bytes(&eb, val->data, val->len);
|
||||
scr_str_release(key);
|
||||
scr_str_release(val);
|
||||
return ib_take(&eb);
|
||||
}
|
||||
|
||||
static size_t insp_view_property_count(ScrDyn *props) {
|
||||
size_t count = 0;
|
||||
for (size_t k = 0; k < insp_view_property_slots(props); k++) {
|
||||
if (k < props->v.obj.len) count += props->v.obj.entries[k].enumerable;
|
||||
else {
|
||||
const ScrDynEntry *ent = scr_dyn_own_symbol_entry(props, props->symbol_keys->v.arr.items[k - props->v.obj.len]);
|
||||
count += ent && ent->enumerable;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Buffer: the custom-inspect hex form `<Buffer aa bb ...>`, capped at
|
||||
* INSPECT_MAX_BYTES (50); renders fully at any depth (custom inspection
|
||||
* runs before the depth check in Node). u8 element kind only (the
|
||||
@@ -816,6 +862,17 @@ ScrStr *scr_insp_buffer(ScrBytes *b) {
|
||||
remaining > 1 ? "s" : "");
|
||||
ib_bytes(&out, more, (size_t)n);
|
||||
}
|
||||
/* Named properties follow as `, key: value`, rendered as the members of
|
||||
* an object at depth 0 (Buffer's custom inspect). */
|
||||
bool first = true;
|
||||
for (size_t k = 0; k < insp_view_property_slots(b->properties); k++) {
|
||||
ScrStr *entry = insp_view_property(b->properties, k, 1, 2);
|
||||
if (!entry) continue;
|
||||
if (!first || b->len) ib_cstr(&out, ", ");
|
||||
first = false;
|
||||
ib_bytes(&out, entry->data, entry->len);
|
||||
scr_str_release(entry);
|
||||
}
|
||||
ib_char(&out, '>');
|
||||
return ib_take(&out);
|
||||
}
|
||||
@@ -899,6 +956,12 @@ static ScrStr *scr_insp_data_view(const ScrBytes *b, double recurse, double dept
|
||||
entry = ib_take(&eb);
|
||||
scr_insp_entry(entry, false);
|
||||
scr_str_release(entry);
|
||||
for (size_t k = 0; k < insp_view_property_slots(b->properties); k++) {
|
||||
entry = insp_view_property(b->properties, k, recurse + 1, depth);
|
||||
if (!entry) continue;
|
||||
scr_insp_entry(entry, false);
|
||||
scr_str_release(entry);
|
||||
}
|
||||
ScrStr *base = scr_str_new("", 0);
|
||||
ScrStr *b0 = scr_str_new("DataView {", 10);
|
||||
ScrStr *b1 = scr_str_new("}", 1);
|
||||
@@ -1537,7 +1600,10 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char
|
||||
ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) {
|
||||
char prefix[48];
|
||||
int pn = snprintf(prefix, sizeof prefix, "%s(%zu) [", scr_bytes_elem_name(b->elem), b->len);
|
||||
if (b->len == 0) {
|
||||
/* Named properties follow the elements as `key: value` entries. */
|
||||
ScrDyn *props = b->properties;
|
||||
size_t nprops = insp_view_property_count(props);
|
||||
if (b->len == 0 && nprops == 0) {
|
||||
InspBuf out = {0};
|
||||
ib_bytes(&out, prefix, (size_t)pn);
|
||||
ib_char(&out, ']');
|
||||
@@ -1561,10 +1627,19 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) {
|
||||
scr_insp_entry(m, true);
|
||||
scr_str_release(m);
|
||||
}
|
||||
size_t entries = shown + (more ? 1 : 0);
|
||||
for (size_t k = 0; nprops && k < insp_view_property_slots(props); k++) {
|
||||
ScrStr *entry = insp_view_property(props, k, recurse + 1, depth);
|
||||
if (!entry) continue;
|
||||
/* Node's grouping pads by the element at the entry's output position. */
|
||||
scr_insp_entry(entry, entries < b->len);
|
||||
scr_str_release(entry);
|
||||
entries++;
|
||||
}
|
||||
ScrStr *base = scr_str_new("", 0);
|
||||
ScrStr *b0 = scr_str_new(prefix, (size_t)pn);
|
||||
ScrStr *b1 = scr_str_new("]", 1);
|
||||
ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, more);
|
||||
ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, nprops ? entries > 100 : more);
|
||||
scr_str_release(base);
|
||||
scr_str_release(b0);
|
||||
scr_str_release(b1);
|
||||
|
||||
+583
-86
File diff suppressed because it is too large
Load Diff
@@ -1205,9 +1205,11 @@ static __attribute__((noinline)) void scr_map_set_num(ScrMap *m, uint64_t hash,
|
||||
}
|
||||
|
||||
/* A number-key map with at least this many entries moves to the direct
|
||||
* index when its keys allow it, so small integer keys are indexed before
|
||||
* the linear scan ends. */
|
||||
#define SCR_MAP_DENSE_AFTER 4
|
||||
* index when its keys allow it: from its second key, so a map of a few
|
||||
* small integer keys (a histogram over 0..3) reads through the inline
|
||||
* direct index rather than the out-of-line linear scan. A map with one
|
||||
* key keeps its linear storage. */
|
||||
#define SCR_MAP_DENSE_AFTER 1
|
||||
|
||||
/* Whether appending one more entry would grow the lookup index rather than
|
||||
* reuse tombstones: scr_map_reserve_append compacts a linear map with a
|
||||
|
||||
@@ -184,9 +184,11 @@ static void iterate_while_mutating(void) {
|
||||
|
||||
static void modes(void) {
|
||||
ScrMap *map = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL);
|
||||
for (int i = 1; i <= 4; i++) scr_map_set_f64_f64(map, i, i);
|
||||
assert(!map->ndense && !map->nbuckets); /* still a linear scan */
|
||||
scr_map_set_f64_f64(map, 5, 5);
|
||||
scr_map_set_f64_f64(map, 1, 1);
|
||||
assert(!map->ndense && !map->nbuckets); /* one key: still a linear scan */
|
||||
scr_map_set_f64_f64(map, 2, 2);
|
||||
assert(map->ndense >= 16 && !map->nbuckets); /* from the second small integer key */
|
||||
for (int i = 3; i <= 5; i++) scr_map_set_f64_f64(map, i, i);
|
||||
assert(map->ndense >= 16 && !map->nbuckets);
|
||||
for (int i = 6; i < 1000; i++) scr_map_set_f64_f64(map, i, i);
|
||||
assert(map->ndense >= 1000 && map->ndense <= 1000 + 1000 / 4 + 64); /* near the largest key */
|
||||
|
||||
@@ -2006,6 +2006,19 @@ double scr_str_utf16_len(ScrStr *s) {
|
||||
return (double)(int64_t)units;
|
||||
}
|
||||
|
||||
/* Records the UTF-16 length of a string whose producer counted it (latin1
|
||||
* text has one unit per byte), so scr_str_utf16_len does not walk it. Only
|
||||
* strings that scr_str_utf16_len would otherwise record are kept. */
|
||||
void scr_str_note_utf16_len(ScrStr *s, size_t units) {
|
||||
if (s->ascii || s->len <= 64 || s->rc == SIZE_MAX) return;
|
||||
if (!scr_u16len_tab) {
|
||||
scr_u16len_tab = calloc(SCR_U16LEN_N, sizeof *scr_u16len_tab);
|
||||
if (!scr_u16len_tab) return;
|
||||
scr_sidx_register_cleanup();
|
||||
}
|
||||
*scr_u16len_slot(s) = (ScrU16Len){s, units};
|
||||
}
|
||||
|
||||
double scr_str_char_code_at(ScrStr *s, double i) {
|
||||
/* A sequential scan of a non-ASCII receiver stays inside the bounded
|
||||
* ASCII span cached at its last resolved position. Its unit offset is an
|
||||
|
||||
@@ -83,6 +83,9 @@ static inline void scr_str_hash_forget(ScrStr *s) {
|
||||
/* True only when every byte is proven below 0x80 (see above). */
|
||||
static inline bool scr_str_known_ascii(const ScrStr *s) { return s->ascii; }
|
||||
|
||||
/* Record the UTF-16 length a string's producer counted (scr_string.c). */
|
||||
void scr_str_note_utf16_len(ScrStr *s, size_t units);
|
||||
|
||||
/* Record a proven-ASCII fact. Immortal strings may live in read-only
|
||||
* memory and are shared across threads, so they are never written. */
|
||||
static inline void scr_str_note_ascii(ScrStr *s) {
|
||||
|
||||
@@ -85,6 +85,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) {
|
||||
scr_str_release(description);
|
||||
symbol->reg_next = g_sym_well_known;
|
||||
g_sym_well_known = scr_sym_retain(symbol);
|
||||
{
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_dyn_install_well_known_symbol(key);
|
||||
scr_dyn_release(key);
|
||||
}
|
||||
if (name->len == 8 && !memcmp(name->data, "iterator", 8)) {
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_dyn_install_iterator_symbol(key);
|
||||
@@ -95,6 +100,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) {
|
||||
scr_dyn_install_has_instance_symbol(key);
|
||||
scr_dyn_release(key);
|
||||
}
|
||||
if (name->len == 7 && !memcmp(name->data, "species", 7)) {
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_dyn_install_species_symbol(key);
|
||||
scr_dyn_release(key);
|
||||
}
|
||||
if (name->len == 11 && !memcmp(name->data, "toStringTag", 11)) {
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_dyn_install_to_string_tag_symbol(key);
|
||||
|
||||
@@ -422,12 +422,14 @@ static void test_live_iteration_indices(void) {
|
||||
/* A small (linear) map that must grow its lookup index while an iteration
|
||||
* is active may not compact: entry indices, tombstones and the append
|
||||
* position stay put. Integer keys in a compact range move to the direct
|
||||
* index (dense) from the fifth entry, any other number keys to the bucket
|
||||
* index (dense) from the second entry, any other number keys to the bucket
|
||||
* table from the ninth; both transitions are covered by running the same
|
||||
* script with `offset` 0 and 0.5. */
|
||||
* script with `offset` 0 and 0.5. The walk deletes `victim`, an existing
|
||||
* key, then appends the key that makes the index grow. */
|
||||
static void run_small_storage_transitions(double offset) {
|
||||
bool dense = offset == 0;
|
||||
int n = dense ? 4 : 8; /* entries a linear map holds before its index grows */
|
||||
int n = dense ? 1 : 8; /* entries a linear map holds before its index grows */
|
||||
int victim = dense ? 0 : 1;
|
||||
ScrMap *m = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL);
|
||||
for (int i = 0; i < n; i++) scr_map_set_f64_f64(m, i + offset, i + 10);
|
||||
check(m->nbuckets == 0 && m->buckets == NULL && m->ndense == 0,
|
||||
@@ -441,22 +443,23 @@ static void run_small_storage_transitions(double offset) {
|
||||
ScrMap *copy = scr_map_clone(m, false, NULL);
|
||||
check(copy->nbuckets == 0 && copy->ndense == 0, "small clone preserves compact storage");
|
||||
scr_map_iter_enter(m);
|
||||
scr_map_delete_f64(m, 1 + offset);
|
||||
scr_map_delete_f64(m, victim + offset);
|
||||
scr_map_set_f64_f64(m, n + offset, 14);
|
||||
check(dense ? m->ndense > 0 && m->nbuckets == 0 : m->nbuckets > 0 && m->ndense == 0,
|
||||
"live iteration promotes without moving entry indices");
|
||||
check(m->nentries == (uint32_t)n + 1 && !scr_map_iter_live(m, 1) &&
|
||||
check(m->nentries == (uint32_t)n + 1 && !scr_map_iter_live(m, victim) &&
|
||||
scr_map_iter_key_f64(m, n) == n + offset,
|
||||
"promotion preserves tombstones and append position");
|
||||
for (int i = n + 1; i < 64; i++) scr_map_set_f64_f64(m, i + offset, i + 10);
|
||||
check(m->nentries == 64 && !scr_map_iter_live(m, 1) && scr_map_iter_key_f64(m, 63) == 63 + offset,
|
||||
check(m->nentries == 64 && !scr_map_iter_live(m, victim) && scr_map_iter_key_f64(m, 63) == 63 + offset,
|
||||
"growth during iteration keeps entry indices");
|
||||
double out = 0;
|
||||
check(scr_map_get_f64_f64(m, offset, &out) && out == 10,
|
||||
"original key survives promotion and growth");
|
||||
check(!scr_map_has_f64(m, 1 + offset), "deleted small key stays absent after rehash");
|
||||
check(dense ? scr_map_get_f64_f64(m, n + offset, &out) && out == 14
|
||||
: scr_map_get_f64_f64(m, offset, &out) && out == 10,
|
||||
"keys written before the promotion survive it and the growth");
|
||||
check(!scr_map_has_f64(m, victim + offset), "deleted small key stays absent after rehash");
|
||||
scr_map_iter_exit(m);
|
||||
check(scr_map_has_f64(copy, 1 + offset) && !scr_map_has_f64(copy, n + offset),
|
||||
check(scr_map_has_f64(copy, victim + offset) && !scr_map_has_f64(copy, n + offset),
|
||||
"small clone remains independent");
|
||||
scr_map_clear(copy);
|
||||
scr_map_iter_enter(copy);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Number-keyed Maps and Sets with two to four small integer keys use the
|
||||
// direct index. Results must match Node through overwrites, deletes and
|
||||
// re-adds, a fractional, negative or large key that moves the map to
|
||||
// hashing, clear, and mutation during a for-of or forEach walk.
|
||||
|
||||
const hist = new Map<number, number>();
|
||||
const codes = [0, 3, 1, 3, 2, 0, 3, 3, 1];
|
||||
for (const c of codes) hist.set(c, (hist.get(c) ?? 0) + 1);
|
||||
console.log([...hist].map(([k, v]) => `${k}:${v}`).join(" "), hist.size, hist.has(4), hist.get(-0));
|
||||
|
||||
// Delete and re-add: the re-added key moves to the end.
|
||||
hist.delete(3);
|
||||
hist.set(3, 9);
|
||||
hist.delete(0);
|
||||
console.log([...hist.keys()].join(","), hist.get(3), hist.has(0));
|
||||
|
||||
// A key the direct index cannot hold moves the map to hashing.
|
||||
for (const odd of [0.5, -1, 2 ** 40]) {
|
||||
const m = new Map<number, string>([
|
||||
[1, "a"],
|
||||
[2, "b"],
|
||||
]);
|
||||
m.set(odd, "c");
|
||||
m.set(1, "A");
|
||||
console.log([...m].map(([k, v]) => `${k}=${v}`).join(" "), m.get(2), m.get(odd), m.size);
|
||||
}
|
||||
|
||||
// Two keys, then clear, then reuse.
|
||||
const pair = new Map<number, number>();
|
||||
pair.set(5, 50);
|
||||
pair.set(7, 70);
|
||||
pair.clear();
|
||||
pair.set(7, 1);
|
||||
pair.set(1, 2);
|
||||
console.log([...pair].map(([k, v]) => `${k}=${v}`).join(" "), pair.size);
|
||||
|
||||
// A walk that deletes ahead and adds keys.
|
||||
const walk = new Map<number, number>([
|
||||
[0, 0],
|
||||
[1, 10],
|
||||
[2, 20],
|
||||
]);
|
||||
const seen: number[] = [];
|
||||
for (const [k, v] of walk) {
|
||||
seen.push(v);
|
||||
if (k === 0) {
|
||||
walk.delete(2);
|
||||
walk.set(3, 30);
|
||||
}
|
||||
if (k === 3) walk.set(0, -1);
|
||||
}
|
||||
console.log(seen.join(","), [...walk].map(([k, v]) => `${k}=${v}`).join(" "));
|
||||
|
||||
// Sets of small integers, forEach with deletion and addition.
|
||||
const set = new Set<number>([2, 0]);
|
||||
set.add(1);
|
||||
set.add(2);
|
||||
const order: number[] = [];
|
||||
set.forEach((v) => {
|
||||
order.push(v);
|
||||
if (v === 2) {
|
||||
set.delete(1);
|
||||
set.add(3);
|
||||
}
|
||||
});
|
||||
console.log(order.join(","), [...set].join(","), set.has(1), set.size);
|
||||
|
||||
// Many small maps, each with a few keys.
|
||||
let total = 0;
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const m = new Map<number, number>();
|
||||
m.set(i % 3, i);
|
||||
m.set((i + 1) % 3, 1);
|
||||
if (i % 5 === 0) m.delete(i % 3);
|
||||
total += (m.get(i % 3) ?? 100) + (m.get((i + 1) % 3) ?? 0) + m.size;
|
||||
}
|
||||
console.log(total);
|
||||
@@ -123,3 +123,15 @@ function plainIterator(TA) {
|
||||
console.log(new TA(source), TA.from(source, (v) => v * 2), gets, nexts);
|
||||
}
|
||||
plainIterator(Int16Array);
|
||||
// ToIndex of a Symbol or BigInt length, toLocaleString, and Symbol descriptions
|
||||
// that are not strings.
|
||||
function small(TA) {
|
||||
for (const bad of [Symbol("s"), 1n]) {
|
||||
try { new TA(bad); console.log("no throw"); } catch (e) { console.log(e.constructor.name); }
|
||||
}
|
||||
console.log(new TA([1, 1234.5, -0, 0]).toLocaleString(), new TA(0).toLocaleString(), typeof new TA(1).toLocaleString);
|
||||
}
|
||||
small(Float64Array);
|
||||
small(Uint16Array);
|
||||
const s = Symbol(1), t = Symbol(true), u = Symbol(2.5);
|
||||
console.log(s.description, t.description, u.toString(), typeof s.description);
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// Named properties on typed arrays, Buffers and DataViews (assignment,
|
||||
// Object.defineProperty, reflection, deletion, integrity levels, JSON and
|
||||
// inspection), and the Symbol.species constructor of map, filter, slice and
|
||||
// subarray.
|
||||
function box(value) {
|
||||
const holder = {};
|
||||
holder.value = value;
|
||||
return holder.value;
|
||||
}
|
||||
|
||||
function expandos(TA) {
|
||||
const a = new TA([1, 2]);
|
||||
a.foo = 1;
|
||||
a.bar = "x";
|
||||
a[5] = 9;
|
||||
a["1.5"] = 7;
|
||||
console.log(a, a.foo, "foo" in a, a.hasOwnProperty("bar"), Object.keys(a), Object.entries(a).length);
|
||||
console.log(Object.getOwnPropertyNames(a), JSON.stringify(Object.getOwnPropertyDescriptor(a, "foo")), JSON.stringify(Object.getOwnPropertyDescriptor(a, "0")));
|
||||
Object.defineProperty(a, "hidden", { value: 3, enumerable: false });
|
||||
Object.defineProperty(a, "0", { value: 8 });
|
||||
console.log(a, a.hidden, Object.keys(a));
|
||||
try { Object.defineProperty(a, "1", { value: 1, configurable: false }); } catch (e) { console.log(e.constructor.name, e.message); }
|
||||
try { Object.defineProperty(a, "7", { value: 1 }); } catch (e) { console.log(e.constructor.name, e.message); }
|
||||
try { Object.defineProperty(a, "-0", { value: 1 }); } catch (e) { console.log(e.constructor.name, e.message); }
|
||||
try { Object.defineProperty(a, "0", { get() { return 1; } }); } catch (e) { console.log(e.constructor.name, e.message); }
|
||||
// An element's descriptor is read through its getters, and must be an object.
|
||||
const got = [];
|
||||
Object.defineProperty(a, "1", { get value() { got.push("value"); return 6; }, get enumerable() { got.push("enumerable"); return true; } });
|
||||
console.log(a[1], got);
|
||||
// Each field is read once, through its getter; the stored value is the
|
||||
// getter's result, and a getter-produced `writable: false` is refused.
|
||||
let valueReads = 0;
|
||||
Object.defineProperty(a, "1", { get value() { valueReads++; return 40 + valueReads; } });
|
||||
console.log(a[1], valueReads);
|
||||
const order = [];
|
||||
try {
|
||||
Object.defineProperty(a, "1", {
|
||||
get writable() { order.push("writable"); return false; },
|
||||
get value() { order.push("value"); return 2; },
|
||||
get configurable() { order.push("configurable"); return true; },
|
||||
});
|
||||
} catch (e) { console.log(e.constructor.name, e.message); }
|
||||
console.log(a[1], order);
|
||||
for (const bad of [null, true, 1n, Symbol("d")]) {
|
||||
try { Object.defineProperty(a, "1", bad); } catch (e) { console.log(e.constructor.name); }
|
||||
}
|
||||
for (const bad of [5, "x", undefined]) {
|
||||
try { Object.defineProperty(a, "0", bad); } catch (e) { console.log(e.constructor.name); }
|
||||
}
|
||||
try { Object.defineProperty(a, "9", 5); } catch (e) { console.log(e.constructor.name); }
|
||||
try { Object.defineProperty(a, "0", { get configurable() { throw new RangeError("read"); } }); } catch (e) { console.log(e.constructor.name, e.message); }
|
||||
try { Object.defineProperty(a, "0", { value: 1, get: undefined }); } catch (e) { console.log(e.constructor.name); }
|
||||
console.log(a[0], a[1]);
|
||||
let reads = 0;
|
||||
Object.defineProperty(a, "g", { get() { reads++; return this.length; }, enumerable: true });
|
||||
console.log(a.g, reads, new TA(0));
|
||||
const e = new TA(0);
|
||||
e.k = { deep: [1] };
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
function species(TA, Other) {
|
||||
const s = new TA([1, 2, 3, 4]);
|
||||
const calls = [];
|
||||
s.constructor = {};
|
||||
s.constructor[Symbol.species] = function (n) { calls.push(n); return new Other(n); };
|
||||
const m = s.map((v) => v * 1.5), f = s.filter((v) => v > 1), sl = s.slice(1, 3);
|
||||
console.log(m, f, sl, calls);
|
||||
s.constructor[Symbol.species] = function (buffer, offset, length) { calls.push([offset, length]); return new TA(buffer, offset, length); };
|
||||
const sub = s.subarray(1, 3);
|
||||
sub[0] = 50;
|
||||
console.log(sub, s[1], calls.length);
|
||||
s.constructor[Symbol.species] = function () { return {}; };
|
||||
try { s.map((v) => v); } catch (err) { console.log(err.constructor.name); }
|
||||
s.constructor[Symbol.species] = function () { return new TA(1); };
|
||||
try { s.slice(); } catch (err) { console.log(err.constructor.name); }
|
||||
s.constructor = 5;
|
||||
try { s.filter(() => true); } catch (err) { console.log(err.constructor.name); }
|
||||
s.constructor = undefined;
|
||||
console.log(s.map((v) => v + 1));
|
||||
const t = new TA([3, 1]);
|
||||
Object.defineProperty(t, "constructor", { get() { calls.push("get"); return undefined; } });
|
||||
console.log(t.slice(), calls[calls.length - 1]);
|
||||
const u = new TA([1, 2, 3]);
|
||||
u.constructor = { [Symbol.species]: Uint16Array };
|
||||
console.log(u.filter(() => true), u.subarray(1), u.subarray(1).constructor === Uint16Array);
|
||||
// A built-in constructor set as `constructor` is its own species.
|
||||
const w = new TA([1, 2, 3]);
|
||||
w.constructor = Float64Array;
|
||||
const wm = w.map((v) => v / 2), ws = w.slice(1);
|
||||
console.log(wm, wm.constructor === Float64Array, ws, ws.constructor === Float64Array, w.filter((v) => v > 1));
|
||||
w.constructor = TA;
|
||||
console.log(w.map((v) => v / 2).constructor === TA);
|
||||
// slice copies the source after the species constructor ran.
|
||||
const x = new TA([1, 2, 3, 4]);
|
||||
x.constructor = {};
|
||||
x.constructor[Symbol.species] = function (n) { x[1] = 40; x[2] = 30; return new Other(n); };
|
||||
console.log(x.slice(1, 3), x.slice(-1), x.slice(2, 1));
|
||||
}
|
||||
|
||||
function reflection(TA) {
|
||||
const a = new TA([1, 2]);
|
||||
a.foo = 1;
|
||||
a[Symbol.for("s")] = 2;
|
||||
const keys = [];
|
||||
for (const k in a) keys.push(k);
|
||||
console.log(keys, { ...a }, JSON.stringify(a), Object.values(a));
|
||||
console.log(a.propertyIsEnumerable("foo"), Object.hasOwn(a, "foo"), Object.getOwnPropertySymbols(a), a[Symbol.for("s")]);
|
||||
console.log(Object.assign(new TA(2), { x: 1, 0: 9 }), Object.assign({}, a));
|
||||
delete a.foo;
|
||||
delete a[5];
|
||||
delete a.length;
|
||||
console.log(a, "foo" in a, a.length);
|
||||
try { delete a[0]; } catch (err) { console.log(err.message); }
|
||||
Object.defineProperty(a, "fixed", { value: 1 });
|
||||
try { delete a.fixed; } catch (err) { console.log(err.message); }
|
||||
try { a.fixed = 2; } catch (err) { console.log(err.message); }
|
||||
console.log(a.subarray(0, 1).fixed, a.slice().fixed);
|
||||
let seen;
|
||||
Object.defineProperty(a, "s", { set(v) { seen = [v, this === a]; } });
|
||||
a.s = 7;
|
||||
console.log(seen);
|
||||
a.map = 5;
|
||||
try { a.map((v) => v); } catch (err) { console.log(err.message); }
|
||||
for (const key of ["length", "byteLength", "byteOffset", "buffer"]) {
|
||||
try { a[key] = 1; } catch (err) { console.log(err.message); }
|
||||
}
|
||||
try { a[Symbol.toStringTag] = "x"; } catch (err) { console.log(err.message); }
|
||||
// Cycles through a view's own properties are collected.
|
||||
const loop = new TA(1);
|
||||
loop.self = loop;
|
||||
loop.size = () => loop.length;
|
||||
console.log(loop.self === loop, loop.size());
|
||||
}
|
||||
|
||||
function integrity(TA) {
|
||||
const results = [];
|
||||
const attempt = (f) => { try { f(); results.push("ok"); } catch (err) { results.push(err.message); } };
|
||||
const sealed = new TA(1);
|
||||
sealed.x = 1;
|
||||
attempt(() => Object.seal(sealed));
|
||||
console.log(results, Object.isExtensible(sealed), Object.isSealed(sealed), JSON.stringify(Object.getOwnPropertyDescriptor(sealed, "x")));
|
||||
attempt(() => Object.seal(sealed));
|
||||
const frozen = new TA(1);
|
||||
frozen.x = 1;
|
||||
attempt(() => Object.freeze(frozen));
|
||||
attempt(() => Object.freeze(frozen));
|
||||
console.log(results, Object.isFrozen(frozen), JSON.stringify(Object.getOwnPropertyDescriptor(frozen, "x")));
|
||||
const closed = new TA(1);
|
||||
Object.defineProperty(closed, "q", { value: 1 });
|
||||
Object.preventExtensions(closed);
|
||||
try { closed.z = 1; } catch (err) { console.log(err.message); }
|
||||
try { closed[Symbol("n")] = 1; } catch (err) { console.log(err.message); }
|
||||
console.log(Object.isExtensible(closed), Object.isSealed(closed), Object.isFrozen(closed));
|
||||
const empty = new TA(0);
|
||||
empty.k = 1;
|
||||
Object.freeze(empty);
|
||||
try { empty.k = 2; } catch (err) { console.log(err.message); }
|
||||
console.log(Object.isFrozen(empty), Object.isSealed(empty), empty.k);
|
||||
}
|
||||
|
||||
expandos(Uint8Array);
|
||||
expandos(Float64Array);
|
||||
species(Int16Array, Float32Array);
|
||||
reflection(Uint8Array);
|
||||
reflection(Int32Array);
|
||||
integrity(Uint16Array);
|
||||
|
||||
const view = box(new DataView(new ArrayBuffer(2)));
|
||||
view[0] = 5;
|
||||
view.x = 1;
|
||||
console.log(view, Object.keys(view), view[0], JSON.stringify(view), "0" in view);
|
||||
try { view.byteLength = 1; } catch (err) { console.log(err.message); }
|
||||
const buffer = box(Buffer.from([1, 2]));
|
||||
buffer.foo = 1;
|
||||
buffer[Symbol.for("k")] = 2;
|
||||
console.log(buffer, Object.keys(buffer), JSON.stringify(buffer));
|
||||
for (const key of ["parent", "offset", "length"]) {
|
||||
try { buffer[key] = 1; } catch (err) { console.log(err.message); }
|
||||
}
|
||||
const empty = box(Buffer.alloc(0));
|
||||
empty.foo = { a: [1, { b: 2 }] };
|
||||
console.log(empty);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user