Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/compiler/src/backend/executable-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
fileHandle: features.fileHandle,
fetch: features.fetch,
...(features.eventTarget ? { eventTarget: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
assert: features.assert,
inspect: features.inspect,
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5881,6 +5881,8 @@ export class LlEmitter {
// Process streams use a numeric descriptor internally, including zero
// for stdin. They remain JavaScript objects in boolean contexts.
case "procStream":
// A module namespace object is always present.
case "moduleNs":
return "true";
case "array":
case "record":
Expand Down
13 changes: 11 additions & 2 deletions packages/compiler/src/backend/llvm/lib-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,15 +546,24 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
B.line(`call void @scr_stdin_on_end(ptr ${cb.name}, i1 ${once.name})`);
return { name: "", type: e.type };
}
// A string listener takes the setEncoding-decoded chunk.
const stringData = e.fn === "stdin.onData" && cbT.params[0]?.kind === "string";
const adapter =
e.fn === "stdin.onData"
? cbT.params.length === 0
? "scr_stdin_data_thunk0"
: "scr_stdin_data_thunk_bytes"
: stringData
? "scr_stdin_data_thunk_str"
: "scr_stdin_data_thunk_bytes"
: cbT.params.length === 0
? "scr_child_err_thunk0"
: "scr_child_err_thunk_error";
const sym = e.fn === "stdin.onData" ? "scr_stdin_on_data" : "scr_stdin_on_error";
const sym =
e.fn === "stdin.onData"
? stringData
? "scr_stdin_on_data_str"
: "scr_stdin_on_data"
: "scr_stdin_on_error";
host.declare(`declare void @${adapter}(ptr, ptr)`);
host.declare(`declare void @${sym}(ptr, ptr, i1 zeroext)`);
B.line(`call void @${sym}(ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`);
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.emitWarningText": "scr_process_emit_warning_text",
"process.deprecationFlag": "scr_process_deprecation_flag",
"process.setDeprecationFlag": "scr_process_set_deprecation_flag",
"process.setEmitWarning": "scr_process_set_emit_warning",
"process.onUncaughtException": "scr_process_on_uncaught_exception",
"process.offUncaughtException": "scr_process_off_uncaught_exception",
"process.onUnhandledRejection": "scr_process_on_unhandled_rejection",
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/backend/native-link-info-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export interface NativeLinkFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
/** The program assigns process.emitWarning: links the
* SCR_WARNING_REPLACE variant of the dyn-async unit. */
warningReplace?: boolean;
/** The program uses process.stdin.setEncoding, string 'data' listeners
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
stdioExtended?: boolean;
zlib: boolean;
assert: boolean;
inspect: boolean;
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/backend/native/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ export interface CcOptions {
* native fetch AbortSignal built on the same engine: compiles
* scr_event_target.c. */
eventTarget?: boolean;
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
warningReplace?: boolean;
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
stdioExtended?: boolean;
/** The program uses the process-events surface (signal/exit listeners,
* stdin events, for-await over stdin — moduleUsesProcessEvents on the
* IR): compiles scr_events.c into the binary. Event-free binaries keep
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/backend/native/executable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@ export async function compileCInternal(
: [optimization === "dev" ? "-O0" : "-O2"]),
...executableSectionFlags.compile,
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
"-fno-math-errno",
// The runtime object model uses type-punned C: a hierarchy
Expand Down Expand Up @@ -644,6 +646,8 @@ export async function compileCInternal(
: [optimization === "dev" ? "-O0" : "-O2"]),
...executableSectionFlags.compile,
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
"-fno-math-errno",
"-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/backend/runtime-pack-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
workers: boolean;
youngNursery: boolean;
eventTarget: boolean;
warningReplace: boolean;
stdioExtended: boolean;
nativeFetch: boolean;
netEffective: boolean;
httpEffective: boolean;
Expand Down Expand Up @@ -269,6 +271,8 @@ export function effectiveRuntimeFeatures(
workers: features.workers === true,
youngNursery: features.youngNursery === true,
eventTarget: features.eventTarget === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
nativeFetch,
netEffective,
httpEffective,
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/executable/executable-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export interface EarlyExecutableNativeFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
warningReplace?: boolean;
stdioExtended?: boolean;
zlib: boolean;
assert: boolean;
inspect: boolean;
Expand Down
129 changes: 127 additions & 2 deletions packages/compiler/src/frontend/lowering/builtins/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
funcOf,
} from "../../../ir/ir.js";
import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js";
import { canonicalBuiltinModule } from "../../builtin-modules.js";
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";

Expand Down Expand Up @@ -148,6 +149,18 @@ export function lowerProcessStreamProperty(
loc: locOf(expr),
};
}
// `if (process.stdin.setRawMode)`: Node's stdin has the method only
// when it is a TTY. The runtime's stream object answers the same
// presence (the method or undefined); calls keep their direct lowering.
if (direct === "stdin" && member === "setRawMode") {
return {
kind: "dynKeyGet",
value: lowerer.lowerExpr(expr.expression),
key: strLit(member, locOf(expr.name)),
type: DYN,
loc: locOf(expr),
};
}
if (member !== "isTTY" && member !== "columns" && member !== "rows") return null;
let recv: ts.Expression = expr.expression;
while (ts.isParenthesizedExpression(recv) || ts.isAsExpression(recv) || ts.isTypeAssertion(recv))
Expand Down Expand Up @@ -364,6 +377,11 @@ export function lowerProcessProperty(
if (member === "versions") {
return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc };
}
// process.features as a value (`"sea" in process.features`): an object
// holding none of the flags, consistent with the member reads above.
if (member === "features") {
return { kind: "dynObjLit", type: DYN, loc };
}
// process.noDeprecation / process.throwDeprecation: the values the
// program assigned (undefined before that — no Node flags reach a
// compiled binary).
Expand Down Expand Up @@ -627,6 +645,73 @@ export function lowerProcessMethodCall(
}
return { kind: "libCall", fn: "process.stdinSetRawMode", args: [mode], type: VOID, loc };
}
// process.stdout.on("resize", cb) (and once/off/removeListener/
// addListener, stderr too): the terminal resize event, registered on the
// runtime's process stream object (a SIGWINCH listener that emits when a
// TTY's size changed; it never keeps the loop alive). Statement position:
// Node returns the stream.
if (
["on", "once", "off", "removeListener", "addListener"].includes(access.name.text) &&
ts.isPropertyAccessExpression(access.expression) &&
call.arguments.nodes.length === 2 &&
!call.arguments.nodes.some(ts.isSpreadElement) &&
ts.isExpressionStatement(call.parent)
) {
const stream = lowerer.stdlibGlobalMember(access.expression, "process");
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
if (
(stream === "stdout" || stream === "stderr") &&
evT.isStringLiteralType() &&
evT.value === "resize"
) {
const loc = locOf(call);
const receiverLocal = lowerer.declareHiddenLocal("%stdioReceiver", DYN);
const receiver = varRef(receiverLocal.id, DYN, loc);
const method =
access.name.text === "off"
? "removeListener"
: access.name.text === "addListener"
? "on"
: access.name.text;
return {
kind: "seqExpr",
stmts: [
{
kind: "varDecl",
localId: receiverLocal.id,
init: {
kind: "libCall",
fn: "process.stdio",
args: [numLit(stream === "stdout" ? 1 : 2, loc)],
type: DYN,
loc,
},
loc,
},
],
result: {
kind: "dynCall",
callee: {
kind: "dynKeyGet",
value: receiver,
key: strLit(method, locOf(access.name)),
type: DYN,
loc,
},
receiver,
calleeName: access.getText(),
args: [
lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN),
lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN),
],
type: DYN,
loc,
},
type: DYN,
loc,
};
}
}
// process.stdin.on/once("data" | "end" | "error", cb): the piped-stdin
// event slice. A 'data' listener keeps the event loop alive until EOF
// (Node's flowing stdin); 'end'/'error' listeners alone do not. `once`
Expand Down Expand Up @@ -678,11 +763,18 @@ export function lowerProcessMethodCall(
const param = cb.type.params[0];
const onceArg: IrExpr = { kind: "boolLit", value: once, type: BOOL, loc };
if (event === "data") {
if (param !== undefined && !(param.kind === "bytes" && param.elem === "u8")) {
// A string parameter receives the chunks process.stdin.setEncoding
// decodes (pi-tui's terminal input); the runtime refuses a delivery
// while no encoding is set, where Node would pass a Buffer.
if (
param !== undefined &&
!(param.kind === "bytes" && param.elem === "u8") &&
param.kind !== "string"
) {
lowerer.unsupported(
"SC1090",
call.arguments.nodes[1]!,
`data listeners whose parameter is not 'Uint8Array' (got '${lowerer.fmt(param)}')`,
`data listeners whose parameter is not 'Uint8Array' or 'string' (got '${lowerer.fmt(param)}')`,
);
}
return { kind: "libCall", fn: "stdin.onData", args: [cb, onceArg], type: VOID, loc };
Expand Down Expand Up @@ -1537,3 +1629,36 @@ function processCredentialCall(member: string, loc: SrcLoc): IrExpr {
loc,
};
}

/** `process.getBuiltinModule("node:os")`: a string-literal id naming a
* supported builtin whose call the checker types as that module's
* namespace (@types/node's BuiltInModule map) answers the namespace value
* `import * as ns` binds, so its members key the builtin tables at their
* use sites. The receiver may be cast (`(process as P)`) and the call
* optional: Node 24's process always has the member, and the cast changes
* no runtime value. Other ids keep the dynamic loader (undefined for
* unknown ids, Node's argument errors). */
export function lowerProcessBuiltinModuleNamespace(
lowerer: Lowerer,
call: ts.CallExpression,
): IrExpr | null {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "getBuiltinModule")
return null;
const id = call.arguments.nodes[0];
if (call.arguments.nodes.length !== 1 || !id || !ts.isStringLiteralLike(id)) return null;
let receiver: ts.Expression = callee.expression;
while (
ts.isParenthesizedExpression(receiver) ||
ts.isAsExpression(receiver) ||
ts.isSatisfiesExpression(receiver) ||
ts.isNonNullExpression(receiver)
)
receiver = receiver.expression;
if (!lowerer.isStdlibGlobal(receiver, "process")) return null;
const module = canonicalBuiltinModule(id.text);
if (module === null) return null;
const type = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(lowerer.typeOf(call)));
if (type?.kind !== "moduleNs" || type.moduleId !== `builtin:${module}`) return null;
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc: locOf(call) };
}
7 changes: 7 additions & 0 deletions packages/compiler/src/frontend/lowering/function-returns.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mapAsyncGeneratorAnnotation } from "../type-mapper.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
Expand Down Expand Up @@ -146,6 +147,12 @@ export function declaredReturnType(
// (`.action(() => { throw ... })`). `never` VALUES stay unmapped.
if (retTsType.flags & ts.TypeFlags.Never) return VOID;
let mappedReturn = lowerer.mapTypeOf(retTsType);
if (
mappedReturn === null &&
decl.asteriskToken &&
decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
)
mappedReturn = mapAsyncGeneratorAnnotation(retTsType, lowerer.typeCtx);
if (
isJsSourceFile(decl.getSourceFile()) &&
decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) &&
Expand Down
39 changes: 38 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-async-iteration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
VOID,
} from "../../ir/ir.js";
import { locOf } from "../program.js";
import { extractIteratorValue } from "./lower-generators.js";
import { extractIteratorValue, type GenType } from "./lower-generators.js";
import { streamSidesOf } from "./lower-stream.js";
import { staticReadableStreamElementType } from "./lower-stdlib.js";
import { genResultRecord } from "../type-mapper.js";
Expand Down Expand Up @@ -282,6 +282,43 @@ function classAsyncIteratorPlan(
};
}

/** `for await (const x of obj)` where obj's class declares (or inherits)
* `async *[Symbol.asyncIterator]()`: the loop opens the iterator with that
* method call, evaluated once, and then drives the async generator it
* returns exactly like `for await (const x of obj[Symbol.asyncIterator]())`
* (pi's EventStream). Null for other sources. */
export function classAsyncGeneratorIterator(
lowerer: Lowerer,
expression: ts.Expression,
): (IrExpr & { type: GenType }) | null {
const sourceType = lowerer.mapTypeOf(lowerer.typeOf(expression));
if (sourceType?.kind !== "object") return null;
const info = lowerer.classes.get(sourceType.className);
const open = info ? lowerer.findMethodOn(info, "sym:asyncIterator") : null;
if (
!open ||
open.sig.abstract === true ||
open.sig.params.length !== 0 ||
open.sig.ret.kind !== "generator" ||
!open.sig.ret.async
)
return null;
const retT = open.sig.ret;
const source = lowerer.lowerExpr(expression);
if (source.type.kind !== "object") return null;
return {
...lowerer.accessorCall(
sourceType.className,
"sym:asyncIterator",
source,
[],
retT,
locOf(expression),
),
type: retT,
};
}

export function checkForAwaitBinding(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
Expand Down
Loading
Loading