Merge remote-tracking branch 'origin/ni/i7-routing' into HEAD

# Conflicts:
#	packages/libnode-common/bridge/scr_node_bridge.cc
This commit is contained in:
Malte Ubl
2026-10-22 20:26:01 -07:00
16 changed files with 298 additions and 0 deletions
@@ -977,6 +977,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"nodeIsland.classMethodsOf": "scr_node_island_class_methods_of",
"nodeIsland.instanceofGlobal": "scr_node_island_instanceof_global",
"nodeIsland.isHandle": "scr_ni_is_handle",
"nodeIsland.copyArrays": "scr_node_island_copy_arrays",
"nodeIsland.awaitCopy": "scr_node_island_await_copy",
"dyn.keySet": "scr_dyn_key_set",
"dyn.keySetComputed": "scr_dyn_key_set_computed",
"dyn.keyDelete": "scr_dyn_key_delete",
@@ -1832,6 +1834,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"nodeIsland.require",
"nodeIsland.defer",
"nodeIsland.import",
"nodeIsland.awaitCopy",
"nodeIsland.get",
"nodeIsland.getKey",
"nodeIsland.set",
@@ -531,6 +531,20 @@ function fromIsland(lowerer: Lowerer, raw: IrExpr, node: ts.Expression): IrExpr
};
return mapped.inner.kind === "dyn" ? promise : lowerer.coerceToExpected(promise, mapped);
}
// Arrays of copied values (or arrays of those) cross as copies of the
// island array as an awaited promise's value, and directly where they do
// not stay handles (below); the checked cast then meets the copy. A
// native array that crossed in comes back as itself.
if (mapped.kind === "promise" && copiedArrayType(lowerer, mapped.inner)) {
const promise: IrExpr = {
kind: "libCall",
fn: "nodeIsland.awaitCopy",
args: [raw],
type: { kind: "promise", inner: DYN },
loc: raw.loc,
};
return lowerer.coerceToExpected(promise, mapped);
}
// NI-5: a native value the program passed in comes back as itself (an
// export); the checked cast unboxes it, and anything else fails the cast
// (types don't lie).
@@ -540,6 +554,17 @@ function fromIsland(lowerer: Lowerer, raw: IrExpr, node: ts.Expression): IrExpr
// operations follow the value).
if (!copiedType(lowerer, mapped) && containerType(lowerer, tsType) && handleUseOk(lowerer, node))
return raw;
// Otherwise an array of copied values is a copy (above).
if (copiedArrayType(lowerer, mapped)) {
const copy: IrExpr = {
kind: "libCall",
fn: "nodeIsland.copyArrays",
args: [raw],
type: DYN,
loc: raw.loc,
};
return lowerer.coerceToExpected(copy, mapped);
}
if (!copiedType(lowerer, mapped) && exportedByReference(lowerer, tsType))
return lowerer.coerceToExpected(raw, mapped);
if (!copiedType(lowerer, mapped))
@@ -551,6 +576,12 @@ function fromIsland(lowerer: Lowerer, raw: IrExpr, node: ts.Expression): IrExpr
return lowerer.coerceToExpected(raw, mapped);
}
/** An array whose elements are copied values or such arrays. */
function copiedArrayType(lowerer: Lowerer, t: IrType): boolean {
if (t.kind !== "array") return false;
return copiedType(lowerer, t.elem) || copiedArrayType(lowerer, t.elem);
}
/** Whether a value of IR type `t` is copied at the boundary: primitives,
* strings, null/undefined, and unions of those. */
function copiedType(lowerer: Lowerer, t: IrType): boolean {
@@ -28,6 +28,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"nodeIsland.defineField",
"nodeIsland.promisify",
"nodeIsland.classMethodsOf",
"nodeIsland.copyArrays",
// Node's argument, cipher-name, IV, tag-length and key-length errors.
"crypto.cipherNew",
// Node's curve, key, point, encoding and format errors.
@@ -229,6 +229,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"nodeIsland.instanceofGlobal": { argTypes: [DYN, STRING], result: BOOL },
/** Whether a dyn value is an island handle (never loads Node). */
"nodeIsland.isHandle": { argTypes: [DYN], result: BOOL },
/** Island arrays of copied values as copies (a result, or an awaited
* promise's value). */
"nodeIsland.copyArrays": { argTypes: [DYN], result: DYN },
"nodeIsland.awaitCopy": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
/** Keyed WRITE on a dyn value — `h.onDone = cb` / `h["k"] = v` on a
* checked-dynamic object (args: receiver, key string, value — all
* borrowed; the runtime copies the key and retains the value in). An
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6837-node-island-clone-and-arrays/main.ts"
],
"diags": []
}
@@ -203,6 +203,16 @@ void KeepOpenSSLAtExit() {
if (init != nullptr) init(kNoAtExit, nullptr);
}
uint32_t ExportIdOf(Isolate *isolate, Local<Context> context, Local<Value> v);
// isExport(value): whether a value is the representative of a native value
// (NI-5), for the bootstrap's structuredClone of native data.
void IsExport(const FunctionCallbackInfo<Value> &info) {
Isolate *isolate = info.GetIsolate();
info.GetReturnValue().Set(info.Length() > 0 &&
ExportIdOf(isolate, isolate->GetCurrentContext(), info[0]) != 0);
}
char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *argv0,
uintptr_t stack_limit) {
if (g_setup) return nullptr;
@@ -265,6 +275,7 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
SetMethod(context, native, "sharedTable", SharedTable);
SetMethod(context, native, "sharedGet", SharedGet);
SetMethod(context, native, "sharedTdz", SharedTdz);
SetMethod(context, native, "isExport", IsExport);
global->Set(context, Utf8(isolate, "__scriptcNodeIslandHost"), native).Check();
TryCatch try_catch(isolate);
if (node::LoadEnvironment(g_setup->env(), bootstrap).IsEmpty()) {
+3
View File
@@ -102,6 +102,9 @@ bool scr_ni_is_function(const ScrDyn *value); /* an island handle that is callab
ScrStr *scr_ni_inspect(const ScrDyn *value, double recurse, double depth);
ScrDyn *scr_ni_own_keys(ScrDyn *obj, int mode); /* 0 Object.keys, 1 Reflect.ownKeys */
ScrPromise *scr_ni_await(ScrDyn *value);
/* Island arrays as copies for results typed as arrays of copied values. */
ScrDyn *scr_node_island_copy_arrays(ScrDyn *value);
ScrPromise *scr_node_island_await_copy(ScrDyn *value);
uint32_t scr_ni_live_handles(void); /* live import-table entries (leak tests) */
/* Between the two island units. */
@@ -1237,6 +1237,58 @@ ScrDyn *scr_ni_value_in(const ScrNodeValue *v) {
return d != NULL ? d : scr_dyn_retain(scr_dyn_undefined());
}
/* ── island arrays as copies (results typed as arrays of copied values) ─
* An island array whose declared native type is an array of primitives or
* strings (or arrays of those) crosses as a copy of its elements, the
* values the checked cast then meets; nested island arrays copy too, any
* other value (a native export coming back, an island object) is itself. */
ScrDyn *scr_node_island_copy_arrays(ScrDyn *value) {
ScrNodeRef *ref = nh_ref_of(value);
if (ref == NULL || (ref->flags & SCR_NVF_ARRAY) == 0) return scr_dyn_retain(value);
ScrDyn *len = scr_ni_get_str(value, "length", 6);
if (len == NULL) return NULL;
double n = len->kind == SCR_DYN_NUM ? len->v.num : 0;
scr_dyn_release(len);
ScrDyn *out = scr_dyn_new_arr();
for (double i = 0; i < n; i++) {
ScrDyn *key = scr_dyn_new_num(i);
ScrDyn *element = scr_ni_get(value, key);
scr_dyn_release(key);
ScrDyn *copy = element != NULL ? scr_node_island_copy_arrays(element) : NULL;
scr_dyn_release(element);
if (copy == NULL) {
scr_dyn_release(out);
return NULL;
}
scr_dyn_arr_push(out, copy);
}
return out;
}
static void nh_copy_fulfilled(ScrPromise *dst, ScrPromise *src) {
ScrDyn *payload = scr_promise_payload_is_dyn(src) ? (ScrDyn *)scr_promise_payload_ref(src)
: scr_dyn_retain(scr_dyn_undefined());
ScrDyn *copy = scr_node_island_copy_arrays(payload);
scr_dyn_release(payload);
if (copy == NULL) {
scr_promise_reject_pending(dst);
return;
}
scr_promise_fulfill_ref(dst, copy, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
}
/* `await` of an island promise whose declared result holds arrays: the
* fulfillment value's island arrays copied (inside the settlement, so no
* reaction turn is added), rejections as they are. +1 promise<dyn>. */
ScrPromise *scr_node_island_await_copy(ScrDyn *value) {
ScrPromise *src = scr_ni_await(value);
ScrPromise *dst = scr_promise_new();
scr_promise_mark_handled(src);
scr_promise_race_add(dst, src, nh_copy_fulfilled);
scr_promise_release(src);
return dst;
}
uint32_t scr_ni_live_handles(void) {
const ScrNodeBridge *b = scr_ni_bridge_loaded();
if (b == NULL) return 0;
@@ -30,6 +30,63 @@ const ERROR_KINDS = [
[URIError, 7],
];
// structuredClone of native values (NI-5). A native record, class
// instance or object is an object with interceptors in V8, which V8's
// serializer cannot clone (DataCloneError). Node clones such values as the
// plain data they are, so when a clone fails on a value holding native
// values, it clones a plain copy instead: native values and the plain
// objects and arrays around them are copied (own enumerable string keys,
// shared references and cycles kept), everything else is cloned as is.
const nodeStructuredClone = globalThis.structuredClone;
function plainCopy(value, memo) {
if (value === null || typeof value !== "object") return value;
if (memo.has(value)) return memo.get(value);
const native = nativeHost.isExport(value);
if (Array.isArray(value)) {
const out = [];
memo.set(value, out);
for (let i = 0; i < value.length; i++) out[i] = plainCopy(value[i], memo);
return out;
}
const proto = Object.getPrototypeOf(value);
if (!native && proto !== Object.prototype && proto !== null) return value;
const out = {};
memo.set(value, out);
for (const key of Object.keys(value)) out[key] = plainCopy(value[key], memo);
return out;
}
function holdsNative(value, seen) {
if (value === null || typeof value !== "object" || seen.has(value)) return false;
seen.add(value);
if (nativeHost.isExport(value)) return true;
const proto = Object.getPrototypeOf(value);
if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return false;
for (const key of Object.keys(value)) if (holdsNative(value[key], seen)) return true;
return false;
}
if (typeof nodeStructuredClone === "function") {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "structuredClone");
const structuredClone = {
structuredClone(value, options = undefined) {
try {
return Reflect.apply(nodeStructuredClone, globalThis, arguments);
} catch (error) {
if (!holdsNative(value, new Set())) throw error;
return Reflect.apply(nodeStructuredClone, globalThis, [
plainCopy(value, new Map()),
options,
]);
}
},
}.structuredClone;
Object.defineProperty(globalThis, "structuredClone", {
configurable: descriptor === undefined || descriptor.configurable,
enumerable: descriptor !== undefined && descriptor.enumerable,
writable: true,
value: structuredClone,
});
}
const requires = new Map();
function exportedFunction(file, specifier, name) {
@@ -37,6 +37,63 @@ static const char scr_node_island_host_js[] =
" [URIError, 7],\012"
"];\012"
"\012"
"// structuredClone of native values (NI-5). A native record, class\012"
"// instance or object is an object with interceptors in V8, which V8's\012"
"// serializer cannot clone (DataCloneError). Node clones such values as the\012"
"// plain data they are, so when a clone fails on a value holding native\012"
"// values, it clones a plain copy instead: native values and the plain\012"
"// objects and arrays around them are copied (own enumerable string keys,\012"
"// shared references and cycles kept), everything else is cloned as is.\012"
"const nodeStructuredClone = globalThis.structuredClone;\012"
"function plainCopy(value, memo) {\012"
" if (value === null || typeof value !== \"object\") return value;\012"
" if (memo.has(value)) return memo.get(value);\012"
" const native = nativeHost.isExport(value);\012"
" if (Array.isArray(value)) {\012"
" const out = [];\012"
" memo.set(value, out);\012"
" for (let i = 0; i < value.length; i++) out[i] = plainCopy(value[i], memo);\012"
" return out;\012"
" }\012"
" const proto = Object.getPrototypeOf(value);\012"
" if (!native && proto !== Object.prototype && proto !== null) return value;\012"
" const out = {};\012"
" memo.set(value, out);\012"
" for (const key of Object.keys(value)) out[key] = plainCopy(value[key], memo);\012"
" return out;\012"
"}\012"
"function holdsNative(value, seen) {\012"
" if (value === null || typeof value !== \"object\" || seen.has(value)) return false;\012"
" seen.add(value);\012"
" if (nativeHost.isExport(value)) return true;\012"
" const proto = Object.getPrototypeOf(value);\012"
" if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return false;\012"
" for (const key of Object.keys(value)) if (holdsNative(value[key], seen)) return true;\012"
" return false;\012"
"}\012"
"if (typeof nodeStructuredClone === \"function\") {\012"
" const descriptor = Object.getOwnPropertyDescriptor(globalThis, \"structuredClone\");\012"
" const structuredClone = {\012"
" structuredClone(value, options = undefined) {\012"
" try {\012"
" return Reflect.apply(nodeStructuredClone, globalThis, arguments);\012"
" } catch (error) {\012"
" if (!holdsNative(value, new Set())) throw error;\012"
" return Reflect.apply(nodeStructuredClone, globalThis, [\012"
" plainCopy(value, new Map()),\012"
" options,\012"
" ]);\012"
" }\012"
" },\012"
" }.structuredClone;\012"
" Object.defineProperty(globalThis, \"structuredClone\", {\012"
" configurable: descriptor === undefined || descriptor.configurable,\012"
" enumerable: descriptor !== undefined && descriptor.enumerable,\012"
" writable: true,\012"
" value: structuredClone,\012"
" });\012"
"}\012"
"\012"
"const requires = new Map();\012"
"\012"
"function exportedFunction(file, specifier, name) {\012"
@@ -0,0 +1,35 @@
// @requires-libnode
// structuredClone in a Node island of native objects passed in clones
// their data, as under Node; arrays of plain values the island returns,
// directly or through a promise, arrive as arrays.
import { cloneClaims, cloneFails, cloneNested, listFiles, matrix, names } from "data-pkg";
interface Claims {
sub: string;
aud: string;
exp: number;
}
class Session {
user = "ada";
level = 2;
}
async function main(): Promise<void> {
const claims: Claims = { sub: "user-1", aud: "api", exp: 1700000000 };
console.log(cloneClaims(claims));
console.log("native unchanged", claims.sub);
console.log(cloneNested({ list: [1, 2], session: new Session() }));
console.log(cloneNested([claims, claims]));
const tags: string[] = ["a", "b"];
console.log(cloneNested({ tags, claims }), cloneNested(tags));
console.log(cloneFails());
const files = await listFiles("/tmp");
for (const f of files) console.log("file", f);
console.log("count", files.length, files.join(","));
console.log("names", names().join("|"), names().length);
const m = await matrix();
console.log("matrix", m.length, m[0].join("+"), m[1][0]);
}
main();
@@ -0,0 +1,6 @@
export declare function cloneClaims(payload: object): string;
export declare function cloneNested(value: unknown): string;
export declare function cloneFails(): string;
export declare function listFiles(dir: string): Promise<string[]>;
export declare function names(): string[];
export declare function matrix(): Promise<number[][]>;
@@ -0,0 +1,29 @@
"use strict";
function cloneClaims(payload) {
const copy = structuredClone(payload);
copy.sub = "changed";
return `${JSON.stringify(copy)} same=${copy === payload} proto=${Object.getPrototypeOf(copy) === Object.prototype}`;
}
function cloneNested(value) {
const copy = structuredClone({ value, when: new Date(0) });
return `${JSON.stringify(copy.value)} date=${copy.when instanceof Date}`;
}
function cloneFails() {
try {
structuredClone({ f() {} });
return "cloned";
} catch (e) {
return `${e.name}`;
}
}
async function listFiles(dir) {
await null;
return [`${dir}/a.txt`, `${dir}/b.txt`];
}
function names() {
return ["x", "y", "z"];
}
async function matrix() {
return [[1, 2], [3]];
}
module.exports = { cloneClaims, cloneNested, cloneFails, listFiles, names, matrix };
@@ -0,0 +1 @@
{ "name": "data-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-clone-and-arrays", "private": true, "type": "module", "scriptc": { "packages": { "data-pkg": "node" } } }
@@ -0,0 +1 @@
{ "plain": 2000, "san": 30000 }