Fix Array.prototype calls on native arrays, typeof on ambient names, and net connections to Unix sockets (#2326)

This commit is contained in:
Chris Tate
2026-10-26 10:56:58 -05:00
committed by GitHub
26 changed files with 516 additions and 115 deletions
+6
View File
@@ -356,6 +356,12 @@ declare module "net" {
},
connectListener?: () => void,
): Socket;
/* A Unix domain socket client (Node's IpcNetConnectOpts and path
* overloads). */
export function connect(path: string, connectListener?: () => void): Socket;
export function connect(options: { path: string }, connectListener?: () => void): Socket;
export function createConnection(path: string, connectListener?: () => void): Socket;
export function createConnection(options: { path: string }, connectListener?: () => void): Socket;
/* The happy-eyeballs attempt budget (Node's process-wide knob; the
* autoSelectFamily dial consults it): one runtime int, default 250ms
* like Node's. */
@@ -401,6 +401,22 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
host.emitPendingCheck();
return out;
}
if (e.fn === "net.connectPath" || e.fn === "net.connectPathCb") {
const args = e.args.map((a) => host.emitExpr(a));
let cb = "null";
if (e.fn === "net.connectPathCb") {
host.toHeap(args[2]!);
cb = args[2]!.name;
}
host.declare(`declare ptr @scr_net_connect_path_checked(ptr, i1 zeroext, ptr)`);
const t = B.tmp();
B.line(
`${t} = call ptr @scr_net_connect_path_checked(ptr ${args[0]!.name}, i1 ${args[1]!.name}, ptr ${cb})`,
);
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
return out;
}
if (e.fn === "net.sockOnData" || e.fn === "http.reqOnData") {
const cbT = e.args[1]!.type;
if (cbT.kind !== "func")
@@ -1980,6 +1980,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"net.listenOptsReusePortCb",
"net.connect",
"net.connectCb",
"net.connectPath",
"net.connectPathCb",
"net.connectLookup",
"net.connectLookupDyn",
"net.connectAttempt",
@@ -45,6 +45,7 @@ const COMPLETION_CALLBACKS: ReadonlyMap<IrLibFn, CallbackPosition> = new Map<
["http.requestDynCb", "last"],
["http.requestUrlCb", "last"],
["net.connectCb", 2],
["net.connectPathCb", 2],
["net.listenCb", 2],
["net.listenOptsCb", 4],
["net.listenOptsReusePortCb", 5],
@@ -56,6 +56,7 @@ import { varRef } from "../../../ir/build.js";
import {
ambientUndefReadType,
ambientUndefinedFnSymbolOf,
ambientUndefinedVarSymbolOf,
contextualUndefReadType,
fenceEarlyAliasUse,
isTemporalGlobalRoot,
@@ -829,14 +830,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// Stdlib/@types/node ambients never reach here (their chokepoint
// is above); only user-file declares do.
{
const sym = lowerer.resolveValueSymbol(expr);
const decl = sym ? sym.declarations[0] : undefined;
if (
decl !== undefined &&
ts.isVariableDeclaration(decl) &&
decl.initializer === undefined &&
ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient
) {
if (ambientUndefinedVarSymbolOf(lowerer, expr)) {
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (declared && declared.kind !== "void") {
return {
@@ -34,7 +34,11 @@ import { varRef } from "../../../ir/build.js";
import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js";
import { InternalCompilerError } from "../../../errors.js";
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
import { isTemporalGlobalRoot } from "../lower-namespaces.js";
import {
ambientUndefinedFnSymbolOf,
ambientUndefinedVarSymbolOf,
isTemporalGlobalRoot,
} from "../lower-namespaces.js";
import { isF64HandleTyped } from "../f64-handles.js";
export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr {
@@ -220,6 +224,16 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
if (isTemporalGlobalRoot(lowerer, tested, sym)) {
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
// A name the program declares ambiently but nothing defines
// (`declare const Bun`, `declare function f(): void`): Node erases the
// declaration, so typeof answers "undefined" without reading it, where
// a read throws ReferenceError.
if (
ambientUndefinedVarSymbolOf(lowerer, tested) ||
ambientUndefinedFnSymbolOf(lowerer, tested)
) {
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
// `const { inspect } = require("util")` / `import { inspect } from
// "node:util"`: a binding of a builtin module's exported function,
// class or object. JavaScript sources represent such a value without
@@ -676,6 +676,30 @@ export function ambientUndefReadType(lowerer: Lowerer, node: ts.Node): IrType |
return null;
}
/** The symbol of a variable a program file declares ambiently without
* an initializer (`declare const Bun: ...`), which nothing defines: Node
* erases the declaration, so a read throws the catchable ReferenceError
* "<name> is not defined" and typeof answers "undefined". Null for other
* identifiers, including the standard library's declarations, which
* describe real runtime values. */
export function ambientUndefinedVarSymbolOf(
lowerer: Lowerer,
ident: ts.Identifier,
): ts.Symbol | null {
const sym = lowerer.resolveValueSymbol(ident);
const decl = sym ? sym.declarations[0] : undefined;
if (
sym === null ||
decl === undefined ||
lowerer.isStdlibSymbol(sym) ||
!ts.isVariableDeclaration(decl) ||
decl.initializer !== undefined ||
!(ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient)
)
return null;
return sym;
}
/** The symbol behind `ident` when it names an ambient `declare function`
* NOTHING defines: every function declaration on the symbol is body-less
* and ambient in a USER program file (merge partners — the fundule's
@@ -804,6 +804,19 @@ export function lowerNetModuleCall(
`the supported form is ${bi.member}(port, host?, connectListener?)`,
);
}
// connect(path[, connectListener]): a string first argument names a
// Unix socket, or a localhost port when it reads as a non-negative
// number (Node's isPipeName, decided at run time).
if (lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind === "string") {
return lowerNetConnectPath(
lowerer,
expr,
bi.member,
lowerer.lowerExpr(args[0]!),
true,
args[1],
);
}
const port = lowerer.lowerExprExpecting(args[0]!, F64);
// The optional middle host: a 2-arg call's second argument is the
// host when string-typed, the connect listener when func-typed.
@@ -841,6 +854,47 @@ export function lowerNetModuleCall(
);
}
/** net.connect / net.createConnection of a Unix socket path (a string
* argument or the `path` option) with an optional connect listener. Node
* ignores any other argument between them for a path, so only a listener
* may follow. */
function lowerNetConnectPath(
lowerer: Lowerer,
expr: ts.CallExpression,
member: string,
path: IrExpr,
pipeNameRule: boolean,
listener: ts.Expression | undefined,
): IrExpr {
const loc = locOf(expr);
const rule: IrExpr = { kind: "boolLit", value: pipeNameRule, type: BOOL, loc };
if (expr.arguments.nodes.length > 2) {
lowerer.noLowering(
`${member} of a path with ${expr.arguments.nodes.length} arguments`,
expr,
`the supported form is ${member}(path[, connectListener])`,
);
}
if (listener === undefined)
return { kind: "libCall", fn: "net.connectPath", args: [path, rule], type: NETSOCKET_T, loc };
const { cb } = lowerCallbackArg(
lowerer,
listener,
"connect listeners",
0,
() => false,
"use ()",
[],
);
return {
kind: "libCall",
fn: "net.connectPathCb",
args: [path, rule, cb],
type: NETSOCKET_T,
loc,
};
}
/** True when `t` is the lookup-function shape the connect lowering
* accepts: (hostname: string, options: unknown, callback) => void where
* callback is (err: <union with a null arm>, addresses: { address:
@@ -929,6 +983,29 @@ function lowerNetConnectOptions(
}
}
}
// connect({ path }[, connectListener]): the Unix-socket form.
{
const keys = optsNode.properties.nodes.map((p) =>
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
? p.name.text
: null,
);
if (keys.length === 1 && keys[0] === "path") {
const prop = optsNode.properties.nodes[0]!;
const path = ts.isPropertyAssignment(prop)
? lowerer.lowerExpr(prop.initializer)
: lowerer.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment);
if (path.type.kind !== "string") {
lowerer.noLowering(
`a ${member} 'path' option of '${lowerer.fmt(path.type)}' values`,
prop,
"the path is a string here",
);
}
return lowerNetConnectPath(lowerer, expr, member, path, false, args[1]);
}
}
let port: IrExpr | null = null;
let host: IrExpr | null = null;
let lookup: IrExpr | null = null;
@@ -1,12 +1,14 @@
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { ambientUndefinedFnSymbolOf, ambientUndefinedVarSymbolOf } from "./lower-namespaces.js";
/** The answer `typeof <operand>` gives without evaluating anything, for
* the global-detection idioms (`typeof window < "u"`, `typeof global !==
* "undefined"`): a name nothing declares answers "undefined" (Node's
* answer for an undeclared global such as window or self, and typeof never
* throws for one), and the Node global object and process answer
* "object". Null for every other operand. */
* "undefined"`): a name nothing declares, or one the program declares
* ambiently and nothing defines, answers "undefined" (Node's answer for an
* undeclared global such as window or self, and typeof never throws for
* one), and the Node global object and process answer "object". Null for
* every other operand. */
export function staticTypeofAnswer(lowerer: Lowerer, operand: ts.Expression): string | null {
let e = operand;
while (ts.isParenthesizedExpression(e)) e = e.expression;
@@ -14,6 +16,10 @@ export function staticTypeofAnswer(lowerer: Lowerer, operand: ts.Expression): st
if (lowerer.checker.getSymbolAtLocation(e) === undefined) return "undefined";
for (const name of ["global", "globalThis", "process"])
if (lowerer.isStdlibGlobal(e, name)) return "object";
// A name the program declares ambiently but nothing defines (`declare
// const Bun`): undeclared at run time.
if (ambientUndefinedVarSymbolOf(lowerer, e) || ambientUndefinedFnSymbolOf(lowerer, e))
return "undefined";
return null;
}
@@ -944,6 +944,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dns.setDefaultResultOrder",
"net.connect",
"net.connectCb",
"net.connectPath",
"net.connectPathCb",
// resetAndDestroy on a TLS socket throws ERR_INVALID_HANDLE_TYPE.
"net.sockResetAndDestroy",
// SocketAddress's option checks and parse()'s input check.
@@ -1554,6 +1554,15 @@ function buildLibFnSignatures() {
argTypes: [F64, STRING, { kind: "func", params: [], ret: VOID }],
result: NETSOCKET_T,
},
/** net.connect/createConnection(path[, connectListener]) and the
* `path` option: a client of the Unix socket at `path`. With the flag
* (a positional string), Node's isPipeName applies first: a string that
* reads as a non-negative number is a localhost port. */
"net.connectPath": { argTypes: [STRING, BOOL], result: NETSOCKET_T },
"net.connectPathCb": {
argTypes: [STRING, BOOL, { kind: "func", params: [], ret: VOID }],
result: NETSOCKET_T,
},
// The lookup's exact func shape is program data (its answer callback's
// union/record types) — checked specially in the libCall case.
/** connect({ port, host, autoSelectFamily: true, lookup }) — the
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/array-prototype-call-native-arrays.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/net-unix-socket-connect-default-import.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/net-unix-socket-connect.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/typeof-ambient-undefined-names.ts"
],
"diags": []
}
+125 -102
View File
@@ -916,6 +916,117 @@ void scr_dyn_invoke_discard_id(ScrDyn *recv, ScrDyn *callee, const char *method,
scr_dyn_release(scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what));
}
/* The callback methods of a dense dyn array that take a thisArg:
* forEach, map, filter, some, every, find, findIndex, findLast,
* findLastIndex and flatMap. The caller binds `this` for the callbacks.
* `len` is the length before the first callback. */
static ScrDyn *dyn_arr_callback_method(ScrDyn *recv, int mid, ScrDyn *const *args, size_t argc,
size_t len, ScrDyn *callback_recv) {
if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) {
if (!dyn_cb_check(args, argc)) return NULL;
ScrDyn *visible_recv = callback_recv ? callback_recv : recv;
for (size_t i = len; i > 0; i--) {
size_t index = i - 1;
ScrDyn *item = index < recv->v.arr.len
? scr_dyn_retain(recv->v.arr.items[index])
: scr_dyn_retain(scr_dyn_undefined());
ScrDyn *result = dyn_call_cb(args[0], item, index, visible_recv);
if (!result) { scr_dyn_release(item); return NULL; }
bool matched = scr_dyn_truthy(result);
scr_dyn_release(result);
if (matched) {
if ((mid == DYN_M_findLast)) return item;
scr_dyn_release(item);
return scr_dyn_new_num((double)index);
}
scr_dyn_release(item);
}
return (mid == DYN_M_findLast)
? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1);
}
if ((mid == DYN_M_forEach) || (mid == DYN_M_map) ||
(mid == DYN_M_filter) || (mid == DYN_M_some) ||
(mid == DYN_M_every) || (mid == DYN_M_find) ||
(mid == DYN_M_findIndex)) {
if (!dyn_cb_check(args, argc)) return NULL;
ScrDyn *cb = args[0];
ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL;
/* Array iteration methods snapshot length before the first callback.
* A shrinking callback can remove a later dense entry; an expanding
* callback must not make the new tail observable to this pass. Live
* Web-boundary capsules are the externally visible receiver, so pass
* that capsule as callback arg 3: mutations through the callback's
* array argument then commit directly to the original static array. */
size_t n = len;
ScrDyn *visible_recv = callback_recv ? callback_recv : recv;
for (size_t step = 0; step < n; step++) {
size_t i = step;
if (i >= recv->v.arr.len) continue;
ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]);
ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv);
if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; }
if ((mid == DYN_M_map)) {
scr_dyn_arr_push(out, r); /* ownership moves in */
r = NULL;
} else {
bool truthy = scr_dyn_truthy(r);
scr_dyn_release(r);
if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item));
if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); }
if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); }
if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */
if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); }
}
scr_dyn_release(item);
}
if (out) return out;
if ((mid == DYN_M_some)) return scr_dyn_new_bool(false);
if ((mid == DYN_M_every)) return scr_dyn_new_bool(true);
if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined());
if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1);
return scr_dyn_retain(scr_dyn_undefined()); /* forEach */
}
if ((mid == DYN_M_flatMap)) {
/* JS Array.prototype.flatMap over a dyn array: map + a depth-1
* flatten. Native dyn-array results flatten element-by-element; a
* WRAPPED engine array flattens through the routed keyed reads
* (elements wrap back scalar-normalized); everything else pushes
* as a single element (JS keeps non-array callback results whole).
* JS's spec snapshots the length up front (elements appended by
* the callback are not visited). */
if (!dyn_cb_check(args, argc)) return NULL;
ScrDyn *cb = args[0];
ScrDyn *out = scr_dyn_new_arr();
size_t n = recv->v.arr.len;
for (size_t i = 0; i < n && i < recv->v.arr.len; i++) {
if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } continue; }
ScrDyn *item = dyn_borrow_item(recv, i);
if (!item) { scr_dyn_release(out); return NULL; }
ScrDyn *r = dyn_call_cb(
cb, item, i, callback_recv ? callback_recv : recv);
scr_dyn_release(item);
if (!r) { scr_dyn_release(out); return NULL; }
if (r->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(r)) {
ScrDyn *view = scr_dyn_typed_ref_materialize(r);
scr_dyn_release(r);
r = view;
if (!r) { scr_dyn_release(out); return NULL; }
}
if (r->kind == SCR_DYN_ARR) {
for (size_t j = 0; j < r->v.arr.len; j++) {
if (dyn_borrow_present(r, j)) scr_dyn_arr_push(out, dyn_borrow_item(r, j));
if (scr_exc_pending()) { scr_dyn_release(r); scr_dyn_release(out); return NULL; }
}
scr_dyn_release(r);
} else {
scr_dyn_arr_push(out, r); /* ownership moves in */
}
}
return out;
}
return NULL;
}
static ScrDyn *scr_dyn_invoke_impl(
ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc,
const char *what, ScrDyn *callback_recv) {
@@ -1367,27 +1478,16 @@ static ScrDyn *scr_dyn_invoke_impl(
}
return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1);
}
if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) {
if (!dyn_cb_check(args, argc)) return NULL;
ScrDyn *visible_recv = callback_recv ? callback_recv : recv;
for (size_t i = len; i > 0; i--) {
size_t index = i - 1;
ScrDyn *item = index < recv->v.arr.len
? scr_dyn_retain(recv->v.arr.items[index])
: scr_dyn_retain(scr_dyn_undefined());
ScrDyn *result = dyn_call_cb(args[0], item, index, visible_recv);
if (!result) { scr_dyn_release(item); return NULL; }
bool matched = scr_dyn_truthy(result);
scr_dyn_release(result);
if (matched) {
if ((mid == DYN_M_findLast)) return item;
scr_dyn_release(item);
return scr_dyn_new_num((double)index);
}
scr_dyn_release(item);
}
return (mid == DYN_M_findLast)
? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1);
if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) ||
(mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) ||
(mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) ||
(mid == DYN_M_flatMap)) {
/* Every callback runs with `this` bound to the thisArg, or undefined
* when there is none. */
scr_dyn_this_push_dyn(argc > 1 ? args[1] : scr_dyn_undefined());
ScrDyn *result = dyn_arr_callback_method(recv, mid, args, argc, len, callback_recv);
scr_dyn_this_pop();
return result;
}
if ((mid == DYN_M_join)) {
ScrJsonBuf b;
@@ -1550,48 +1650,6 @@ static ScrDyn *scr_dyn_invoke_impl(
}
return scr_dyn_retain(recv);
}
if ((mid == DYN_M_forEach) || (mid == DYN_M_map) ||
(mid == DYN_M_filter) || (mid == DYN_M_some) ||
(mid == DYN_M_every) || (mid == DYN_M_find) ||
(mid == DYN_M_findIndex)) {
if (!dyn_cb_check(args, argc)) return NULL;
ScrDyn *cb = args[0];
ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL;
/* Array iteration methods snapshot length before the first callback.
* A shrinking callback can remove a later dense entry; an expanding
* callback must not make the new tail observable to this pass. Live
* Web-boundary capsules are the externally visible receiver, so pass
* that capsule as callback arg 3: mutations through the callback's
* array argument then commit directly to the original static array. */
size_t n = len;
ScrDyn *visible_recv = callback_recv ? callback_recv : recv;
for (size_t step = 0; step < n; step++) {
size_t i = step;
if (i >= recv->v.arr.len) continue;
ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]);
ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv);
if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; }
if ((mid == DYN_M_map)) {
scr_dyn_arr_push(out, r); /* ownership moves in */
r = NULL;
} else {
bool truthy = scr_dyn_truthy(r);
scr_dyn_release(r);
if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item));
if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); }
if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); }
if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */
if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); }
}
scr_dyn_release(item);
}
if (out) return out;
if ((mid == DYN_M_some)) return scr_dyn_new_bool(false);
if ((mid == DYN_M_every)) return scr_dyn_new_bool(true);
if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined());
if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1);
return scr_dyn_retain(scr_dyn_undefined()); /* forEach */
}
if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) {
if (!dyn_cb_check(args, argc)) return NULL;
bool backward = (mid == DYN_M_reduceRight);
@@ -1616,44 +1674,6 @@ static ScrDyn *scr_dyn_invoke_impl(
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
if ((mid == DYN_M_flatMap)) {
/* JS Array.prototype.flatMap over a dyn array: map + a depth-1
* flatten. Native dyn-array results flatten element-by-element; a
* WRAPPED engine array flattens through the routed keyed reads
* (elements wrap back scalar-normalized); everything else pushes
* as a single element (JS keeps non-array callback results whole).
* JS's spec snapshots the length up front (elements appended by
* the callback are not visited). */
if (!dyn_cb_check(args, argc)) return NULL;
ScrDyn *cb = args[0];
ScrDyn *out = scr_dyn_new_arr();
size_t n = recv->v.arr.len;
for (size_t i = 0; i < n && i < recv->v.arr.len; i++) {
if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } continue; }
ScrDyn *item = dyn_borrow_item(recv, i);
if (!item) { scr_dyn_release(out); return NULL; }
ScrDyn *r = dyn_call_cb(
cb, item, i, callback_recv ? callback_recv : recv);
scr_dyn_release(item);
if (!r) { scr_dyn_release(out); return NULL; }
if (r->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(r)) {
ScrDyn *view = scr_dyn_typed_ref_materialize(r);
scr_dyn_release(r);
r = view;
if (!r) { scr_dyn_release(out); return NULL; }
}
if (r->kind == SCR_DYN_ARR) {
for (size_t j = 0; j < r->v.arr.len; j++) {
if (dyn_borrow_present(r, j)) scr_dyn_arr_push(out, dyn_borrow_item(r, j));
if (scr_exc_pending()) { scr_dyn_release(r); scr_dyn_release(out); return NULL; }
}
scr_dyn_release(r);
} else {
scr_dyn_arr_push(out, r); /* ownership moves in */
}
}
return out;
}
if ((mid == DYN_M_sort)) {
ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined();
if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) {
@@ -1801,7 +1821,10 @@ static ScrDyn *dyn_array_method_value_call(ScrClosure *closure, ScrDyn *const *a
ScrStr *method = scr_box_get_ref(closure->caps[0]);
ScrDyn *receiver = scr_dyn_this_get();
ScrDyn *result;
if (receiver->kind == SCR_DYN_ARR) {
/* An array, or a live view of a native array, runs the method its own
* dispatch has; other receivers take the generic array-like algorithms. */
if (receiver->kind == SCR_DYN_ARR ||
(receiver->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(receiver))) {
result = scr_dyn_invoke(receiver, method->data, args, argc, method->data);
} else {
ScrDyn *pack = scr_dyn_new_arr();
+18
View File
@@ -2741,6 +2741,24 @@ ScrNetSocket *scr_net_connect_checked(double port, ScrStr *host /*borrowed, null
return scr_net_connect(port, host, cb);
}
/* net.connect/createConnection(path[, connectListener]) and the `path`
* option from compiled code: a client of the Unix socket at `path`
* (borrowed). For a positional string (`pipe_name_rule`), Node's isPipeName
* applies first: a string that reads as a non-negative number is a port on
* localhost. `cb` (moves, nullable) is the connect listener. */
ScrNetSocket *scr_net_connect_path_checked(ScrStr *path, bool pipe_name_rule, ScrClosure *cb) {
double port = pipe_name_rule ? scr_string_to_number(path) : -1;
if (port >= 0) {
ScrStr *host = scr_str_new("localhost", 9);
ScrNetSocket *s = scr_net_connect_checked(port, host, cb);
scr_str_release(host);
return s;
}
ScrNetSocket *s = scr_net_connect_path(path, false);
if (cb) scr_net_ls_add(&s->conn_ls, cb, NULL, true);
return s;
}
static bool scr_net_dyn_truthy(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_UNDEF:
+4
View File
@@ -265,6 +265,10 @@ ScrNetSocket *scr_net_connect_lookup_dyn(double port, ScrStr *host /*borrowed*/,
/* A client socket for the Unix socket at `path`, dialed now or (deferred)
* by scr_net_sock_dial_start. A failure is the deferred 'error'. */
ScrNetSocket *scr_net_connect_path(ScrStr *path /*borrowed*/, bool deferred); /* +1 */
/* net.connect(path[, cb]) and connect({ path }): with `pipe_name_rule`, a
* numeric string is a localhost port, as in Node. */
ScrNetSocket *scr_net_connect_path_checked(ScrStr *path /*borrowed*/, bool pipe_name_rule,
ScrClosure *cb /*moves, nullable*/); /* +1 */
/* server.listen(path[, cb]): a Unix-socket server, its file removed at close. */
void scr_net_listen_path(ScrNetServer *s, ScrStr *path /*borrowed*/, ScrClosure *cb /*moves, nullable*/);
void scr_net_sock_dial_start(ScrNetSocket *s);
@@ -0,0 +1,65 @@
// Array.prototype methods called with .call on native arrays run the arrays' own
// methods, which mutate the array in place: copyWithin, fill, reverse,
// sort, push, pop, splice, and the read-only join, indexOf and includes.
const optional: (number | undefined)[] = [1, 2, 3, 4];
Array.prototype.copyWithin.call(optional, 0, 2, 4);
console.log(optional.join(","));
const nums: number[] = [5, 1, 4, 2, 3];
Array.prototype.fill.call(nums, 0, 0, 1);
console.log(nums.join(","));
console.log(Array.prototype.reverse.call(nums) === nums, nums.join(","));
Array.prototype.sort.call(nums, (a: number, b: number) => a - b);
console.log(nums.join(","));
console.log(Array.prototype.push.call(nums, 9, 10), nums.join(","));
console.log(Array.prototype.pop.call(nums), nums.length);
console.log(Array.prototype.splice.call(nums, 1, 2).join(","), nums.join(","));
console.log(Array.prototype.join.call(nums, "-"), Array.prototype.indexOf.call(nums, 4));
const words: string[] = ["c", "a", "b"];
Array.prototype.sort.call(words);
console.log(words.join(","), Array.prototype.includes.call(words, "a"));
const records: { id: number }[] = [{ id: 1 }, { id: 2 }, { id: 3 }];
Array.prototype.copyWithin.call(records, 1, 0, 1);
console.log(records.map((r) => r.id).join(","), records[0] === records[1]);
// The callback methods bind `this` to their thisArg, for native arrays
// through .call and for dynamic arrays called directly.
type Scale = { k: number; seen: number[] };
const scale: Scale = { k: 10, seen: [] };
const values: number[] = [1, 2, 3, 4];
Array.prototype.forEach.call(values, function (this: Scale, v: number) {
this.seen.push(v);
}, scale);
console.log(scale.seen.join(","));
console.log(Array.prototype.map.call(values, function (this: Scale, v: number) {
return v * this.k;
}, scale).join(","));
console.log(Array.prototype.filter.call(values, function (this: Scale, v: number) {
return v * this.k > 15;
}, scale).join(","));
console.log(
Array.prototype.some.call(values, function (this: Scale, v: number) { return v === this.k / 5; }, scale),
Array.prototype.every.call(values, function (this: Scale, v: number) { return v < this.k; }, scale),
Array.prototype.find.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale),
Array.prototype.findIndex.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale),
Array.prototype.findLast.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale),
Array.prototype.findLastIndex.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale),
);
console.log(Array.prototype.flatMap.call(values, function (this: unknown, v: number) {
return [v, v * (this as Scale).k];
}, scale).join(","));
const parsed: any = JSON.parse("[1, 2, 3]");
const sum = { total: 0 };
parsed.forEach(function (this: { total: number }, v: number) {
this.total += v;
}, sum);
console.log(sum.total, parsed.map(function (this: Scale, v: number) { return v * this.k; }, scale).join(","));
class Counter {
total = 0;
addAll(items: number[]): number {
Array.prototype.forEach.call(items, (v: number) => {
this.total += v;
});
return this.total;
}
}
console.log(new Counter().addAll(values));
@@ -0,0 +1,22 @@
// net.createConnection(path) through the default import of node:net, as a
// JavaScript module reads it: a client of a Unix domain socket server.
import net from "node:net";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-unix-default-"));
const socketPath = path.join(dir, "server.sock");
const server = net.createServer((socket) => socket.end("hello"));
server.listen(socketPath, () => {
const sock = net.createConnection(socketPath);
sock.once("connect", () => console.log("connected"));
sock.on("data", (chunk) => console.log("data", String(chunk)));
sock.once("error", (e) => console.log("error", String(e)));
sock.on("close", () => {
server.close(() => {
fs.rmSync(dir, { recursive: true, force: true });
console.log("closed");
});
});
});
+69
View File
@@ -0,0 +1,69 @@
// net.createConnection and net.connect to a Unix domain socket: a path
// argument, the `path` option, a connect listener, data both ways, the
// error for a path nothing listens on, and a numeric string, which Node
// treats as a localhost port.
import * as net from "node:net";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-unix-connect-"));
const socketPath = path.join(dir, "server.sock");
const missingPath = path.join(dir, "missing.sock");
const server = net.createServer((socket) => {
socket.on("data", (chunk) => {
socket.write(`echo:${chunk.toString()}`);
});
});
function exchange(label: string, socket: net.Socket, message: string): Promise<void> {
return new Promise((resolve) => {
socket.on("data", (chunk) => {
console.log(label, chunk.toString());
socket.end();
});
socket.on("close", () => resolve());
socket.write(message);
});
}
server.listen(socketPath, async () => {
console.log("listening");
const first = net.createConnection(socketPath);
first.once("connect", () => console.log("first connected"));
await exchange("first", first, "one");
let listenerCalls = 0;
const second = net.connect(socketPath, () => {
listenerCalls++;
});
await exchange("second", second, "two");
console.log("second listener calls", listenerCalls);
const third = net.createConnection({ path: socketPath });
await exchange("third", third, "three");
await new Promise<void>((resolve) => {
const missing = net.createConnection(missingPath);
missing.once("connect", () => console.log("missing connected"));
missing.once("error", (error: NodeJS.ErrnoException) => {
console.log("missing", error.code, error.message === `connect ENOENT ${missingPath}`);
resolve();
});
});
server.close(() => {
const tcp = net.createServer((socket) => socket.end("tcp"));
tcp.listen(0, "localhost", () => {
const { port } = tcp.address() as net.AddressInfo;
const client = net.createConnection(String(port));
client.on("data", (chunk) => console.log("numeric string", chunk.toString()));
client.on("close", () => {
tcp.close();
fs.rmSync(dir, { recursive: true, force: true });
console.log("done");
});
});
});
});
@@ -0,0 +1,21 @@
// typeof on names the program declares (`declare const`, `declare function`)
// but nothing defines at run time answers "undefined"; reading them throws
// ReferenceError.
declare const Bun: { randomUUIDv7: () => string } | undefined;
declare const Deno: { version: { deno: string } };
declare function webkitRequestAnimationFrame(callback: () => void): number;
console.log("start");
console.log(typeof Bun, typeof Deno, typeof webkitRequestAnimationFrame);
console.log(typeof Bun !== "undefined" ? Bun?.randomUUIDv7() : "no Bun");
const runtime = typeof Deno === "object" ? "deno" : typeof Bun === "object" ? "bun" : "node";
console.log(runtime);
try {
console.log(Bun);
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
try {
console.log(typeof Deno.version);
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
@@ -0,0 +1 @@
{ "plain": 5514, "san": 7205 }
@@ -0,0 +1 @@
{ "plain": 3242, "san": 5402 }
@@ -0,0 +1 @@
{ "plain": 3555, "san": 6682 }
@@ -0,0 +1 @@
{ "plain": 4212, "san": 4590 }