diff --git a/docs/content/limitations/builtin-apis/map-and-set.mdx b/docs/content/limitations/builtin-apis/map-and-set.mdx index bf77001ee..ee7435b2d 100644 --- a/docs/content/limitations/builtin-apis/map-and-set.mdx +++ b/docs/content/limitations/builtin-apis/map-and-set.mdx @@ -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. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 519608393..5f3805813 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -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; diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index e2f344a4d..aa0e9c36b 100644 Binary files a/packages/compiler/native/cli.profdata.gz and b/packages/compiler/native/cli.profdata.gz differ diff --git a/packages/compiler/native/host.c b/packages/compiler/native/host.c index 6aa1b2a28..457774f81 100644 --- a/packages/compiler/native/host.c +++ b/packages/compiler/native/host.c @@ -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); +} diff --git a/packages/compiler/native/host.ffi.json b/packages/compiler/native/host.ffi.json index d7e0e92aa..3d15a1e0e 100644 --- a/packages/compiler/native/host.ffi.json +++ b/packages/compiler/native/host.ffi.json @@ -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" } ] } diff --git a/packages/compiler/shims/chalk/v5.ts b/packages/compiler/shims/chalk/v5.ts new file mode 100644 index 000000000..2d34c5c53 --- /dev/null +++ b/packages/compiler/shims/chalk/v5.ts @@ -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; diff --git a/packages/compiler/shims/hot-shots/client.ts b/packages/compiler/shims/hot-shots/client.ts new file mode 100644 index 000000000..354744e7b --- /dev/null +++ b/packages/compiler/shims/hot-shots/client.ts @@ -0,0 +1,1126 @@ +// scriptc's built-in implementation of the `hot-shots` package, 8.x API: +// a typed port of hot-shots 8.5's StatsD client with the same datagrams, +// callbacks, error messages and child-client sharing, over node:dgram +// (udp), node:net (tcp) and a native Unix datagram socket (uds). The +// entry modules (v8.ts, v8-unix-dgram.ts) say whether the optional +// unix-dgram addon that hot-shots' uds protocol loads is installed. +import * as dgram from "node:dgram"; +import * as dns from "node:dns"; +import * as net from "node:net"; +import * as os from "node:os"; + +export type Tags = { [key: string]: string } | string[]; + +export interface ClientOptions { + bufferFlushInterval?: number; + bufferHolder?: { buffer: string }; + cacheDns?: boolean; + cacheDnsTtl?: number; + errorHandler?: (err: Error) => void; + globalTags?: Tags; + globalize?: boolean; + host?: string; + isChild?: boolean; + maxBufferSize?: number; + mock?: boolean; + path?: string; + port?: number; + prefix?: string; + protocol?: "tcp" | "udp" | "uds"; + sampleRate?: number; + suffix?: string; + telegraf?: boolean; + useDefaultRoute?: boolean; + tagPrefix?: string; + tagSeparator?: string; + udsGracefulErrorHandling?: boolean; + udsGracefulRestartRateLimit?: number; + closingFlushInterval?: number; +} + +export interface ChildClientOptions { + globalTags?: Tags; + prefix?: string; + suffix?: string; + errorHandler?: (err: Error) => void; +} + +export interface CheckOptions { + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + message?: string; +} + +export interface DatadogChecks { + OK: 0; + WARNING: 1; + CRITICAL: 2; + UNKNOWN: 3; +} + +export type DatadogChecksValues = 0 | 1 | 2 | 3; + +export interface EventOptions { + aggregation_key?: string; + alert_type?: "info" | "warning" | "success" | "error"; + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + priority?: "low" | "normal"; + source_type_name?: string; +} + +/** A send's completion: hot-shots passes `null` on success. */ +export type StatsCb = (error: Error | null | undefined, bytes?: any) => void; + +type SampleArg = number | Tags | StatsCb | undefined; +type TagsArg = Tags | StatsCb | undefined; + +const PROTOCOL_TCP = "tcp"; +const PROTOCOL_UDS = "uds"; +const PROTOCOL_UDP = "udp"; +const UDS_DEFAULT_GRACEFUL_RESTART_LIMIT = 1000; +const CACHE_DNS_TTL_DEFAULT = 60000; + +function udsErrorCodes(): number[] { + if (process.platform === "linux") { + return [os.constants.errno.ENOTCONN, os.constants.errno.ECONNREFUSED]; + } + if (process.platform === "darwin") { + return [os.constants.errno.EDESTADDRREQ, os.constants.errno.ECONNRESET]; + } + return []; +} +const UDS_ERROR_CODES = udsErrorCodes(); + +// ── helpers ────────────────────────────────────────────────────────────── + +function sanitizeTags(value: string, telegraf: boolean): string { + const blacklist = telegraf ? /:|\||,/g : /:|\||@|,/g; + return String(value).replace(blacklist, "_"); +} + +function formatTags(tags: Tags, telegraf: boolean): string[] { + if (Array.isArray(tags)) return tags; + const out: string[] = []; + for (const key of Object.keys(tags)) { + out.push(`${sanitizeTags(key, telegraf)}:${sanitizeTags(tags[key]!, telegraf)}`); + } + return out; +} + +function isArrayIndexKey(key: string): boolean { + return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 4294967295; +} + +function overrideTags(parent: string[], child: Tags, telegraf: boolean): string[] { + const childKeys: string[] = []; + const childValues = new Map(); + const toAppend: string[] = []; + for (const tag of formatTags(child, telegraf)) { + const idx = tag.indexOf(":"); + if (idx < 1) { + toAppend.push(tag); + } else { + const key = tag.substring(0, idx); + const value = tag.substring(idx + 1); + let values = childValues.get(key); + if (values === undefined) { + values = []; + childValues.set(key, values); + childKeys.push(key); + } + values.push(value); + } + } + const result = parent.filter((tag) => { + const idx = tag.indexOf(":"); + if (idx < 1) return true; + return !childValues.has(tag.substring(0, idx)); + }); + // hot-shots walks an object's keys: array-index keys ascending first, + // then the rest in insertion order. + const indexKeys = childKeys.filter(isArrayIndexKey).sort((a, b) => Number(a) - Number(b)); + const ordered = indexKeys.concat(childKeys.filter((key) => !isArrayIndexKey(key))); + for (const key of ordered) { + for (const value of childValues.get(key)!) result.push(`${key}:${value}`); + } + return result.concat(toAppend); +} + +function formatDate(date: number): number { + return Math.round(date); +} + +/** console.error of an arbitrary value, as hot-shots logs errors. The + * value crosses as unknown: the typed Error path has no inspect lowering + * in programs whose Error subclasses define accessors. */ +function logError(value: unknown): void { + console.error(value); +} + +function errorWithCode(message: string, code: string | undefined): Error { + const error: NodeJS.ErrnoException = new Error(message); + error.code = code; + return error; +} + +// ── transports ─────────────────────────────────────────────────────────── + +type SendCallback = (err: Error | null, bytes?: number) => void; + +/** The socket a client sends through (hot-shots' transport interface). */ +interface Transport { + createdAt: number; + send(buf: Buffer, callback: SendCallback): void; + close(): void; + onError(listener: (err: Error) => void): void; + offError(listener: (err: Error) => void): void; + /** Emits 'error' on the socket, as hot-shots' `socket.emit('error')`. */ + emitError(err: Error): void; + onClose(listener: () => void): void; +} + +/** A socket's 'error' listeners, kept by the transport (the typed + * sockets have no removeListener): one socket listener dispatches to a + * snapshot, and an error nobody listens for throws, as an unhandled + * 'error' event does. */ +class ErrorListeners { + private listeners: ((err: Error) => void)[] = []; + add(listener: (err: Error) => void): void { + this.listeners.push(listener); + } + remove(listener: (err: Error) => void): void { + const index = this.listeners.indexOf(listener); + if (index !== -1) this.listeners.splice(index, 1); + } + emit(err: Error): void { + const snapshot = this.listeners.slice(); + if (snapshot.length === 0) throw err; + for (const listener of snapshot) listener(err); + } +} + +function addEol(buf: Buffer): string { + let msg = buf.toString(); + if (msg.length > 0 && msg[msg.length - 1] !== "\n") msg += "\n"; + return msg; +} + +class UdpTransport implements Transport { + createdAt = 0; + private readonly socket: dgram.Socket; + private readonly errors = new ErrorListeners(); + private resolvedAddress: string | undefined = undefined; + private resolvedAt = 0; + private readonly host: string | undefined; + private readonly port: number; + private readonly cacheDns: boolean; + private readonly cacheDnsTtl: number; + constructor(host: string | undefined, port: number, cacheDns: boolean, cacheDnsTtl: number) { + this.host = host; + this.port = port; + this.cacheDns = cacheDns; + this.cacheDnsTtl = cacheDnsTtl; + this.socket = dgram.createSocket("udp4"); + this.socket.unref(); + // The listener captures the list, not the transport: a socket that is + // never closed keeps its listener, and a capture of `this` would make + // a cycle the reference counts cannot free at exit. + const errors = this.errors; + this.socket.on("error", (err) => errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + if (!this.cacheDns) { + this.socket.send(buf, this.port, this.host || "127.0.0.1", callback); + return; + } + const now = Date.now(); + const cached = this.resolvedAddress; + if (cached === undefined || now - this.resolvedAt > this.cacheDnsTtl) { + dns.lookup(this.host ?? "localhost", (error, address) => { + if (error) { + callback(error); + return; + } + this.resolvedAddress = address; + this.resolvedAt = now; + this.socket.send(buf, this.port, address, callback); + }); + } else { + this.socket.send(buf, this.port, cached, callback); + } + } + close(): void { + this.socket.close(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + emitError(err: Error): void { + this.errors.emit(err); + } + onClose(listener: () => void): void { + this.socket.on("close", listener); + } +} + +class TcpTransport implements Transport { + createdAt = 0; + private readonly socket: net.Socket; + private readonly errors = new ErrorListeners(); + constructor(host: string | undefined, port: number) { + this.socket = net.connect(port, host ?? "localhost"); + this.socket.setKeepAlive(true); + this.socket.unref(); + // The listener captures the list, not the transport: a socket that is + // never closed keeps its listener, and a capture of `this` would make + // a cycle the reference counts cannot free at exit. + const errors = this.errors; + this.socket.on("error", (err) => errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + // hot-shots writes the line with the 'ascii' encoding, which encodes + // strings as 'latin1' does. The callback runs once the data is written, + // so a message held back by a full socket buffer stays in flight. + this.socket.write(Buffer.from(addEol(buf), "latin1"), (err) => callback(err ?? null)); + } + close(): void { + this.socket.destroy(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + emitError(err: Error): void { + this.errors.emit(err); + } + onClose(listener: () => void): void { + this.socket.on("close", () => listener()); + } +} + +// The runtime's AF_UNIX datagram calls (the unix-dgram addon's binding): +// results >= 0, or a negative errno; send answers 1 when it would block. +declare function scriptcUnixDgramSocket(): number; +declare function scriptcUnixDgramConnect(fd: number, path: string): number; +declare function scriptcUnixDgramSend(fd: number, data: Buffer): number; +declare function scriptcUnixDgramClose(fd: number): number; + +let unixDgramInstalled = false; + +/** Whether the unix-dgram addon is installed beside hot-shots (set by the + * entry module). */ +export function configureUnixDgram(installed: boolean): void { + unixDgramInstalled = installed; +} + +const UDS_PATH_DEFAULT = "/var/run/datadog/dsd.socket"; + +/** unix-dgram's errnoException: " " (the code is the + * negative errno; this Error's code field holds its decimal string). */ +function errnoException(errorno: number, syscall: string): Error { + const error: NodeJS.ErrnoException = new Error(`${syscall} ${errorno}`); + error.errno = errorno; + error.code = String(errorno); + error.syscall = syscall; + return error; +} + +/** hot-shots' uds transport over unix-dgram: a connected, non-blocking + * AF_UNIX datagram socket. Sends complete synchronously, as the addon's + * do. The addon polls the socket for reads, which holds the event loop + * until close; a ref'd timer stands in for that poll. */ +class UdsTransport implements Transport { + createdAt = 0; + private fd: number; + private readonly errors = new ErrorListeners(); + private readonly closeListeners: (() => void)[] = []; + private readonly stopHold: () => void; + constructor(path: string) { + const fd = scriptcUnixDgramSocket(); + if (fd < 0) throw errnoException(fd, "socket"); + this.fd = fd; + const hold = setInterval(() => {}, 2147483647); + this.stopHold = () => clearInterval(hold); + const err = scriptcUnixDgramConnect(fd, path); + if (err < 0) { + // The addon emits the connect failure as 'error' before anyone + // listens, which throws; hot-shots closes the socket and rethrows. + this.close(); + throw errnoException(err, "connect"); + } + } + send(buf: Buffer, callback: SendCallback): void { + const result = scriptcUnixDgramSend(this.fd, buf); + if (result < 0) { + callback(errnoException(result, "send")); + } else if (result === 1) { + const congestion: NodeJS.ErrnoException = new Error("congestion"); + congestion.code = "1"; + callback(congestion); + } else { + callback(null); + } + } + close(): void { + const result = scriptcUnixDgramClose(this.fd); + if (result < 0) throw errnoException(result, "close"); + this.fd = -1; + this.stopHold(); + for (const listener of this.closeListeners.slice()) listener(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + emitError(err: Error): void { + this.errors.emit(err); + } + onClose(listener: () => void): void { + this.closeListeners.push(listener); + } +} + +/** hot-shots' createUdsTransport: without the unix-dgram addon the + * protocol reports it missing. */ +function createUdsTransport(path: string | undefined): Transport { + if (!unixDgramInstalled) { + throw new Error( + "The library `unix_dgram`, needed for the uds protocol to work, is not installed. " + + "You need to pick another protocol to use hot-shots. " + + "See the hot-shots README for additional details.", + ); + } + return new UdsTransport(path ? path : UDS_PATH_DEFAULT); +} + +function createTransport(client: StatsD): Transport | null { + let transport: Transport | null = null; + const protocol = client.protocol; + try { + if (protocol === PROTOCOL_TCP) { + transport = new TcpTransport(client.host, client.port); + } else if (protocol === PROTOCOL_UDS) { + transport = createUdsTransport(client.path); + } else if (protocol === PROTOCOL_UDP) { + transport = new UdpTransport(client.host, client.port, client.cacheDns, client.cacheDnsTtl); + } else { + throw new Error(`Unsupported protocol '${protocol}'`); + } + transport.createdAt = Date.now(); + } catch (e) { + const handler = client.errorHandler; + if (handler) { + handler(e as Error); + } else { + console.error(e); + } + } + return transport; +} + +// ── the client ─────────────────────────────────────────────────────────── + +class BufferHolder { + buffer = ""; +} + +/** What a child client inherits from its parent. */ +class ChildInit { + socket: Transport | null; + bufferHolder: BufferHolder; + dnsError: Error | undefined; + constructor(socket: Transport | null, bufferHolder: BufferHolder, dnsError: Error | undefined) { + this.socket = socket; + this.bufferHolder = bufferHolder; + this.dnsError = dnsError; + } +} + +export class StatsD { + protocol: string; + cacheDns: boolean; + cacheDnsTtl: number; + host: string | undefined; + port: number; + path: string | undefined; + prefix: string; + suffix: string; + tagPrefix: string; + tagSeparator: string; + mock: boolean; + globalTags: string[]; + telegraf: boolean; + maxBufferSize: number; + sampleRate: number; + bufferFlushInterval: number; + errorHandler: ((err: Error) => void) | undefined; + udsGracefulErrorHandling: boolean; + udsGracefulRestartRateLimit: number; + isChild: boolean; + closingFlushInterval: number; + mockBuffer: string[] | undefined = undefined; + messagesInFlight = 0; + CHECKS: DatadogChecks = { OK: 0, WARNING: 1, CRITICAL: 2, UNKNOWN: 3 }; + private bufferHolder: BufferHolder; + private transport: Transport | null = null; + private dnsError: Error | undefined = undefined; + private stopFlushInterval: (() => void) | undefined = undefined; + + constructor(options?: ClientOptions, child?: ChildInit) { + const o: ClientOptions = options ?? {}; + const protocol = o.protocol ? o.protocol.toLowerCase() : ""; + this.protocol = protocol ? protocol : PROTOCOL_UDP; + this.cacheDns = o.cacheDns === true; + this.cacheDnsTtl = o.cacheDnsTtl || CACHE_DNS_TTL_DEFAULT; + this.host = o.host || process.env["DD_AGENT_HOST"]; + this.port = o.port || parseInt(process.env["DD_DOGSTATSD_PORT"] ?? "", 10) || 8125; + this.path = o.path; + this.prefix = o.prefix || ""; + this.suffix = o.suffix || ""; + this.tagPrefix = o.tagPrefix || "#"; + this.tagSeparator = o.tagSeparator || ","; + this.mock = o.mock === true; + this.telegraf = o.telegraf || false; + const globalTags = o.globalTags; + this.globalTags = globalTags !== undefined ? formatTags(globalTags, this.telegraf) : []; + const entityId = process.env["DD_ENTITY_ID"]; + if (entityId) { + this.globalTags = this.globalTags.filter( + (item) => item.indexOf("dd.internal.entity_id:") !== 0, + ); + this.globalTags.push("dd.internal.entity_id:".concat(sanitizeTags(entityId, false))); + } + this.maxBufferSize = o.maxBufferSize || 0; + this.sampleRate = o.sampleRate || 1; + this.bufferFlushInterval = o.bufferFlushInterval || 1000; + this.bufferHolder = child !== undefined ? child.bufferHolder : new BufferHolder(); + this.errorHandler = o.errorHandler; + const graceful = o.udsGracefulErrorHandling; + this.udsGracefulErrorHandling = graceful === undefined ? true : graceful; + this.udsGracefulRestartRateLimit = + o.udsGracefulRestartRateLimit || UDS_DEFAULT_GRACEFUL_RESTART_LIMIT; + this.isChild = child !== undefined; + this.closingFlushInterval = o.closingFlushInterval || 50; + if (this.mock) this.mockBuffer = []; + if (child === undefined && this.maxBufferSize > 0) { + const handle = setInterval(() => this.onBufferFlushInterval(), this.bufferFlushInterval); + handle.unref(); + this.stopFlushInterval = () => clearInterval(handle); + } + if (child !== undefined) { + if (child.dnsError) this.dnsError = child.dnsError; + this.transport = child.socket; + } + if (!this.transport) this.transport = createTransport(this); + const handler = o.errorHandler; + const transport = this.transport; + if (transport !== null && child === undefined && handler) transport.onError(handler); + if (transport !== null && o.protocol === PROTOCOL_UDS) this.maybeAddUDSErrorHandler(); + } + + private maybeAddUDSErrorHandler(): void { + if (this.isChild || !this.udsGracefulErrorHandling) return; + const transport = this.transport; + if (transport !== null) transport.onError((err) => this.udsErrorHandler(err)); + } + + private udsErrorHandler(err: Error): void { + const code = Number((err as NodeJS.ErrnoException).code); + if (!UDS_ERROR_CODES.includes(-code)) return; + const socket = this.transport; + if (socket === null) return; + if (!socket.createdAt || Date.now() - socket.createdAt < this.udsGracefulRestartRateLimit) { + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + const newSocket = createTransport(this); + if (newSocket === null) { + const errorMessage = "Could not replace UDS connection with new socket"; + if (handler) { + handler(new Error(errorMessage)); + } else { + console.error(errorMessage); + } + return; + } + socket.close(); + this.transport = newSocket; + this.maybeAddUDSErrorHandler(); + if (handler) { + newSocket.onError(handler); + } else { + newSocket.onError((error) => console.error(`hot-shots UDS error: ${String(error)}`)); + } + } + + // ── stats functions ── + + timing( + stat: string | string[], + time: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + timing(stat: string | string[], time: number, tags?: Tags, callback?: StatsCb): void; + timing(stat: string | string[], time: number, callback?: StatsCb): void; + timing(stat: string | string[], time: number, sampleRate?: number, callback?: StatsCb): void; + timing( + stat: string | string[], + time: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, time, "ms", sampleRate, tags, callback); + } + + increment(stat: string, tags?: Tags): void; + increment( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + increment(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + increment(stat: string | string[], value: number, callback?: StatsCb): void; + increment(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + increment( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + // A non-number second argument of a two-argument call is the tags. + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, amount, "c", sampleRate, tagArg, callback); + } + + decrement(stat: string, tags?: Tags): void; + decrement( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + decrement(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + decrement( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, -amount, "c", sampleRate, tagArg, callback); + } + + histogram( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + histogram(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + histogram( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "h", sampleRate, tags, callback); + } + + distribution( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + distribution(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + distribution(stat: string | string[], value: number, callback?: StatsCb): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: number, + callback?: StatsCb, + ): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "d", sampleRate, tags, callback); + } + + gauge( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + gauge(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + gauge( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "g", sampleRate, tags, callback); + } + + set( + stat: string | string[], + value: number | string, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + set(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + set(stat: string | string[], value: number | string, callback?: StatsCb): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: number, + callback?: StatsCb, + ): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + unique( + stat: string | string[], + value: number | string, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + unique(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + unique(stat: string | string[], value: number | string, callback?: StatsCb): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: number, + callback?: StatsCb, + ): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + check( + name: string, + status: DatadogChecksValues, + options?: CheckOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const check: string[] = ["_sc", this.prefix + name + this.suffix, String(status)]; + const metadata: CheckOptions = options ?? {}; + const happened = metadata.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) check.push(`d:${timestamp}`); + } + if (metadata.hostname) check.push(`h:${metadata.hostname}`); + let mergedTags = this.globalTags; + if (tags !== undefined && typeof tags !== "function") { + mergedTags = overrideTags(mergedTags, tags, this.telegraf); + } + if (mergedTags.length > 0) check.push(`#${mergedTags.join(",")}`); + if (metadata.message) check.push(`m:${metadata.message}`); + let cb = callback; + if (typeof tags === "function" && callback === undefined) cb = tags; + this.sendRaw(check.join("|"), cb); + } + + event( + title: string, + text?: string, + options?: EventOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const msgTitle = String(title ? title : ""); + let msgText = String(text ? text : msgTitle); + msgText = msgText.replace(/\n/g, "\\n"); + let message = `_e{${msgTitle.length},${msgText.length}}:${msgTitle}|${msgText}`; + if (options) { + const happened = options.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) message += `|d:${timestamp}`; + } + if (options.hostname) message += `|h:${options.hostname}`; + if (options.aggregation_key) message += `|k:${options.aggregation_key}`; + if (options.priority) message += `|p:${options.priority}`; + if (options.source_type_name) message += `|s:${options.source_type_name}`; + if (options.alert_type) message += `|t:${options.alert_type}`; + } + let cb = callback; + let tagList: Tags | undefined = undefined; + if (typeof tags === "function") { + if (callback === undefined) cb = tags; + } else { + tagList = tags; + } + this.send(message, tagList, cb); + } + + // ── sending ── + + private sendAll( + stat: string | string[], + value: number | string, + type: string, + sampleRateArg: SampleArg, + tagsArg: TagsArg, + callbackArg: StatsCb | undefined, + ): void { + let sampleRate: number | undefined = undefined; + let tags: Tags | undefined = undefined; + let callback: StatsCb | undefined = callbackArg; + if (typeof sampleRateArg === "number") { + sampleRate = sampleRateArg; + if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + } else if (sampleRateArg !== undefined) { + // A non-number sample rate shifts into the tags, and the tags + // argument into the callback. + if (typeof sampleRateArg === "function") { + callback = sampleRateArg; + } else { + tags = sampleRateArg; + callback = typeof tagsArg === "function" ? tagsArg : undefined; + } + } else if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + + if (!Array.isArray(stat)) { + this.sendStat(stat, value, type, sampleRate, tags, callback); + return; + } + let completed = 0; + let calledback = false; + let sentBytes = 0; + const count = stat.length; + const onSend: StatsCb = (error, bytes) => { + completed += 1; + if (calledback) return; + if (error) { + if (callback !== undefined) { + calledback = true; + callback(error); + } else { + const handler = this.errorHandler; + if (handler) { + calledback = true; + handler(error); + } + } + return; + } + if (bytes) sentBytes += bytes as number; + if (completed === count && callback !== undefined) callback(null, sentBytes); + }; + for (const item of stat) this.sendStat(item, value, type, sampleRate, tags, onSend); + } + + private sendStat( + stat: string, + value: number | string, + type: string, + sampleRateArg: number | undefined, + tags: Tags | undefined, + callback: StatsCb | undefined, + ): void { + let message = `${this.prefix + stat + this.suffix}:${value}|${type}`; + const sampleRate = sampleRateArg || this.sampleRate; + if (sampleRate && sampleRate < 1) { + if (Math.random() < sampleRate) { + message += `|@${sampleRate}`; + } else { + if (callback) callback(undefined); + return; + } + } + this.send(message, tags, callback); + } + + private send(message: string, tags: Tags | undefined, callback: StatsCb | undefined): void { + let mergedTags = this.globalTags; + if (tags !== undefined) mergedTags = overrideTags(mergedTags, tags, this.telegraf); + if (mergedTags.length > 0) { + if (this.telegraf) { + const parts = message.split(":"); + message = `${parts[0]!},${mergedTags.join(",").replace(/:/g, "=")}:${parts.slice(1).join(":")}`; + } else { + message += `|${this.tagPrefix}${mergedTags.join(this.tagSeparator)}`; + } + } + this.sendRaw(message, callback); + } + + /** hot-shots' `_send`: the message with its tags. */ + private sendRaw(message: string, callback: StatsCb | undefined): void { + const dnsError = this.dnsError; + if (dnsError) { + if (callback) return callback(dnsError); + const handler = this.errorHandler; + if (handler) return handler(dnsError); + throw dnsError; + } + if (!this.mock) { + if (this.maxBufferSize === 0) { + this.sendMessage(message, callback); + } else { + this.enqueue(message, callback); + } + } else { + const buffer = this.mockBuffer; + if (buffer !== undefined) buffer.push(message); + if (callback) callback(null, 0); + } + } + + private enqueue(message: string, callback: StatsCb | undefined): void { + message += "\n"; + if (this.bufferHolder.buffer.length + message.length > this.maxBufferSize) { + this.flushQueue(callback); + this.bufferHolder.buffer += message; + } else { + this.bufferHolder.buffer += message; + if (callback) callback(null); + } + } + + private flushQueue(callback?: StatsCb): void { + this.sendMessage(this.bufferHolder.buffer, callback); + this.bufferHolder.buffer = ""; + } + + private sendMessage(message: string, callback: StatsCb | undefined): void { + if (message === "") { + if (callback) callback(null); + return; + } + const socketWasMissing = this.transport === null; + if (socketWasMissing && this.protocol === PROTOCOL_UDS) { + this.transport = createTransport(this); + if (this.transport !== null) this.maybeAddUDSErrorHandler(); + } + const socket = this.transport; + if (socketWasMissing || socket === null) { + const error = new Error("Socket not created properly. Check previous errors for details."); + if (callback) return callback(error); + const handler = this.errorHandler; + if (handler) return handler(error); + console.error(String(error)); + return; + } + const handleCallback = (err: Error | null, bytes?: number): void => { + this.messagesInFlight--; + let errFormatted: Error | null = null; + if (err) { + errFormatted = errorWithCode( + `Error sending hot-shots message: ${String(err)}`, + (err as NodeJS.ErrnoException).code, + ); + if (this.protocol === PROTOCOL_UDS && (callback || this.errorHandler)) { + this.udsErrorHandler(err); + } + } + if (callback) { + callback(errFormatted, bytes); + } else if (errFormatted) { + const handler = this.errorHandler; + if (handler) { + handler(errFormatted); + } else { + console.error(String(errFormatted)); + // hot-shots also emits the error on the current socket, where the + // uds reconnection handler listens; with no listener it throws. + const current = this.transport; + if (current !== null) current.emitError(errFormatted); + } + } + }; + try { + this.messagesInFlight++; + socket.send(Buffer.from(message), handleCallback); + } catch (err) { + handleCallback(err as Error); + } + } + + private onBufferFlushInterval(): void { + this.flushQueue(); + } + + close(callback?: (error?: Error) => void): void { + const stop = this.stopFlushInterval; + if (stop !== undefined) stop(); + this.flushQueue((err) => { + if (err) { + if (callback) return callback(err); + logError(err); + return; + } + let intervalAttempts = 0; + const waitForMessages = setInterval(() => { + intervalAttempts++; + if (intervalAttempts > 10) { + console.log("hot-shots could not clear out messages in flight but closing anyways"); + this.messagesInFlight = 0; + } + if (this.messagesInFlight <= 0) { + clearInterval(waitForMessages); + this.closeSocket(callback); + } + }, this.closingFlushInterval); + }); + } + + private closeSocket(callback: ((error?: Error) => void) | undefined): void { + let handledError = false; + const handleErr = (err: unknown): void => { + const errMessage = `Error closing hot-shots socket: ${String(err)}`; + if (handledError) { + console.error(errMessage); + } else { + handledError = true; + if (callback) { + callback(new Error(errMessage)); + } else { + const handler = this.errorHandler; + if (handler) { + handler(new Error(errMessage)); + } else { + console.error(errMessage); + } + } + } + }; + const socket = this.transport; + if (socket === null) { + handleErr(new TypeError("Cannot read properties of null (reading 'removeListener')")); + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + socket.onError(handleErr); + if (callback) { + socket.onClose(() => { + if (!handledError) callback(undefined); + }); + } + try { + socket.close(); + } catch (err) { + handleErr(err); + } + } + + childClient(options?: ChildClientOptions): StatsD { + const o: ChildClientOptions = options ?? {}; + const childTags = o.globalTags; + return new StatsD( + { + isChild: true, + errorHandler: o.errorHandler || this.errorHandler, + host: this.host, + port: this.port, + tagPrefix: this.tagPrefix, + tagSeparator: this.tagSeparator, + prefix: (o.prefix || "") + this.prefix, + suffix: this.suffix + (o.suffix || ""), + globalize: false, + mock: this.mock, + globalTags: + childTags !== undefined + ? overrideTags(this.globalTags, childTags, this.telegraf) + : this.globalTags, + maxBufferSize: this.maxBufferSize, + bufferFlushInterval: this.bufferFlushInterval, + telegraf: this.telegraf, + protocol: this.protocol === "tcp" ? "tcp" : this.protocol === "uds" ? "uds" : "udp", + closingFlushInterval: this.closingFlushInterval, + }, + new ChildInit(this.transport, this.bufferHolder, this.dnsError), + ); + } +} + +export default StatsD; diff --git a/packages/compiler/shims/hot-shots/v8-unix-dgram.ts b/packages/compiler/shims/hot-shots/v8-unix-dgram.ts new file mode 100644 index 000000000..f4bfb180e --- /dev/null +++ b/packages/compiler/shims/hot-shots/v8-unix-dgram.ts @@ -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"; diff --git a/packages/compiler/shims/hot-shots/v8.ts b/packages/compiler/shims/hot-shots/v8.ts new file mode 100644 index 000000000..0d1871170 --- /dev/null +++ b/packages/compiler/shims/hot-shots/v8.ts @@ -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"; diff --git a/packages/compiler/src/backend/cycle-analysis.test.ts b/packages/compiler/src/backend/cycle-analysis.test.ts index c78420f73..340ffa8f9 100644 --- a/packages/compiler/src/backend/cycle-analysis.test.ts +++ b/packages/compiler/src/backend/cycle-analysis.test.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({ diff --git a/packages/compiler/src/backend/cycle-analysis.ts b/packages/compiler/src/backend/cycle-analysis.ts index aa624579f..fbd96e854 100644 --- a/packages/compiler/src/backend/cycle-analysis.ts +++ b/packages/compiler/src/backend/cycle-analysis.ts @@ -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; unions: Set } { - 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; unions: Set; @@ -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}`, diff --git a/packages/compiler/src/backend/cycle-mutability.ts b/packages/compiler/src/backend/cycle-mutability.ts index 78daa6204..541a1312f 100644 --- a/packages/compiler/src/backend/cycle-mutability.ts +++ b/packages/compiler/src/backend/cycle-mutability.ts @@ -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:`, 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(); + 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}`); diff --git a/packages/compiler/src/backend/immutability.ts b/packages/compiler/src/backend/immutability.ts index 6c37731e3..21380de8f 100644 --- a/packages/compiler/src/backend/immutability.ts +++ b/packages/compiler/src/backend/immutability.ts @@ -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 { } 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; } diff --git a/packages/compiler/src/backend/llvm/appended-array-literals.ts b/packages/compiler/src/backend/llvm/appended-array-literals.ts index 1d4577ae6..09a2976de 100644 --- a/packages/compiler/src/backend/llvm/appended-array-literals.ts +++ b/packages/compiler/src/backend/llvm/appended-array-literals.ts @@ -85,6 +85,7 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet for (const field of fieldAliases.get(binding) ?? []) appendedFields.add(field); const result = new Set(); + 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); }; diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.ts b/packages/compiler/src/backend/llvm/constant-callbacks.ts index 8a898d6a0..cf5c1b062 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.ts @@ -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(); + const result = new Map>(); + // 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>(); for (const fn of mod.functions) { const needed = indirectCallees.get(fn.name)!; if (!needed.size) continue; diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 39ff5dcd3..c5043aa3f 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -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" && diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 7adf2a89f..ed5a8ba46 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 062bfdfbe..1419cbef7 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index 41ecd189e..6eb9be8f7 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -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); }); diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts index 184fdb99e..6e10e9c62 100644 --- a/packages/compiler/src/backend/llvm/lib-datagram.ts +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -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", diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8c598a3d3..d0888ccbe 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -758,6 +758,7 @@ export const LIB_FN_SYMS: Record = { "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", diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts index 8729e9649..2c355f551 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.ts @@ -279,6 +279,8 @@ function shapeUnion( * tag and payload inside the C map. */ function boxedUnionIds(mod: IrModule): Set { const boxed = new Set(); + // 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); }; diff --git a/packages/compiler/src/backend/llvm/record-results.ts b/packages/compiler/src/backend/llvm/record-results.ts index 47a7983d2..db25016cd 100644 --- a/packages/compiler/src/backend/llvm/record-results.ts +++ b/packages/compiler/src/backend/llvm/record-results.ts @@ -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.(...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; diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index d31f14d12..6d1593a55 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -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. */ diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index b72dc545d..a04cce856 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -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; + } = {}, ): { fns: Set; /** 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[]; diff --git a/packages/compiler/src/backend/result-slots.ts b/packages/compiler/src/backend/result-slots.ts new file mode 100644 index 000000000..608877f06 --- /dev/null +++ b/packages/compiler/src/backend/result-slots.ts @@ -0,0 +1,29 @@ +import type { IrStmt } from "../ir/ir.js"; + +/** Prefix of a function's result-slot entry: `%into.(...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) + ); +} diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts index fa67c071f..c448897f1 100644 --- a/packages/compiler/src/backend/stack-observations.ts +++ b/packages/compiler/src/backend/stack-observations.ts @@ -29,35 +29,50 @@ const NON_STACK_KEY_TYPES: ReadonlySet = 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(); - private readonly named: Set; + private spelled: boolean | undefined; + private census: + | { calls: Map; named: Set } + | undefined; private readonly inits = new Map>(); /** 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(); - 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; + named: Set; + } { + if (this.census !== undefined) return this.census; + const mod = this.mod; + const calls = new Map(); + 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): 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 || diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index ea2dd379d..9096316a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -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, diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index 2618dacf9..c8a8d6ba6 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -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 = 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) { diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index de16e5d61..8632bad47 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -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( diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 2fc9a6cbd..4adfb87ab 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 39b7207c0..301670c3a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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 = 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 }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index aa90fc7c7..0e0ddf975 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -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) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 19123dae3..ea844ab1e 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ae91a87d5..34b345c94 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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[]`): diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 86a41d3a6..a0c2e4cb9 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -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 = new Map([ +const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map< + string, + ImplementationShim +>([ [ "fs-extra", { @@ -79,6 +90,33 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap = 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(); - 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: `/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(); diff --git a/packages/compiler/src/frontend/ts7/ast-node.test.ts b/packages/compiler/src/frontend/ts7/ast-node.test.ts index 6b2725b93..8b391e622 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.test.ts @@ -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; + 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 }); diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index 40e77e6f1..4c7726c00 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -95,6 +95,8 @@ export class AstFile { private readonly strings = new Map(); private lines: number[] | undefined; private childIndices: Uint32Array | undefined; + /** The root source file's name (AstNode.fileName). */ + private rootFileName: string | undefined; /** Whether nodes answer write targets without their wrappers (see * AstFileOptions.writeTargets). */ @@ -123,7 +125,8 @@ export class AstFile { } node(index: number): AstNode { - if (Object.hasOwn(this.nodes, index)) return this.nodes[index]!; + const existing: AstNode | undefined = this.nodes[index]; + if (existing !== undefined) return existing; if (index === 0 || this.wire.kind(index) === KIND_NODE_LIST) throw new AstDecodeError("expected a node index"); const node = new AstNode(this, index); @@ -150,8 +153,13 @@ export class AstFile { } namedChild(node: AstNode, name: string): number { + return this.namedChildAt(node, astChildOrder(node.kind, name)); + } + + /** The child in wire slot `order` (astChildOrder), or 0 when the slot is + * negative or empty. */ + namedChildAt(node: AstNode, order: number): number { if (node.file !== this) throw new AstDecodeError("named child belongs to another source file"); - const order = astChildOrder(node.kind, name); if (order < 0) return 0; const mask = node.data >>> 30 === 0 ? node.data & 0xff : 0xff; if ((mask & (1 << order)) === 0) return 0; @@ -218,6 +226,15 @@ export class AstFile { sourceTextObserver(this.root.fileName); } + /** The file name of the source file node at `index`; the root's is + * read once. */ + sourceFileName(index: number): string { + if (index === 1 && this.rootFileName !== undefined) return this.rootFileName; + const name = this.wire.string(this.wire.extendedWord(index, 4)); + if (index === 1) this.rootFileName = name; + return name; + } + lineStarts(): number[] { this.noteSourceText(); if (this.lines === undefined) this.lines = astLineStarts(this.root.text ?? ""); @@ -344,6 +361,22 @@ export class AstNode { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.node(index); } + /** childNode by wire slot, as the generated getters select it. */ + childNodeAt(order: number): AstNode | undefined { + const index = this.file.namedChildAt(this, order); + return index === 0 ? undefined : this.file.node(index); + } + childListAt(order: number): AstNodeList | undefined { + const index = this.file.namedChildAt(this, order); + return index === 0 ? undefined : this.file.list(index); + } + childAt(order: number): AstNode | AstNodeList | undefined { + const index = this.file.namedChildAt(this, order); + if (index === 0) return undefined; + return this.file.wire.kind(index) === KIND_NODE_LIST + ? this.file.list(index) + : this.file.node(index); + } childList(name: string): AstNodeList | undefined { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.list(index); @@ -585,18 +618,18 @@ export class AstNode { } get modifierFlags(): number { let result = 0; - for (const modifier of this.childList("modifiers")?.nodes ?? []) + for (const modifier of this.childListAt(this.modifiersSlot())?.nodes ?? []) result |= astModifierFlag(modifier.kind); return result; } get modifiers(): AstModifierList | undefined { - const index = this.file.namedChild(this, "modifiers"); + const index = this.file.namedChildAt(this, this.modifiersSlot()); return index === 0 ? undefined : this.file.modifierList(index); } get body(): AstNode | undefined { - const body = this.childNode("body"); + const body = this.childNodeAt(this.bodySlot()); // TS7 can serialize a reparsed @type callable before a method's // body without setting the type presence bit. The linked Block is // still the real body; keep the JSDoc node out of this view. @@ -618,11 +651,11 @@ export class AstNode { } get questionToken(): AstNode | undefined { - const direct = this.childNode("questionToken"); + const direct = this.childNodeAt(this.questionTokenSlot()); if (direct !== undefined) return direct; // TypeScript 7 shares one postfix slot for optional and definite- // assignment declarations. The familiar helper surface keeps them apart. - const token = this.childNode("postfixToken"); + const token = this.postfixToken; return token?.kind === SyntaxKind.QuestionToken ? token : undefined; } @@ -630,21 +663,21 @@ export class AstNode { // tree; with AstFileOptions.writeTargets these views return the target // without them. get initializer(): AstNode | undefined { - const initializer = this.childNode("initializer"); + const initializer = this.childNodeAt(this.initializerSlot()); return initializer !== undefined && this.file.writeTargets && writesTarget(this, "initializer") ? unwrapTarget(initializer) : initializer; } get left(): AstNode | undefined { - const left = this.childNode("left"); + const left = this.childNodeAt(this.leftSlot()); return left !== undefined && this.file.writeTargets && writesTarget(this, "left") ? unwrapTarget(left) : left; } get operand(): AstNode | undefined { - const operand = this.childNode("operand"); + const operand = this.childNodeAt(this.operandSlot()); return operand !== undefined && this.file.writeTargets && writesTarget(this, "operand") ? unwrapTarget(operand) : operand; @@ -653,281 +686,1173 @@ export class AstNode { // BEGIN GENERATED CHILD GETTERS // Generated from typescript@7.0.2; run scripts/generate-ts7-ast-schema.mjs. get argument(): AstNode | undefined { - return this.childNode("argument"); + switch (this.kind) { + case 206: + return this.childNodeAt(0); + default: + return undefined; + } } get argumentExpression(): AstNode | undefined { - return this.childNode("argumentExpression"); + switch (this.kind) { + case 213: + return this.childNodeAt(2); + default: + return undefined; + } } get arguments(): AstNodeList | undefined { - return this.childList("arguments"); + switch (this.kind) { + case 215: + return this.childListAt(2); + case 214: + return this.childListAt(3); + default: + return undefined; + } } get assertsModifier(): AstNode | undefined { - return this.childNode("assertsModifier"); + switch (this.kind) { + case 183: + return this.childNodeAt(0); + default: + return undefined; + } } get asteriskToken(): AstNode | undefined { - return this.childNode("asteriskToken"); + switch (this.kind) { + case 230: + return this.childNodeAt(0); + case 175: + case 219: + case 263: + return this.childNodeAt(1); + default: + return undefined; + } } get attributes(): AstNode | AstNodeList | undefined { - return this.child("attributes"); + switch (this.kind) { + case 301: + return this.childAt(0); + case 206: + return this.childAt(1); + case 286: + case 287: + return this.childAt(2); + case 273: + case 279: + case 343: + case 346: + return this.childAt(3); + default: + return undefined; + } } get awaitModifier(): AstNode | undefined { - return this.childNode("awaitModifier"); + switch (this.kind) { + case 250: + case 251: + return this.childNodeAt(0); + default: + return undefined; + } } get block(): AstNode | undefined { - return this.childNode("block"); + switch (this.kind) { + case 300: + return this.childNodeAt(1); + default: + return undefined; + } } get caseBlock(): AstNode | undefined { - return this.childNode("caseBlock"); + switch (this.kind) { + case 256: + return this.childNodeAt(1); + default: + return undefined; + } } get catchClause(): AstNode | undefined { - return this.childNode("catchClause"); + switch (this.kind) { + case 259: + return this.childNodeAt(1); + default: + return undefined; + } } get checkType(): AstNode | undefined { - return this.childNode("checkType"); + switch (this.kind) { + case 195: + return this.childNodeAt(0); + default: + return undefined; + } } get children(): AstNode | AstNodeList | undefined { - return this.child("children"); + switch (this.kind) { + case 344: + return this.childAt(0); + case 285: + case 289: + return this.childAt(1); + default: + return undefined; + } } get className(): AstNode | undefined { - return this.childNode("className"); + switch (this.kind) { + case 323: + case 324: + return this.childNodeAt(1); + default: + return undefined; + } } get clauses(): AstNodeList | undefined { - return this.childList("clauses"); + switch (this.kind) { + case 270: + return this.childListAt(0); + default: + return undefined; + } } get closingElement(): AstNode | undefined { - return this.childNode("closingElement"); + switch (this.kind) { + case 285: + return this.childNodeAt(2); + default: + return undefined; + } } get closingFragment(): AstNode | undefined { - return this.childNode("closingFragment"); + switch (this.kind) { + case 289: + return this.childNodeAt(2); + default: + return undefined; + } } get colonToken(): AstNode | undefined { - return this.childNode("colonToken"); + switch (this.kind) { + case 228: + return this.childNodeAt(3); + default: + return undefined; + } } get comment(): AstNodeList | undefined { - return this.childList("comment"); + switch (this.kind) { + case 315: + return this.childListAt(0); + case 322: + case 325: + case 326: + case 327: + case 328: + case 329: + case 330: + return this.childListAt(1); + case 323: + case 324: + case 332: + case 334: + case 335: + case 336: + case 339: + case 341: + case 342: + return this.childListAt(2); + case 331: + case 333: + case 337: + case 338: + case 340: + return this.childListAt(3); + case 343: + return this.childListAt(4); + default: + return undefined; + } } get condition(): AstNode | undefined { - return this.childNode("condition"); + switch (this.kind) { + case 228: + return this.childNodeAt(0); + case 249: + return this.childNodeAt(1); + default: + return undefined; + } } get constraint(): AstNode | undefined { - return this.childNode("constraint"); + switch (this.kind) { + case 337: + return this.childNodeAt(1); + case 169: + return this.childNodeAt(2); + default: + return undefined; + } } get declarationList(): AstNode | undefined { - return this.childNode("declarationList"); + switch (this.kind) { + case 244: + return this.childNodeAt(1); + default: + return undefined; + } } get declarations(): AstNodeList | undefined { - return this.childList("declarations"); + switch (this.kind) { + case 262: + return this.childListAt(0); + default: + return undefined; + } } get defaultType(): AstNode | undefined { - return this.childNode("defaultType"); + switch (this.kind) { + case 169: + return this.childNodeAt(4); + default: + return undefined; + } } get dotDotDotToken(): AstNode | undefined { - return this.childNode("dotDotDotToken"); + switch (this.kind) { + case 203: + case 209: + case 295: + return this.childNodeAt(0); + case 170: + return this.childNodeAt(1); + default: + return undefined; + } } get elementType(): AstNode | undefined { - return this.childNode("elementType"); + switch (this.kind) { + case 189: + return this.childNodeAt(0); + default: + return undefined; + } } get elements(): AstNodeList | undefined { - return this.childList("elements"); + switch (this.kind) { + case 190: + case 207: + case 208: + case 210: + case 276: + case 280: + return this.childListAt(0); + default: + return undefined; + } } get elseStatement(): AstNode | undefined { - return this.childNode("elseStatement"); + switch (this.kind) { + case 246: + return this.childNodeAt(2); + default: + return undefined; + } } get endOfFileToken(): AstNode | undefined { - return this.childNode("endOfFileToken"); + switch (this.kind) { + case 307: + return this.childNodeAt(1); + default: + return undefined; + } } get equalsGreaterThanToken(): AstNode | undefined { - return this.childNode("equalsGreaterThanToken"); + switch (this.kind) { + case 220: + return this.childNodeAt(4); + default: + return undefined; + } } get equalsToken(): AstNode | undefined { - return this.childNode("equalsToken"); + switch (this.kind) { + case 304: + return this.childNodeAt(4); + default: + return undefined; + } } get exclamationToken(): AstNode | undefined { - return this.childNode("exclamationToken"); + switch (this.kind) { + case 261: + return this.childNodeAt(1); + default: + return undefined; + } } get exportClause(): AstNode | undefined { - return this.childNode("exportClause"); + switch (this.kind) { + case 279: + return this.childNodeAt(1); + default: + return undefined; + } } get exprName(): AstNode | undefined { - return this.childNode("exprName"); + switch (this.kind) { + case 187: + return this.childNodeAt(0); + default: + return undefined; + } } get expression(): AstNode | undefined { - return this.childNode("expression"); + switch (this.kind) { + case 168: + case 171: + case 212: + case 213: + case 214: + case 215: + case 218: + case 221: + case 222: + case 223: + case 224: + case 231: + case 234: + case 235: + case 236: + case 239: + case 240: + case 245: + case 246: + case 248: + case 254: + case 255: + case 256: + case 258: + case 284: + case 294: + case 297: + case 298: + case 305: + case 348: + case 349: + return this.childNodeAt(0); + case 217: + case 230: + case 247: + case 295: + return this.childNodeAt(1); + case 250: + case 251: + case 278: + return this.childNodeAt(2); + case 169: + return this.childNodeAt(3); + default: + return undefined; + } } get extendsType(): AstNode | undefined { - return this.childNode("extendsType"); + switch (this.kind) { + case 195: + return this.childNodeAt(1); + default: + return undefined; + } } get falseType(): AstNode | undefined { - return this.childNode("falseType"); + switch (this.kind) { + case 195: + return this.childNodeAt(3); + default: + return undefined; + } } get finallyBlock(): AstNode | undefined { - return this.childNode("finallyBlock"); + switch (this.kind) { + case 259: + return this.childNodeAt(2); + default: + return undefined; + } } get head(): AstNode | undefined { - return this.childNode("head"); + switch (this.kind) { + case 204: + case 229: + return this.childNodeAt(0); + default: + return undefined; + } } get heritageClauses(): AstNodeList | undefined { - return this.childList("heritageClauses"); + switch (this.kind) { + case 232: + case 264: + case 265: + return this.childListAt(3); + default: + return undefined; + } } get importClause(): AstNode | undefined { - return this.childNode("importClause"); + switch (this.kind) { + case 273: + case 343: + case 346: + return this.childNodeAt(1); + default: + return undefined; + } } get incrementor(): AstNode | undefined { - return this.childNode("incrementor"); + switch (this.kind) { + case 249: + return this.childNodeAt(2); + default: + return undefined; + } } get indexType(): AstNode | undefined { - return this.childNode("indexType"); + switch (this.kind) { + case 200: + return this.childNodeAt(1); + default: + return undefined; + } } get jsdocPropertyTags(): AstNode | undefined { - return this.childNode("jsdocPropertyTags"); + switch (this.kind) { + case 317: + return this.childNodeAt(0); + default: + return undefined; + } } get label(): AstNode | undefined { - return this.childNode("label"); + switch (this.kind) { + case 252: + case 253: + case 257: + return this.childNodeAt(0); + default: + return undefined; + } } get literal(): AstNode | undefined { - return this.childNode("literal"); + switch (this.kind) { + case 202: + return this.childNodeAt(0); + case 205: + case 240: + return this.childNodeAt(1); + default: + return undefined; + } } get members(): AstNodeList | undefined { - return this.childList("members"); + switch (this.kind) { + case 188: + return this.childListAt(0); + case 267: + return this.childListAt(2); + case 232: + case 264: + case 265: + return this.childListAt(4); + case 201: + return this.childListAt(5); + default: + return undefined; + } } get moduleReference(): AstNode | undefined { - return this.childNode("moduleReference"); + switch (this.kind) { + case 272: + return this.childNodeAt(2); + default: + return undefined; + } } get moduleSpecifier(): AstNode | undefined { - return this.childNode("moduleSpecifier"); + switch (this.kind) { + case 273: + case 279: + case 343: + case 346: + return this.childNodeAt(2); + default: + return undefined; + } } get name(): AstNode | undefined { - return this.childNode("name"); + switch (this.kind) { + case 237: + case 261: + case 274: + case 275: + case 281: + case 292: + case 302: + case 306: + case 309: + case 319: + case 320: + case 321: + return this.childNodeAt(0); + case 169: + case 172: + case 173: + case 174: + case 178: + case 179: + case 203: + case 232: + case 264: + case 265: + case 266: + case 267: + case 268: + case 271: + case 272: + case 277: + case 282: + case 296: + case 303: + case 304: + case 333: + case 340: + case 345: + return this.childNodeAt(1); + case 170: + case 175: + case 209: + case 212: + case 219: + case 263: + case 331: + case 338: + return this.childNodeAt(2); + default: + return undefined; + } } get nameExpression(): AstNode | undefined { - return this.childNode("nameExpression"); + switch (this.kind) { + case 339: + return this.childNodeAt(1); + default: + return undefined; + } } get nameType(): AstNode | undefined { - return this.childNode("nameType"); + switch (this.kind) { + case 201: + return this.childNodeAt(2); + default: + return undefined; + } } get namedBindings(): AstNode | undefined { - return this.childNode("namedBindings"); + switch (this.kind) { + case 274: + return this.childNodeAt(1); + default: + return undefined; + } } get namespace(): AstNode | undefined { - return this.childNode("namespace"); + switch (this.kind) { + case 296: + return this.childNodeAt(0); + default: + return undefined; + } } get objectAssignmentInitializer(): AstNode | undefined { - return this.childNode("objectAssignmentInitializer"); + switch (this.kind) { + case 304: + return this.childNodeAt(5); + default: + return undefined; + } } get objectType(): AstNode | undefined { - return this.childNode("objectType"); + switch (this.kind) { + case 200: + return this.childNodeAt(0); + default: + return undefined; + } } get openingElement(): AstNode | undefined { - return this.childNode("openingElement"); + switch (this.kind) { + case 285: + return this.childNodeAt(0); + default: + return undefined; + } } get openingFragment(): AstNode | undefined { - return this.childNode("openingFragment"); + switch (this.kind) { + case 289: + return this.childNodeAt(0); + default: + return undefined; + } } get operatorToken(): AstNode | undefined { - return this.childNode("operatorToken"); + switch (this.kind) { + case 227: + return this.childNodeAt(3); + default: + return undefined; + } } get parameterName(): AstNode | undefined { - return this.childNode("parameterName"); + switch (this.kind) { + case 183: + return this.childNodeAt(1); + default: + return undefined; + } } get parameters(): AstNodeList | undefined { - return this.childList("parameters"); + switch (this.kind) { + case 180: + case 181: + case 182: + case 185: + case 318: + return this.childListAt(1); + case 177: + case 186: + case 220: + return this.childListAt(2); + case 178: + case 179: + return this.childListAt(3); + case 174: + case 219: + case 263: + return this.childListAt(4); + case 175: + return this.childListAt(5); + default: + return undefined; + } } get postfixToken(): AstNode | undefined { - return this.childNode("postfixToken"); + switch (this.kind) { + case 172: + case 173: + case 174: + case 303: + case 304: + return this.childNodeAt(2); + case 175: + return this.childNodeAt(3); + default: + return undefined; + } } get properties(): AstNodeList | undefined { - return this.childList("properties"); + switch (this.kind) { + case 211: + case 293: + return this.childListAt(0); + default: + return undefined; + } } get propertyName(): AstNode | undefined { - return this.childNode("propertyName"); + switch (this.kind) { + case 277: + case 282: + return this.childNodeAt(0); + case 209: + return this.childNodeAt(1); + default: + return undefined; + } } get qualifier(): AstNode | undefined { - return this.childNode("qualifier"); + switch (this.kind) { + case 206: + return this.childNodeAt(2); + default: + return undefined; + } } get questionDotToken(): AstNode | undefined { - return this.childNode("questionDotToken"); + switch (this.kind) { + case 212: + case 213: + case 214: + case 216: + return this.childNodeAt(1); + default: + return undefined; + } } get readonlyToken(): AstNode | undefined { - return this.childNode("readonlyToken"); + switch (this.kind) { + case 201: + return this.childNodeAt(0); + default: + return undefined; + } } get right(): AstNode | undefined { - return this.childNode("right"); + switch (this.kind) { + case 167: + return this.childNodeAt(1); + case 227: + return this.childNodeAt(4); + default: + return undefined; + } } get statement(): AstNode | undefined { - return this.childNode("statement"); + switch (this.kind) { + case 247: + return this.childNodeAt(0); + case 248: + case 255: + case 257: + return this.childNodeAt(1); + case 249: + case 250: + case 251: + return this.childNodeAt(3); + default: + return undefined; + } } get statements(): AstNodeList | undefined { - return this.childList("statements"); + switch (this.kind) { + case 242: + case 269: + case 307: + return this.childListAt(0); + case 297: + case 298: + return this.childListAt(1); + default: + return undefined; + } } get tag(): AstNode | undefined { - return this.childNode("tag"); + switch (this.kind) { + case 216: + return this.childNodeAt(0); + default: + return undefined; + } } get tagName(): AstNode | undefined { - return this.childNode("tagName"); + switch (this.kind) { + case 286: + case 287: + case 288: + case 322: + case 323: + case 324: + case 325: + case 326: + case 327: + case 328: + case 329: + case 330: + case 331: + case 332: + case 333: + case 334: + case 335: + case 336: + case 337: + case 338: + case 339: + case 340: + case 341: + case 342: + case 343: + return this.childNodeAt(0); + default: + return undefined; + } } get tags(): AstNodeList | undefined { - return this.childList("tags"); + switch (this.kind) { + case 315: + return this.childListAt(1); + default: + return undefined; + } } get template(): AstNode | undefined { - return this.childNode("template"); + switch (this.kind) { + case 216: + return this.childNodeAt(3); + default: + return undefined; + } } get templateSpans(): AstNodeList | undefined { - return this.childList("templateSpans"); + switch (this.kind) { + case 204: + case 229: + return this.childListAt(1); + default: + return undefined; + } } get thenStatement(): AstNode | undefined { - return this.childNode("thenStatement"); + switch (this.kind) { + case 246: + return this.childNodeAt(1); + default: + return undefined; + } } get thisArg(): AstNode | undefined { - return this.childNode("thisArg"); + switch (this.kind) { + case 349: + return this.childNodeAt(1); + default: + return undefined; + } } get trueType(): AstNode | undefined { - return this.childNode("trueType"); + switch (this.kind) { + case 195: + return this.childNodeAt(2); + default: + return undefined; + } } get tryBlock(): AstNode | undefined { - return this.childNode("tryBlock"); + switch (this.kind) { + case 259: + return this.childNodeAt(0); + default: + return undefined; + } } get tupleNameSource(): AstNode | undefined { - return this.childNode("tupleNameSource"); + switch (this.kind) { + case 238: + return this.childNodeAt(0); + default: + return undefined; + } } get type(): AstNode | undefined { - return this.childNode("type"); + switch (this.kind) { + case 191: + case 192: + case 197: + case 199: + case 205: + case 217: + case 308: + case 311: + case 312: + case 313: + case 314: + return this.childNodeAt(0); + case 235: + case 239: + case 278: + return this.childNodeAt(1); + case 180: + case 181: + case 182: + case 183: + case 185: + case 227: + case 261: + case 318: + return this.childNodeAt(2); + case 172: + case 173: + case 177: + case 186: + case 203: + case 220: + case 266: + case 303: + case 304: + case 345: + return this.childNodeAt(3); + case 170: + case 178: + case 179: + case 201: + return this.childNodeAt(4); + case 174: + case 219: + case 263: + return this.childNodeAt(5); + case 175: + return this.childNodeAt(6); + default: + return undefined; + } } get typeArguments(): AstNodeList | undefined { - return this.childList("typeArguments"); + switch (this.kind) { + case 184: + case 187: + case 215: + case 234: + case 286: + case 287: + return this.childListAt(1); + case 214: + case 216: + return this.childListAt(2); + case 206: + return this.childListAt(3); + default: + return undefined; + } } get typeExpression(): AstNode | undefined { - return this.childNode("typeExpression"); + switch (this.kind) { + case 331: + case 332: + case 334: + case 335: + case 336: + case 338: + case 341: + case 342: + return this.childNodeAt(1); + case 333: + case 340: + return this.childNodeAt(2); + default: + return undefined; + } } get typeName(): AstNode | undefined { - return this.childNode("typeName"); + switch (this.kind) { + case 184: + return this.childNodeAt(0); + default: + return undefined; + } } get typeParameter(): AstNode | undefined { - return this.childNode("typeParameter"); + switch (this.kind) { + case 196: + return this.childNodeAt(0); + case 201: + return this.childNodeAt(1); + default: + return undefined; + } } get typeParameters(): AstNodeList | undefined { - return this.childList("typeParameters"); + switch (this.kind) { + case 180: + case 181: + case 185: + case 318: + return this.childListAt(0); + case 177: + case 186: + case 220: + return this.childListAt(1); + case 178: + case 179: + case 232: + case 264: + case 265: + case 266: + case 337: + case 345: + return this.childListAt(2); + case 174: + case 219: + case 263: + return this.childListAt(3); + case 175: + return this.childListAt(4); + default: + return undefined; + } } get types(): AstNodeList | undefined { - return this.childList("types"); + switch (this.kind) { + case 193: + case 194: + case 299: + return this.childListAt(0); + default: + return undefined; + } } get value(): AstNode | undefined { - return this.childNode("value"); + switch (this.kind) { + case 302: + return this.childNodeAt(1); + default: + return undefined; + } } get variableDeclaration(): AstNode | undefined { - return this.childNode("variableDeclaration"); + switch (this.kind) { + case 300: + return this.childNodeAt(0); + default: + return undefined; + } } get whenFalse(): AstNode | undefined { - return this.childNode("whenFalse"); + switch (this.kind) { + case 228: + return this.childNodeAt(4); + default: + return undefined; + } } get whenTrue(): AstNode | undefined { - return this.childNode("whenTrue"); + switch (this.kind) { + case 228: + return this.childNodeAt(2); + default: + return undefined; + } + } + private bodySlot(): number { + switch (this.kind) { + case 176: + return 1; + case 268: + return 2; + case 177: + return 4; + case 178: + case 179: + case 220: + return 5; + case 219: + case 263: + return 6; + case 175: + return 7; + default: + return -1; + } + } + private initializerSlot(): number { + switch (this.kind) { + case 249: + return 0; + case 250: + case 251: + case 292: + case 306: + return 1; + case 209: + case 261: + return 3; + case 172: + case 173: + case 303: + return 4; + case 170: + return 5; + default: + return -1; + } + } + private leftSlot(): number { + switch (this.kind) { + case 167: + return 0; + case 227: + return 1; + default: + return -1; + } + } + private modifiersSlot(): number { + switch (this.kind) { + case 169: + case 170: + case 172: + case 173: + case 174: + case 175: + case 176: + case 177: + case 178: + case 179: + case 182: + case 186: + case 219: + case 220: + case 227: + case 232: + case 244: + case 263: + case 264: + case 265: + case 266: + case 267: + case 268: + case 271: + case 272: + case 273: + case 278: + case 279: + case 283: + case 303: + case 304: + case 345: + case 346: + return 0; + default: + return -1; + } + } + private operandSlot(): number { + switch (this.kind) { + case 225: + case 226: + return 0; + default: + return -1; + } + } + private questionTokenSlot(): number { + switch (this.kind) { + case 228: + return 1; + case 203: + return 2; + case 170: + case 201: + return 3; + default: + return -1; + } } // END GENERATED CHILD GETTERS // Source-file fields stay on the same nominal class, matching the wire's // single node representation. Their extended-data accesses are checked. get fileName(): string { - return this.file.wire.string(this.file.wire.extendedWord(this.index, 4)); + return this.file.sourceFileName(this.index); } get path(): string { return this.file.wire.string(this.file.wire.extendedWord(this.index, 8)); diff --git a/packages/compiler/src/frontend/ts7/ast-wire.ts b/packages/compiler/src/frontend/ts7/ast-wire.ts index ff70148df..9dd8c9f5c 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.ts @@ -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; } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index e98ae0bd9..7d01bb83d 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -1007,3 +1007,25 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = 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 = 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", +]); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index b26b5d1d8..6bf864da6 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -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 diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 21223862a..ac0747276 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -508,7 +508,10 @@ export function dynamicFieldStores( }; const dynWritten = new Set(); 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 { + 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(); + // 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(); 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(); const seenNamed = new Set(); 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(); 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. diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index ec2edb931..893c2ad07 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -605,14 +605,23 @@ const STDIO_EXTENDED_NAMES: ReadonlySet = 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 = 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")) { diff --git a/packages/compiler/src/ir/traverse.ts b/packages/compiler/src/ir/traverse.ts index 11b20a210..251904351 100644 --- a/packages/compiler/src/ir/traverse.ts +++ b/packages/compiler/src/ir/traverse.ts @@ -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); diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb36c7007..163c2a41c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -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> = { @@ -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)) { diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index e2c3a895b..d073df5f4 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -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 { + compilerNativeBoundCycleGrowth(); setFrontendDisposedHook(() => compilerNativeCollectCycles()); const manifestPath = process.env["SCRIPTC_TOOLCHAIN"] ?? process.execPath + ".json"; let compiler: NativeCompiler | null = null; diff --git a/packages/compiler/test/package-shim-guard.test.ts b/packages/compiler/test/package-shim-guard.test.ts index ff01acbb8..a8620c1ce 100644 --- a/packages/compiler/test/package-shim-guard.test.ts +++ b/packages/compiler/test/package-shim-guard.test.ts @@ -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", ]); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4815-touint32-wide-magnitudes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4815-touint32-wide-magnitudes.ts.json new file mode 100644 index 000000000..80dcce24d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4815-touint32-wide-magnitudes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4815-touint32-wide-magnitudes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json new file mode 100644 index 000000000..83f8cc5d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6915-key-order-narrowed-throw.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json new file mode 100644 index 000000000..32edfc402 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6926-dynamic-collection-seeds.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json new file mode 100644 index 000000000..89c591dd7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6927-dynamic-collection-seeds.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json new file mode 100644 index 000000000..f8afbe00b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7030-spawn-sync-env.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json new file mode 100644 index 000000000..171ab060a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7036-child-env-path-lookup.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7261-acyclic-result-builders.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7261-acyclic-result-builders.ts.json new file mode 100644 index 000000000..4f4afd821 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7261-acyclic-result-builders.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7261-acyclic-result-builders.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json new file mode 100644 index 000000000..8f7268daf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/packages/compiler/shims/chalk/v5.ts", + "/tests/fixtures/npm/cases/chalk5-builder/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json index 065c96347..8365d4db9 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json @@ -1,5 +1,6 @@ { "order": [ + "/packages/compiler/shims/chalk/v5.ts", "/tests/fixtures/npm/cases/chalk5/main.ts" ], "diags": [] diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json new file mode 100644 index 000000000..a48e1af78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-buffered/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json new file mode 100644 index 000000000..716cfe02f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-send-error/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json new file mode 100644 index 000000000..f131361e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-tcp/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json index 54baff890..a3014fb9f 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json @@ -1,5 +1,7 @@ { "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", "/tests/fixtures/npm/cases/hot-shots-udp/main.ts" ], "diags": [] diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index ec685656d..df7543500 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -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); diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index e899130d8..a5853ad59 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -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, diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 5e5029e14..844c47001 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -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 diff --git a/packages/runtime/src/scr_dgram.c b/packages/runtime/src/scr_dgram.c index bb14d063e..8d1c7b058 100644 --- a/packages/runtime/src/scr_dgram.c +++ b/packages/runtime/src/scr_dgram.c @@ -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 +#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 +} diff --git a/packages/runtime/src/scr_dgram.h b/packages/runtime/src/scr_dgram.h index 8396f0693..08db077df 100644 --- a/packages/runtime/src/scr_dgram.h +++ b/packages/runtime/src/scr_dgram.h @@ -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 " * ", ERR_SOCKET_BAD_BUFFER_SIZE, ERR_SOCKET_BUFFER_SIZE, the * TypeError after close()). The TTL setters and setMulticastLoopback diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index a5847e59b..cb88297bf 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -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); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 68d91b26c..ee45f6d93 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 7a921b451..18df3a237 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -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 diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index 4e00b852e..160083d6b 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -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; diff --git a/scripts/generate-ts7-ast-schema.mjs b/scripts/generate-ts7-ast-schema.mjs index 07af474de..3218c8962 100644 --- a/scripts/generate-ts7-ast-schema.mjs +++ b/scripts/generate-ts7-ast-schema.mjs @@ -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"), ); diff --git a/tests/corpus/4815-touint32-wide-magnitudes.ts b/tests/corpus/4815-touint32-wide-magnitudes.ts new file mode 100644 index 000000000..ab60b8c0f --- /dev/null +++ b/tests/corpus/4815-touint32-wide-magnitudes.ts @@ -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)); diff --git a/tests/corpus/6915-key-order-narrowed-throw.ts b/tests/corpus/6915-key-order-narrowed-throw.ts new file mode 100644 index 000000000..d4bc693f2 --- /dev/null +++ b/tests/corpus/6915-key-order-narrowed-throw.ts @@ -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); diff --git a/tests/corpus/6926-dynamic-collection-seeds.mjs b/tests/corpus/6926-dynamic-collection-seeds.mjs new file mode 100644 index 000000000..a48fab894 --- /dev/null +++ b/tests/corpus/6926-dynamic-collection-seeds.mjs @@ -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()))); diff --git a/tests/corpus/6927-dynamic-collection-seeds.ts b/tests/corpus/6927-dynamic-collection-seeds.ts new file mode 100644 index 000000000..c57433d57 --- /dev/null +++ b/tests/corpus/6927-dynamic-collection-seeds.ts @@ -0,0 +1,9 @@ +// Set and Map seeds held as any: native iterators and generators. +function* values(): Generator { yield 1; yield 2; yield 2; } +function* pairs(): Generator<[string, number]> { yield ["a", 1]; yield ["b", 2]; } +const m = new Map([["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); diff --git a/tests/corpus/7030-spawn-sync-env.ts b/tests/corpus/7030-spawn-sync-env.ts new file mode 100644 index 000000000..0ab001b8f --- /dev/null +++ b/tests/corpus/7030-spawn-sync-env.ts @@ -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 }); diff --git a/tests/corpus/7036-child-env-path-lookup.ts b/tests/corpus/7036-child-env-path-lookup.ts new file mode 100644 index 000000000..97c93d835 --- /dev/null +++ b/tests/corpus/7036-child-env-path-lookup.ts @@ -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, "")}`; +}; + +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 }); + }); +}); diff --git a/tests/corpus/7261-acyclic-result-builders.ts b/tests/corpus/7261-acyclic-result-builders.ts new file mode 100644 index 000000000..b6a98fce4 --- /dev/null +++ b/tests/corpus/7261-acyclic-result-builders.ts @@ -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); diff --git a/tests/fixtures/npm/cases/chalk5-builder/main.ts b/tests/fixtures/npm/cases/chalk5-builder/main.ts new file mode 100644 index 000000000..2a4fff61b --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/main.ts @@ -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"); diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license new file mode 100644 index 000000000..fa7ceba3e --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (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. diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json new file mode 100644 index 000000000..c9e0dc52b --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json @@ -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" + ] + } +} diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts new file mode 100644 index 000000000..8295d9252 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js new file mode 100644 index 000000000..8bc993da5 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js new file mode 100644 index 000000000..4366dee0d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js @@ -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; +} diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts new file mode 100644 index 000000000..58f133abe --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts @@ -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; +} & ForegroundColor & BackgroundColor & Modifier & ConvertColor; + +export default ansiStyles; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js new file mode 100644 index 000000000..eaa7bed6c --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts new file mode 100644 index 000000000..0e8a9ccb8 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts @@ -0,0 +1 @@ +export {default} from './index.js'; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js new file mode 100644 index 000000000..fbb6ce0fc --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.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; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts new file mode 100644 index 000000000..db44a7851 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js new file mode 100644 index 000000000..265d7f858 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5-builder/package.json b/tests/fixtures/npm/cases/chalk5-builder/package.json new file mode 100644 index 000000000..3804daa8d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/package.json @@ -0,0 +1 @@ +{ "name": "chalk5-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-buffered/main.ts b/tests/fixtures/npm/cases/hot-shots-buffered/main.ts new file mode 100644 index 000000000..d17ce1167 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-buffered/main.ts @@ -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 { + 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 { + 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((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((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((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"))); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-buffered/package.json b/tests/fixtures/npm/cases/hot-shots-buffered/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-buffered/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-send-error/main.ts b/tests/fixtures/npm/cases/hot-shots-send-error/main.ts new file mode 100644 index 000000000..e829c5e33 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-send-error/main.ts @@ -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); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-send-error/package.json b/tests/fixtures/npm/cases/hot-shots-send-error/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-send-error/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-tcp/main.ts b/tests/fixtures/npm/cases/hot-shots-tcp/main.ts new file mode 100644 index 000000000..0812c90f1 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-tcp/main.ts @@ -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((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((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")); + }); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-tcp/package.json b/tests/fixtures/npm/cases/hot-shots-tcp/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-tcp/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/harness/corpus-costs/4815-touint32-wide-magnitudes.ts.json b/tests/harness/corpus-costs/4815-touint32-wide-magnitudes.ts.json new file mode 100644 index 000000000..ee076af6b --- /dev/null +++ b/tests/harness/corpus-costs/4815-touint32-wide-magnitudes.ts.json @@ -0,0 +1 @@ +{"plain":2871,"san":4039} diff --git a/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json b/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json new file mode 100644 index 000000000..fae2adeb4 --- /dev/null +++ b/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 9000 } diff --git a/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json b/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json new file mode 100644 index 000000000..b4a56f5c5 --- /dev/null +++ b/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json @@ -0,0 +1 @@ +{ "plain": 3813, "san": 3374 } diff --git a/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json b/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json new file mode 100644 index 000000000..93da087d2 --- /dev/null +++ b/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json @@ -0,0 +1 @@ +{ "plain": 2436, "san": 2273 } diff --git a/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json b/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json new file mode 100644 index 000000000..cf8ef4d26 --- /dev/null +++ b/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json @@ -0,0 +1 @@ +{ "plain": 2460, "san": 4060 } diff --git a/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json b/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json new file mode 100644 index 000000000..f9bfce7d9 --- /dev/null +++ b/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json @@ -0,0 +1 @@ +{ "plain": 2570, "san": 4110 } diff --git a/tests/harness/corpus-costs/7261-acyclic-result-builders.ts.json b/tests/harness/corpus-costs/7261-acyclic-result-builders.ts.json new file mode 100644 index 000000000..30b7c116f --- /dev/null +++ b/tests/harness/corpus-costs/7261-acyclic-result-builders.ts.json @@ -0,0 +1 @@ +{ "plain": 3336, "san": 4712 }