Merge main

This commit is contained in:
Chris Tate
2026-10-22 17:13:25 -05:00
103 changed files with 5856 additions and 270 deletions
@@ -7,4 +7,6 @@
They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`.
They also support the iterator helper methods, such as `.map()` and `.take()`.
Detached collection methods are unsupported.
Constructing a Map, Set, WeakMap or WeakSet, or calling `Object.fromEntries`, with a checked-dynamic iterable (a native iterator, a generator, or an object with a `Symbol.iterator` method) takes all of its values before adding the first one.
When an entry of a Map seed is not an object, the TypeError comes after the iterable is exhausted; Node closes the iterator at that entry.
The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
+3 -1
View File
@@ -3568,7 +3568,8 @@ declare module "child_process" {
* killSignal at the deadline and the result carries error: ETIMEDOUT +
* the signal, never a throw), killSignal (a signal NAME literal),
* stdio ("ignore"/"inherit"/"pipe" or a 3-tuple of those — captures
* read "" for non-piped streams), and windowsHide (a POSIX no-op). */
* read "" for non-piped streams), env (replaces the child's
* environment), and windowsHide (a POSIX no-op). */
export function spawnSync(
command: string,
args?: string[],
@@ -3577,6 +3578,7 @@ declare module "child_process" {
timeout?: number;
killSignal?: string;
stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[];
env?: { [k: string]: string | undefined };
windowsHide?: boolean;
},
): SpawnSyncReturns;
Binary file not shown.
+15
View File
@@ -56,3 +56,18 @@ void scr_collect_cycles_phase(void);
void scriptc_native_collect_cycles(void) {
scr_collect_cycles_phase();
}
/* The runtime's growth cap for full cycle passes (scr_rc.h). */
void scr_cyc_set_growth_cap(long cap);
/* Bounds the cyclic garbage a build can hold to twice its live heap.
* Lowering a large program leaves hundreds of megabytes of cyclic garbage,
* and whether a scheduled full pass reclaims it before the build's peak
* depends on where the growth rule fires: with the default cap of 16, small
* changes to the compiler moved the peak of one build by 390 MB. With a cap
* of 2 a full pass runs once the heap has grown to three times its size
* after the last one; a cap of 1 bounds the garbage further but runs more
* passes over the live heap. SCR_CYCLE_GROWTH_CAP still overrides it. */
void scriptc_native_bound_cycle_growth(void) {
scr_cyc_set_growth_cap(2);
}
+2 -1
View File
@@ -2,6 +2,7 @@
"ffi_format": 6,
"functions": [
{ "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" },
{ "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" }
{ "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" },
{ "name": "compilerNativeBoundCycleGrowth", "symbol": "scriptc_native_bound_cycle_growth", "params": [], "returns": "void" }
]
}
+688
View File
@@ -0,0 +1,688 @@
// scriptc's built-in implementation of the `chalk` package, 5.x API.
//
// Programs whose installed chalk is 5.x compile this module in its place
// (packages/compiler/src/frontend/package-shims.ts). chalk builds its
// styles as functions whose prototypes it swaps at run time; here a style
// chain is an instance of one class whose style members are cached
// getters, and the instance is callable through its `%call` field (the
// merged interface below gives it chalk's call signature; the lowerer calls
// the field). The output, color-level detection (supports-color 10:
// FORCE_COLOR, the --color/--no-color flags, TTY, TERM, CI variables) and
// the shared, settable `level` follow chalk 5.6 byte for byte.
import * as os from "node:os";
import * as tty from "node:tty";
export type ColorSupportLevel = 0 | 1 | 2 | 3;
export interface ColorSupport {
level: ColorSupportLevel;
hasBasic: boolean;
has256: boolean;
has16m: boolean;
}
export type ColorInfo = ColorSupport | false;
export interface Options {
readonly level?: ColorSupportLevel;
}
export type ModifierName =
| "reset"
| "bold"
| "dim"
| "italic"
| "underline"
| "overline"
| "inverse"
| "hidden"
| "strikethrough";
export type ForegroundColorName =
| "black"
| "red"
| "green"
| "yellow"
| "blue"
| "cyan"
| "magenta"
| "white"
| "gray"
| "grey"
| "blackBright"
| "redBright"
| "greenBright"
| "yellowBright"
| "blueBright"
| "cyanBright"
| "magentaBright"
| "whiteBright";
export type BackgroundColorName =
| "bgBlack"
| "bgRed"
| "bgGreen"
| "bgYellow"
| "bgBlue"
| "bgCyan"
| "bgMagenta"
| "bgWhite"
| "bgGray"
| "bgGrey"
| "bgBlackBright"
| "bgRedBright"
| "bgGreenBright"
| "bgYellowBright"
| "bgBlueBright"
| "bgCyanBright"
| "bgMagentaBright"
| "bgWhiteBright";
export type ColorName = ForegroundColorName | BackgroundColorName;
export type Modifiers = ModifierName;
export type ForegroundColor = ForegroundColorName;
export type BackgroundColor = BackgroundColorName;
export type Color = ColorName;
// ── ansi-styles ──────────────────────────────────────────────────────────
export const modifierNames: ModifierName[] = [
"reset",
"bold",
"dim",
"italic",
"underline",
"overline",
"inverse",
"hidden",
"strikethrough",
];
export const foregroundColorNames: ForegroundColorName[] = [
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"blackBright",
"gray",
"grey",
"redBright",
"greenBright",
"yellowBright",
"blueBright",
"magentaBright",
"cyanBright",
"whiteBright",
];
export const backgroundColorNames: BackgroundColorName[] = [
"bgBlack",
"bgRed",
"bgGreen",
"bgYellow",
"bgBlue",
"bgMagenta",
"bgCyan",
"bgWhite",
"bgBlackBright",
"bgGray",
"bgGrey",
"bgRedBright",
"bgGreenBright",
"bgYellowBright",
"bgBlueBright",
"bgMagentaBright",
"bgCyanBright",
"bgWhiteBright",
];
export const colorNames: ColorName[] = [...foregroundColorNames, ...backgroundColorNames];
export const modifiers: Modifiers[] = modifierNames;
export const foregroundColors: ForegroundColor[] = foregroundColorNames;
export const backgroundColors: BackgroundColor[] = backgroundColorNames;
export const colors: Color[] = colorNames;
const ESC = "\u001B[";
/** Open and close codes, indexed by style id (the order of the getters). */
const OPEN_CODES: number[] = [
0, 1, 2, 3, 4, 53, 7, 8, 9, 30, 31, 32, 33, 34, 35, 36, 37, 90, 90, 90, 91, 92, 93, 94, 95, 96,
97, 40, 41, 42, 43, 44, 45, 46, 47, 100, 100, 100, 101, 102, 103, 104, 105, 106, 107,
];
const CLOSE_CODES: number[] = [
0, 22, 22, 23, 24, 55, 27, 28, 29, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
39, 39, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49,
];
const VISIBLE = 45;
function rgbToAnsi256(red: number, green: number, blue: number): number {
if (red === green && green === blue) {
if (red < 8) return 16;
if (red > 248) return 231;
return Math.round(((red - 8) / 247) * 24) + 232;
}
return (
16 +
36 * Math.round((red / 255) * 5) +
6 * Math.round((green / 255) * 5) +
Math.round((blue / 255) * 5)
);
}
function hexToRgb(hex: string): number[] {
const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex);
if (matches === null) return [0, 0, 0];
let colorString = matches[0];
if (colorString.length === 3) {
let doubled = "";
for (const character of colorString) doubled += character + character;
colorString = doubled;
}
const integer = Number.parseInt(colorString, 16);
return [(integer >> 16) & 0xff, (integer >> 8) & 0xff, integer & 0xff];
}
function ansi256ToAnsi(code: number): number {
if (code < 8) return 30 + code;
if (code < 16) return 90 + (code - 8);
let red: number;
let green: number;
let blue: number;
if (code >= 232) {
red = ((code - 232) * 10 + 8) / 255;
green = red;
blue = red;
} else {
code -= 16;
const remainder = code % 36;
red = Math.floor(code / 36) / 5;
green = Math.floor(remainder / 6) / 5;
blue = (remainder % 6) / 5;
}
const value = Math.max(red, green, blue) * 2;
if (value === 0) return 30;
let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red));
if (value === 2) result += 60;
return result;
}
/** The open code of a color model at a level: offset 0 is the foreground,
* 10 the background (ansi-styles' wrapAnsi16/256/16m). */
function rgbOpen(level: number, offset: number, red: number, green: number, blue: number): string {
if (level === 3) return `${ESC}${38 + offset};2;${red};${green};${blue}m`;
if (level === 2) return `${ESC}${38 + offset};5;${rgbToAnsi256(red, green, blue)}m`;
return `${ESC}${ansi256ToAnsi(rgbToAnsi256(red, green, blue)) + offset}m`;
}
/** chalk's ansi256 model emits the 256-color code at every level (only
* the rgb and hex models downsample). */
function ansi256Open(offset: number, code: number): string {
return `${ESC}${38 + offset};5;${code}m`;
}
// ── supports-color ───────────────────────────────────────────────────────
function hasFlag(flag: string): boolean {
const argv = process.argv;
const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
const position = argv.indexOf(prefix + flag);
const terminatorPosition = argv.indexOf("--");
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
}
let flagForceColor: number | undefined = undefined;
if (
hasFlag("no-color") ||
hasFlag("no-colors") ||
hasFlag("color=false") ||
hasFlag("color=never")
) {
flagForceColor = 0;
} else if (
hasFlag("color") ||
hasFlag("colors") ||
hasFlag("color=true") ||
hasFlag("color=always")
) {
flagForceColor = 1;
}
function envForceColor(): number | undefined {
const force = process.env["FORCE_COLOR"];
if (force !== undefined) {
if (force === "true") return 1;
if (force === "false") return 0;
return force.length === 0 ? 1 : Math.min(Number.parseInt(force, 10), 3);
}
return undefined;
}
function translateLevel(level: number): ColorInfo {
if (level === 0) return false;
return {
level: level as ColorSupportLevel,
hasBasic: true,
has256: level >= 2,
has16m: level >= 3,
};
}
function inEnv(name: string): boolean {
return process.env[name] !== undefined;
}
function detectLevel(streamIsTTY: boolean): number {
const noFlagForceColor = envForceColor();
if (noFlagForceColor !== undefined) flagForceColor = noFlagForceColor;
const forceColor = flagForceColor;
if (forceColor === 0) return 0;
if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) return 3;
if (hasFlag("color=256")) return 2;
if (inEnv("TF_BUILD") && inEnv("AGENT_NAME")) return 1;
if (!streamIsTTY && forceColor === undefined) return 0;
const min =
forceColor === undefined || forceColor === 0 || Number.isNaN(forceColor) ? 0 : forceColor;
const env = process.env;
const term = env["TERM"];
if (term === "dumb") return min;
if (process.platform === "win32") {
const osRelease = os.release().split(".");
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10_586) {
return Number(osRelease[2]) >= 14_931 ? 3 : 2;
}
return 1;
}
if (inEnv("CI")) {
if (inEnv("GITHUB_ACTIONS") || inEnv("GITEA_ACTIONS") || inEnv("CIRCLECI")) return 3;
if (
inEnv("TRAVIS") ||
inEnv("APPVEYOR") ||
inEnv("GITLAB_CI") ||
inEnv("BUILDKITE") ||
inEnv("DRONE") ||
env["CI_NAME"] === "codeship"
) {
return 1;
}
return min;
}
const teamcity = env["TEAMCITY_VERSION"];
if (teamcity !== undefined) {
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(teamcity) ? 1 : 0;
}
if (env["COLORTERM"] === "truecolor") return 3;
if (term === "xterm-kitty") return 3;
if (term === "xterm-ghostty") return 3;
if (term === "wezterm") return 3;
const termProgram = env["TERM_PROGRAM"];
if (termProgram !== undefined) {
const version = Number.parseInt((env["TERM_PROGRAM_VERSION"] || "").split(".")[0]!, 10);
if (termProgram === "iTerm.app") return version >= 3 ? 3 : 2;
if (termProgram === "Apple_Terminal") return 2;
}
// supports-color tests these against `env.TERM`, which is the string
// "undefined" when TERM is unset.
const termText = term === undefined ? "undefined" : term;
if (/-256(color)?$/i.test(termText)) return 2;
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(termText)) return 1;
if (inEnv("COLORTERM")) return 1;
return min;
}
const stdoutColor: ColorInfo = translateLevel(detectLevel(tty.isatty(1)));
const stderrColor: ColorInfo = translateLevel(detectLevel(tty.isatty(2)));
export const supportsColor: ColorInfo = stdoutColor;
export const supportsColorStderr: ColorInfo = stderrColor;
// ── the builder ──────────────────────────────────────────────────────────
/** One link of a style chain (chalk's styler). */
class Styler {
open: string;
close: string;
openAll: string;
closeAll: string;
parent: Styler | undefined;
constructor(open: string, close: string, parent: Styler | undefined) {
this.open = open;
this.close = close;
this.parent = parent;
if (parent === undefined) {
this.openAll = open;
this.closeAll = close;
} else {
this.openAll = parent.openAll + open;
this.closeAll = close + parent.closeAll;
}
}
}
/** The level every builder of one chalk instance shares. */
class LevelBox {
level: number;
constructor(level: number) {
this.level = level;
}
}
function stringReplaceAll(string: string, substring: string, replacer: string): string {
let index = string.indexOf(substring);
if (index === -1) return string;
const substringLength = substring.length;
let endIndex = 0;
let returnValue = "";
do {
returnValue += string.slice(endIndex, index) + substring + replacer;
endIndex = index + substringLength;
index = string.indexOf(substring, endIndex);
} while (index !== -1);
returnValue += string.slice(endIndex);
return returnValue;
}
function stringEncaseCRLFWithFirstIndex(
string: string,
prefix: string,
postfix: string,
index: number,
): string {
let endIndex = 0;
let returnValue = "";
do {
const gotCR = string[index - 1] === "\r";
returnValue +=
string.slice(endIndex, gotCR ? index - 1 : index) +
prefix +
(gotCR ? "\r\n" : "\n") +
postfix;
endIndex = index + 1;
index = string.indexOf("\n", endIndex);
} while (index !== -1);
returnValue += string.slice(endIndex);
return returnValue;
}
function applyStyle(self: ChalkInstance, string: string): string {
if (self.box.level <= 0 || !string) return self.isEmpty ? "" : string;
let styler = self.styler;
if (styler === undefined) return string;
const openAll = styler.openAll;
const closeAll = styler.closeAll;
if (string.includes("\u001B")) {
while (styler !== undefined) {
string = stringReplaceAll(string, styler.close, styler.open);
styler = styler.parent;
}
}
const lfIndex = string.indexOf("\n");
if (lfIndex !== -1) string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
return openAll + string + closeAll;
}
function joinArguments(args: unknown[]): string {
let out = "";
for (let i = 0; i < args.length; i++) {
if (i > 0) out += " ";
const value = args[i];
if (value !== undefined && value !== null) out += String(value);
}
return out;
}
/** A chalk instance or one of its style chains: callable (chalk's call
* signature merges in from the interface), with every style a cached
* getter. */
// The merge is the point: the interface gives instances chalk's call
// signature, and the class's `%call` field implements it.
// oxlint-disable-next-line typescript/no-unsafe-declaration-merging
export interface ChalkInstance {
(...text: unknown[]): string;
}
// oxlint-disable-next-line typescript/no-unsafe-declaration-merging
export class ChalkInstance {
"%call": (...text: unknown[]) => string = (...text: unknown[]): string =>
this.root
? joinArguments(text)
: applyStyle(this, text.length === 1 ? "" + String(text[0]) : joinArguments(text));
box: LevelBox;
styler: Styler | undefined;
isEmpty: boolean;
private readonly root: boolean;
private readonly cache: (ChalkInstance | undefined)[] = [];
constructor(box: LevelBox, styler: Styler | undefined, isEmpty: boolean, root: boolean) {
this.box = box;
this.styler = styler;
this.isEmpty = isEmpty;
this.root = root;
}
/** One string argument: the compiler calls this instead of `%call`
* (no variadic arguments to box). */
"%call1"(text: string): string {
return this.root ? text : applyStyle(this, text);
}
get level(): ColorSupportLevel {
return this.box.level as ColorSupportLevel;
}
set level(level: ColorSupportLevel) {
this.box.level = level;
}
private style(id: number): ChalkInstance {
const cached = this.cache[id];
if (cached !== undefined) return cached;
let builder: ChalkInstance;
if (id === VISIBLE) {
builder = new ChalkInstance(this.box, this.styler, true, false);
} else {
const styler = new Styler(
`${ESC}${OPEN_CODES[id]!}m`,
`${ESC}${CLOSE_CODES[id]!}m`,
this.styler,
);
builder = new ChalkInstance(this.box, styler, this.isEmpty, false);
}
while (this.cache.length <= id) this.cache.push(undefined);
this.cache[id] = builder;
return builder;
}
private model(open: string, background: boolean): ChalkInstance {
const styler = new Styler(open, background ? `${ESC}49m` : `${ESC}39m`, this.styler);
return new ChalkInstance(this.box, styler, this.isEmpty, false);
}
rgb(red: number, green: number, blue: number): ChalkInstance {
return this.model(rgbOpen(this.box.level, 0, red, green, blue), false);
}
hex(color: string): ChalkInstance {
const [red, green, blue] = hexToRgb(color);
return this.model(rgbOpen(this.box.level, 0, red!, green!, blue!), false);
}
ansi256(index: number): ChalkInstance {
return this.model(ansi256Open(0, index), false);
}
bgRgb(red: number, green: number, blue: number): ChalkInstance {
return this.model(rgbOpen(this.box.level, 10, red, green, blue), true);
}
bgHex(color: string): ChalkInstance {
const [red, green, blue] = hexToRgb(color);
return this.model(rgbOpen(this.box.level, 10, red!, green!, blue!), true);
}
bgAnsi256(index: number): ChalkInstance {
return this.model(ansi256Open(10, index), true);
}
get reset(): ChalkInstance {
return this.style(0);
}
get bold(): ChalkInstance {
return this.style(1);
}
get dim(): ChalkInstance {
return this.style(2);
}
get italic(): ChalkInstance {
return this.style(3);
}
get underline(): ChalkInstance {
return this.style(4);
}
get overline(): ChalkInstance {
return this.style(5);
}
get inverse(): ChalkInstance {
return this.style(6);
}
get hidden(): ChalkInstance {
return this.style(7);
}
get strikethrough(): ChalkInstance {
return this.style(8);
}
get visible(): ChalkInstance {
return this.style(VISIBLE);
}
get black(): ChalkInstance {
return this.style(9);
}
get red(): ChalkInstance {
return this.style(10);
}
get green(): ChalkInstance {
return this.style(11);
}
get yellow(): ChalkInstance {
return this.style(12);
}
get blue(): ChalkInstance {
return this.style(13);
}
get magenta(): ChalkInstance {
return this.style(14);
}
get cyan(): ChalkInstance {
return this.style(15);
}
get white(): ChalkInstance {
return this.style(16);
}
get blackBright(): ChalkInstance {
return this.style(17);
}
get gray(): ChalkInstance {
return this.style(18);
}
get grey(): ChalkInstance {
return this.style(19);
}
get redBright(): ChalkInstance {
return this.style(20);
}
get greenBright(): ChalkInstance {
return this.style(21);
}
get yellowBright(): ChalkInstance {
return this.style(22);
}
get blueBright(): ChalkInstance {
return this.style(23);
}
get magentaBright(): ChalkInstance {
return this.style(24);
}
get cyanBright(): ChalkInstance {
return this.style(25);
}
get whiteBright(): ChalkInstance {
return this.style(26);
}
get bgBlack(): ChalkInstance {
return this.style(27);
}
get bgRed(): ChalkInstance {
return this.style(28);
}
get bgGreen(): ChalkInstance {
return this.style(29);
}
get bgYellow(): ChalkInstance {
return this.style(30);
}
get bgBlue(): ChalkInstance {
return this.style(31);
}
get bgMagenta(): ChalkInstance {
return this.style(32);
}
get bgCyan(): ChalkInstance {
return this.style(33);
}
get bgWhite(): ChalkInstance {
return this.style(34);
}
get bgBlackBright(): ChalkInstance {
return this.style(35);
}
get bgGray(): ChalkInstance {
return this.style(36);
}
get bgGrey(): ChalkInstance {
return this.style(37);
}
get bgRedBright(): ChalkInstance {
return this.style(38);
}
get bgGreenBright(): ChalkInstance {
return this.style(39);
}
get bgYellowBright(): ChalkInstance {
return this.style(40);
}
get bgBlueBright(): ChalkInstance {
return this.style(41);
}
get bgMagentaBright(): ChalkInstance {
return this.style(42);
}
get bgCyanBright(): ChalkInstance {
return this.style(43);
}
get bgWhiteBright(): ChalkInstance {
return this.style(44);
}
}
function optionsLevel(options: Options | undefined): number {
const level = options === undefined ? undefined : options.level;
if (
level !== undefined &&
level !== 0 &&
!(Number.isInteger(level) && level >= 0 && level <= 3)
) {
throw new Error("The `level` option should be an integer from 0 to 3");
}
if (level === undefined) return stdoutColor === false ? 0 : stdoutColor.level;
return level;
}
/** `new Chalk(options)`: a fresh chalk instance with its own level. */
export class Chalk extends ChalkInstance {
constructor(options?: Options) {
super(new LevelBox(optionsLevel(options)), undefined, false, true);
}
}
const chalk: ChalkInstance = new Chalk();
export const chalkStderr: ChalkInstance = new Chalk({
level: stderrColor === false ? 0 : stderrColor.level,
});
export default chalk;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// The hot-shots 8.x shim for installations with the optional unix-dgram
// addon: the uds protocol sends through a native Unix datagram socket.
import { configureUnixDgram } from "./client.ts";
configureUnixDgram(true);
export * from "./client.ts";
export { default } from "./client.ts";
+8
View File
@@ -0,0 +1,8 @@
// The hot-shots 8.x shim for installations without the optional unix-dgram
// addon: the uds protocol reports it missing, as hot-shots does under Node.
import { configureUnixDgram } from "./client.ts";
configureUnixDgram(false);
export * from "./client.ts";
export { default } from "./client.ts";
@@ -17,6 +17,7 @@ import {
} from "../ir/ir.js";
import { computeTraced, explainCycle } from "./cycle-analysis.js";
import { computeTraced as llvmTraced } from "./llvm/shapes.js";
import { addRecordResultEntries } from "./llvm/record-results.js";
const loc = { file: "cycles.ts", start: 0, end: 1 };
function module(): IrModule {
@@ -404,6 +405,61 @@ test("a later write through any alias makes the edge mutable", () => {
checkExact(record, ["record:list"]);
});
test("a result-slot entry's stores into its destination are not writes", () => {
const next: IrType = { kind: "union", unionId: "next" };
const listModule = (): IrModule => {
const mod = module();
mod.records = [shape("list", [next, F64])];
mod.unions = [{ id: "next", arms: [ref("list"), { kind: "nullT" }] }];
mod.functions.push({
name: "cell",
params: [{ localId: "rest", name: "rest", type: next }],
locals: [],
returnType: ref("list"),
body: [
{
kind: "return",
value: {
kind: "recordLit",
fields: [
{ name: "field0", value: { kind: "varRef", localId: "rest", type: next, loc } },
{ name: "field1", value: { kind: "numLit", value: 1, type: F64, loc } },
],
type: ref("list"),
loc,
},
loc,
},
],
loc,
});
return addRecordResultEntries(mod);
};
// The entry stores the literal's fields into caller frame storage that
// nothing can point to: the list stays acyclic.
const entries = listModule();
expect(entries.functions.map((fn) => fn.name)).toContain("%into.cell");
checkExact(entries, []);
// The same store into the destination's local id outside an entry, and a
// store into any other binding of the shape inside one, are writes.
const store = (localId: string): IrStmt => ({
kind: "recordSet",
obj: { kind: "varRef", localId, type: ref("list"), loc },
shapeId: "list",
field: "field0",
value: value(next),
loc,
});
for (const [fn, localId] of [
["main", "%into.dst"],
["%into.cell", "v"],
] as const) {
const written = listModule();
written.functions.find((f) => f.name === fn)!.body.unshift(store(localId));
checkExact(written, ["record:list"], ["next"]);
}
});
test("field increments count as writes", () => {
const mod = tree();
mod.functions[0]!.body.push({
@@ -11,7 +11,7 @@ import {
STRING,
VOID,
} from "../ir/ir.js";
import { computeCycleMutability, fieldKey } from "./cycle-mutability.js";
import { type CycleMutability, computeCycleMutability, fieldKey } from "./cycle-mutability.js";
/** Cycle capability for native code generation.
* Nodes are shape units and unions; edges are the references their fields
@@ -38,8 +38,10 @@ import { computeCycleMutability, fieldKey } from "./cycle-mutability.js";
export function computeTraced(
mod: IrModule,
stores?: DynamicFieldStores,
/** computeCycleMutability's answer for this module, when the caller has it. */
precomputed?: CycleMutability,
): { shapes: Set<string>; unions: Set<string> } {
const { shapes, unions } = analyzeCycles(mod, false, stores);
const { shapes, unions } = analyzeCycles(mod, false, stores, precomputed);
return { shapes, unions };
}
@@ -93,6 +95,7 @@ function analyzeCycles(
mod: IrModule,
labels: boolean,
stores?: DynamicFieldStores,
precomputed?: CycleMutability,
): {
shapes: Set<string>;
unions: Set<string>;
@@ -125,7 +128,7 @@ function analyzeCycles(
const lifetimeRoots = new Set(mod.contracts?.programLifetime ?? []);
const programLifetime = (className: string): boolean =>
lifetimeRoots.size > 0 && lifetimeRoots.has(rootOf(className));
const mutability = computeCycleMutability(mod, unitOfClass, stores);
const mutability = precomputed ?? computeCycleMutability(mod, unitOfClass, stores);
const shapeDefs = [
...classes.map((c) => ({
key: `object:${c.name}`,
@@ -2,6 +2,7 @@ import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js";
import { RUNTIME_EMITTER_CLASS, RUNTIME_STREAM_CLASSES, typeKey } from "../ir/ir.js";
import { dynamicFieldStores, type DynamicFieldStores } from "../ir/int32-slots.js";
import { everyExprChild, everyStmtChild, everyTypeChild } from "../ir/traverse.js";
import { resultSlotStore } from "./result-slots.js";
/** Which shape fields can be written after their object escapes.
*
@@ -17,6 +18,10 @@ import { everyExprChild, everyStmtChild, everyTypeChild } from "../ir/traverse.j
* - every `fieldSet` outside a constructor's own `this`, `fieldIncDec`, and
* `recordSet` makes its (unit, field) mutable; `recordKeySet` on declared
* fields and `recordKeyDelete` make the whole record mutable;
* - a result-slot entry's stores into its destination (result-slots.ts) are
* not writes: the destination is frame storage of a caller that only
* projects the value, so no object can point to it and its fields cannot
* lie on a cycle;
* - a constructor's stores into its own `this` are construction-only up to
* the first top-level statement in which `this` escapes (any use other
* than a field store target, a field read, or the receiver of the direct
@@ -69,6 +74,22 @@ export function fieldKey(unit: string, field: string): string {
return `${unit}\0${field}`;
}
/** The cycle unit of a class: `object:<root>`, the root of its hierarchy,
* as computeImmutableFields and computeTraced group classes. */
export function hierarchyUnitOfClass(mod: IrModule): (className: string) => string {
const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const));
const roots = new Map<string, string>();
return (name) => {
const known = roots.get(name);
if (known !== undefined) return `object:${known}`;
let cur = name;
for (let base = baseOf.get(cur); base !== null && base !== undefined; base = baseOf.get(cur))
cur = base;
roots.set(name, cur);
return `object:${cur}`;
};
}
export function computeCycleMutability(
mod: IrModule,
unitOfClass: (className: string) => string,
@@ -296,7 +317,8 @@ export function computeCycleMutability(
result.fields.add(fieldKey(unitOfClass(s.className), s.field));
break;
case "recordSet":
result.fields.add(fieldKey(`record:${s.shapeId}`, s.field));
if (!resultSlotStore(fn.name, s))
result.fields.add(fieldKey(`record:${s.shapeId}`, s.field));
break;
case "recordKeySet":
if (s.overflowOnly !== true) result.units.add(`record:${s.shapeId}`);
@@ -48,6 +48,8 @@ export class ImmutableFields {
export function computeImmutableFields(
mod: IrModule,
stores?: DynamicFieldStores,
/** computeCycleMutability's answer for this module, when the caller has it. */
precomputed?: CycleMutability,
): ImmutableFields | null {
if (mod.lib !== undefined) return null;
const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const));
@@ -62,7 +64,7 @@ export function computeImmutableFields(
return cur;
};
const unitOfClass = (name: string): string => `object:${rootOf(name)}`;
const mutability = computeCycleMutability(mod, unitOfClass, stores);
const mutability = precomputed ?? computeCycleMutability(mod, unitOfClass, stores);
return mutability.all ? null : new ImmutableFields(mutability, unitOfClass);
}
@@ -213,6 +215,9 @@ export function stableGlobals(mod: IrModule): Set<string> {
}
return everyStmtChild(s, expr, stmt);
};
for (const fn of mod.functions) fn.body.every(stmt);
for (const fn of mod.functions) {
if (candidates.size === 0) break;
fn.body.every(stmt);
}
return candidates;
}
@@ -85,6 +85,7 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr>
for (const field of fieldAliases.get(binding) ?? []) appendedFields.add(field);
const result = new Set<IrExpr>();
if (appendedBindings.size === 0 && appendedFields.size === 0) return result;
const consider = (value: IrExpr | null, appended: boolean): void => {
if (value && appended && isEmptyLiteral(value)) result.add(value);
};
@@ -252,11 +252,13 @@ export function findConstantCallbacks(
// entries actually used as indirect callees in each body. A module-wide
// global/function cross product would dominate large-source emission.
const globalTargets = new Map<string, string>();
const result = new Map<string, Map<string, string>>();
// Only indirect callees get targets: without one, nothing resolves.
if (![...indirectCallees.values()].some((callees) => callees.size > 0)) return result;
for (const [id, value] of globals) {
const target = resolve(null, value);
if (target) globalTargets.set(id, target);
}
const result = new Map<string, Map<string, string>>();
for (const fn of mod.functions) {
const needed = indirectCallees.get(fn.name)!;
if (!needed.size) continue;
@@ -37,7 +37,7 @@ import { InternalCompilerError } from "../../errors.js";
import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js";
import { emitBorrowedInput } from "./borrowed-inputs.js";
import { f64Lit, llvmCommentText, loadArrCount, sizeToDouble } from "./common.js";
import { RESULT_DST_ID, RESULT_ENTRY_PREFIX } from "./record-results.js";
import { resultSlotStore } from "../result-slots.js";
import {
matchIntegerArrayForLoop,
matchIntegerBytesForLoop,
@@ -573,13 +573,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
? emitter.classFieldPtr(obj.name, s.className, s.field, "compressed")
: emitter.recordFieldPtr(obj.name, s.shapeId, s.field);
// A result-slot entry fills zeroed caller storage, each field once.
if (
s.kind === "recordSet" &&
s.obj.kind === "varRef" &&
s.obj.localId === RESULT_DST_ID &&
emitter.currentFunctionName.startsWith(RESULT_ENTRY_PREFIX)
)
emitter.initField(ptr, type, inline, v);
if (resultSlotStore(emitter.currentFunctionName, s)) emitter.initField(ptr, type, inline, v);
else emitter.replaceField(ptr, type, inline, v);
if (
s.kind === "fieldSet" &&
@@ -176,6 +176,7 @@ import { emitDynamicExpr } from "./expr-dynamic.js";
import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js";
import { PublishWalkers } from "./publish.js";
import { computeImmutableFields, stableGlobals, type ImmutableFields } from "../immutability.js";
import { computeCycleMutability, hierarchyUnitOfClass } from "../cycle-mutability.js";
import { computePublishedTypes, type PublishedTypes } from "../publication.js";
import { emitExpr } from "./expr-dispatch.js";
@@ -1330,8 +1331,10 @@ export class LlEmitter {
// and per async fiber; WASI, Windows and library builds have none.
const triple = (options.targetTriple ?? "").toLowerCase();
const windows = triple === "" ? process.platform === "win32" : triple.includes("windows");
this.publishedTypes = computePublishedTypes(mod);
const mt = computeMayThrow(mod, {
stackChecks: !this.wasi && mod.lib === undefined && !windows,
published: this.publishedTypes,
});
this.mayThrow = mt.fns;
this.workerEntryPolls = mt.workerEntryPolls ?? null;
@@ -1340,11 +1343,12 @@ export class LlEmitter {
// runtime moves deep prefix nests to a fresh stack using the guard,
// which Windows lacks.
this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows;
this.publishedTypes = computePublishedTypes(mod);
this.unframedFunctions = this.stackTraces
? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes)
: new Set();
this.immutableFields = computeImmutableFields(mod, dynamicStores);
// One field-mutability answer serves immutable fields and traced shapes.
const cycleMutability = computeCycleMutability(mod, hierarchyUnitOfClass(mod), dynamicStores);
this.immutableFields = computeImmutableFields(mod, dynamicStores, cycleMutability);
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
this.indirectMayThrow = mt.indirect || mod.workers === true;
for (const cls of mod.classes ?? []) {
@@ -1469,7 +1473,7 @@ export class LlEmitter {
this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
this.lazyCaptures = new LazyCaptures(this.fnByName);
const traced = computeTraced(mod, dynamicStores);
const traced = computeTraced(mod, dynamicStores, cycleMutability);
this.tracedShapes = traced.shapes;
this.tracedUnions = traced.unions;
// A shape union's value is whichever record it holds, and the collector
@@ -455,11 +455,13 @@ export function emitToUint32(
B.line(`${out} = select i1 ${ordered}, i32 ${low}, i32 0`);
return out;
}
// Below 2^63 in magnitude, the i64 conversion truncates exactly (every
// double above 2^53 is an integer), and its low 32 bits are ToUint32.
const aboveMin = B.tmp();
const belowMax = B.tmp();
const fast = B.tmp();
B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9007199254740992)}`);
B.line(`${belowMax} = fcmp ole double ${value}, ${f64Lit(9007199254740992)}`);
B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9223372036854775808)}`);
B.line(`${belowMax} = fcmp olt double ${value}, ${f64Lit(9223372036854775808)}`);
B.line(`${fast} = and i1 ${aboveMin}, ${belowMax}`);
const fastLabel = B.newLabel("uint32.coerce.fast");
@@ -475,8 +477,8 @@ export function emitToUint32(
B.br(done);
B.startBlock(slowLabel);
// Only NaN, the infinities and magnitudes above 2^53 reach this block.
// Such a finite double is the integer mantissa * 2^shift with shift >= 1,
// Only NaN, the infinities and magnitudes from 2^63 reach this block.
// Such a finite double is the integer mantissa * 2^shift with shift >= 11,
// so its low 32 bits are the shifted mantissa's (none once shift >= 64),
// negated for a negative value. NaN and the infinities have the maximum
// exponent, a shift of 972, and convert to 0 like ToUint32. Integer
@@ -200,6 +200,10 @@ test("identity bitwise forms over a proven int32 return the operand's number", (
}
// Unproven operands keep the full ToInt32; other operators still compute.
expect(body("unknown_or")).toContain("uint32.coerce.slow");
// The i64 conversion covers every magnitude below 2^63; only NaN, the
// infinities and larger magnitudes take the slow block.
expect(body("unknown_or")).toMatch(/ = fcmp oge double %\S+, 0xC3E0000000000000$/m);
expect(body("unknown_or")).toMatch(/ = fcmp olt double %\S+, 0x43E0000000000000$/m);
expect(body("length_and")).toMatch(/ = and i32 .*, 255$/m);
});
@@ -105,6 +105,10 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
"dgram.sendStr": "scr_dgram_send_str",
"dgram.sendBytes": "scr_dgram_send_bytes",
"dgram.sendChk": "scr_dgram_send_chk",
"dgram.unixSocket": "scr_dgram_unix_socket",
"dgram.unixConnect": "scr_dgram_unix_connect",
"dgram.unixSend": "scr_dgram_unix_send",
"dgram.unixClose": "scr_dgram_unix_close",
"dgram.ref": "scr_dgram_ref",
"dgram.unref": "scr_dgram_unref",
"dgram.setBroadcast": "scr_dgram_set_broadcast",
@@ -758,6 +758,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"cp.spawnSync": "scr_spawn_sync",
"cp.spawnSyncOpts": "scr_spawn_sync_opts",
"cp.spawnSyncStdioStr": "scr_spawn_sync_stdio_str",
"cp.spawnSyncEnv": "scr_spawn_sync_env",
"cp.execSync": "scr_exec_sync",
"cp.execCapture": "scr_exec_capture",
"spawnRes.stdout": "scr_spawn_res_stdout",
@@ -279,6 +279,8 @@ function shapeUnion(
* tag and payload inside the C map. */
function boxedUnionIds(mod: IrModule): Set<string> {
const boxed = new Set<string>();
// Only the module's union definitions consult the result.
if (!mod.unions?.length) return boxed;
const mark = (t: IrType): void => {
if (t.kind === "union") boxed.add(t.unionId);
};
@@ -12,20 +12,9 @@ import {
import { dynamicFieldStores } from "../../ir/int32-slots.js";
import { everyExpr, everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { InlineUnionFields } from "./inline-unions.js";
import { RESULT_DST_ID, resultEntryName } from "../result-slots.js";
/** Prefix of a function's result-slot entry: `%into.<name>(...params, dst)`
* writes the function's fresh result object into `dst` and returns nothing. */
export const RESULT_ENTRY_PREFIX = "%into.";
export function resultEntryName(name: string): string {
return `${RESULT_ENTRY_PREFIX}${name}`;
}
/** The local id of a result-slot entry's destination parameter. The
* caller passes zeroed frame storage, and on every path the entry stores
* each of its fields at most once, so a store into it is an
* initialization: there is no old value to release. */
export const RESULT_DST_ID = "%into.dst";
export { RESULT_DST_ID, RESULT_ENTRY_PREFIX, resultEntryName } from "../result-slots.js";
function distinctNames(names: readonly string[]): boolean {
return new Set(names).size === names.length;
@@ -118,6 +118,7 @@ const INERT_LIB_CALLS = new Set([
"worker.parentPort",
"worker.root",
"cp.spawnSyncOpts",
"cp.spawnSyncEnv",
]);
/** jsval conversions (ir/jsval.ts): no user code, no stores. */
+8 -2
View File
@@ -35,7 +35,12 @@ export type MayThrowReason =
* and a generator's `.return()` unwind through `catch`. */
export function computeMayThrow(
mod: IrModule,
options: { stackChecks?: boolean; explain?: boolean } = {},
options: {
stackChecks?: boolean;
explain?: boolean;
/** computePublishedTypes(mod), when the caller already has it. */
published?: ReturnType<typeof computePublishedTypes>;
} = {},
): {
fns: Set<string>;
/** With `explain`: the reason each may-throw function was marked. */
@@ -50,7 +55,8 @@ export function computeMayThrow(
const terminations = mod.workers === true ? workerTerminationPolls(mod) : null;
const stackChecks = options.stackChecks === true ? stackCheckedFunctions(mod) : null;
// Stores that can reach a published object throw the frozen TypeError.
const published = computePublishedTypes(mod);
const published =
options.published !== undefined ? options.published : computePublishedTypes(mod);
interface Facts {
throws: boolean;
callees: string[];
@@ -0,0 +1,29 @@
import type { IrStmt } from "../ir/ir.js";
/** Prefix of a function's result-slot entry: `%into.<name>(...params, dst)`
* writes the function's fresh result object into `dst` and returns nothing
* (llvm/record-results.ts). */
export const RESULT_ENTRY_PREFIX = "%into.";
export function resultEntryName(name: string): string {
return `${RESULT_ENTRY_PREFIX}${name}`;
}
/** The local id of a result-slot entry's destination parameter. The
* caller passes zeroed frame storage, and on every path the entry stores
* each of its fields at most once, so a store into it is an
* initialization: there is no old value to release. */
export const RESULT_DST_ID = "%into.dst";
/** Whether `s`, a statement of the function `fnName`, is a result-slot
* entry's store into its destination. The destination is frame storage of
* a caller that only projects the value (llvm/stack-objects.ts), so the
* store initializes a field of an object no other object can point to. */
export function resultSlotStore(fnName: string, s: IrStmt): boolean {
return (
s.kind === "recordSet" &&
s.obj.kind === "varRef" &&
s.obj.localId === RESULT_DST_ID &&
fnName.startsWith(RESULT_ENTRY_PREFIX)
);
}
@@ -29,35 +29,50 @@ const NON_STACK_KEY_TYPES: ReadonlySet<IrType["kind"]> = new Set(["f64", "bigint
* containers or calls; a key the program assembles from other strings or
* from data it reads keeps the static answer. */
class StackKeys {
private readonly spelled: boolean;
private readonly calls = new Map<string, { caller: IrFunction; args: IrExpr[] }[]>();
private readonly named: Set<string>;
private spelled: boolean | undefined;
private census:
| { calls: Map<string, { caller: IrFunction; args: IrExpr[] }[]>; named: Set<string> }
| undefined;
private readonly inits = new Map<IrFunction, Map<string, IrExpr | null>>();
/** Settled answers by function and local: true where a search reached a
* key that can be "stack", false where a search that found none passed. */
private readonly answers = new Map<string, boolean>();
constructor(mod: IrModule) {
this.spelled = spellsStack(mod);
this.named = new Set(moduleFunctionReferences(mod));
// The module walks run on the first question that needs them: a program
// without a dynamic key read asks none.
constructor(private readonly mod: IrModule) {}
private spellsStack(): boolean {
return (this.spelled ??= spellsStack(this.mod));
}
private callCensus(): {
calls: Map<string, { caller: IrFunction; args: IrExpr[] }[]>;
named: Set<string>;
} {
if (this.census !== undefined) return this.census;
const mod = this.mod;
const calls = new Map<string, { caller: IrFunction; args: IrExpr[] }[]>();
const named = new Set(moduleFunctionReferences(mod));
// Class methods, accessors and constructors are called through dispatch.
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
for (const fn of mod.functions) {
if (isClassMember(fn.name, classNames)) this.named.add(fn.name);
if (isClassMember(fn.name, classNames)) named.add(fn.name);
everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
if (expr.kind === "call") {
const sites = this.calls.get(expr.callee) ?? [];
const sites = calls.get(expr.callee) ?? [];
sites.push({ caller: fn, args: expr.args });
this.calls.set(expr.callee, sites);
calls.set(expr.callee, sites);
}
const referenced = exprFunctionReference(expr);
if (referenced !== undefined) this.named.add(referenced);
if (referenced !== undefined) named.add(referenced);
return true;
},
});
}
return (this.census = { calls, named });
}
mayBeStack(fn: IrFunction, key: IrExpr): boolean {
@@ -71,7 +86,7 @@ class StackKeys {
private keyMayBeStack(fn: IrFunction, key: IrExpr, visited: Set<string>): boolean {
if (key.kind === "strLit") return key.value === "stack";
if (!this.spelled) return false;
if (!this.spellsStack()) return false;
if (NON_STACK_KEY_TYPES.has(key.type.kind)) return false;
if (key.kind === "toString" && NON_STACK_KEY_TYPES.has(key.operand.type.kind)) return false;
if (key.kind === "dynFrom" && NON_STACK_KEY_TYPES.has(key.value.type.kind)) return false;
@@ -93,8 +108,9 @@ class StackKeys {
if (local === undefined || local.mutable) return true;
const index = fn.params.findIndex((param) => param.localId === localId);
if (index !== -1) {
const sites = this.calls.get(fn.name);
if (this.named.has(fn.name) || sites === undefined) return true;
const { calls, named } = this.callCensus();
const sites = calls.get(fn.name);
if (named.has(fn.name) || sites === undefined) return true;
return sites.some(
(site) =>
site.args.length !== fn.params.length ||
@@ -115,6 +115,8 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
// "pipe"`) — accepted when its TYPE proves every arm is a supported
// literal; the runtime maps the value to the modes at the call.
let stdioStr: IrExpr | null = null;
// The `env` option's [k,v,...] pairs, which replace the child's environment.
let envPairs: IrExpr | null = null;
let plain = true; // no behavior-changing option: the historical libCall
if (optsNode) {
@@ -227,11 +229,16 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
case "windowsHide":
lowerer.lowerExpr(m.value); // Node no-op on POSIX
break;
case "env":
// A REPLACEMENT environment, as for spawn and execFileSync.
envPairs = recordToEnvPairs(lowerer, m.value);
plain = false;
break;
default:
lowerer.noLowering(
`spawnSync option '${m.name}'`,
p,
"encoding, timeout, killSignal, stdio, and windowsHide are the supported options",
"encoding, timeout, killSignal, stdio, env, and windowsHide are the supported options",
);
}
}
@@ -239,6 +246,13 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
if (plain) {
return { kind: "libCall", fn: "cp.spawnSync", args: [cmd, argv], type: SPAWNRES_T, loc };
}
if (stdioStr !== null && envPairs !== null) {
lowerer.noLowering(
"spawnSync with a runtime stdio value and env",
optsNode!,
'pass stdio as a "pipe"/"ignore"/"inherit" literal when env is set',
);
}
if (stdioStr !== null) {
return {
kind: "libCall",
@@ -250,7 +264,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
}
return {
kind: "libCall",
fn: "cp.spawnSyncOpts",
fn: envPairs !== null ? "cp.spawnSyncEnv" : "cp.spawnSyncOpts",
args: [
cmd,
argv,
@@ -259,6 +273,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
numLit(inMode, loc),
numLit(outMode, loc),
numLit(errMode, loc),
...(envPairs !== null ? [envPairs] : []),
],
type: SPAWNRES_T,
loc,
@@ -11,6 +11,18 @@ import { everyStmtList } from "../../ir/traverse.js";
import { toPrimitiveHint } from "./class-dynamic-dispatch.js";
import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js";
/** Runtime entry points that step a checked-dynamic iterable's iterator:
* destructuring rest, and the collection constructors' and
* Object.fromEntries' seeds. */
const RUNTIME_STEP_LIB_FNS: ReadonlySet<string> = new Set([
"dyn.destructureRest",
"dyn.nativeSetNew",
"dyn.mapSeedEntries",
"dyn.fromEntries",
"weakMap.new",
"weakSet.new",
]);
/** What one function's body holds for the class dispatch pass and its
* caught-value, generator and class construction sub-passes, collected in
* one walk. Lists keep the order the walk meets their entries. */
@@ -186,7 +198,7 @@ function scanSites(statements: IrStmt[]): FunctionSites {
expr.key.kind === "strLit" &&
HELPER_METHODS.has(expr.key.value)) ||
expr.kind === "dynIterN" ||
(expr.kind === "libCall" && expr.fn === "dyn.destructureRest")
(expr.kind === "libCall" && RUNTIME_STEP_LIB_FNS.has(expr.fn))
)
sites.runtimeSteps = true;
switch (expr.kind) {
@@ -157,6 +157,36 @@ test("a synthesized construction pins today's table", () => {
expect(f.order(ab)).toEqual(["a", "b"]);
});
test("a runtime builder typed by its use site pins today's table", () => {
const f = fixture();
const ab = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
// net.serverAddress's signature result is VOID: the call takes its record
// type from its use site, and the runtime builds the record in that order.
const built: IrExpr = { kind: "libCall", fn: "net.serverAddress", args: [], type: ab, loc };
f.run([observe(f.literal(ab, ["b", "a"])), observe(built)]);
expect(f.order(ab)).toEqual(["a", "b"]);
});
test("an always-throwing call typed as the record leaves its literals' order", () => {
const f = fixture();
const ab = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
const thrown: IrExpr = { kind: "libCall", fn: "error.nodeThrow", args: [], type: ab, loc };
f.run([observe(f.literal(ab, ["b", "a"])), observe(thrown)]);
expect(f.order(ab)).toEqual(["b", "a"]);
});
test("a checked conversion alone pins today's table", () => {
const f = fixture();
const ab = f.record(
@@ -45,6 +45,7 @@ import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir
import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js";
import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js";
import type { Lowerer } from "./lowerer.js";
import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
/** One property of an object literal, in source order. */
export type KeyOrderSegment =
@@ -312,7 +313,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
// Runtime surfaces (console, util.format, workers) that take a
// record may walk its keys; runtime builders make new records.
for (const arg of e.args) observedRoots.push(arg.type);
producedBy(e.type, true);
// An always-throwing call stands in for a value (`error.nodeThrow`
// in a narrowing ternary) and never produces one. Other calls
// whose signature result is VOID are typed by their use site and
// build the record at run time (`server.address()`).
if (!ALWAYS_THROW_LIB_FNS.has(e.fn)) producedBy(e.type, true);
return;
case "intrinsic":
// Intrinsics hand back values built elsewhere, except allSettled's
@@ -45,6 +45,7 @@ import { errorToStringCall } from "./error-methods.js";
import { lowerClassPrototypeAssign, lowerClassPrototypeDescriptors } from "./class-prototypes.js";
import {
BYTES_ELEMENT_NAME,
BYTES_U8,
BIGINT_T,
BOOL,
CAUGHT,
@@ -53,6 +54,7 @@ import {
F64,
type IrExpr,
type IrFunction,
type IrLibFn,
type IrLocal,
type IrParam,
type IrStmt,
@@ -76,6 +78,7 @@ import {
} from "../../ir/ir.js";
import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js";
import { isJsSourceFile, locOf, requireSpecOf } from "../program.js";
import { isPackageShimFile, isShimIntrinsicName } from "../package-shims.js";
import { lowerJsonRequireCall } from "./json-require.js";
import { lowerAssertInstanceCall } from "./assert-instances.js";
import {
@@ -292,6 +295,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression);
if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration);
}
// A package shim's runtime intrinsic (a bodiless `declare function`
// named scriptcUnixDgram* in a shipped shim): a direct runtime call.
if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) {
const intrinsic = lowerShimIntrinsicCall(lowerer, expr, expr.expression, loc);
if (intrinsic) return intrinsic;
}
// Inline require() of a program module (cjs-inline-require.ts): a
// member call over its export table resolves like a binding's member
// call after the module's %init at this position; the require itself
@@ -1619,7 +1628,16 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
}
const accessorTarget = lowerer.fieldTarget(expr.expression);
if (accessorTarget?.container === "accessor") {
const callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression);
let callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression);
// A getter returning a callable class instance (chalk's
// `chalk.red("x")`): call through its `%call` field.
if (callee.type.kind === "object") {
const direct = callableInstanceStringCall(lowerer, expr, callee, locOf(expr));
if (direct) return direct;
callee = lowerer.hybridCallUnwrap(callee);
if (callee.type.kind === "func")
return lowerCallThroughValue(lowerer, expr, callee, locOf(expr));
}
if (callee.type.kind === "func") {
const args = completeFuncValueArgs(lowerer, expr, callee.type, locOf(expr));
return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(expr) };
@@ -1871,7 +1889,55 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// a non-func IR type was already rejected while lowering the callee.
// HYBRID (function-with-properties) values call through their %call slot.
let callee = lowerer.lowerExpr(expr.expression);
if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee);
{
const direct = callableInstanceStringCall(lowerer, expr, callee, loc);
if (direct) return direct;
}
if (callee.type.kind === "record" || callee.type.kind === "object")
callee = lowerer.hybridCallUnwrap(callee);
return lowerCallThroughValue(lowerer, expr, callee, loc);
}
/** A callable class instance called with one string: its `%call1` method
* when the class has one (the chalk shim's typed fast path), skipping the
* variadic closure's checked-dynamic arguments. */
function callableInstanceStringCall(
lowerer: Lowerer,
expr: ts.CallExpression,
callee: IrExpr,
loc: SrcLoc,
): IrExpr | null {
if (callee.type.kind !== "object" || expr.arguments.nodes.length !== 1) return null;
const arg = expr.arguments.nodes[0]!;
if (ts.isSpreadElement(arg)) return null;
const info = lowerer.classes.get(callee.type.className);
if (info === undefined || !info.fields.has("%call") || !lowerer.findMethodOn(info, "%call1"))
return null;
const value = lowerer.lowerExpr(arg);
if (value.type.kind === "string")
return lowerer.accessorCall(callee.type.className, "%call1", callee, [value], STRING, loc);
// Any other argument (a number behind a cast): the variadic closure,
// through the checked-dynamic call as lowerCallThroughValue would.
const closure = lowerer.hybridCallUnwrap(callee);
if (closure.type.kind !== "func") return null;
return {
kind: "dynCall",
callee: { kind: "dynFrom", value: closure, type: DYN, loc },
calleeName: ts.isIdentifier(expr.expression) ? expr.expression.text : "value",
args: [lowerer.coerceToExpected(value, DYN)],
type: DYN,
loc,
};
}
/** A call through an evaluated callee value: checked-dynamic callees,
* function unions, typed-rest and runtime-arity closures. */
function lowerCallThroughValue(
lowerer: Lowerer,
expr: ts.CallExpression,
callee: IrExpr,
loc: SrcLoc,
): IrExpr {
// A CHECKED-DYNAMIC callee — `fn(a, b)` where fn is an implicit-any
// JS binding (the mustCall body's `fn(...args)`), a dyn capture, or a
// keyed read off a dyn value: the dynCall boundary. Arguments convert
@@ -9181,7 +9247,16 @@ export function lowerRecordFieldCall(
// A HYBRID (function-with-properties) field is callable through its
// reserved %call slot — `colors.blue("x")` where blue also carries
// `.bold` (the chalk shape).
if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee);
if (callee.type.kind === "object" && !init) {
const direct = callableInstanceStringCall(lowerer, call, callee, loc);
if (direct) return direct;
// Other argument counts call the %call closure through its value, as
// a call on the builder itself does (`theme.error("a", "b")`).
const closure = lowerer.hybridCallUnwrap(callee);
if (closure !== callee) return lowerCallThroughValue(lowerer, call, closure, loc);
}
if (callee.type.kind === "record" || callee.type.kind === "object")
callee = lowerer.hybridCallUnwrap(callee);
// Inferred JavaScript option records can retain checked callables in
// fields even when the checker only describes those members as any.
// TypeScript reaches here for callable library types that are checked
@@ -10939,3 +11014,37 @@ function fenceClassCopyIntoFreshObject(
`Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`,
);
}
/** The runtime calls package shims declare as bodiless functions. */
const SHIM_INTRINSICS: ReadonlyMap<string, { fn: IrLibFn; args: IrType[]; ret: IrType }> = new Map<
string,
{ fn: IrLibFn; args: IrType[]; ret: IrType }
>([
["scriptcUnixDgramSocket", { fn: "dgram.unixSocket", args: [], ret: F64 }],
["scriptcUnixDgramConnect", { fn: "dgram.unixConnect", args: [F64, STRING], ret: F64 }],
["scriptcUnixDgramSend", { fn: "dgram.unixSend", args: [F64, BYTES_U8], ret: F64 }],
["scriptcUnixDgramClose", { fn: "dgram.unixClose", args: [F64], ret: F64 }],
]);
function lowerShimIntrinsicCall(
lowerer: Lowerer,
expr: ts.CallExpression,
callee: ts.Identifier,
loc: SrcLoc,
): IrExpr | null {
const intrinsic = SHIM_INTRINSICS.get(callee.text);
if (intrinsic === undefined) return null;
const decl = lowerer.checker.getSymbolAtLocation(callee)?.valueDeclaration;
if (
decl === undefined ||
!ts.isFunctionDeclaration(decl) ||
decl.body ||
!isPackageShimFile(ts.getSourceFileOfNode(decl).fileName) ||
expr.arguments.nodes.length !== intrinsic.args.length
)
return null;
const args = expr.arguments.nodes.map((arg, i) =>
lowerer.lowerExprExpecting(arg, intrinsic.args[i]!),
);
return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc };
}
@@ -612,8 +612,10 @@ export function lowerDgramMethodCall(
const fn: IrLibFn = data.type.kind === "string" ? "dgram.sendStr" : "dgram.sendBytes";
return { kind: "libCall", fn, args: [receiver, data, port, host], type: VOID, loc };
}
// TypeScript reaches the ladder for the completion-callback form
// send(msg, port, address, callback) (the hot-shots package shim).
if (
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
(isJsSourceFile(ts.getSourceFileOfNode(call)) || args.length === 4) &&
args.length <= 6 &&
!args.some(ts.isSpreadElement)
) {
@@ -38,7 +38,7 @@
* calls). */
import { positionTag, type PositionTagOwner } from "./structural-positions.js";
import * as ts from "../ts7/adapter.js";
import { packageShimFor } from "../package-shims.js";
import { isPackageShimFile, isShimIntrinsicName, packageShimFor } from "../package-shims.js";
import type { Lowerer } from "./lowerer.js";
import { boundIdentifiersOf } from "./lowerer.js";
import type { FileParts } from "./lower-modules.js";
@@ -654,6 +654,12 @@ export function ambientUndefinedFnSymbolOf(
const sym = lowerer.resolveValueSymbol(ident);
if (!sym || !(sym.flags & ts.SymbolFlags.Function)) return null;
if (lowerer.isStdlibSymbol(sym)) return null;
// A package shim's runtime intrinsic (lower-calls.ts) is defined.
if (
isShimIntrinsicName(ident.text) &&
sym.declarations.some((d) => isPackageShimFile(ts.getSourceFileOfNode(d).fileName))
)
return null;
let sawFn = false;
for (const d of sym.declarations) {
if (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) continue; // type-world merge partners
@@ -230,6 +230,7 @@ import {
arrayOf,
BOOL,
canAdaptDynFuncTo,
canBoxFuncIntoDyn,
canDynCheckTo,
canConvertToDyn,
DYN,
@@ -9334,6 +9335,21 @@ export class Lowerer {
* type-mapper.ts's chalk-shape mapping — call paths and func-slot coercions
* share it. */
hybridCallUnwrap(expr: IrExpr): IrExpr {
// A callable CLASS instance: a class whose merged interface carries a
// call signature stores its call behavior in a `%call` field (the
// chalk package shim's builder). Calls read the field.
if (expr.type.kind === "object") {
const callType = this.classes.get(expr.type.className)?.fields.get("%call");
if (callType === undefined || callType.kind !== "func") return expr;
return {
kind: "fieldGet",
obj: expr,
className: expr.type.className,
field: "%call",
type: callType,
loc: expr.loc,
};
}
if (expr.type.kind !== "record") return expr;
const shape = this.shapes.get(expr.type.shapeId);
const call = shape?.fields.find((f) => f.name === "%call");
@@ -9664,9 +9680,27 @@ export class Lowerer {
// A HYBRID (function-with-properties) record flowing into a plain
// func slot extracts its reserved %call field — the chalk shape's
// "the value IS callable" half (type-mapper.ts's hybrid mapping).
if (expected.kind === "func" && expr.type.kind === "record") {
if (expected.kind === "func" && (expr.type.kind === "record" || expr.type.kind === "object")) {
const un = this.hybridCallUnwrap(expr);
if (un !== expr && typeEquals(un.type, expected)) return un;
// A callable class instance (the chalk shim's variadic builder) in a
// narrower function slot: its call closure crosses the checked
// function boundary, which adapts the arity.
if (
un !== expr &&
expr.type.kind === "object" &&
un.type.kind === "func" &&
canBoxFuncIntoDyn(
un.type,
(id) => this.shapes.get(id),
(id) => this.unions.get(id),
)
) {
return this.coerceToExpected(
{ kind: "dynFrom", value: un, type: DYN, loc: un.loc },
expected,
);
}
}
// A zero-param function whose RETURN is a wider record array than the
// slot's (`getRoutes: () => cachedRoutes` against `() => Narrow[]`):
@@ -62,9 +62,20 @@ interface ImplementationShim {
* of it (a __commonJS chunk whose key names the package, its version
* and this file) is the package itself. */
mainFile: string;
/** The package's declaration entry relative to its directory, when it
* is not `index.d.ts`. */
typesFile?: string;
/** An alternative entry for this installation (relative to the shim
* root), or undefined for the version's own entry. */
variant?: (pkgDir: string) => string | undefined;
/** Every variant entry, for the guard test that compiles each one. */
variants?: readonly string[];
}
const IMPLEMENTATION_SHIMS: ReadonlyMap<string, ImplementationShim> = new Map([
const IMPLEMENTATION_SHIMS: ReadonlyMap<string, ImplementationShim> = new Map<
string,
ImplementationShim
>([
[
"fs-extra",
{
@@ -79,6 +90,33 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap<string, ImplementationShim> = new Map([
]),
},
],
[
"chalk",
{
name: "chalk",
defaultExport: true,
mainFile: "source/index.js",
typesFile: "source/index.d.ts",
versions: new Map([[5, "chalk/v5.ts"]]),
},
],
[
"hot-shots",
{
name: "hot-shots",
defaultExport: true,
mainFile: "index.js",
typesFile: "types.d.ts",
versions: new Map([[8, "hot-shots/v8.ts"]]),
// hot-shots' uds protocol loads the optional unix-dgram addon: where
// it is installed, the entry with the native Unix datagram transport.
variant: (pkgDir: string) =>
installedPackageDir(join(pkgDir, "index.js"), "unix-dgram") === null
? undefined
: "hot-shots/v8-unix-dgram.ts",
variants: ["hot-shots/v8-unix-dgram.ts"],
},
],
[
"undici",
{
@@ -141,8 +179,10 @@ export function throughBundledShimForwarder(resolved: string): string {
* the guard test that compiles each one. */
export function packageShimEntries(): string[] {
const entries = new Set<string>();
for (const shim of IMPLEMENTATION_SHIMS.values())
for (const shim of IMPLEMENTATION_SHIMS.values()) {
for (const entry of shim.versions.values()) entries.add(join(packageShimRoot(), entry));
for (const entry of shim.variants ?? []) entries.add(join(packageShimRoot(), entry));
}
return [...entries].sort();
}
@@ -240,7 +280,8 @@ function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): st
if (shim === undefined) return null;
const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN);
const entry = shim.versions.get(major);
return entry === undefined ? null : join(packageShimRoot(), entry);
if (entry === undefined) return null;
return join(packageShimRoot(), shim.variant?.(pkgDir) ?? entry);
}
/** The shim module `specifier` resolves to from `fromFile`, or null. Only
@@ -280,16 +321,33 @@ export function isShimmedImport(fromFile: string, specifier: string): boolean {
return isUnloadedPackage(specifier) || packageShimFor(fromFile, specifier) !== null;
}
/** The shimmed package whose declaration entry `path` is, with the
* package directory: `<pkg>/index.d.ts`, or the entry's own `typesFile`. */
function shimOfDeclarationPath(path: string): { pkgDir: string; shim: ImplementationShim } | null {
if (!path.endsWith(".d.ts")) return null;
if (path.endsWith("/index.d.ts")) {
const pkgDir = dirname(path);
const shim = IMPLEMENTATION_SHIMS.get(pkgDir.slice(pkgDir.lastIndexOf("/") + 1));
if (shim !== undefined && shim.typesFile === undefined) return { pkgDir, shim };
}
for (const shim of IMPLEMENTATION_SHIMS.values()) {
if (shim.typesFile === undefined) continue;
const suffix = `/${shim.name}/${shim.typesFile}`;
if (path.endsWith(suffix))
return { pkgDir: path.slice(0, path.length - suffix.length + shim.name.length + 1), shim };
}
return null;
}
/** The fs-shadow answer for a shimmed package's declaration entry: the
* installed `index.d.ts` reads as a re-export of the version's shim module,
* so the checker types each importer against its own version. Undefined
* for every other path. */
export function packageShimDeclarationText(path: string): string | undefined {
if (!path.endsWith("/index.d.ts")) return undefined;
const pkgDir = dirname(path);
const name = pkgDir.slice(pkgDir.lastIndexOf("/") + 1);
const shim = IMPLEMENTATION_SHIMS.get(name);
if (shim === undefined) return undefined;
const located = shimOfDeclarationPath(path);
if (located === null) return undefined;
const { pkgDir, shim } = located;
const name = shim.name;
// A DefinitelyTyped twin (`@types/fs-extra`) types a package that ships no
// declarations. Nothing at its path names the installed runtime version,
// so it reads as the shim only when every supported version shares one
@@ -316,6 +374,12 @@ export function isPackageShimFile(fileName: string): boolean {
return fileName.startsWith(root.endsWith("/") ? root : `${root}/`);
}
/** True for a name a shipped shim may declare as a bodiless runtime
* intrinsic (lower-calls.ts lowers its calls to runtime calls). */
export function isShimIntrinsicName(name: string): boolean {
return name.startsWith("scriptcUnixDgram");
}
/** Resets per-compile caches. */
export function clearPackageShimCaches(): void {
installedCache.clear();
@@ -169,6 +169,30 @@ function ids(
return nodes?.map((n) => (n as OracleNode).index);
}
test("generated child getters select the child their name selects", () => {
// These getters adapt their raw child; their slot functions are compared instead.
const adapted = ["body", "initializer", "left", "modifiers", "operand", "questionToken"];
const names = [...new Set(Object.values(childProperties).flat())];
const lists = (value: unknown): unknown => (value instanceof AstNodeList ? value.nodes : value);
let compared = 0;
for (const { file } of decoded.values()) {
walkPreorder(file.root, (node) => {
const view = node as unknown as Record<string, unknown>;
for (const name of names) {
if (adapted.includes(name)) {
const slot = (view[`${name}Slot`] as () => number).call(node);
expect(slot, `${node.kind}.${name}`).toBe(astChildOrder(node.kind, name));
} else {
expect(lists(view[name]), `${node.kind}.${name}`).toBe(lists(node.child(name)));
}
compared++;
}
return undefined;
});
}
expect(compared).toBeGreaterThan(100_000);
});
test("the writeTargets option answers write targets without their wrappers", () => {
const { bytes, file: plain } = decoded.get("targets.ts")!;
const file = new AstFile(bytes, undefined, { writeTargets: true });
File diff suppressed because it is too large Load Diff
@@ -240,12 +240,12 @@ export class AstWireFile {
string(index: number): string {
if (index >>> 0 !== index || index % 2 !== 0 || index >= this.stringCount * 2)
throw new AstDecodeError(`invalid string index ${index}`);
const cached = this.stringCache[index / 2];
const cached = this.stringCache[index >>> 1];
if (cached !== undefined) return cached;
const start = astU32(this.bytes, this.stringOffsets + index * 4);
const end = astU32(this.bytes, this.stringOffsets + (index + 1) * 4);
const text = decodeAstString(this.bytes, this.strings + start, end - start);
this.stringCache[index / 2] = text;
this.stringCache[index >>> 1] = text;
return text;
}
@@ -1007,3 +1007,25 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.createSign",
"crypto.createVerify",
]);
/** libCall members that always throw. Their result type is the replaced
* expression's own and is never materialized: global.undefRead's is the
* declared type of the undefined global; the others stand in for a value
* of the expression they replace. The fs Chk ladders qualify: every
* validation failure throws Node's typed error, and a full pass throws the
* trailing compiler-rendered fence. */
export const ALWAYS_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"global.undefRead",
"error.nodeThrow",
"error.argTypeThrow",
"error.propTypeThrow",
"fs.mkdtempChk",
"fs.readFileChk",
"fs.opendirChk",
"fs.watchFileChk",
"fs.lchmodChk",
"fs.readChk",
"fs.streamOptsChk",
"net.connectOptsChk",
"tls.caCertsChk",
]);
@@ -1318,6 +1318,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64],
result: SPAWNRES_T,
},
/** cp.spawnSyncOpts with the `env` option: the trailing [k,v,...]
* pairs replace the child's environment (recordToEnvPairs). */
"cp.spawnSyncEnv": {
argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64, arrayOf(STRING)],
result: SPAWNRES_T,
},
/** cp.spawnSyncOpts with the stdio carried as a RUNTIME string —
* "pipe" | "ignore" | "inherit", proven by the call site's TYPE (the
* defaultRunner idiom `stdio: options?.stdio ?? "pipe"`); the runtime
@@ -1528,6 +1534,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
},
"dgram.sendStr": { argTypes: [DGRAMSOCK_T, STRING, F64, STRING], result: VOID },
"dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID },
/** Connected AF_UNIX datagram sockets for the hot-shots package shim
* (the unix-dgram addon's surface): fd or result >= 0, -errno on
* failure; send answers 1 when the socket would block. */
"dgram.unixSocket": { argTypes: [], result: F64 },
"dgram.unixConnect": { argTypes: [F64, STRING], result: F64 },
"dgram.unixSend": { argTypes: [F64, BYTES_U8], result: F64 },
"dgram.unixClose": { argTypes: [F64], result: F64 },
/** The send argument-validation ladder over dyn arguments (Node's
* signature shuffle: slice bounds, list/type contracts, port/address
* validation, connected-state errors), then the send: lists, connected
+29 -18
View File
@@ -508,7 +508,10 @@ export function dynamicFieldStores(
};
const dynWritten = new Set<string>();
let dynWritesAll = false;
for (const fn of mod.functions)
// A store whose key is not a literal names every field: later stores add
// nothing, so the walk stops there.
for (const fn of mod.functions) {
if (dynWritesAll) break;
walkFunction(fn, {
expr: (e) => {
if (dynWritesAll) return;
@@ -524,6 +527,7 @@ export function dynamicFieldStores(
}
},
});
}
return { all: dynWritesAll, names: dynWritten };
}
@@ -536,6 +540,22 @@ export function dynamicallyStoredFamilies(
slots: Int32Slots,
stores?: DynamicFieldStores,
): Set<string> {
const records = new Map((mod.records ?? []).map((r) => [r.id, r]));
const unions = new Map((mod.unions ?? []).map((u) => [u.id, u]));
// A live dyn view (typed-ref capsule) of an instance writes back into
// its fields only after a runtime store into the capsule; the commit then
// writes every field from the view, which holds the exact snapshot value
// of any property no store named. So reaching dynamic code disqualifies
// exactly the field names some dynamic store in the program can name.
const { all: dynWritesAll, names: dynWritten } =
stores ?? dynamicFieldStores(mod, records, unions);
const storable = (field: { name: string; type: IrType }): boolean =>
field.type.kind === "f64" &&
!field.name.startsWith("%") &&
(dynWritesAll || dynWritten.has(field.name));
const families = new Set<string>();
// Without a field some store can name, no class is worth exposing.
if (!(mod.classes ?? []).some((cls) => cls.fields.some(storable))) return families;
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
const children = new Map<string, string[]>();
for (const cls of classes.values())
@@ -554,8 +574,6 @@ export function dynamicallyStoredFamilies(
}
return out;
};
const records = new Map((mod.records ?? []).map((r) => [r.id, r]));
const unions = new Map((mod.unions ?? []).map((u) => [u.id, u]));
const exposedClasses = new Set<string>();
const seenNamed = new Set<string>();
let exposeAll = false;
@@ -596,13 +614,6 @@ export function dynamicallyStoredFamilies(
});
}
};
// A live dyn view (typed-ref capsule) of an instance writes back into
// its fields only after a runtime store into the capsule; the commit then
// writes every field from the view, which holds the exact snapshot value
// of any property no store named. So reaching dynamic code disqualifies
// exactly the field names some dynamic store in the program can name.
const { all: dynWritesAll, names: dynWritten } =
stores ?? dynamicFieldStores(mod, records, unions);
for (const fn of mod.functions)
walkFunction(fn, {
expr: (e) => {
@@ -628,16 +639,10 @@ export function dynamicallyStoredFamilies(
if (s.kind === "throw") expose(s.value.type);
},
});
const families = new Set<string>();
for (const cls of classes.values()) {
if (!exposeAll && !exposedClasses.has(cls.name)) continue;
for (const field of cls.fields)
if (
field.type.kind === "f64" &&
!field.name.startsWith("%") &&
(dynWritesAll || dynWritten.has(field.name))
)
families.add(slots.family(cls.name, field.name)!);
if (storable(field)) families.add(slots.family(cls.name, field.name)!);
}
return families;
}
@@ -1058,12 +1063,18 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
if (params) for (const id of params.keys()) reads(`param:${fn.name}\0${id}`, fn.name);
// Range facts are consulted only at writes into candidate slots; slots
// are only ever dropped, so a body without such a write needs none.
const ranges: IntegerRanges = hasCandidateWrite(fn)
const candidate = hasCandidateWrite(fn);
const ranges: IntegerRanges = candidate
? analyzeIntegerRanges(numeric(fn), slots.facts(fn.name))
: new Map();
if (ranges.size > 0) lastWrites.set(fn.name, slotWrites(fn, ranges, functions));
else lastWrites.delete(fn.name);
const proven: IrExpr[] = [];
// The walk below acts only at writes into candidate slots.
if (!candidate) {
provenByFunction.set(fn.name, proven);
continue;
}
const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e));
// An int32 written into a slot still assumed non-negative: widen the
// assumption when the value may be negative, and reanalyze its readers.
+42 -14
View File
@@ -605,14 +605,23 @@ const STDIO_EXTENDED_NAMES: ReadonlySet<string> = new Set(["setEncoding", "resiz
/** The libCall nodes the rules below test. */
type LibCallNode = IrExpr & { kind: "libCall" };
/** A condition over several nodes of the module (a WebCrypto call and an
* algorithm name): the scan's one walk shows it every expression, and the
* feature is set when `result` answers true after the walk. */
interface RuleScan {
expr(node: IrExpr): void;
result(): boolean;
}
/** What makes a program need one optional runtime unit. The feature is set
* when any condition holds: `initial` from the module's declarations, a
* libCall named in `fns` or starting with one of `prefixes`, a libCall
* `libCall` accepts, another expression node `expr` accepts, or a type
* slot of one of the `types` kinds. A libCall or type that needs several
* units appears in each unit's rule. */
* `libCall` accepts, another expression node `expr` accepts, a type slot of
* one of the `types` kinds, or a `scan` over the whole module. A libCall or
* type that needs several units appears in each unit's rule. */
interface RuntimeFeatureRule {
initial?: (mod: IrModule) => boolean;
scan?: () => RuleScan;
fns?: readonly IrLibFn[];
prefixes?: readonly string[];
libCall?: (node: LibCallNode) => boolean;
@@ -660,17 +669,32 @@ const WEBCRYPTO_ASYMMETRIC_NAMES: ReadonlySet<string> = new Set([
/** Whether a string literal in the module names Ed25519, without regard
* to case ("ed25519" for node:crypto, "Ed25519" for WebCrypto): the
* Ed25519 unit links then, alongside the pki unit. */
export function moduleNamesEd25519(mod: IrModule): boolean {
function ed25519NameScan(): RuleScan {
let named = false;
everyModuleNode(mod, {
return {
expr: (node) => {
if (node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true;
return !named;
if (!named && node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true;
},
stmt: () => !named,
type: () => true,
});
return named;
result: () => named,
};
}
/** moduleUsesWebCryptoAsymmetric's condition as a runtime-feature scan. */
function webCryptoAsymmetricScan(): RuleScan {
let webcrypto = false;
let named = false;
return {
expr: (node) => {
if (node.kind === "libCall" && node.fn.startsWith("webcrypto.")) webcrypto = true;
else if (
!named &&
node.kind === "strLit" &&
WEBCRYPTO_ASYMMETRIC_NAMES.has(node.value.toLowerCase())
)
named = true;
},
result: () => webcrypto && named,
};
}
/** Whether the module calls WebCrypto and a string literal in it names one
@@ -916,10 +940,10 @@ const RUNTIME_FEATURE_RULES = {
perfLoop: rule({ fns: ["perf.idleTime"], prefixes: ["perf.loop", "perf.eld"] }),
/** scr_pki.c over the mbedTLS archive: asymmetric KeyObjects and
* signatures. */
pki: rule({ initial: moduleUsesWebCryptoAsymmetric, fns: [...PKI_LIB_FNS] }),
pki: rule({ scan: webCryptoAsymmetricScan, fns: [...PKI_LIB_FNS] }),
/** scr_ed25519.c (the vendored libsodium Ed25519), linked with pki: a
* string literal in the module names Ed25519. */
ed25519: rule({ initial: moduleNamesEd25519 }),
ed25519: rule({ scan: ed25519NameScan }),
processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }),
/** scr_url_legacy.c parses and stringifies queries. */
qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }),
@@ -1080,9 +1104,11 @@ for (const name of RUNTIME_FEATURE_NAMES) {
function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): RuntimeFeatures {
const features = noRuntimeFeatures();
const scans: { name: RuntimeFeatureName; scan: RuleScan }[] = [];
for (const name of RUNTIME_FEATURE_NAMES) {
const initial = ruleOf(name).initial;
const { initial, scan } = ruleOf(name);
if (initial && initial(mod)) features[name] = true;
if (scan) scans.push({ name, scan: scan() });
}
const keepGoing = (): boolean => stopAt === undefined || !features[stopAt];
let tables: TypeTables | undefined;
@@ -1093,6 +1119,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): Runtim
classes: new Map((mod.classes ?? []).map((c) => [c.name, c])),
});
const expr = (node: IrExpr): boolean => {
for (let i = 0; i < scans.length; i++) scans[i]!.scan.expr(node);
if (node.kind === "libCall") {
const fn = node.fn;
for (const name of FEATURES_BY_FN.get(fn) ?? []) features[name] = true;
@@ -1111,6 +1138,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): Runtim
return keepGoing();
};
if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type });
for (const { name, scan } of scans) if (scan.result()) features[name] = true;
// Inline-cache sites of dynamic-tier functions use the tier's shapes
// (scr_js_object.c, with the SCR_JS_TIER build of scr_json.c).
if (!features.dynTier && mod.functions.some((fn) => fn.tier === "dynamic")) {
+9 -2
View File
@@ -109,8 +109,15 @@ export interface IrModuleVisitor extends IrVisitor {
* source text, locations, labels, or other metadata. A false predicate stops
* the whole walk. Results are never retained across mutable compiler passes. */
export function everyModuleNode(mod: IrModule, visitor: IrModuleVisitor): boolean {
const type = (node: IrType, loc: SrcLoc): boolean =>
visitor.type(node, loc) && everyTypeChild(node, (child) => type(child, loc));
// Every node of one type tree reports the tree's location: one shared
// child callback serves them all.
let typeLoc: SrcLoc;
const typeNode = (node: IrType): boolean =>
visitor.type(node, typeLoc) && everyTypeChild(node, typeNode);
const type = (node: IrType, loc: SrcLoc): boolean => {
typeLoc = loc;
return typeNode(node);
};
const expr = (node: IrExpr): boolean =>
visitor.expr(node) && everyExprChild(node, expr, stmt) && type(node.type, node.loc);
const stmt = (node: IrStmt): boolean => visitor.stmt(node) && everyStmtChild(node, expr, stmt);
+3 -26
View File
@@ -61,6 +61,7 @@ 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 { ALWAYS_THROW_LIB_FNS } from "./builtin-effects.js";
/** The promisified crypto calls' value kinds besides checked-dynamic. */
const CRYPTO_PROMISE_VALUES: Readonly<Record<string, "buffer" | "view" | "number" | "dyn">> = {
@@ -5345,32 +5346,8 @@ function validateFunction(
const callback = e.args[e.args.length - 1]?.type;
if (callback?.kind !== "func") err(`libCall ${e.fn} callback must be a function`, e.loc);
}
if (e.fn === "global.undefRead") {
// Always throws — the result type is whatever the declared type
// of the undefined global mapped to (never materialized).
return true;
}
if (
e.fn === "error.nodeThrow" ||
e.fn === "error.argTypeThrow" ||
e.fn === "error.propTypeThrow" ||
e.fn === "fs.mkdtempChk" ||
e.fn === "fs.readFileChk" ||
e.fn === "fs.opendirChk" ||
e.fn === "fs.watchFileChk" ||
e.fn === "fs.lchmodChk" ||
e.fn === "fs.readChk" ||
e.fn === "fs.streamOptsChk" ||
e.fn === "net.connectOptsChk" ||
e.fn === "tls.caCertsChk"
) {
// Always throws — the result type is the replaced expression's
// own (never materialized; the global.undefRead pattern). The
// fs Chk ladders qualify: every validation failure throws
// Node's typed error, and a full pass throws the trailing
// compiler-rendered fence.
return true;
}
// Always throws: the result type is never materialized.
if (ALWAYS_THROW_LIB_FNS.has(e.fn)) return true;
if (e.fn === "error.new" || e.fn === "error.newOptions") {
// Which builtin the runtime constructs is named by the result type.
if (!isBuiltinErrorObject(e.type)) {
+2
View File
@@ -23,8 +23,10 @@ import { runCompilerTask } from "./task.js";
declare function compilerNativePrivateDirectory(path: string, harden: boolean): boolean;
declare function compilerNativeCollectCycles(): void;
declare function compilerNativeBoundCycleGrowth(): void;
async function main(): Promise<number> {
compilerNativeBoundCycleGrowth();
setFrontendDisposedHook(() => compilerNativeCollectCycles());
const manifestPath = process.env["SCRIPTC_TOOLCHAIN"] ?? process.execPath + ".json";
let compiler: NativeCompiler | null = null;
@@ -49,7 +49,10 @@ describe("package shims compile without refusals", () => {
const entries = packageShimEntries();
test("the registry lists its shim modules", () => {
expect(entries.map((e) => e.slice(shimRoot.length + 1))).toEqual([
"chalk/v5.ts",
"fs-extra/index.ts",
"hot-shots/v8-unix-dgram.ts",
"hot-shots/v8.ts",
"undici/v5.ts",
"undici/v7.ts",
]);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/4815-touint32-wide-magnitudes.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6915-key-order-narrowed-throw.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6926-dynamic-collection-seeds.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6927-dynamic-collection-seeds.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7030-spawn-sync-env.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7036-child-env-path-lookup.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7261-acyclic-result-builders.ts"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/packages/compiler/shims/chalk/v5.ts",
"<repo>/tests/fixtures/npm/cases/chalk5-builder/main.ts"
],
"diags": []
}
@@ -1,5 +1,6 @@
{
"order": [
"<repo>/packages/compiler/shims/chalk/v5.ts",
"<repo>/tests/fixtures/npm/cases/chalk5/main.ts"
],
"diags": []
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/packages/compiler/shims/hot-shots/client.ts",
"<repo>/packages/compiler/shims/hot-shots/v8.ts",
"<repo>/tests/fixtures/npm/cases/hot-shots-buffered/main.ts"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/packages/compiler/shims/hot-shots/client.ts",
"<repo>/packages/compiler/shims/hot-shots/v8.ts",
"<repo>/tests/fixtures/npm/cases/hot-shots-send-error/main.ts"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/packages/compiler/shims/hot-shots/client.ts",
"<repo>/packages/compiler/shims/hot-shots/v8.ts",
"<repo>/tests/fixtures/npm/cases/hot-shots-tcp/main.ts"
],
"diags": []
}
@@ -1,5 +1,7 @@
{
"order": [
"<repo>/packages/compiler/shims/hot-shots/client.ts",
"<repo>/packages/compiler/shims/hot-shots/v8.ts",
"<repo>/tests/fixtures/npm/cases/hot-shots-udp/main.ts"
],
"diags": []
+80 -12
View File
@@ -1105,9 +1105,9 @@ char **scr_child_argv(ScrStr *cmd, ScrArr *args) {
static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms,
int killsig, int in_mode, int out_mode,
int err_mode) {
int err_mode, ScrArr *env_pairs) {
ScrWinSyncRes w;
scr_win_run_sync(cmd, args, NULL, in_mode, out_mode, err_mode, NULL, NULL,
scr_win_run_sync(cmd, args, NULL, in_mode, out_mode, err_mode, NULL, env_pairs,
timeout_ms, &w);
if (w.spawn_errname != NULL) {
free(w.out.data);
@@ -1136,19 +1136,26 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou
}
ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) {
return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0);
return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0, NULL);
}
ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms,
ScrStr *killsignal, double in_mode,
double out_mode, double err_mode) {
return scr_spawn_sync_env(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode,
NULL);
}
ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms,
ScrStr *killsignal, double in_mode, double out_mode,
double err_mode, ScrArr *env_pairs) {
int killsig = SIGTERM;
if (killsignal->len > 0) {
int resolved = scr_signal_from_name(killsignal);
if (resolved > 0) killsig = resolved;
}
return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode,
(int)out_mode, (int)err_mode);
(int)out_mode, (int)err_mode, env_pairs);
}
ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms,
@@ -3101,9 +3108,61 @@ static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status,
* documented divergence). timeout_ms > 0 sends `killsig` at the deadline
* and the result carries error: ETIMEDOUT + the signal — never a throw,
* Node's spawnSync shape. */
static char **scr_exec_envp(ScrArr *pairs);
static void scr_exec_envp_free(char **envp);
/* posix_spawnp for the child_process spawns. Without `envp` the command
* resolves on this process's PATH over `environ`. With `envp` (a
* replacement environment) it resolves the way Node's libuv runs execvp
* in the child: a name with a '/' runs as given; otherwise each entry of
* the replacement's PATH (an empty entry is the current directory), or of
* the default path when the replacement has no PATH, is tried in order,
* and a candidate whose exec fails with ENOENT, ENOTDIR or EACCES (a
* missing file, or a script whose interpreter is missing) moves on to the
* next entry. The last such failure is EACCES if any candidate was denied,
* else ENOENT. Relative entries are resolved by the child, which starts in
* `cwd`. Returns posix_spawn's error number. */
static int scr_spawn_command(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa,
const posix_spawnattr_t *attr, char *const *argv,
char *const *envp, const char *cwd) {
(void)cwd; /* the file actions change into it before the exec */
if (envp == NULL) return posix_spawnp(pid, file, fa, attr, argv, environ);
if (strchr(file, '/')) return posix_spawn(pid, file, fa, attr, argv, envp);
const char *path = "/usr/bin:/bin";
for (char *const *e = envp; *e; e++) {
if (strncmp(*e, "PATH=", 5) == 0) {
path = *e + 5;
break;
}
}
size_t file_len = strlen(file);
bool denied = false;
for (const char *dir = path;;) {
const char *end = strchr(dir, ':');
size_t dir_len = end ? (size_t)(end - dir) : strlen(dir);
const char *prefix = dir_len ? dir : ".";
size_t prefix_len = dir_len ? dir_len : 1;
char *candidate = malloc(prefix_len + 1 + file_len + 1);
if (!candidate) scr_child_oom();
memcpy(candidate, prefix, prefix_len);
candidate[prefix_len] = '/';
memcpy(candidate + prefix_len + 1, file, file_len + 1);
int err = posix_spawn(pid, candidate, fa, attr, argv, envp);
free(candidate);
if (err == EACCES) denied = true;
else if (err != ENOENT && err != ENOTDIR) return err;
if (!end) break;
dir = end + 1;
}
return denied ? EACCES : ENOENT;
}
/* env_pairs: NULL inherits the parent's environment; else [k,v,...]
* replaces it, as Node's `env` option does, and the command is looked up
* on the replacement's PATH (scr_spawn_command). */
static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms,
int killsig, int in_mode, int out_mode,
int err_mode) {
int err_mode, ScrArr *env_pairs) {
if (out_mode == 2 || err_mode == 2) {
/* Inherited output fds: flush the parent's buffers first so earlier
* logs precede the child's writes (Node's practical ordering). */
@@ -3138,10 +3197,12 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou
}
char **argv = scr_child_argv(cmd, args);
char **envp = env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL;
pid_t pid = -1;
int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, environ);
int spawn_err = scr_spawn_command(&pid, cmd->data, &fa, NULL, argv, envp, NULL);
posix_spawn_file_actions_destroy(&fa);
free(argv);
scr_exec_envp_free(envp);
if (cap_out) close(outfd[1]);
if (cap_err) close(errfd[1]);
@@ -3242,7 +3303,7 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou
}
ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) {
return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0);
return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0, NULL);
}
/* The options entry (cp.spawnSyncOpts): killSignal as a NAME ("" = the
@@ -3252,13 +3313,20 @@ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) {
ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms,
ScrStr *killsignal, double in_mode,
double out_mode, double err_mode) {
return scr_spawn_sync_env(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode,
NULL);
}
ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms,
ScrStr *killsignal, double in_mode, double out_mode,
double err_mode, ScrArr *env_pairs) {
int killsig = SIGTERM;
if (killsignal->len > 0) {
int resolved = scr_signal_from_name(killsignal);
if (resolved > 0) killsig = resolved;
}
return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode,
(int)out_mode, (int)err_mode);
(int)out_mode, (int)err_mode, env_pairs);
}
/* The runtime-string stdio entry (cp.spawnSyncStdioStr): the compiler
@@ -3478,8 +3546,8 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) {
char **argv = scr_child_argv(cmd, args);
char **envp = o->env_pairs != NULL ? scr_exec_envp(o->env_pairs) : NULL;
pid_t pid = -1;
int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv,
envp != NULL ? envp : environ);
int spawn_err = scr_spawn_command(&pid, cmd->data, &fa, NULL, argv, envp,
o->cwd != NULL ? o->cwd->data : NULL);
posix_spawn_file_actions_destroy(&fa);
free(argv);
scr_exec_envp_free(envp);
@@ -5037,8 +5105,8 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode,
char **envp = has_env && env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL;
pid_t pid = -1;
const char *file = shell ? "/bin/sh" : cmd->data;
int spawn_err = posix_spawnp(&pid, file, &fa, &at, argv,
envp != NULL ? envp : environ);
int spawn_err = scr_spawn_command(&pid, file, &fa, &at, argv, envp,
cwd != NULL && cwd->len > 0 ? cwd->data : NULL);
posix_spawnattr_destroy(&at);
posix_spawn_file_actions_destroy(&fa);
if (!shell) free(argv);
+5
View File
@@ -23,6 +23,11 @@ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args); /* +1, never throws */
ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms,
ScrStr *killsignal, double in_mode,
double out_mode, double err_mode);
/* scr_spawn_sync_opts with an `env` option: [k,v,...] pairs replace the
* child's environment (NULL inherits the parent's). */
ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms,
ScrStr *killsignal, double in_mode, double out_mode,
double err_mode, ScrArr *env_pairs);
/* The runtime-string stdio entry: the compiler proved the value is
* "pipe" | "ignore" | "inherit" by type; the mode mapping happens here. */
ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms,
+20 -5
View File
@@ -529,7 +529,8 @@ static SCR_TL size_t scr_cyc_live_after[SCR_CYC_NGENS];
* large program, say) those walks dominate collection time. The cap trades
* that walking for memory only when passes find nothing: while they find
* garbage, the yield keeps the fraction narrow. SCR_CYCLE_GROWTH_CAP lowers
* the cap for memory-sensitive programs: a power of two from 1 to 16, with
* the cap for memory-sensitive programs, as scr_cyc_set_growth_cap does for
* a program that sets its own: a power of two from 1 to 16, with
* other positive values rounded down to a power of two and larger ones read
* as 16. At 1 the heap may at most double past its size after the last pass
* at a level before the next. */
@@ -539,20 +540,34 @@ static SCR_TL unsigned char scr_cyc_growth_shift[SCR_CYC_NGENS];
static SCR_TL unsigned char scr_cyc_growth_streak[SCR_CYC_NGENS];
#endif
/* The growth cap a program sets for itself (scr_cyc_set_growth_cap), 0 for
* the default; SCR_CYCLE_GROWTH_CAP takes precedence over it. */
static long scr_cyc_growth_cap_set = 0;
static SCR_TL unsigned char scr_cyc_growth_max_shift_cached = 0;
/* Shift of the configured growth cap: 2 + log2(cap), since the base
* fraction is 1/4. */
static unsigned scr_cyc_growth_max_shift(void) {
static SCR_TL unsigned char cached = 0;
if (cached == 0) {
if (scr_cyc_growth_max_shift_cached == 0) {
const char *env = scr_getenv("SCR_CYCLE_GROWTH_CAP");
long v = env ? strtol(env, NULL, 10) : 0;
if (v <= 0) v = scr_cyc_growth_cap_set;
unsigned shift = SCR_CYC_GROWTH_MAX_SHIFT;
if (v > 0)
for (shift = 2; shift < SCR_CYC_GROWTH_MAX_SHIFT && (2L << (shift - 2)) <= v;)
shift++;
cached = (unsigned char)shift;
scr_cyc_growth_max_shift_cached = (unsigned char)shift;
}
return cached;
return scr_cyc_growth_max_shift_cached;
}
void scr_cyc_set_growth_cap(long cap) {
scr_cyc_growth_cap_set = cap;
scr_cyc_growth_max_shift_cached = 0;
/* Fractions already widened past the new cap start at it. */
unsigned max = scr_cyc_growth_max_shift();
for (unsigned gen = 0; gen < SCR_CYC_NGENS; gen++)
if (scr_cyc_growth_shift[gen] > max) scr_cyc_growth_shift[gen] = (unsigned char)max;
}
/* Doublings of each generation's backlog threshold earned by consecutive
+70
View File
@@ -2158,3 +2158,73 @@ void scr_dgram_send_chk(ScrDgramSocket *s, const ScrDyn *buffer, const ScrDyn *a
* it removes a 'message' listener. Defined last so the functions above
* keep their emission order. */
void scr_dgram_rearm(ScrDgramSocket *s) { scr_dgram_update_read(s); }
/* ── Unix datagram sockets for the hot-shots package shim ──────────────
* The unix-dgram addon's surface, which hot-shots' uds transport uses: a
* non-blocking AF_UNIX datagram socket connected to a path. Each call
* answers its result (fd, 0) or a negative errno, as the addon's binding
* does; send answers 1 when the socket would block (the addon's
* "congestion"). Defined after the functions above so their emission
* order is unchanged. */
#ifndef _WIN32
#include <sys/un.h>
#endif
double scr_dgram_unix_socket(void) {
#ifdef _WIN32
return -(double)ENOSYS;
#else
int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (fd < 0) return -(double)errno;
int flags = fcntl(fd, F_GETFL, 0);
if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
(void)fcntl(fd, F_SETFD, FD_CLOEXEC);
return (double)fd;
#endif
}
double scr_dgram_unix_connect(double fd, ScrStr *path) {
#ifdef _WIN32
(void)fd;
(void)path;
return -(double)ENOSYS;
#else
struct sockaddr_un addr;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
char *owned;
const char *bytes = scr_str_utf8(path, &owned);
size_t n = strlen(bytes);
if (n > sizeof addr.sun_path - 1) n = sizeof addr.sun_path - 1;
memcpy(addr.sun_path, bytes, n);
scr_str_utf8_end(owned);
if (connect((int)fd, (struct sockaddr *)&addr, sizeof addr) != 0) return -(double)errno;
return 0;
#endif
}
double scr_dgram_unix_send(double fd, ScrBytes *data) {
#ifdef _WIN32
(void)fd;
(void)data;
return -(double)ENOSYS;
#else
SCR_BYTES_SNAPSHOT(data);
ssize_t sent;
do {
sent = send((int)fd, data->data, data->len, 0);
} while (sent < 0 && errno == EINTR);
if (sent >= 0) return 0;
if (errno == EAGAIN || errno == EWOULDBLOCK) return 1;
return -(double)errno;
#endif
}
double scr_dgram_unix_close(double fd) {
#ifdef _WIN32
(void)fd;
return -(double)ENOSYS;
#else
return close((int)fd) != 0 ? -(double)errno : 0;
#endif
}
+7
View File
@@ -55,6 +55,13 @@ double scr_dgram_addr_port(ScrDgramSocket *s);
void scr_dgram_close(ScrDgramSocket *s, ScrClosure *cb /*moves, nullable*/); /* throws on closed */
void scr_dgram_unref(ScrDgramSocket *s);
void scr_dgram_ref(ScrDgramSocket *s);
/* Unix datagram sockets for the hot-shots package shim (the unix-dgram
* addon's surface): each answers its result (fd, 0) or a negative errno;
* send answers 1 when the socket would block. */
double scr_dgram_unix_socket(void);
double scr_dgram_unix_connect(double fd, ScrStr *path /*borrowed*/);
double scr_dgram_unix_send(double fd, ScrBytes *data /*borrowed*/);
double scr_dgram_unix_close(double fd);
/* Socket options: Node's checks and errors (ErrnoException "<syscall>
* <CODE>", ERR_SOCKET_BAD_BUFFER_SIZE, ERR_SOCKET_BUFFER_SIZE, the
* TypeError after close()). The TTL setters and setMulticastLoopback
+21 -3
View File
@@ -4323,14 +4323,32 @@ static void scr_http_client_end_raw(ScrHttpClientReq *c, const char *data, size_
}
if (!c->head_sent) {
/* whole body known NOW: Content-Length framing (or none — the head
* serializer's empty-body method split) */
* serializer's empty-body method split). Head and body go out as one
* write, as Node sends them: on a connected (reused) socket two writes
* would be two segments, and the server could answer before the body
* arrives. */
c->hold_head = true;
scr_http_client_send_head(c, (long long)len);
c->hold_head = false;
if (scr_exc_pending()) return;
if (c->chunked) {
scr_http_client_write_raw(c, data, len);
if (c->sock) scr_http_send_chunk_end(c->sock, &c->trailers);
} else if (c->sock && len > 0) {
scr_net_sock_write_native(c->sock, data, len);
} else if (c->held_head != NULL) {
char *head = c->held_head;
size_t head_len = c->held_head_len;
c->held_head = NULL;
c->held_head_len = c->held_head_cap = 0;
if (c->sock) {
if (len > 0) {
char *joined = realloc(head, head_len + len);
if (!joined) scr_http_oom();
head = joined;
memcpy(head + head_len, data, len);
}
scr_net_sock_write_native(c->sock, head, head_len + len);
}
free(head);
}
} else {
scr_http_client_write_raw(c, data, len);
+19 -13
View File
@@ -1577,6 +1577,20 @@ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) {
static ScrDyn *scr_native_iterator_drain(const ScrDyn *src);
/* The values a collection constructor's iterable seed gives, in order: +1
* array, or NULL with the exception pending. A native iterator (a Map's
* keys(), a Set's values()) drains, which runs no program code; arrays,
* strings, typed arrays and collections pack; other iterables (a
* generator, an object with a Symbol.iterator method) step their
* iterator to the end, so their values are all taken before the
* constructor adds the first one. */
static ScrDyn *scr_dyn_seed_values(const ScrDyn *src) {
if (src->kind == SCR_DYN_HANDLE &&
(src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step))
return scr_native_iterator_drain(src);
return scr_dyn_destructure_rest(src, NULL);
}
/* The for-of-over-dyn pack accessors: the emitted index loop drives them
* over a scr_dyn_iter_pack result (ARR by construction — the defensive
* arms cover nothing reachable from that lowering). Never throw. */
@@ -1592,18 +1606,10 @@ ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src) {
if (scr_exc_pending()) return NULL;
}
/* A native iterator (sp.entries(), a Map's entries()) or a handle that
* steps through live entries (URLSearchParams): producing its entries
* runs no program code, so draining them first is what the consumer
* observes. */
if (src->kind == SCR_DYN_HANDLE &&
(src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step))
return scr_native_iterator_drain(src);
if (src->kind == SCR_DYN_HANDLE || src->kind == SCR_DYN_TYPED_REF) {
static const char message[] = "new Map(entries) over native non-array iterables has no lowering";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return NULL;
}
return scr_dyn_iter_pack(src, NULL);
* steps through live entries (URLSearchParams) drains; a Set, a
* generator or another iterable gives its values in order
* (scr_dyn_seed_values). */
return scr_dyn_seed_values(src);
}
ScrDyn *scr_dyn_map_seed_entry(const ScrDyn *entry) {
@@ -14860,7 +14866,7 @@ ScrDyn *scr_dyn_native_set(ScrMap *value) {
ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed) {
ScrDyn *items = NULL;
if (seed->kind != SCR_DYN_NULL && seed->kind != SCR_DYN_UNDEF) {
items = scr_dyn_iter_pack(seed, NULL);
items = scr_dyn_seed_values(seed);
if (!items) return NULL;
}
ScrMap *set = scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_F64, &scr_map_dyn_set_ops);
+4
View File
@@ -670,6 +670,10 @@ static inline void scr_cyc_dead(void *obj) {
* live heap — do not put it on a per-turn path. */
void scr_collect_cycles(void);
void scr_collect_cycles_phase(void);
/* Sets the growth cap SCR_CYCLE_GROWTH_CAP sets (scr_cycle.c), for a program
* that knows its own memory profile; the environment variable takes
* precedence. Call it before the program allocates much. */
void scr_cyc_set_growth_cap(long cap);
void scr_cyc_context_cleanup(void);
/* One pass on the normal generational schedule, for callers that reach a
+22
View File
@@ -536,6 +536,27 @@ static void check_mixed_dead_structure_narrows(void) {
check(live <= narrowed, "a pass that freed a dead mature and old structure did not narrow the growth fraction");
}
/* scr_cyc_set_growth_cap sets the cap SCR_CYCLE_GROWTH_CAP sets, and a
* fraction already widened past it starts at it: after passes that found
* nothing widened the old level's fraction to the configured cap, a cap of
* 1 has the next dead cycle go once the heap has doubled past the floor.
* Restores the configured cap. */
static void check_program_growth_cap(void) {
enum { FLOOR = 4096 };
if (getenv("SCR_CYCLE_GROWTH_CAP") != NULL) return; /* it takes precedence */
size_t slack = 2 * configured_nursery_threshold() + 2;
size_t live = 0;
for (int i = 0; i < 4; i++) live = grow_dead_cycle_until_full_pass(0, false);
check(live > FLOOR + FLOOR * configured_growth_cap(),
"passes that found nothing did not widen the growth fraction to the cap");
scr_cyc_set_growth_cap(1);
for (int i = 0; i < 4; i++) {
live = grow_dead_cycle_until_full_pass(0, false);
check(live <= FLOOR + FLOOR + slack, "a program's growth cap of 1 did not bound the heap");
}
scr_cyc_set_growth_cap(0);
}
/* The phase sweep (scr_collect_cycles_phase) is for a caller that knows a
* phase's data has just died: it sweeps, and the growth schedule starts
* over. After passes that find nothing have widened the old level's
@@ -1500,6 +1521,7 @@ int main(void) {
check_large_old_buffer_with_nursery_candidates();
check_unjudged_backlog_follows_growth();
check_acyclic_objects();
check_program_growth_cap();
printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n",
configured_nursery_threshold(), configured_growth_cap());
return 0;
+32 -4
View File
@@ -282,6 +282,26 @@ const adaptedGetters = new Set([
"operand",
"questionToken",
]);
// A getter selects its child's wire slot by kind directly: an integer
// switch, without astChildOrder's switch on the child's name. `answer`
// maps a slot to the returned expression; `missing` is returned for kinds
// without the child.
const slotCases = (name, answer, missing) => {
const kindsBySlot = new Map();
for (const [kind, childNames] of Object.entries(protocol.childProperties)) {
const slot = childNames.indexOf(name);
if (slot < 0) continue;
if (!kindsBySlot.has(slot)) kindsBySlot.set(slot, []);
kindsBySlot.get(slot).push(kind);
}
const lines = [" switch (this.kind) {"];
for (const [slot, kinds] of [...kindsBySlot].sort((a, b) => a[0] - b[0])) {
for (const kind of kinds) lines.push(` case ${kind}:`);
lines.push(` return ${answer(slot)};`);
}
lines.push(" default:", ` return ${missing};`, " }");
return lines.join("\n");
};
const getters = names
.filter((name) => !adaptedGetters.has(name))
.map((name) => {
@@ -292,12 +312,19 @@ const getters = names
.replaceAll("RemoteNode", "AstNode");
const method =
type.includes("AstNode |") && type.includes("AstNodeList")
? "child"
? "childAt"
: type.includes("AstNodeList")
? "childList"
: "childNode";
return ` get ${name}(): ${type} {\n return this.${method}(${JSON.stringify(name)});\n }`;
? "childListAt"
: "childNodeAt";
return ` get ${name}(): ${type} {\n${slotCases(name, (slot) => `this.${method}(${slot})`, "undefined")}\n }`;
});
// The adapted getters read their raw child through the same slot switch.
const adaptedSlots = [...adaptedGetters]
.sort()
.map(
(name) =>
` private ${name}Slot(): number {\n${slotCases(name, (slot) => String(slot), "-1")}\n }`,
);
const nodeTarget = join(root, "packages/compiler/src/frontend/ts7/ast-node.ts");
const before = readFileSync(nodeTarget, "utf8");
if (
@@ -311,6 +338,7 @@ const after = before.replace(
" // BEGIN GENERATED CHILD GETTERS",
` // Generated from typescript@${version}; run scripts/generate-ts7-ast-schema.mjs.`,
...getters,
...adaptedSlots,
" // END GENERATED CHILD GETTERS",
].join("\n"),
);
@@ -0,0 +1,79 @@
// ToInt32 and ToUint32 (`| 0`, `>>> 0`, `^`, `<<`, `>>`) of doubles around
// 2^53 and 2^63, of multiplicative hash products that pass 2^53, and of
// NaN, the infinities and -0.
const values: number[] = [
2 ** 53 - 1,
2 ** 53,
2 ** 53 + 2,
-(2 ** 53) - 2,
2 ** 53 * 3 + 4,
2 ** 62,
2 ** 62 + 2 ** 40 + 5 * 2 ** 10,
2 ** 63 - 1024,
-(2 ** 63) + 1024,
2 ** 63,
-(2 ** 63),
2 ** 63 + 2048,
2 ** 64,
2 ** 64 - 2048,
-(2 ** 64) + 4096,
2 ** 80 + 2 ** 33,
1.5e300,
Infinity,
-Infinity,
NaN,
-0,
0.75,
-2.5,
4294967295.5,
];
function opaque(i: number): number {
return values[i % values.length]!;
}
for (let i = 0; i < values.length; i++) {
const v = opaque(i);
console.log(
String(v),
v | 0,
v >>> 0,
v ^ 0x5a5a5a5a,
(v << 3) | 0,
v >> 7,
v >>> 9,
);
}
function fnv(bytes: Uint8Array): number {
let h = 2166136261;
for (let i = 0; i < bytes.length; i++) h = ((h ^ bytes[i]!) * 16777619) >>> 0;
return h;
}
function mix(bytes: Uint8Array): number {
let h = 0;
for (let i = 0; i < bytes.length; i++) h = ((h ^ (bytes[i]! - 64)) * 16777619) | 0;
return h;
}
function poly(bytes: Uint8Array): number {
let h = 7;
for (let i = 0; i < bytes.length; i++) h = (h * 1000003 + bytes[i]!) | 0;
return h;
}
function wide(bytes: Uint8Array): number {
// Products near 2^63 and past it: the conversion of each must match.
let h = 3;
for (let i = 0; i < bytes.length; i++) h = ((h ^ bytes[i]!) * 2147483647 * (bytes[i]! * 17 + 1)) | 0;
return h;
}
const bytes = new Uint8Array(4096);
let seed = 12345;
for (let i = 0; i < bytes.length; i++) {
seed = (seed * 1103515245 + 12345) % 2147483648;
bytes[i] = seed >>> 23;
}
console.log("fnv", fnv(bytes), "mix", mix(bytes), "poly", poly(bytes), "wide", wide(bytes));
@@ -0,0 +1,72 @@
// A record whose reads narrow a possibly-undefined binding (the narrowing
// ternary's throw arm stands in for a value of the record's type) still
// takes its literal's key order: a module-level accumulator typed by an
// interface declared in another order, as an event batcher builds it.
interface Entry {
id: string;
at: number;
text?: string;
stream: "out" | "err";
}
interface Batch {
team: string;
project: string;
job: string;
origin: "task" | "worker";
host: string;
entries: Entry[];
sizes?: number[];
stage?: "live" | "staging";
branch?: string;
run?: string;
entry?: string;
path?: string;
}
interface Job {
id: string;
run?: string;
project: string;
team: string;
host: string;
stage: string | null;
}
let acc: Batch | undefined;
function append(j: Job, text: string): void {
if (!acc) {
acc = {
run: j.run,
job: j.id,
stage: j.stage === null ? "staging" : "live",
project: j.project,
team: j.team,
origin: "task",
host: j.host,
entry: ".",
entries: [],
};
}
acc.entries.push({ id: String(acc.entries.length), at: 1, text, stream: "out" });
setTimeout(() => {
const batch = acc;
if (!batch) return;
acc = undefined;
for (const line of split(batch)) console.log(line);
}, 1);
}
function split(batch: Batch): string[] {
const json = JSON.stringify(batch);
if (json.length < 200 || batch.entries.length < 2) return [json];
const index = Math.floor(batch.entries.length / 2);
const first = { ...batch, entries: batch.entries.slice(0, index) };
const second = { ...batch, entries: batch.entries.slice(index) };
return [...split(first), ...split(second)];
}
const j: Job = { id: "job", run: "r1", project: "prj", team: "core", host: "h", stage: "live" };
append(j, "one");
append(j, "two");
setTimeout(() => {
for (let i = 0; i < 6; i++) append(j, "line " + i);
}, 5);
@@ -0,0 +1,28 @@
// Set, Map, WeakSet and Object.fromEntries over checked-dynamic iterables:
// native iterators, generators, objects with a Symbol.iterator method and
// collections, as seeds that reach the runtime untyped.
function* pairs() { yield ["g", 1]; yield ["h", 2]; }
function* values() { yield 1; yield 2; yield 2; }
const iterable = { *[Symbol.iterator]() { yield ["i", 3]; } };
const valueIterable = { *[Symbol.iterator]() { yield "a"; yield "b"; } };
function attempt(label, make) {
try { console.log(label, make()); } catch (e) { console.log(label, "threw", e.name, e.message); }
}
function viaSet(x) { return new Set(x).size; }
function viaMap(x) { return [...new Map(x).keys()].join(); }
function viaWeakSet(x) { return new WeakSet(x).has(key); }
const key = {};
const m = new Map([["a", 1], ["b", 2]]);
attempt("set keys()", () => viaSet(m.keys()));
attempt("set values()", () => viaSet(new Set([1, 2]).values()));
attempt("set generator", () => viaSet(values()));
attempt("set iterable", () => viaSet(valueIterable));
attempt("set string", () => viaSet("abca"));
attempt("set number", () => viaSet(5));
attempt("map generator", () => viaMap(pairs()));
attempt("map iterable", () => viaMap(iterable));
attempt("map set", () => viaMap(new Set([["s", 1]])));
attempt("map entries()", () => viaMap(m.entries()));
attempt("map bad entry", () => viaMap([1]));
attempt("weakset generator", () => viaWeakSet((function* () { yield key; })()));
attempt("fromEntries generator", () => JSON.stringify(Object.fromEntries(pairs())));
@@ -0,0 +1,9 @@
// Set and Map seeds held as any: native iterators and generators.
function* values(): Generator<number> { yield 1; yield 2; yield 2; }
function* pairs(): Generator<[string, number]> { yield ["a", 1]; yield ["b", 2]; }
const m = new Map<string, number>([["x", 1], ["y", 2]]);
const anyKeys: any = m.keys();
const anyGen: any = values();
const anyPairs: any = pairs();
console.log(new Set(anyKeys).size, new Set(anyGen).size, new Map(anyPairs).size);
console.log(new Set(values()).size, new Map(pairs()).get("b"), new Set(m.values()).size);
+47
View File
@@ -0,0 +1,47 @@
// spawnSync's env option replaces the child's environment, alone or with
// the other options.
import { spawnSync } from "node:child_process";
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const merged = spawnSync("sh", ["-c", 'echo "$GREETING $HOME_SET"'], {
env: { ...process.env, GREETING: "hello", HOME_SET: process.env.HOME ? "yes" : "no" },
encoding: "utf8",
});
console.log(merged.stdout.trim(), merged.status);
const only = spawnSync("/bin/sh", ["-c", 'echo "${GREETING:-unset} ${HOME:-nohome}"'], {
env: { GREETING: "only" },
encoding: "utf8",
});
console.log(only.stdout.trim());
const empty = spawnSync("/bin/sh", ["-c", "env | grep -c ."], { env: {}, encoding: "utf8" });
console.log(Number(empty.stdout.trim()) <= 3, empty.status);
const quiet = spawnSync("sh", ["-c", 'echo "$MODE" >&2; exit 4'], {
env: { PATH: process.env.PATH ?? "/usr/bin:/bin", MODE: "loud" },
stdio: ["ignore", "pipe", "pipe"],
timeout: 5000,
});
console.log(quiet.status, quiet.stderr.toString().trim(), quiet.stdout.toString() === "");
const missing = spawnSync("/nonexistent/tool", [], { env: { A: "1" } });
console.log(missing.status, missing.error?.message);
// The command is found on the replacement environment's PATH, or on the
// default path when the replacement has none, not on the parent's PATH.
const dir = mkdtempSync(join(tmpdir(), "scr-spawn-path-"));
writeFileSync(join(dir, "child-only-tool"), "#!/bin/sh\necho child-only\n");
chmodSync(join(dir, "child-only-tool"), 0o755);
writeFileSync(join(dir, "not-executable"), "#!/bin/sh\necho never\n");
const viaPath = spawnSync("child-only-tool", [], { env: { PATH: `/nonexistent:${dir}` }, encoding: "utf8" });
console.log(viaPath.status, viaPath.stdout.trim());
const defaultPath = spawnSync("sh", ["-c", "echo default"], { env: { A: "1" }, encoding: "utf8" });
console.log(defaultPath.status, defaultPath.stdout.trim());
const notOnPath = spawnSync("sh", ["-c", "echo never"], { env: { PATH: dir }, encoding: "utf8" });
console.log(notOnPath.status, notOnPath.error?.message);
const denied = spawnSync("not-executable", [], { env: { PATH: dir }, encoding: "utf8" });
console.log(denied.status, denied.error?.message);
rmSync(dir, { recursive: true });
@@ -0,0 +1,53 @@
// spawn and execFileSync with an env option find the command on the
// replacement environment's PATH, or on the default path when it has
// none, as Node does; a relative PATH entry is read from the cwd option.
import { execFileSync, spawn } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const dir = mkdtempSync(join(tmpdir(), "scr-child-path-"));
mkdirSync(join(dir, "tools"));
writeFileSync(join(dir, "tools", "child-tool"), "#!/bin/sh\necho child-tool $1\n");
chmodSync(join(dir, "tools", "child-tool"), 0o755);
// An earlier PATH entry holding a script whose interpreter is missing
// moves the lookup on to the next entry, as execvp does.
mkdirSync(join(dir, "broken"));
writeFileSync(join(dir, "broken", "child-tool"), "#!/nonexistent/interpreter\n");
chmodSync(join(dir, "broken", "child-tool"), 0o755);
const show = (e: unknown): string => {
const err = e as NodeJS.ErrnoException;
return `${err.code} ${err.message.replace(dir, "<dir>")}`;
};
console.log(execFileSync("child-tool", ["sync"], { env: { PATH: join(dir, "tools") }, encoding: "utf8" }).trim());
console.log(execFileSync("sh", ["-c", "echo default"], { env: { A: "1" }, encoding: "utf8" }).trim());
try {
execFileSync("sh", ["-c", "echo never"], { env: { PATH: "/nonexistent" }, encoding: "utf8" });
} catch (e) {
console.log(show(e));
}
const twoEntries = `${join(dir, "broken")}:${join(dir, "tools")}`;
console.log(execFileSync("child-tool", ["fallthrough"], { env: { PATH: twoEntries }, encoding: "utf8" }).trim());
try {
execFileSync("child-tool", [], { env: { PATH: join(dir, "broken") }, encoding: "utf8" });
} catch (e) {
console.log(show(e));
}
console.log(
execFileSync("child-tool", ["relative"], { env: { PATH: "tools" }, cwd: dir, encoding: "utf8" }).trim(),
);
const child = spawn("child-tool", ["async"], { env: { PATH: twoEntries }, stdio: ["ignore", "pipe", "ignore"] });
let out = "";
child.stdout!.on("data", (chunk: Buffer) => (out += chunk.toString()));
child.on("close", (code) => {
console.log(out.trim(), code);
const missing = spawn("sh", ["-c", "true"], { env: { PATH: "/nonexistent" }, stdio: "ignore" });
missing.on("error", (e) => {
console.log("error", show(e));
rmSync(dir, { recursive: true });
});
});
@@ -0,0 +1,86 @@
// Records built by functions that return object literals, as tree and list
// builders are written. The backend gives such a function a result-slot
// entry that writes the literal's fields into a caller's frame storage, and
// those stores do not make the record's fields mutable: a type no store
// changes after construction stays acyclic and needs no cycle-collector
// header. This pins what such programs print: rebuilt trees, a shared
// subtree (a DAG), a builder whose result the caller only projects (so the
// entry runs), a list longer than the runtime's teardown depth budget, and
// a record type the program writes after construction, whose rings only
// the collector frees. The sanitized lane asserts every object is freed.
interface Node {
value: number;
left: Node | null;
right: Node | null;
}
function leaf(value: number): Node {
return { value, left: null, right: null };
}
function branch(value: number, left: Node | null, right: Node | null): Node {
return { value, left, right };
}
function tree(depth: number, value: number): Node {
return depth === 0 ? leaf(value) : branch(value, tree(depth - 1, value * 2), tree(depth - 1, value * 2 + 1));
}
function sum(node: Node | null): number {
return node === null ? 0 : node.value + sum(node.left) + sum(node.right);
}
let total = 0;
for (let generation = 0; generation < 8; generation++) total += sum(tree(8, generation + 1));
console.log("trees", total);
// A subtree shared by both branches is released once.
const shared = tree(3, 1);
const dag = branch(0, shared, shared);
console.log("dag", sum(dag), JSON.stringify(branch(1, leaf(2), null)));
// Only projected: the result lives in the caller's frame, which releases
// the subtree it holds.
const top = branch(5, tree(2, 1), null);
console.log("top", top.value, sum(top.left), top.right === null);
interface Cell {
n: number;
next: Cell | null;
}
function cons(n: number, next: Cell | null): Cell {
return { n, next };
}
let list: Cell | null = null;
for (let i = 0; i < 5000; i++) list = cons(i, list);
let length = 0;
let last = -1;
for (let c = list; c !== null; c = c.next) {
length++;
last = c.n;
}
console.log("list", length, last);
list = null;
// A record type written after construction keeps its cycle-collector
// header: these rings are garbage only the collector can free.
interface Ring {
id: number;
next: Ring | null;
}
function ring(id: number, next: Ring | null): Ring {
return { id, next };
}
let ids = 0;
for (let i = 0; i < 200; i++) {
const a = ring(i, null);
const b = ring(i + 1, a);
a.next = b;
ids += a.next.id + b.next!.id;
}
console.log("rings", ids);
+48
View File
@@ -0,0 +1,48 @@
// chalk 5 builders used as values (the package compiles from scriptc's
// built-in shim): builders in function slots and arrays, styles picked at
// run time, builders in record fields called with any number of arguments,
// visible at level 0, chalkStderr, and the root's own call.
import chalk, { Chalk, chalkStderr, type ChalkInstance, type ForegroundColorName } from "chalk";
const show = (label: string, value: string): void => {
console.log(label + " " + JSON.stringify(value));
};
const c = new Chalk({ level: 1 });
const paint: (text: string) => string = c.green;
show("slot", paint("go"));
const styles: ChalkInstance[] = [c.red, c.bold, c.underline.blue];
show("array", styles.map((style) => style("x")).join("|"));
const byName = (name: ForegroundColorName): ChalkInstance => {
switch (name) {
case "red":
return c.red;
case "green":
return c.green;
default:
return c.white;
}
};
show("picked", byName("red")("r") + byName("green")("g") + byName("cyan")("w"));
let chain: ChalkInstance = c;
for (const step of ["bold", "italic", "yellow"]) {
chain = step === "bold" ? chain.bold : step === "italic" ? chain.italic : chain.yellow;
}
show("built", chain("chain"));
show("cached", c.red === c.red ? "same" : "different");
show("nested close", c.red("a" + c.blue("b") + "c" + c.red("d") + "e"));
show("bg nested", c.bgRed("a" + c.bgBlue("b") + "c"));
const off = new Chalk({ level: 0 });
show("visible off", off.visible("v") + "|" + off.red.visible("v") + "|" + off.red("plain"));
show("root call", c("a", 1, true) + "|" + c() + "|" + c(undefined) + "|" + c(null, "n"));
show("builder args", c.red(undefined) + "|" + c.red(null) + "|" + c.red(1, 2) + "|" + c.red());
const theme = { error: c.red, note: c.bold.blue };
show("field args", theme.error("a", "b") + "|" + theme.note("x", 1, true) + "|" + theme.error("one") + "|" + theme.error());
show("multiline", c.cyan("one\r\ntwo\nthree"));
show("stderr level", String(chalkStderr.level >= 0));
show("rgb levels", [1, 2, 3].map((level) => new Chalk({ level: level as 1 | 2 | 3 }).rgb(255, 136, 0)("o")).join("|"));
show("hex bg", new Chalk({ level: 2 }).bgHex("#abcdef")("h") + new Chalk({ level: 1 }).bgHex("123")("i"));
c.level = 0;
show("after level 0", paint("go") + "|" + c.red("r"));
show("default red", chalk.level >= 0 ? "ok" : "bad");
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,83 @@
{
"name": "chalk",
"version": "5.6.2",
"description": "Terminal string styling done right",
"license": "MIT",
"repository": "chalk/chalk",
"funding": "https://github.com/chalk/chalk?sponsor=1",
"type": "module",
"main": "./source/index.js",
"exports": "./source/index.js",
"imports": {
"#ansi-styles": "./source/vendor/ansi-styles/index.js",
"#supports-color": {
"node": "./source/vendor/supports-color/index.js",
"default": "./source/vendor/supports-color/browser.js"
}
},
"types": "./source/index.d.ts",
"sideEffects": false,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"scripts": {
"test": "xo && c8 ava && tsd",
"bench": "matcha benchmark.js"
},
"files": [
"source",
"!source/index.test-d.ts"
],
"keywords": [
"color",
"colour",
"colors",
"terminal",
"console",
"cli",
"string",
"ansi",
"style",
"styles",
"tty",
"formatting",
"rgb",
"256",
"shell",
"xterm",
"log",
"logging",
"command-line",
"text"
],
"devDependencies": {
"@types/node": "^16.11.10",
"ava": "^3.15.0",
"c8": "^7.10.0",
"color-convert": "^2.0.1",
"execa": "^6.0.0",
"log-update": "^5.0.0",
"matcha": "^0.7.0",
"tsd": "^0.19.0",
"xo": "^0.57.0",
"yoctodelay": "^2.0.0"
},
"xo": {
"rules": {
"unicorn/prefer-string-slice": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/consistent-type-exports": "off",
"@typescript-eslint/consistent-type-definitions": "off",
"unicorn/expiring-todo-comments": "off"
}
},
"c8": {
"reporter": [
"text",
"lcov"
],
"exclude": [
"source/vendor"
]
}
}
@@ -0,0 +1,325 @@
// TODO: Make it this when TS suports that.
// import {ModifierName, ForegroundColor, BackgroundColor, ColorName} from '#ansi-styles';
// import {ColorInfo, ColorSupportLevel} from '#supports-color';
import {
ModifierName,
ForegroundColorName,
BackgroundColorName,
ColorName,
} from './vendor/ansi-styles/index.js';
import {ColorInfo, ColorSupportLevel} from './vendor/supports-color/index.js';
export interface Options {
/**
Specify the color support for Chalk.
By default, color support is automatically detected based on the environment.
Levels:
- `0` - All colors disabled.
- `1` - Basic 16 colors support.
- `2` - ANSI 256 colors support.
- `3` - Truecolor 16 million colors support.
*/
readonly level?: ColorSupportLevel;
}
/**
Return a new Chalk instance.
*/
export const Chalk: new (options?: Options) => ChalkInstance; // eslint-disable-line @typescript-eslint/naming-convention
export interface ChalkInstance {
(...text: unknown[]): string;
/**
The color support for Chalk.
By default, color support is automatically detected based on the environment.
Levels:
- `0` - All colors disabled.
- `1` - Basic 16 colors support.
- `2` - ANSI 256 colors support.
- `3` - Truecolor 16 million colors support.
*/
level: ColorSupportLevel;
/**
Use RGB values to set text color.
@example
```
import chalk from 'chalk';
chalk.rgb(222, 173, 237);
```
*/
rgb: (red: number, green: number, blue: number) => this;
/**
Use HEX value to set text color.
@param color - Hexadecimal value representing the desired color.
@example
```
import chalk from 'chalk';
chalk.hex('#DEADED');
```
*/
hex: (color: string) => this;
/**
Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color.
@example
```
import chalk from 'chalk';
chalk.ansi256(201);
```
*/
ansi256: (index: number) => this;
/**
Use RGB values to set background color.
@example
```
import chalk from 'chalk';
chalk.bgRgb(222, 173, 237);
```
*/
bgRgb: (red: number, green: number, blue: number) => this;
/**
Use HEX value to set background color.
@param color - Hexadecimal value representing the desired color.
@example
```
import chalk from 'chalk';
chalk.bgHex('#DEADED');
```
*/
bgHex: (color: string) => this;
/**
Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color.
@example
```
import chalk from 'chalk';
chalk.bgAnsi256(201);
```
*/
bgAnsi256: (index: number) => this;
/**
Modifier: Reset the current style.
*/
readonly reset: this;
/**
Modifier: Make the text bold.
*/
readonly bold: this;
/**
Modifier: Make the text have lower opacity.
*/
readonly dim: this;
/**
Modifier: Make the text italic. *(Not widely supported)*
*/
readonly italic: this;
/**
Modifier: Put a horizontal line below the text. *(Not widely supported)*
*/
readonly underline: this;
/**
Modifier: Put a horizontal line above the text. *(Not widely supported)*
*/
readonly overline: this;
/**
Modifier: Invert background and foreground colors.
*/
readonly inverse: this;
/**
Modifier: Print the text but make it invisible.
*/
readonly hidden: this;
/**
Modifier: Puts a horizontal line through the center of the text. *(Not widely supported)*
*/
readonly strikethrough: this;
/**
Modifier: Print the text only when Chalk has a color level above zero.
Can be useful for things that are purely cosmetic.
*/
readonly visible: this;
readonly black: this;
readonly red: this;
readonly green: this;
readonly yellow: this;
readonly blue: this;
readonly magenta: this;
readonly cyan: this;
readonly white: this;
/*
Alias for `blackBright`.
*/
readonly gray: this;
/*
Alias for `blackBright`.
*/
readonly grey: this;
readonly blackBright: this;
readonly redBright: this;
readonly greenBright: this;
readonly yellowBright: this;
readonly blueBright: this;
readonly magentaBright: this;
readonly cyanBright: this;
readonly whiteBright: this;
readonly bgBlack: this;
readonly bgRed: this;
readonly bgGreen: this;
readonly bgYellow: this;
readonly bgBlue: this;
readonly bgMagenta: this;
readonly bgCyan: this;
readonly bgWhite: this;
/*
Alias for `bgBlackBright`.
*/
readonly bgGray: this;
/*
Alias for `bgBlackBright`.
*/
readonly bgGrey: this;
readonly bgBlackBright: this;
readonly bgRedBright: this;
readonly bgGreenBright: this;
readonly bgYellowBright: this;
readonly bgBlueBright: this;
readonly bgMagentaBright: this;
readonly bgCyanBright: this;
readonly bgWhiteBright: this;
}
/**
Main Chalk object that allows to chain styles together.
Call the last one as a method with a string argument.
Order doesn't matter, and later styles take precedent in case of a conflict.
This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
*/
declare const chalk: ChalkInstance;
export const supportsColor: ColorInfo;
export const chalkStderr: typeof chalk;
export const supportsColorStderr: typeof supportsColor;
export {
ModifierName, ForegroundColorName, BackgroundColorName, ColorName,
modifierNames, foregroundColorNames, backgroundColorNames, colorNames,
// } from '#ansi-styles';
} from './vendor/ansi-styles/index.js';
export {
ColorInfo,
ColorSupport,
ColorSupportLevel,
// } from '#supports-color';
} from './vendor/supports-color/index.js';
// TODO: Remove these aliases in the next major version
/**
@deprecated Use `ModifierName` instead.
Basic modifier names.
*/
export type Modifiers = ModifierName;
/**
@deprecated Use `ForegroundColorName` instead.
Basic foreground color names.
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
*/
export type ForegroundColor = ForegroundColorName;
/**
@deprecated Use `BackgroundColorName` instead.
Basic background color names.
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
*/
export type BackgroundColor = BackgroundColorName;
/**
@deprecated Use `ColorName` instead.
Basic color names. The combination of foreground and background color names.
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
*/
export type Color = ColorName;
/**
@deprecated Use `modifierNames` instead.
Basic modifier names.
*/
export const modifiers: readonly Modifiers[];
/**
@deprecated Use `foregroundColorNames` instead.
Basic foreground color names.
*/
export const foregroundColors: readonly ForegroundColor[];
/**
@deprecated Use `backgroundColorNames` instead.
Basic background color names.
*/
export const backgroundColors: readonly BackgroundColor[];
/**
@deprecated Use `colorNames` instead.
Basic color names. The combination of foreground and background color names.
*/
export const colors: readonly Color[];
export default chalk;
@@ -0,0 +1,225 @@
import ansiStyles from '#ansi-styles';
import supportsColor from '#supports-color';
import { // eslint-disable-line import/order
stringReplaceAll,
stringEncaseCRLFWithFirstIndex,
} from './utilities.js';
const {stdout: stdoutColor, stderr: stderrColor} = supportsColor;
const GENERATOR = Symbol('GENERATOR');
const STYLER = Symbol('STYLER');
const IS_EMPTY = Symbol('IS_EMPTY');
// `supportsColor.level` → `ansiStyles.color[name]` mapping
const levelMapping = [
'ansi',
'ansi',
'ansi256',
'ansi16m',
];
const styles = Object.create(null);
const applyOptions = (object, options = {}) => {
if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
throw new Error('The `level` option should be an integer from 0 to 3');
}
// Detect level if not set manually
const colorLevel = stdoutColor ? stdoutColor.level : 0;
object.level = options.level === undefined ? colorLevel : options.level;
};
export class Chalk {
constructor(options) {
// eslint-disable-next-line no-constructor-return
return chalkFactory(options);
}
}
const chalkFactory = options => {
const chalk = (...strings) => strings.join(' ');
applyOptions(chalk, options);
Object.setPrototypeOf(chalk, createChalk.prototype);
return chalk;
};
function createChalk(options) {
return chalkFactory(options);
}
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
for (const [styleName, style] of Object.entries(ansiStyles)) {
styles[styleName] = {
get() {
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
Object.defineProperty(this, styleName, {value: builder});
return builder;
},
};
}
styles.visible = {
get() {
const builder = createBuilder(this, this[STYLER], true);
Object.defineProperty(this, 'visible', {value: builder});
return builder;
},
};
const getModelAnsi = (model, level, type, ...arguments_) => {
if (model === 'rgb') {
if (level === 'ansi16m') {
return ansiStyles[type].ansi16m(...arguments_);
}
if (level === 'ansi256') {
return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_));
}
return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_));
}
if (model === 'hex') {
return getModelAnsi('rgb', level, type, ...ansiStyles.hexToRgb(...arguments_));
}
return ansiStyles[type][model](...arguments_);
};
const usedModels = ['rgb', 'hex', 'ansi256'];
for (const model of usedModels) {
styles[model] = {
get() {
const {level} = this;
return function (...arguments_) {
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'color', ...arguments_), ansiStyles.color.close, this[STYLER]);
return createBuilder(this, styler, this[IS_EMPTY]);
};
},
};
const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1);
styles[bgModel] = {
get() {
const {level} = this;
return function (...arguments_) {
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'bgColor', ...arguments_), ansiStyles.bgColor.close, this[STYLER]);
return createBuilder(this, styler, this[IS_EMPTY]);
};
},
};
}
const proto = Object.defineProperties(() => {}, {
...styles,
level: {
enumerable: true,
get() {
return this[GENERATOR].level;
},
set(level) {
this[GENERATOR].level = level;
},
},
});
const createStyler = (open, close, parent) => {
let openAll;
let closeAll;
if (parent === undefined) {
openAll = open;
closeAll = close;
} else {
openAll = parent.openAll + open;
closeAll = close + parent.closeAll;
}
return {
open,
close,
openAll,
closeAll,
parent,
};
};
const createBuilder = (self, _styler, _isEmpty) => {
// Single argument is hot path, implicit coercion is faster than anything
// eslint-disable-next-line no-implicit-coercion
const builder = (...arguments_) => applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' '));
// We alter the prototype because we must return a function, but there is
// no way to create a function with a different prototype
Object.setPrototypeOf(builder, proto);
builder[GENERATOR] = self;
builder[STYLER] = _styler;
builder[IS_EMPTY] = _isEmpty;
return builder;
};
const applyStyle = (self, string) => {
if (self.level <= 0 || !string) {
return self[IS_EMPTY] ? '' : string;
}
let styler = self[STYLER];
if (styler === undefined) {
return string;
}
const {openAll, closeAll} = styler;
if (string.includes('\u001B')) {
while (styler !== undefined) {
// Replace any instances already present with a re-opening code
// otherwise only the part of the string until said closing code
// will be colored, and the rest will simply be 'plain'.
string = stringReplaceAll(string, styler.close, styler.open);
styler = styler.parent;
}
}
// We can move both next actions out of loop, because remaining actions in loop won't have
// any/visible effect on parts we add here. Close the styling before a linebreak and reopen
// after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
const lfIndex = string.indexOf('\n');
if (lfIndex !== -1) {
string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
}
return openAll + string + closeAll;
};
Object.defineProperties(createChalk.prototype, styles);
const chalk = createChalk();
export const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0});
export {
modifierNames,
foregroundColorNames,
backgroundColorNames,
colorNames,
// TODO: Remove these aliases in the next major version
modifierNames as modifiers,
foregroundColorNames as foregroundColors,
backgroundColorNames as backgroundColors,
colorNames as colors,
} from './vendor/ansi-styles/index.js';
export {
stdoutColor as supportsColor,
stderrColor as supportsColorStderr,
};
export default chalk;
@@ -0,0 +1,33 @@
// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`.
export function stringReplaceAll(string, substring, replacer) {
let index = string.indexOf(substring);
if (index === -1) {
return string;
}
const substringLength = substring.length;
let endIndex = 0;
let returnValue = '';
do {
returnValue += string.slice(endIndex, index) + substring + replacer;
endIndex = index + substringLength;
index = string.indexOf(substring, endIndex);
} while (index !== -1);
returnValue += string.slice(endIndex);
return returnValue;
}
export function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
let endIndex = 0;
let returnValue = '';
do {
const gotCR = string[index - 1] === '\r';
returnValue += string.slice(endIndex, (gotCR ? index - 1 : index)) + prefix + (gotCR ? '\r\n' : '\n') + postfix;
endIndex = index + 1;
index = string.indexOf('\n', endIndex);
} while (index !== -1);
returnValue += string.slice(endIndex);
return returnValue;
}
@@ -0,0 +1,236 @@
export interface CSPair { // eslint-disable-line @typescript-eslint/naming-convention
/**
The ANSI terminal control sequence for starting this style.
*/
readonly open: string;
/**
The ANSI terminal control sequence for ending this style.
*/
readonly close: string;
}
export interface ColorBase {
/**
The ANSI terminal control sequence for ending this color.
*/
readonly close: string;
ansi(code: number): string;
ansi256(code: number): string;
ansi16m(red: number, green: number, blue: number): string;
}
export interface Modifier {
/**
Resets the current color chain.
*/
readonly reset: CSPair;
/**
Make text bold.
*/
readonly bold: CSPair;
/**
Emitting only a small amount of light.
*/
readonly dim: CSPair;
/**
Make text italic. (Not widely supported)
*/
readonly italic: CSPair;
/**
Make text underline. (Not widely supported)
*/
readonly underline: CSPair;
/**
Make text overline.
Supported on VTE-based terminals, the GNOME terminal, mintty, and Git Bash.
*/
readonly overline: CSPair;
/**
Inverse background and foreground colors.
*/
readonly inverse: CSPair;
/**
Prints the text, but makes it invisible.
*/
readonly hidden: CSPair;
/**
Puts a horizontal line through the center of the text. (Not widely supported)
*/
readonly strikethrough: CSPair;
}
export interface ForegroundColor {
readonly black: CSPair;
readonly red: CSPair;
readonly green: CSPair;
readonly yellow: CSPair;
readonly blue: CSPair;
readonly cyan: CSPair;
readonly magenta: CSPair;
readonly white: CSPair;
/**
Alias for `blackBright`.
*/
readonly gray: CSPair;
/**
Alias for `blackBright`.
*/
readonly grey: CSPair;
readonly blackBright: CSPair;
readonly redBright: CSPair;
readonly greenBright: CSPair;
readonly yellowBright: CSPair;
readonly blueBright: CSPair;
readonly cyanBright: CSPair;
readonly magentaBright: CSPair;
readonly whiteBright: CSPair;
}
export interface BackgroundColor {
readonly bgBlack: CSPair;
readonly bgRed: CSPair;
readonly bgGreen: CSPair;
readonly bgYellow: CSPair;
readonly bgBlue: CSPair;
readonly bgCyan: CSPair;
readonly bgMagenta: CSPair;
readonly bgWhite: CSPair;
/**
Alias for `bgBlackBright`.
*/
readonly bgGray: CSPair;
/**
Alias for `bgBlackBright`.
*/
readonly bgGrey: CSPair;
readonly bgBlackBright: CSPair;
readonly bgRedBright: CSPair;
readonly bgGreenBright: CSPair;
readonly bgYellowBright: CSPair;
readonly bgBlueBright: CSPair;
readonly bgCyanBright: CSPair;
readonly bgMagentaBright: CSPair;
readonly bgWhiteBright: CSPair;
}
export interface ConvertColor {
/**
Convert from the RGB color space to the ANSI 256 color space.
@param red - (`0...255`)
@param green - (`0...255`)
@param blue - (`0...255`)
*/
rgbToAnsi256(red: number, green: number, blue: number): number;
/**
Convert from the RGB HEX color space to the RGB color space.
@param hex - A hexadecimal string containing RGB data.
*/
hexToRgb(hex: string): [red: number, green: number, blue: number];
/**
Convert from the RGB HEX color space to the ANSI 256 color space.
@param hex - A hexadecimal string containing RGB data.
*/
hexToAnsi256(hex: string): number;
/**
Convert from the ANSI 256 color space to the ANSI 16 color space.
@param code - A number representing the ANSI 256 color.
*/
ansi256ToAnsi(code: number): number;
/**
Convert from the RGB color space to the ANSI 16 color space.
@param red - (`0...255`)
@param green - (`0...255`)
@param blue - (`0...255`)
*/
rgbToAnsi(red: number, green: number, blue: number): number;
/**
Convert from the RGB HEX color space to the ANSI 16 color space.
@param hex - A hexadecimal string containing RGB data.
*/
hexToAnsi(hex: string): number;
}
/**
Basic modifier names.
*/
export type ModifierName = keyof Modifier;
/**
Basic foreground color names.
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
*/
export type ForegroundColorName = keyof ForegroundColor;
/**
Basic background color names.
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
*/
export type BackgroundColorName = keyof BackgroundColor;
/**
Basic color names. The combination of foreground and background color names.
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
*/
export type ColorName = ForegroundColorName | BackgroundColorName;
/**
Basic modifier names.
*/
export const modifierNames: readonly ModifierName[];
/**
Basic foreground color names.
*/
export const foregroundColorNames: readonly ForegroundColorName[];
/**
Basic background color names.
*/
export const backgroundColorNames: readonly BackgroundColorName[];
/*
Basic color names. The combination of foreground and background color names.
*/
export const colorNames: readonly ColorName[];
declare const ansiStyles: {
readonly modifier: Modifier;
readonly color: ColorBase & ForegroundColor;
readonly bgColor: ColorBase & BackgroundColor;
readonly codes: ReadonlyMap<number, number>;
} & ForegroundColor & BackgroundColor & Modifier & ConvertColor;
export default ansiStyles;
@@ -0,0 +1,223 @@
const ANSI_BACKGROUND_OFFSET = 10;
const wrapAnsi16 = (offset = 0) => code => `\u001B[${code + offset}m`;
const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`;
const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
const styles = {
modifier: {
reset: [0, 0],
// 21 isn't widely supported and 22 does the same thing
bold: [1, 22],
dim: [2, 22],
italic: [3, 23],
underline: [4, 24],
overline: [53, 55],
inverse: [7, 27],
hidden: [8, 28],
strikethrough: [9, 29],
},
color: {
black: [30, 39],
red: [31, 39],
green: [32, 39],
yellow: [33, 39],
blue: [34, 39],
magenta: [35, 39],
cyan: [36, 39],
white: [37, 39],
// Bright color
blackBright: [90, 39],
gray: [90, 39], // Alias of `blackBright`
grey: [90, 39], // Alias of `blackBright`
redBright: [91, 39],
greenBright: [92, 39],
yellowBright: [93, 39],
blueBright: [94, 39],
magentaBright: [95, 39],
cyanBright: [96, 39],
whiteBright: [97, 39],
},
bgColor: {
bgBlack: [40, 49],
bgRed: [41, 49],
bgGreen: [42, 49],
bgYellow: [43, 49],
bgBlue: [44, 49],
bgMagenta: [45, 49],
bgCyan: [46, 49],
bgWhite: [47, 49],
// Bright color
bgBlackBright: [100, 49],
bgGray: [100, 49], // Alias of `bgBlackBright`
bgGrey: [100, 49], // Alias of `bgBlackBright`
bgRedBright: [101, 49],
bgGreenBright: [102, 49],
bgYellowBright: [103, 49],
bgBlueBright: [104, 49],
bgMagentaBright: [105, 49],
bgCyanBright: [106, 49],
bgWhiteBright: [107, 49],
},
};
export const modifierNames = Object.keys(styles.modifier);
export const foregroundColorNames = Object.keys(styles.color);
export const backgroundColorNames = Object.keys(styles.bgColor);
export const colorNames = [...foregroundColorNames, ...backgroundColorNames];
function assembleStyles() {
const codes = new Map();
for (const [groupName, group] of Object.entries(styles)) {
for (const [styleName, style] of Object.entries(group)) {
styles[styleName] = {
open: `\u001B[${style[0]}m`,
close: `\u001B[${style[1]}m`,
};
group[styleName] = styles[styleName];
codes.set(style[0], style[1]);
}
Object.defineProperty(styles, groupName, {
value: group,
enumerable: false,
});
}
Object.defineProperty(styles, 'codes', {
value: codes,
enumerable: false,
});
styles.color.close = '\u001B[39m';
styles.bgColor.close = '\u001B[49m';
styles.color.ansi = wrapAnsi16();
styles.color.ansi256 = wrapAnsi256();
styles.color.ansi16m = wrapAnsi16m();
styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
// From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js
Object.defineProperties(styles, {
rgbToAnsi256: {
value(red, green, blue) {
// We use the extended greyscale palette here, with the exception of
// black and white. normal palette only has 4 greyscale shades.
if (red === green && green === blue) {
if (red < 8) {
return 16;
}
if (red > 248) {
return 231;
}
return Math.round(((red - 8) / 247) * 24) + 232;
}
return 16
+ (36 * Math.round(red / 255 * 5))
+ (6 * Math.round(green / 255 * 5))
+ Math.round(blue / 255 * 5);
},
enumerable: false,
},
hexToRgb: {
value(hex) {
const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
if (!matches) {
return [0, 0, 0];
}
let [colorString] = matches;
if (colorString.length === 3) {
colorString = [...colorString].map(character => character + character).join('');
}
const integer = Number.parseInt(colorString, 16);
return [
/* eslint-disable no-bitwise */
(integer >> 16) & 0xFF,
(integer >> 8) & 0xFF,
integer & 0xFF,
/* eslint-enable no-bitwise */
];
},
enumerable: false,
},
hexToAnsi256: {
value: hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
enumerable: false,
},
ansi256ToAnsi: {
value(code) {
if (code < 8) {
return 30 + code;
}
if (code < 16) {
return 90 + (code - 8);
}
let red;
let green;
let blue;
if (code >= 232) {
red = (((code - 232) * 10) + 8) / 255;
green = red;
blue = red;
} else {
code -= 16;
const remainder = code % 36;
red = Math.floor(code / 36) / 5;
green = Math.floor(remainder / 6) / 5;
blue = (remainder % 6) / 5;
}
const value = Math.max(red, green, blue) * 2;
if (value === 0) {
return 30;
}
// eslint-disable-next-line no-bitwise
let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red));
if (value === 2) {
result += 60;
}
return result;
},
enumerable: false,
},
rgbToAnsi: {
value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
enumerable: false,
},
hexToAnsi: {
value: hex => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
enumerable: false,
},
});
return styles;
}
const ansiStyles = assembleStyles();
export default ansiStyles;
@@ -0,0 +1 @@
export {default} from './index.js';
@@ -0,0 +1,34 @@
/* eslint-env browser */
const level = (() => {
if (!('navigator' in globalThis)) {
return 0;
}
if (globalThis.navigator.userAgentData) {
const brand = navigator.userAgentData.brands.find(({brand}) => brand === 'Chromium');
if (brand && brand.version > 93) {
return 3;
}
}
if (/\b(Chrome|Chromium)\//.test(globalThis.navigator.userAgent)) {
return 1;
}
return 0;
})();
const colorSupport = level !== 0 && {
level,
hasBasic: true,
has256: level >= 2,
has16m: level >= 3,
};
const supportsColor = {
stdout: colorSupport,
stderr: colorSupport,
};
export default supportsColor;
@@ -0,0 +1,55 @@
import type {WriteStream} from 'node:tty';
export type Options = {
/**
Whether `process.argv` should be sniffed for `--color` and `--no-color` flags.
@default true
*/
readonly sniffFlags?: boolean;
};
/**
Levels:
- `0` - All colors disabled.
- `1` - Basic 16 colors support.
- `2` - ANSI 256 colors support.
- `3` - Truecolor 16 million colors support.
*/
export type ColorSupportLevel = 0 | 1 | 2 | 3;
/**
Detect whether the terminal supports color.
*/
export type ColorSupport = {
/**
The color level.
*/
level: ColorSupportLevel;
/**
Whether basic 16 colors are supported.
*/
hasBasic: boolean;
/**
Whether ANSI 256 colors are supported.
*/
has256: boolean;
/**
Whether Truecolor 16 million colors are supported.
*/
has16m: boolean;
};
export type ColorInfo = ColorSupport | false;
export function createSupportsColor(stream?: WriteStream, options?: Options): ColorInfo;
declare const supportsColor: {
stdout: ColorInfo;
stderr: ColorInfo;
};
export default supportsColor;
@@ -0,0 +1,190 @@
import process from 'node:process';
import os from 'node:os';
import tty from 'node:tty';
// From: https://github.com/sindresorhus/has-flag/blob/main/index.js
/// function hasFlag(flag, argv = globalThis.Deno?.args ?? process.argv) {
function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process.argv) {
const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--');
const position = argv.indexOf(prefix + flag);
const terminatorPosition = argv.indexOf('--');
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
}
const {env} = process;
let flagForceColor;
if (
hasFlag('no-color')
|| hasFlag('no-colors')
|| hasFlag('color=false')
|| hasFlag('color=never')
) {
flagForceColor = 0;
} else if (
hasFlag('color')
|| hasFlag('colors')
|| hasFlag('color=true')
|| hasFlag('color=always')
) {
flagForceColor = 1;
}
function envForceColor() {
if ('FORCE_COLOR' in env) {
if (env.FORCE_COLOR === 'true') {
return 1;
}
if (env.FORCE_COLOR === 'false') {
return 0;
}
return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
}
}
function translateLevel(level) {
if (level === 0) {
return false;
}
return {
level,
hasBasic: true,
has256: level >= 2,
has16m: level >= 3,
};
}
function _supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) {
const noFlagForceColor = envForceColor();
if (noFlagForceColor !== undefined) {
flagForceColor = noFlagForceColor;
}
const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
if (forceColor === 0) {
return 0;
}
if (sniffFlags) {
if (hasFlag('color=16m')
|| hasFlag('color=full')
|| hasFlag('color=truecolor')) {
return 3;
}
if (hasFlag('color=256')) {
return 2;
}
}
// Check for Azure DevOps pipelines.
// Has to be above the `!streamIsTTY` check.
if ('TF_BUILD' in env && 'AGENT_NAME' in env) {
return 1;
}
if (haveStream && !streamIsTTY && forceColor === undefined) {
return 0;
}
const min = forceColor || 0;
if (env.TERM === 'dumb') {
return min;
}
if (process.platform === 'win32') {
// Windows 10 build 10586 is the first Windows release that supports 256 colors.
// Windows 10 build 14931 is the first release that supports 16m/TrueColor.
const osRelease = os.release().split('.');
if (
Number(osRelease[0]) >= 10
&& Number(osRelease[2]) >= 10_586
) {
return Number(osRelease[2]) >= 14_931 ? 3 : 2;
}
return 1;
}
if ('CI' in env) {
if (['GITHUB_ACTIONS', 'GITEA_ACTIONS', 'CIRCLECI'].some(key => key in env)) {
return 3;
}
if (['TRAVIS', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') {
return 1;
}
return min;
}
if ('TEAMCITY_VERSION' in env) {
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
}
if (env.COLORTERM === 'truecolor') {
return 3;
}
if (env.TERM === 'xterm-kitty') {
return 3;
}
if (env.TERM === 'xterm-ghostty') {
return 3;
}
if (env.TERM === 'wezterm') {
return 3;
}
if ('TERM_PROGRAM' in env) {
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10);
switch (env.TERM_PROGRAM) {
case 'iTerm.app': {
return version >= 3 ? 3 : 2;
}
case 'Apple_Terminal': {
return 2;
}
// No default
}
}
if (/-256(color)?$/i.test(env.TERM)) {
return 2;
}
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
return 1;
}
if ('COLORTERM' in env) {
return 1;
}
return min;
}
export function createSupportsColor(stream, options = {}) {
const level = _supportsColor(stream, {
streamIsTTY: stream && stream.isTTY,
...options,
});
return translateLevel(level);
}
const supportsColor = {
stdout: createSupportsColor({isTTY: tty.isatty(1)}),
stderr: createSupportsColor({isTTY: tty.isatty(2)}),
};
export default supportsColor;
+1
View File
@@ -0,0 +1 @@
{ "name": "chalk5-case", "private": true, "type": "module" }
+80
View File
@@ -0,0 +1,80 @@
// The hot-shots StatsD client (compiled from scriptc's built-in shim) in
// buffered mode: metrics join one datagram until maxBufferSize, close
// flushes the rest, child clients share the buffer. Also explicit sample
// rates of 1, the telegraf line format, stat arrays with a callback, and
// suffixes.
import * as dgram from "node:dgram";
import { StatsD } from "hot-shots";
const server = dgram.createSocket("udp4");
const received: string[] = [];
let waiting: (() => void) | null = null;
let want = 0;
server.on("message", (msg) => {
received.push(msg.toString());
if (waiting !== null && received.length >= want) {
const done = waiting;
waiting = null;
done();
}
});
function datagrams(count: number): Promise<void> {
want = count;
return new Promise((resolve) => {
if (received.length >= want) resolve();
else waiting = resolve;
});
}
function flush(label: string): void {
console.log("--", label);
for (const line of received.splice(0)) console.log(JSON.stringify(line));
}
async function run(port: number): Promise<void> {
const buffered = new StatsD({
host: "127.0.0.1",
port,
maxBufferSize: 64,
bufferFlushInterval: 60000,
suffix: ".s",
globalTags: ["team:core"],
});
buffered.increment("one");
buffered.increment("two", 2);
buffered.gauge("three", 3);
buffered.histogram("four", 4, ["h:1"]);
const child = buffered.childClient({ prefix: "kid.", suffix: ".x" });
child.increment("five");
await new Promise<void>((resolve) => buffered.close(() => resolve()));
await datagrams(2);
flush("buffered");
const telegraf = new StatsD({ host: "127.0.0.1", port, telegraf: true, globalTags: { region: "eu" } });
telegraf.increment("hits", 1, 1, { route: "/x" });
telegraf.timing("lat", 7, ["kind:a"]);
await datagrams(2);
flush("telegraf");
const plain = new StatsD({ host: "127.0.0.1", port, prefix: "p." });
await new Promise<void>((resolve) => {
plain.increment(["a", "b", "c"], 1, 1, ["t:1"], (err: unknown, bytes: unknown) => {
console.log("array sent", err, bytes);
resolve();
});
});
plain.set("users", 42, 1);
plain.unique("visitors", "v1");
plain.decrement("down", 3, ["d:1"]);
plain.decrement("gone");
await datagrams(7);
flush("plain");
await new Promise<void>((resolve) => {
telegraf.close(() => plain.close(() => resolve()));
});
}
server.bind(0, "127.0.0.1", () => {
void run(server.address().port).then(() => server.close(() => console.log("server closed")));
});
@@ -0,0 +1 @@
{ "type": "module" }
+27
View File
@@ -0,0 +1,27 @@
// The hot-shots StatsD client (compiled from scriptc's built-in shim) when a
// send fails: with a callback or an errorHandler the error goes there; with
// neither, the client logs it and emits it on the socket, where an 'error'
// event nobody listens for ends the process with status 1.
import { StatsD } from "hot-shots";
// A datagram larger than udp allows fails with EMSGSIZE before it leaves
// the host; port 9 keeps the message the same on every run.
const big = "x".repeat(70000);
const withCallback = new StatsD({ host: "127.0.0.1", port: 9 });
withCallback.increment(big, 1, 1, [], (err: Error | null | undefined) => {
console.log("callback:", err ? err.message : "no error");
withCallback.close();
const withHandler = new StatsD({
host: "127.0.0.1",
port: 9,
errorHandler: (error: Error) => {
console.log("errorHandler:", error.message);
withHandler.close();
console.log("without a callback or errorHandler");
new StatsD({ host: "127.0.0.1", port: 9 }).increment(big);
},
});
withHandler.increment(big);
});
@@ -0,0 +1 @@
{ "type": "module" }
+33
View File
@@ -0,0 +1,33 @@
// The hot-shots StatsD client over tcp (compiled from scriptc's built-in
// shim): a send completes once the socket has written its line, so close()
// waits for lines that a full socket buffer holds back, and the receiver
// gets every line before the connection ends.
import * as net from "node:net";
import { StatsD } from "hot-shots";
const LINES = 20000;
const name = "m".repeat(1000);
let bytes = 0;
let ended: () => void = () => {};
const serverDone = new Promise<void>((resolve) => {
ended = resolve;
});
const server = net.createServer((socket) => {
socket.pause();
setTimeout(() => socket.resume(), 150);
socket.on("data", (chunk: Buffer) => {
bytes += chunk.length;
});
socket.on("end", () => ended());
});
server.listen(0, "127.0.0.1", () => {
const port = (server.address() as net.AddressInfo).port;
const client = new StatsD({ host: "127.0.0.1", port, protocol: "tcp" });
for (let i = 0; i < LINES; i++) client.increment(name);
const closed = new Promise<void>((resolve) => client.close(() => resolve()));
void Promise.all([closed, serverDone]).then(() => {
console.log("closed; received", bytes, "of", LINES * (name.length + 5), "bytes");
server.close(() => console.log("server closed"));
});
});
+1
View File
@@ -0,0 +1 @@
{ "type": "module" }
@@ -0,0 +1 @@
{"plain":2871,"san":4039}
@@ -0,0 +1 @@
{ "plain": 1500, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 3813, "san": 3374 }
@@ -0,0 +1 @@
{ "plain": 2436, "san": 2273 }

Some files were not shown because too many files have changed in this diff Show More