Pass native closures, promises and objects into Node islands by reference (NI-5) (#1900)

This commit is contained in:
Chris Tate
2026-10-28 02:40:19 -05:00
committed by GitHub
72 changed files with 2407 additions and 50 deletions
@@ -1056,6 +1056,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"nodeIsland.applyTo": "scr_node_island_apply_to",
"nodeIsland.construct": "scr_node_island_construct",
"nodeIsland.instanceof": "scr_node_island_instanceof",
"nodeIsland.classMethods": "scr_node_island_class_methods",
"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",
@@ -2035,6 +2038,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"nodeIsland.callSync",
"nodeIsland.require",
"nodeIsland.import",
"nodeIsland.awaitCopy",
"nodeIsland.get",
"nodeIsland.getKey",
"nodeIsland.set",
@@ -371,7 +371,12 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])),
// Fetch uses the native network, TLS, and HTTP runtime.
unit((u) => u.nativeFetch, "scr_fetch.c"),
unit((u) => u.opts.nodeIsland === true, "scr_node_island.c", "scr_node_island_handle.c"),
unit(
(u) => u.opts.nodeIsland === true,
"scr_node_island.c",
"scr_node_island_handle.c",
"scr_node_island_export.c",
),
// Readable.fromWeb reads a web ReadableStream through its reader.
unit((u) => u.nativeFetch && u.opts.stream, "scr_stream_from_web.c"),
];
+1 -1
View File
@@ -115,6 +115,7 @@ export const DIAGNOSTIC_CODES = {
SC8005: "no usable libnode pack for a program that calls into Node.js",
SC8006: "Node island modules the build cannot collect",
SC8010: "value crossing a Node island boundary in a form without an exact copy",
SC8011: "native class extending a Node island class outside the supported forms",
SC8030: "type patch: no range covers the installed copy",
SC8031: "type patch: selector matches nothing",
SC8032: "type patch: declaration already has the type",
@@ -152,7 +153,6 @@ export const RETIRED_DIAGNOSTIC_CODES = {
* that work lands it moves its codes into DIAGNOSTIC_CODES; when it is
* abandoned, delete them here. */
export const RESERVED_DIAGNOSTIC_CODES = {
SC8011: "Node islands (ni/* branches)",
SC8012: "Node islands (ni/* branches)",
SC8020: "Node islands (ni/* branches)",
SC8021: "Node islands (ni/* branches)",
@@ -336,6 +336,18 @@ export function projectConfigDiag(
};
}
/** SC8011 — a native class extends a class owned by a Node island. Native
* subclasses of island classes are scope-B milestone NI-5b. */
export function nativeSubclassOfIslandDiag(loc: SrcLoc): ScrDiagnostic {
return {
code: "SC8011",
message:
"a native class cannot extend a class owned by a Node island yet (native subclasses of island classes)",
loc,
hint: "keep the subclass in Node: move it into the island module, or mark its module as a Node island; a registry shim of the package also lifts this",
};
}
/** SC8002 — Node islands are off (or limited to marked islands) and an
* npm package is outside the native subset. */
export function nodeIslandsOffDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic {
@@ -125,6 +125,8 @@ import {
requireSpecOf,
} from "../program.js";
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
import { islandValueOrigin } from "../node-api-import.js";
import { nativeSubclassOfIslandDiag } from "../../diagnostics/diagnostic.js";
import { declarationGroupKey } from "./type-failure.js";
import { isAssertClassNew } from "./assert-instances.js";
import { isCjsRequireGlobal } from "./lower-node-module.js";
@@ -2212,6 +2214,10 @@ export function collectClassShapeInner(
continue;
}
}
if (t && islandValueOrigin(lowerer.program, t.expression)) {
lowerer.pushDiag(nativeSubclassOfIslandDiag(locOf(t)));
throw new PoisonError();
}
if (!t || !ts.isIdentifier(t.expression)) {
if (t && adoptCallableBase(t.expression)) continue;
lowerer.unsupported("SC1090", clause, "extending computed expressions");
@@ -35,6 +35,8 @@ import {
type SrcLoc,
} from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import { classMethodValue } from "./class-method-values.js";
import { funcTypeFromParamShapes } from "./call-signatures.js";
import { resolveBareModule } from "../resolve.js";
import { nodeIslandBoundaryDiag, nodeIslandModuleDiag } from "../../diagnostics/diagnostic.js";
import {
@@ -528,6 +530,35 @@ 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, directly or as an awaited promise's value; 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);
}
if (copiedArrayType(lowerer, mapped)) {
const copy: IrExpr = {
kind: "libCall",
fn: "nodeIsland.copyArrays",
args: [raw],
type: DYN,
loc: raw.loc,
};
return lowerer.coerceToExpected(copy, 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).
if (!copiedType(lowerer, mapped) && exportedByReference(lowerer, tsType))
return lowerer.coerceToExpected(raw, mapped);
if (!copiedType(lowerer, mapped))
boundaryFail(
lowerer,
@@ -537,6 +568,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 {
@@ -589,7 +626,20 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
case "handle":
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
case "dyn":
// NI-5: the runtime exports dyn objects by reference and copies only
// primitives, strings and (for now) arrays.
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
case "refused":
// NI-5: closures, promises, records and class instances cross by
// reference (scr_node_island_export.c): their dyn boxes keep the
// native referent, and the closure's thunk checks its arguments.
if (exportedByReference(lowerer, lowerer.typeOf(node))) {
const lowered = lowerer.lowerExpr(node);
const value = lowerer.coerceToExpected(lowered, DYN);
return lowered.type.kind === "object"
? withClassMethods(lowerer, value, lowered.type.className, node)
: value;
}
return boundaryFail(
lowerer,
`a '${lowerer.checker.typeToString(lowerer.typeOf(node))}' value (a native object would cross as a copy and lose its identity)`,
@@ -598,6 +648,95 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
}
}
/** Whether an island reads the lowered method `name` by that name: a
* public, string-keyed prototype method. The constructor, #private methods,
* accessor halves (`get:x`, `set:x`) and symbol-keyed slots (`sym:iterator`)
* are not properties an island can name. */
function islandMethodName(name: string): boolean {
return name !== "constructor" && !name.startsWith("#") && !/^(?:get|set|sym):/.test(name);
}
/** A class instance crossing into an island: its capsule answers island
* reads of its fields, and its prototype methods come from a table the
* runtime builds once per class (nodeIsland.classMethods): the unbound
* method values, whose thunks check the receiver Node calls them on. */
function withClassMethods(
lowerer: Lowerer,
value: IrExpr,
className: string,
blame: ts.Node,
): IrExpr {
const info = lowerer.classes.get(className);
if (info === undefined || info.def.runtime) return value;
const names: string[] = [];
for (
let c: typeof info | undefined = info;
c !== undefined;
c = c.def.base ? lowerer.classes.get(c.def.base) : undefined
) {
for (const m of c.def.methods ?? [])
if (!names.includes(m) && islandMethodName(m)) names.push(m);
}
if (names.length === 0) return value;
const loc = value.loc;
const builder = `%island.methods:${className}`;
if (!lowerer.classMethodValueHelpers.has(builder)) {
lowerer.classMethodValueHelpers.add(builder);
const fields: { key: IrExpr; value: IrExpr }[] = [];
for (const m of names) {
const method = classMethodValue(lowerer, blame, info, m, loc);
if (method === null) continue;
fields.push({
key: { kind: "strLit", value: m, type: STRING, loc },
value: lowerer.coerceToExpected(method, DYN),
});
}
lowerer.liftedFns.push({
name: builder,
params: [],
returnType: DYN,
locals: [],
body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }],
loc,
});
}
const closure: IrExpr = {
kind: "closure",
fnName: builder,
captures: [],
type: funcTypeFromParamShapes([], DYN),
loc,
};
return {
kind: "libCall",
fn: "nodeIsland.classMethods",
args: [value, lowerer.coerceToExpected(closure, DYN)],
type: DYN,
loc,
};
}
/** Native values NI-5 exports into the island by reference: functions,
* promises, and object types other than arrays, tuples, Maps and Sets
* (records, interfaces, class instances). A union qualifies when every
* arm does or is copied. */
function exportedByReference(lowerer: Lowerer, type: ts.Type): boolean {
const checker = lowerer.checker;
if (type.isUnionType())
return ts
.constituentTypes(type)
.every(
(t) =>
(t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0 ||
exportedByReference(lowerer, t),
);
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
if (checker.isArrayType(type) || checker.isTupleType(type)) return false;
const name = type.getSymbol()?.escapedName;
if (name === "Map" || name === "Set" || name === "WeakMap" || name === "WeakSet") return false;
return true;
}
/** A fresh object or array literal crosses as a copy (a plain dyn object
* or array the runtime copies into V8): nothing native can alias it. Its
* members cross by the same rules (toIsland). */
@@ -62,3 +62,28 @@ export function nodeApiCalleeOf(program: ts.Program, callee: ts.Expression): Nod
if (!isNodeApiMember(member)) return null;
return isNodeApiNamespace(program, callee.expression) ? member : null;
}
/** Whether an expression evaluates to a value owned by a Node island: a
* requireModule(...) result or anything reached from one through member
* reads, element reads, destructuring or a const alias (`const { Point } =
* requireModule("./lib.cjs")`, `lib.Point`, `const P = lib.Point`). Used
* to refuse native subclasses of island classes (SC8011). */
export function islandValueOrigin(program: ts.Program, expr: ts.Expression, depth = 0): boolean {
if (depth > 8) return false;
let e: ts.Node = expr;
for (;;) {
if (ts.isParenthesizedExpression(e)) e = e.expression;
else if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) e = e.expression;
else if (ts.isAsExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
else break;
}
if (ts.isCallExpression(e)) return nodeApiCalleeOf(program, e.expression) === "requireModule";
if (!ts.isIdentifier(e)) return false;
const symbol = program.getTypeChecker().getSymbolAtLocation(e);
let decl: ts.Node | undefined = symbol ? symbol.declarations[0] : undefined;
// A destructuring binding: climb to the declaration that initializes it.
while (decl !== undefined && ts.isBindingElement(decl)) decl = decl.parent?.parent;
if (decl === undefined || !ts.isVariableDeclaration(decl) || decl.initializer === undefined)
return false;
return islandValueOrigin(program, decl.initializer, depth + 1);
}
@@ -21,6 +21,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"nodeIsland.applyTo",
"nodeIsland.construct",
"nodeIsland.instanceof",
"nodeIsland.classMethods",
"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.
+10 -3
View File
@@ -188,9 +188,9 @@ function buildLibFnSignatures() {
/** Node islands (scope-B NI-4, scr_node_island_handle.c): island values
* are dyn values (`foreign` lowers as dyn), objects are handle boxes
* whose identity is the V8 object's. require/import load an island
* module by its absolute (or `$ORIGIN/`) path (also @scriptc/node
* requireModule); the rest are member reads,
* writes and calls on island values, argument lists as one dyn array. */
* module by its absolute (or `$ORIGIN/`) path; the rest are member reads,
* writes and calls on island values, argument lists as one dyn array.
* requireModule(specifier) of @scriptc/node lowers to require too. */
"nodeIsland.require": { argTypes: [STRING], result: DYN },
"nodeIsland.import": { argTypes: [STRING], result: { kind: "promise", inner: DYN } },
"nodeIsland.get": { argTypes: [DYN, STRING], result: DYN },
@@ -202,6 +202,13 @@ function buildLibFnSignatures() {
"nodeIsland.applyTo": { argTypes: [DYN, DYN, DYN], result: DYN },
"nodeIsland.construct": { argTypes: [DYN, DYN], result: DYN },
"nodeIsland.instanceof": { argTypes: [DYN, DYN], result: BOOL },
/** NI-5: a class instance crossing into an island, with the builder of
* its class's method table (run once per class). Answers the value. */
"nodeIsland.classMethods": { argTypes: [DYN, DYN], result: DYN },
/** 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
@@ -41,7 +41,7 @@ import {
export const NODE_ISLAND_MIN_NODE = "24.13.1";
/** The bridge ABI this compiler's runtime speaks (scr_node_bridge.h). */
export const NODE_BRIDGE_ABI = 5;
export const NODE_BRIDGE_ABI = 6;
/** The prefix of a recorded path relative to the executable's directory. */
export const ORIGIN = "$ORIGIN/";
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6830-node-island-callbacks/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6831-node-island-promises/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6832-node-island-objects/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6833-node-island-typed-callbacks/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6834-node-island-package-callbacks/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6835-node-island-package-promises/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6836-node-island-package-objects/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6837-node-island-clone-and-arrays/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6838-node-island-private-methods/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/node-island-subclass/main.ts"
],
"diags": []
}
@@ -58,6 +58,8 @@ using v8::Value;
const ScrNodeBridgeHost *g_host = nullptr;
void DropAllHandles();
void DropAllExports();
void FlushReleasedExports();
std::unique_ptr<node::MultiIsolatePlatform> g_platform;
node::EmbedderSnapshotData::Pointer g_snapshot;
std::unique_ptr<node::CommonEnvironmentSetup> g_setup;
@@ -188,6 +190,16 @@ bool TakeFunction(Isolate *isolate, Local<Context> context, Local<Object> from,
return true;
}
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;
@@ -255,6 +267,7 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
SetMethod(context, native, "settle", Settle);
SetMethod(context, native, "run", Run);
SetMethod(context, native, "listen", Listen);
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()) {
@@ -487,6 +500,7 @@ void SyncIo(int phase) {
// its liveness, which the callback may have changed after the turn's check.
void OnMicrotasksDone(Isolate *, void *) {
if (!g_hosted) return;
FlushReleasedExports();
g_host->microtasks_done();
SyncIo(SCR_NODE_IO_JOBS);
}
@@ -621,6 +635,7 @@ void Stop() {
g_call.Reset();
g_load.Reset();
DropAllHandles();
DropAllExports();
for (Global<Function> *fn :
{&g_set_timer, &g_clear_timer, &g_ref_timer, &g_refresh_timer, &g_set_immediate,
&g_clear_immediate, &g_ref_immediate, &g_next_tick, &g_emit})
@@ -808,6 +823,326 @@ size_t ElemSize(uint32_t elem) {
return elem < 10 ? sizes[elem] : 1;
}
Local<Value> ToV8(Isolate *isolate, Local<Context> context, const ScrNodeValue *in);
void FromV8(Isolate *isolate, Local<Context> context, Local<Value> v, ScrNodeValue *out);
// ── exports: native values in V8 (scope-B NI-5) ─────────────────────────
// A native value that crosses into Node by reference has an export id in
// the runtime's table and one V8 representative here, held weakly: a
// function whose calls run the native closure (export_call), a promise the
// runtime settles inside its own promise's settlement (export_settle), or
// an object whose property operations all go to the native storage
// (interceptors). The representative carries its id in a private symbol, so
// it crosses back as the native value itself. When V8 collects one, the
// runtime hears export_release at the next microtask checkpoint.
Global<v8::Private> g_export_key;
Global<v8::ObjectTemplate> g_export_template;
std::unordered_map<uint32_t, Global<Object>> g_exports;
std::unordered_map<uint32_t, Global<v8::Promise::Resolver>> g_resolvers;
std::vector<uint32_t> g_released_exports;
Local<v8::Private> ExportKey(Isolate *isolate) {
if (g_export_key.IsEmpty())
g_export_key.Reset(isolate, v8::Private::New(isolate, Utf8(isolate, "scriptc:export")));
return g_export_key.Get(isolate);
}
uint32_t ExportIdOf(Isolate *isolate, Local<Context> context, Local<Value> v) {
if (!v->IsObject() || v->IsProxy() || g_exports.empty()) return 0;
Local<Value> id;
if (!v.As<Object>()->GetPrivate(context, ExportKey(isolate)).ToLocal(&id) || !id->IsUint32())
return 0;
return id.As<v8::Uint32>()->Value();
}
void OnExportGone(const v8::WeakCallbackInfo<void> &info) {
uint32_t id = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(info.GetParameter()));
auto it = g_exports.find(id);
if (it != g_exports.end()) {
it->second.Reset();
g_exports.erase(it);
}
g_released_exports.push_back(id);
}
// Delivers the releases V8's collector queued (never from inside a GC).
void FlushReleasedExports() {
while (!g_released_exports.empty()) {
uint32_t id = g_released_exports.back();
g_released_exports.pop_back();
if (g_exports.count(id) != 0) continue; // crossed again since
g_host->export_release(id);
}
}
uint32_t ExportIdArg(Local<Value> data) {
return data->IsUint32() ? data.As<v8::Uint32>()->Value() : 0;
}
// Out-value scratch of host calls made from outside any runtime entry (a
// listener Node's loop dispatches) is reset when the outermost call
// starts. A call that puts its own inputs in the scratch (a property
// name) opens its scope before converting them.
struct HostCall {
HostCall() {
if (g_entry_depth++ == 0) g_scratch.clear();
}
~HostCall() { g_entry_depth--; }
};
void ThrowOrReturn(Isolate *isolate, Local<Context> context, int status, const ScrNodeValue *out,
Local<Value> *result) {
Local<Value> v = ToV8(isolate, context, out);
if (status == 1) isolate->ThrowException(v);
else *result = v;
}
void ExportCall(const FunctionCallbackInfo<Value> &info) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
HostCall scope;
uint32_t id = ExportIdArg(info.Data());
std::vector<ScrNodeValue> args(static_cast<size_t>(info.Length()));
for (int i = 0; i < info.Length(); i++) FromV8(isolate, context, info[i], &args[i]);
ScrNodeValue out;
std::memset(&out, 0, sizeof out);
int status;
if (info.IsConstructCall()) {
status = g_host->export_construct(id, args.data(), args.size(), &out);
} else {
ScrNodeValue self;
FromV8(isolate, context, info.This(), &self);
status = g_host->export_call(id, &self, args.data(), args.size(), &out);
}
Local<Value> result;
ThrowOrReturn(isolate, context, status, &out, &result);
if (!result.IsEmpty()) info.GetReturnValue().Set(result);
}
uint32_t HolderId(Local<Object> holder) {
if (holder->InternalFieldCount() < 1) return 0;
return ExportIdArg(holder->GetInternalField(0).As<Value>());
}
template <typename T>
uint32_t InfoId(const v8::PropertyCallbackInfo<T> &info) {
return HolderId(info.HolderV2());
}
void NameKey(Isolate *isolate, Local<Context> context, Local<v8::Name> name, ScrNodeValue *key) {
FromV8(isolate, context, name, key);
}
void IndexKey(uint32_t index, ScrNodeValue *key) {
std::memset(key, 0, sizeof *key);
key->tag = SCR_NV_NUMBER;
key->u.num = index;
}
v8::Intercepted GetKey(const v8::PropertyCallbackInfo<Value> &info, const ScrNodeValue *key) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
HostCall scope;
ScrNodeValue out;
std::memset(&out, 0, sizeof out);
int status = g_host->export_get(InfoId(info), key, &out);
if (status == 2) return v8::Intercepted::kNo;
Local<Value> result;
ThrowOrReturn(isolate, context, status, &out, &result);
if (!result.IsEmpty()) info.GetReturnValue().Set(result);
return v8::Intercepted::kYes;
}
v8::Intercepted SetKey(const v8::PropertyCallbackInfo<void> &info, const ScrNodeValue *key,
Local<Value> value) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
HostCall scope;
ScrNodeValue v, out;
FromV8(isolate, context, value, &v);
std::memset(&out, 0, sizeof out);
int status = g_host->export_set(InfoId(info), key, &v, &out);
if (status == 2) return v8::Intercepted::kNo;
if (status == 1) isolate->ThrowException(ToV8(isolate, context, &out));
return v8::Intercepted::kYes;
}
v8::Intercepted QueryKey(const v8::PropertyCallbackInfo<v8::Integer> &info,
const ScrNodeValue *key) {
HostCall scope;
if (!g_host->export_has(InfoId(info), key)) return v8::Intercepted::kNo;
info.GetReturnValue().Set(static_cast<int32_t>(v8::None));
return v8::Intercepted::kYes;
}
v8::Intercepted DeleteKey(const v8::PropertyCallbackInfo<v8::Boolean> &info,
const ScrNodeValue *key) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
HostCall scope;
ScrNodeValue out;
std::memset(&out, 0, sizeof out);
int status = g_host->export_delete(InfoId(info), key, &out);
if (status == 2) return v8::Intercepted::kNo;
if (status == 1) isolate->ThrowException(ToV8(isolate, context, &out));
else info.GetReturnValue().Set(true);
return v8::Intercepted::kYes;
}
v8::Intercepted NamedGet(Local<v8::Name> name, const v8::PropertyCallbackInfo<Value> &info) {
HostCall scope; // the key's text lives in the scratch this call keeps
ScrNodeValue key;
NameKey(info.GetIsolate(), info.GetIsolate()->GetCurrentContext(), name, &key);
return GetKey(info, &key);
}
v8::Intercepted NamedSet(Local<v8::Name> name, Local<Value> value,
const v8::PropertyCallbackInfo<void> &info) {
HostCall scope; // the key's text lives in the scratch this call keeps
ScrNodeValue key;
NameKey(info.GetIsolate(), info.GetIsolate()->GetCurrentContext(), name, &key);
return SetKey(info, &key, value);
}
v8::Intercepted NamedQuery(Local<v8::Name> name, const v8::PropertyCallbackInfo<v8::Integer> &info) {
HostCall scope; // the key's text lives in the scratch this call keeps
ScrNodeValue key;
NameKey(info.GetIsolate(), info.GetIsolate()->GetCurrentContext(), name, &key);
return QueryKey(info, &key);
}
v8::Intercepted NamedDelete(Local<v8::Name> name, const v8::PropertyCallbackInfo<v8::Boolean> &info) {
HostCall scope; // the key's text lives in the scratch this call keeps
ScrNodeValue key;
NameKey(info.GetIsolate(), info.GetIsolate()->GetCurrentContext(), name, &key);
return DeleteKey(info, &key);
}
v8::Intercepted IndexedGet(uint32_t index, const v8::PropertyCallbackInfo<Value> &info) {
ScrNodeValue key;
IndexKey(index, &key);
return GetKey(info, &key);
}
v8::Intercepted IndexedSet(uint32_t index, Local<Value> value,
const v8::PropertyCallbackInfo<void> &info) {
ScrNodeValue key;
IndexKey(index, &key);
return SetKey(info, &key, value);
}
v8::Intercepted IndexedQuery(uint32_t index, const v8::PropertyCallbackInfo<v8::Integer> &info) {
ScrNodeValue key;
IndexKey(index, &key);
return QueryKey(info, &key);
}
v8::Intercepted IndexedDelete(uint32_t index, const v8::PropertyCallbackInfo<v8::Boolean> &info) {
ScrNodeValue key;
IndexKey(index, &key);
return DeleteKey(info, &key);
}
// Own enumerable keys, split for V8: names for the named enumerator, array
// indices for the indexed one.
Local<v8::Array> ExportKeys(const v8::PropertyCallbackInfo<v8::Array> &info, bool indices) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
HostCall scope;
ScrNodeValue out;
std::memset(&out, 0, sizeof out);
std::vector<Local<Value>> keep;
if (g_host->export_keys(InfoId(info), &out) == 0) {
Local<Value> all = ToV8(isolate, context, &out);
if (all->IsArray()) {
Local<v8::Array> list = all.As<v8::Array>();
for (uint32_t i = 0; i < list->Length(); i++) {
Local<Value> key = list->Get(context, i).ToLocalChecked();
uint32_t index = 0;
bool is_index = false;
if (key->IsString()) {
Local<v8::Uint32> as_index;
if (key->ToArrayIndex(context).ToLocal(&as_index)) {
is_index = true;
index = as_index->Value();
}
}
if (is_index != indices) continue;
keep.push_back(indices ? Local<Value>(v8::Integer::NewFromUnsigned(isolate, index)) : key);
}
}
}
return v8::Array::New(isolate, keep.data(), keep.size());
}
void NamedEnum(const v8::PropertyCallbackInfo<v8::Array> &info) {
info.GetReturnValue().Set(ExportKeys(info, false));
}
void IndexedEnum(const v8::PropertyCallbackInfo<v8::Array> &info) {
info.GetReturnValue().Set(ExportKeys(info, true));
}
Local<v8::ObjectTemplate> ExportTemplate(Isolate *isolate) {
if (g_export_template.IsEmpty()) {
Local<v8::ObjectTemplate> t = v8::ObjectTemplate::New(isolate);
t->SetInternalFieldCount(1);
t->SetHandler(v8::NamedPropertyHandlerConfiguration(
NamedGet, NamedSet, NamedQuery, NamedDelete, NamedEnum, Local<Value>(),
v8::PropertyHandlerFlags::kOnlyInterceptStrings));
t->SetHandler(v8::IndexedPropertyHandlerConfiguration(IndexedGet, IndexedSet, IndexedQuery,
IndexedDelete, IndexedEnum));
g_export_template.Reset(isolate, t);
}
return g_export_template.Get(isolate);
}
Local<Value> ExportValue(Isolate *isolate, Local<Context> context, const ScrNodeValue *in) {
uint32_t id = in->u.exp.id;
auto found = g_exports.find(id);
if (found != g_exports.end()) return found->second.Get(isolate);
Local<Object> obj;
Local<Value> data = v8::Integer::NewFromUnsigned(isolate, id);
if (in->flags & SCR_NVF_FUNCTION) {
Local<Function> fn =
Function::New(context, ExportCall, data, static_cast<int>(in->u.exp.length))
.ToLocalChecked();
fn->SetName(in->u.exp.name != nullptr
? FromWtf8(isolate, in->u.exp.name, in->u.exp.name_len)
: String::Empty(isolate));
obj = fn;
} else if (in->flags & SCR_NVF_PROMISE) {
Local<v8::Promise::Resolver> resolver = v8::Promise::Resolver::New(context).ToLocalChecked();
g_resolvers[id].Reset(isolate, resolver);
obj = resolver->GetPromise();
} else {
obj = ExportTemplate(isolate)->NewInstance(context).ToLocalChecked();
obj->SetInternalField(0, data);
}
obj->SetPrivate(context, ExportKey(isolate), data).Check();
Global<Object> &slot = g_exports[id];
slot.Reset(isolate, obj);
slot.SetWeak(reinterpret_cast<void *>(static_cast<uintptr_t>(id)), OnExportGone,
v8::WeakCallbackType::kParameter);
if (in->flags & SCR_NVF_PROMISE) g_host->export_watch(id);
return obj;
}
void ExportSettle(uint32_t id, int rejected, const ScrNodeValue *value) {
Enter e;
auto it = g_resolvers.find(id);
if (it == g_resolvers.end()) return;
Local<v8::Promise::Resolver> resolver = it->second.Get(e.isolate);
it->second.Reset();
g_resolvers.erase(it);
Local<Value> v = ToV8(e.isolate, e.context, value);
if (rejected) resolver->Reject(e.context, v).Check();
else resolver->Resolve(e.context, v).Check();
}
void DropAllExports() {
for (auto &it : g_exports) it.second.Reset();
g_exports.clear();
for (auto &it : g_resolvers) it.second.Reset();
g_resolvers.clear();
g_released_exports.clear();
g_export_template.Reset();
g_export_key.Reset();
}
void FromV8(Isolate *isolate, Local<Context> context, Local<Value> v, ScrNodeValue *out) {
std::memset(out, 0, sizeof *out);
if (v.IsEmpty() || v->IsUndefined()) { out->tag = SCR_NV_UNDEFINED; return; }
@@ -843,6 +1178,11 @@ void FromV8(Isolate *isolate, Local<Context> context, Local<Value> v, ScrNodeVal
return;
}
}
if (uint32_t export_id = ExportIdOf(isolate, context, v)) {
out->tag = SCR_NV_EXPORT;
out->u.exp.id = export_id;
return;
}
out->tag = SCR_NV_HANDLE;
uint32_t flags = 0;
if (v->IsSymbol()) flags |= SCR_NVF_SYMBOL;
@@ -925,6 +1265,7 @@ Local<Value> ToV8(Isolate *isolate, Local<Context> context, const ScrNodeValue *
FromWtf8(isolate, in->u.error.code, in->u.error.code_len)).Check();
return error;
}
case SCR_NV_EXPORT: return ExportValue(isolate, context, in);
case SCR_NV_JSON: {
Local<Value> parsed;
if (v8::JSON::Parse(context, FromWtf8(isolate, in->u.str.data, in->u.str.len)).ToLocal(&parsed))
@@ -1257,6 +1598,7 @@ const ScrNodeBridge kBridge = {
ErrorInfo,
DropHandle,
LiveHandles,
ExportSettle,
};
} // namespace
@@ -195,6 +195,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [
// over the SCR_NODE_ISLANDS build of scr_async.c.
["scr_node_island.c", "nodeIsland"],
["scr_node_island_handle.c", "nodeIsland"],
["scr_node_island_export.c", "nodeIsland"],
["scr_stream_from_web.c", all("stream", "nativeFetch")],
];
+38
View File
@@ -172,6 +172,9 @@ static void scr_fiber_stack_free(void *stack) {
enum { SCR_PROM_PENDING = 0, SCR_PROM_FULFILLED = 1, SCR_PROM_REJECTED = 2 };
#define SCR_CB_SYNC SIZE_MAX
#ifdef SCR_NODE_ISLANDS
#define SCR_CB_ANY SIZE_MAX /* all_idx of an any-settlement callback */
#endif
#define SCR_CB_BATCH (SIZE_MAX - 1)
typedef struct ScrFiber ScrFiber;
@@ -388,6 +391,26 @@ void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src) {
dst->identity->rc++;
}
#ifdef SCR_NODE_ISLANDS
/* Node islands (NI-5) key their export table by a promise's identity
* (scr_node_island_export.c); other programs link none of this. */
const void *scr_promise_identity_key(ScrPromise *p) {
if (!p->identity) {
p->identity = malloc(sizeof *p->identity);
if (!p->identity) scr_oom();
p->identity->rc = 1;
}
return p->identity;
}
void scr_promise_identity_hold(const void *key) { ((ScrPromiseIdentity *)key)->rc++; }
void scr_promise_identity_drop(const void *key) {
ScrPromiseIdentity *identity = (ScrPromiseIdentity *)key;
if (--identity->rc == 0) free(identity);
}
#endif
bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b) {
return a == b || (a->identity != NULL && a->identity == b->identity);
}
@@ -1831,6 +1854,10 @@ static void scr_promise_cb_run(struct ScrPromiseCbWaiter *cb, ScrPromise *src) {
scr_promise_all_state_release(cb->all);
} else if (src->state == SCR_PROM_FULFILLED) {
cb->adapt(cb->dst, src);
#ifdef SCR_NODE_ISLANDS
} else if (cb->all_idx == SCR_CB_ANY) {
cb->adapt(cb->dst, src); /* scr_promise_on_settle_sync: rejections too */
#endif
} else {
scr_promise_settle_from(cb->dst, src);
}
@@ -1944,6 +1971,17 @@ void scr_promise_race_join(ScrPromise *race, ScrPromise *in,
in, (struct ScrPromiseCbWaiter){ .adapt = adapt, .dst = scr_promise_retain(race) });
}
#ifdef SCR_NODE_ISLANDS
/* Node islands (NI-5): runs `cb(in, in)` inside `in`'s settlement, for
* fulfillments and rejections alike (a native promise's V8 representative
* settles at the same moment, so its reactions queue where a JavaScript
* promise's would). `in` must be pending. */
void scr_promise_on_settle_sync(ScrPromise *in, void (*cb)(ScrPromise *dst, ScrPromise *src)) {
*scr_promise_cb_push(in) =
(struct ScrPromiseCbWaiter){ cb, scr_promise_retain(in), NULL, SCR_CB_ANY, SCR_CB_SYNC };
}
#endif
void scr_promise_race_add(ScrPromise *race, ScrPromise *in,
void (*adapt)(ScrPromise *dst, ScrPromise *src)) {
if (in->state != SCR_PROM_PENDING) {
+7
View File
@@ -23,6 +23,13 @@ ScrPromise *scr_tp_set_immediate(void);
ScrPromise *scr_promise_new(void);
void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src);
bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b);
/* A key every promise sharing `p`'s identity answers (each adapter box of
* a typed promise), creating the shared cell when `p` has none. */
const void *scr_promise_identity_key(ScrPromise *p);
/* A reference on a key from scr_promise_identity_key, for a table that
* keeps the key after the promises that share it are gone. */
void scr_promise_identity_hold(const void *key);
void scr_promise_identity_drop(const void *key);
ScrPromise *scr_promise_retain(ScrPromise *p);
void scr_promise_release(ScrPromise *p);
void *scr_promise_retain_v(void *p);
+32 -2
View File
@@ -17,7 +17,7 @@
#include <stddef.h>
#include <stdint.h>
#define SCR_NODE_BRIDGE_ABI 5u
#define SCR_NODE_BRIDGE_ABI 6u
/* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */
#define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1"
@@ -82,7 +82,8 @@ enum {
SCR_NV_BYTES = 7, /* bytes: a typed array / Buffer / DataView over shared memory */
SCR_NV_ERROR = 8, /* into V8 only: a new Error of an intrinsic class */
SCR_NV_JSON = 9, /* into V8 only: str is JSON text, parsed there (a copy) */
SCR_NV_EXPORT = 10, /* reserved for NI-5: a runtime object exported by reference */
SCR_NV_EXPORT = 10, /* exp: a runtime value exported by reference (NI-5); flags: SCR_NVF_FUNCTION,
SCR_NVF_PROMISE, SCR_NVF_ARRAY or 0 for an object */
};
/* What a handle is, reported with every handle the bridge answers. */
enum {
@@ -119,6 +120,12 @@ typedef struct ScrNodeValue {
const char *name, *message, *stack, *code; /* code NULL: none */
size_t name_len, message_len, stack_len, code_len;
} error;
struct {
uint32_t id; /* the runtime's export-table id */
uint32_t length; /* functions: their `length` */
const char *name; /* functions: their `name` (NULL: anonymous) */
size_t name_len;
} exp;
} u;
} ScrNodeValue;
@@ -172,6 +179,27 @@ typedef struct ScrNodeBridgeHost {
* value that went into V8). May run on any thread: the runtime queues
* the release for its own thread. */
void (*bytes_free)(void *owner);
/* Native values in Node (NI-5): the V8 representative of export `id`
* was called, constructed, or had a property read, written, tested,
* deleted or enumerated. 0: returned (*out), 1: threw (*out is the thrown
* value), 2: absent (get/set/delete: the ordinary object answers). Out
* strings are runtime-owned until the next host call. Keys are strings
* or array indices (numbers). export_keys answers SCR_NV_JSON: the own
* enumerable string keys. */
int (*export_call)(uint32_t id, const ScrNodeValue *this_, const ScrNodeValue *args,
size_t argc, ScrNodeValue *out);
int (*export_construct)(uint32_t id, const ScrNodeValue *args, size_t argc, ScrNodeValue *out);
int (*export_get)(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out);
int (*export_set)(uint32_t id, const ScrNodeValue *key, const ScrNodeValue *value,
ScrNodeValue *out);
int (*export_has)(uint32_t id, const ScrNodeValue *key);
int (*export_delete)(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out);
int (*export_keys)(uint32_t id, ScrNodeValue *out);
/* The V8 promise representing native promise `id` exists: settle it
* (bridge->export_settle) when the native promise settles. */
void (*export_watch)(uint32_t id);
/* V8 collected the representative of `id`. */
void (*export_release)(uint32_t id);
} ScrNodeBridgeHost;
typedef struct ScrNodeBridge {
@@ -260,6 +288,8 @@ typedef struct ScrNodeBridge {
void (*drop)(uint32_t h);
/* The number of live import-table entries (leak tests). */
uint32_t (*live_handles)(void);
/* Settles the V8 promise representing native promise `id` (NI-5). */
void (*export_settle)(uint32_t id, int rejected, const ScrNodeValue *value);
} ScrNodeBridge;
#endif /* SCR_NODE_BRIDGE_H */
+11
View File
@@ -1165,6 +1165,15 @@ static const ScrNodeBridgeHost ni_host = {
&ni_stack_start,
&scr_ni_settle_value,
&scr_ni_bytes_free,
&scr_ni_host_export_call,
&scr_ni_host_export_construct,
&scr_ni_host_export_get,
&scr_ni_host_export_set,
&scr_ni_host_export_has,
&scr_ni_host_export_delete,
&scr_ni_host_export_keys,
&scr_ni_host_export_watch,
&scr_ni_host_export_release,
};
typedef struct {
@@ -1210,6 +1219,8 @@ static void ni_stop_atexit(void) {
fprintf(stderr, "[node-islands] live handles %u\n", scr_ni_live_handles());
}
ni_on_node_stack(ni_stop_job, NULL);
/* Node is gone: no representative can call in again. */
scr_ni_exports_release_all();
}
/* The hosted loop's run_loop on Node's stack. */
+33
View File
@@ -95,6 +95,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. */
@@ -111,6 +114,36 @@ void scr_ni_bytes_free(void *owner);
void scr_ni_handles_install(void);
void scr_ni_stop(void);
/* ── native values in Node (scope-B NI-5, scr_node_island_export.c) ───────
* Closures, promises, records, class instances and dyn objects cross into
* Node by reference: one export id and one V8 representative per native
* referent. The conversions of scr_node_island_handle.c call these. */
bool scr_ni_value_out(ScrDyn *v, struct ScrNodeValue *out); /* NI-4's into-V8 conversion */
ScrDyn *scr_ni_value_in(const struct ScrNodeValue *v); /* +1 */
ScrDyn *scr_ni_arg_in(const struct ScrNodeValue *v); /* +1; island Errors as their ScrError */
bool scr_ni_exportable(const ScrDyn *v);
void scr_ni_export_out(ScrDyn *v, struct ScrNodeValue *out);
ScrDyn *scr_ni_export_value(uint32_t id); /* borrowed, or NULL */
uint32_t scr_ni_live_exports(void);
/* nodeIsland.classMethods: registers the method table of a class capsule. */
ScrDyn *scr_node_island_class_methods(ScrDyn *value, ScrDyn *builder);
void scr_ni_bridge_export_settle(uint32_t id, int rejected, const struct ScrNodeValue *value);
int scr_ni_host_export_call(uint32_t id, const struct ScrNodeValue *this_,
const struct ScrNodeValue *args, size_t argc, struct ScrNodeValue *out);
int scr_ni_host_export_construct(uint32_t id, const struct ScrNodeValue *args, size_t argc,
struct ScrNodeValue *out);
int scr_ni_host_export_get(uint32_t id, const struct ScrNodeValue *key, struct ScrNodeValue *out);
int scr_ni_host_export_set(uint32_t id, const struct ScrNodeValue *key,
const struct ScrNodeValue *value, struct ScrNodeValue *out);
int scr_ni_host_export_has(uint32_t id, const struct ScrNodeValue *key);
int scr_ni_host_export_delete(uint32_t id, const struct ScrNodeValue *key, struct ScrNodeValue *out);
int scr_ni_host_export_keys(uint32_t id, struct ScrNodeValue *out);
void scr_ni_host_export_watch(uint32_t id);
void scr_ni_host_export_release(uint32_t id);
void scr_ni_exports_release_all(void); /* at exit, after Node stopped */
/* Promise settlement callback (SCR_NODE_ISLANDS build of scr_async.c). */
void scr_promise_on_settle_sync(ScrPromise *in, void (*cb)(ScrPromise *dst, ScrPromise *src));
/* Island operations the compiler emits (lib-shared.ts adapters). */
ScrDyn *scr_node_island_apply(ScrDyn *fn, ScrDyn *args);
/* fn called with `this` (a method read before its arguments ran). */
@@ -0,0 +1,602 @@
/* Node islands: native values in Node (scope-B milestone NI-5). Links with
* scr_node_island.c (the `nodeIsland` runtime feature) and nothing else.
*
* The export table. A native value that crosses into Node by reference (a
* closure, a promise, a record or class instance, a checked-dynamic object
* or array) gets an export id: the table holds it strongly (+1) and the
* bridge keeps one V8 representative per id, weakly. The same native value
* always gets the same id, so it always meets the same V8 object, and a
* representative crossing back is answered with the original value
* (SCR_NV_EXPORT), so identity holds both ways. Identity is the referent,
* not the dyn box: a record boxed twice is two capsules over one record.
*
* What the representative does (scr_node_bridge.cc):
* - closures are V8 functions whose calls run export_call: the arguments
* cross into the runtime per the boundary rules and meet the closure's
* declared parameter types through its dyn thunk (a mismatch throws the
* runtime's TypeError into JavaScript); the result crosses back; a
* runtime exception becomes a JavaScript throw and a JavaScript throw
* inside a nested island call propagates out as a runtime exception;
* - promises are V8 promises resolved inside the runtime promise's own
* settlement (export_watch), so their reactions queue exactly when a
* JavaScript promise's would;
* - objects use interceptors: every property read, write, `in`, delete
* and key enumeration goes to the native storage (Reflect.get/set over
* the dyn value: a typed record capsule reads and commits its fields).
*
* When V8 collects a representative, the bridge reports export_release at
* its next entry (never inside a GC) and the table drops its reference.
* Cycles through both heaps leak until NI-8. */
#define _XOPEN_SOURCE 700
#define _DARWIN_C_SOURCE 1
#include "scr_runtime.h"
#include "scr_node_bridge.h"
#include "scr_node_island.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if !defined(_WIN32) && !defined(__wasi__)
typedef struct {
ScrDyn *value; /* +1; NULL for a free slot */
const void *identity;
uint32_t flags;
} NiExport;
static NiExport *ni_exports = NULL; /* index = id; id 0 unused */
static uint32_t ni_exports_len = 1, ni_exports_cap = 0;
static uint32_t *ni_free = NULL;
static uint32_t ni_nfree = 0, ni_free_cap = 0;
/* identity -> id, open addressing over a power-of-two table. */
typedef struct {
const void *key;
uint32_t id;
} NiIdEntry;
static NiIdEntry *ni_ids = NULL;
static size_t ni_ids_cap = 0, ni_ids_len = 0, ni_ids_dead = 0;
static const char ni_tomb;
SCR_COLD static void ni_export_oom(void) { scr_trap("scriptc: out of memory\n"); }
static size_t ni_hash(const void *p) {
uintptr_t x = (uintptr_t)p;
x ^= x >> 33;
x *= 0xff51afd7ed558ccdULL;
x ^= x >> 33;
return (size_t)x;
}
static void ni_ids_put(const void *key, uint32_t id);
static void ni_ids_grow(void) {
NiIdEntry *old = ni_ids;
size_t old_cap = ni_ids_cap;
ni_ids_cap = old_cap ? old_cap * 2 : 64;
while (ni_ids_len * 2 >= ni_ids_cap) ni_ids_cap *= 2;
ni_ids = calloc(ni_ids_cap, sizeof *ni_ids);
if (ni_ids == NULL) ni_export_oom();
ni_ids_len = 0;
ni_ids_dead = 0;
for (size_t i = 0; i < old_cap; i++)
if (old[i].key != NULL && old[i].key != &ni_tomb) ni_ids_put(old[i].key, old[i].id);
free(old);
}
static void ni_ids_put(const void *key, uint32_t id) {
if ((ni_ids_len + ni_ids_dead + 1) * 2 > ni_ids_cap) ni_ids_grow();
size_t mask = ni_ids_cap - 1;
for (size_t i = ni_hash(key) & mask;; i = (i + 1) & mask) {
if (ni_ids[i].key == NULL || ni_ids[i].key == &ni_tomb) {
if (ni_ids[i].key == &ni_tomb) ni_ids_dead--;
ni_ids[i].key = key;
ni_ids[i].id = id;
ni_ids_len++;
return;
}
}
}
static uint32_t ni_ids_get(const void *key) {
if (ni_ids_cap == 0) return 0;
size_t mask = ni_ids_cap - 1;
for (size_t i = ni_hash(key) & mask;; i = (i + 1) & mask) {
if (ni_ids[i].key == NULL) return 0;
if (ni_ids[i].key == key) return ni_ids[i].id;
}
}
static void ni_ids_del(const void *key) {
if (ni_ids_cap == 0) return;
size_t mask = ni_ids_cap - 1;
for (size_t i = ni_hash(key) & mask;; i = (i + 1) & mask) {
if (ni_ids[i].key == NULL) return;
if (ni_ids[i].key == key) {
ni_ids[i].key = &ni_tomb;
ni_ids_len--;
ni_ids_dead++;
return;
}
}
}
/* The identity of a value that crosses by reference: the referent for
* boxes that wrap one (closures, typed capsules), a promise's shared
* identity (a typed promise boxes a fresh adapter each time it crosses),
* else the box. */
static const void *ni_identity(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_FUNC:
return v->v.fn.clo != NULL ? (const void *)v->v.fn.clo : (const void *)v;
case SCR_DYN_PROMISE:
return scr_promise_identity_key(v->v.promise);
case SCR_DYN_TYPED_REF:
return v->v.typed_ref.ptr;
default:
return v;
}
}
uint32_t scr_ni_export_flags(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_FUNC:
return SCR_NVF_FUNCTION;
case SCR_DYN_PROMISE:
return SCR_NVF_PROMISE;
case SCR_DYN_ARR:
return SCR_NVF_ARRAY;
case SCR_DYN_TYPED_REF:
return scr_dyn_typed_ref_array_ops(v) != NULL ? SCR_NVF_ARRAY : 0;
default:
return 0;
}
}
/* Whether `v` crosses into Node as an export. Arrays keep NI-4's JSON
* copy for now: an interceptor object is not an Array in V8 (NOTES.md). */
bool scr_ni_exportable(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_FUNC:
case SCR_DYN_PROMISE:
case SCR_DYN_OBJ:
return true;
case SCR_DYN_TYPED_REF:
return scr_dyn_typed_ref_array_ops(v) == NULL;
default:
return false;
}
}
/* The export id of `v` (borrowed), minting one on first crossing. */
uint32_t scr_ni_export_id(ScrDyn *v) {
const void *identity = ni_identity(v);
uint32_t id = ni_ids_get(identity);
if (id != 0) return id;
if (ni_nfree > 0) {
id = ni_free[--ni_nfree];
} else {
if (ni_exports_len >= ni_exports_cap) {
uint32_t cap = ni_exports_cap ? ni_exports_cap * 2 : 64;
NiExport *grown = realloc(ni_exports, cap * sizeof *grown);
if (grown == NULL) ni_export_oom();
ni_exports = grown;
ni_exports_cap = cap;
if (ni_exports_len == 1) memset(&ni_exports[0], 0, sizeof ni_exports[0]);
}
id = ni_exports_len++;
}
ni_exports[id].value = scr_dyn_retain(v);
ni_exports[id].identity = identity;
/* The table keeps a promise's identity cell while the export lives: a
* freed cell's address could come back as another promise's. */
if (v->kind == SCR_DYN_PROMISE) scr_promise_identity_hold(identity);
ni_exports[id].flags = scr_ni_export_flags(v);
ni_ids_put(identity, id);
return id;
}
/* The value of export `id` (borrowed), or NULL. */
ScrDyn *scr_ni_export_value(uint32_t id) {
if (id == 0 || id >= ni_exports_len) return NULL;
return ni_exports[id].value;
}
/* Fills `out` with the export of `v` (borrowed). */
void scr_ni_export_out(ScrDyn *v, ScrNodeValue *out) {
uint32_t id = scr_ni_export_id(v);
memset(out, 0, sizeof *out);
out->tag = SCR_NV_EXPORT;
out->flags = ni_exports[id].flags;
out->u.exp.id = id;
if (v->kind == SCR_DYN_FUNC) {
out->u.exp.length = v->v.fn.arity;
if (v->v.fn.name != NULL) {
out->u.exp.name = v->v.fn.name;
out->u.exp.name_len = strlen(v->v.fn.name);
}
}
}
uint32_t scr_ni_live_exports(void) {
uint32_t n = 0;
for (uint32_t i = 1; i < ni_exports_len; i++) n += ni_exports[i].value != NULL;
return n;
}
/* ── host callbacks ───────────────────────────────────────────────────── */
/* Values the runtime hands the bridge stay alive until the next host call
* (their string data points into them). */
static ScrDyn *ni_keep = NULL;
static void ni_keep_set(ScrDyn *v) {
ScrDyn *old = ni_keep;
ni_keep = v;
scr_dyn_release(old);
}
/* The pending runtime exception as an out value: 1. */
static int ni_threw(ScrNodeValue *out) {
ScrCaught *caught = scr_exc_take();
ScrDyn *thrown = scr_caught_to_dyn(caught);
scr_caught_release(caught);
if (thrown == NULL) thrown = scr_dyn_retain(scr_dyn_undefined());
if (!scr_ni_value_out(thrown, out)) {
/* The thrown value itself cannot cross: throw what that raised. */
scr_dyn_release(thrown);
caught = scr_exc_take();
thrown = scr_caught_to_dyn(caught);
scr_caught_release(caught);
if (thrown == NULL || !scr_ni_value_out(thrown, out)) {
scr_exc_clear();
memset(out, 0, sizeof *out);
out->tag = SCR_NV_UNDEFINED;
}
}
ni_keep_set(thrown);
return 1;
}
/* A runtime result as an out value: 0, or 1 when it cannot cross. */
static int ni_returned(ScrDyn *result, ScrNodeValue *out) {
if (!scr_ni_value_out(result, out)) {
scr_dyn_release(result);
return ni_threw(out);
}
ni_keep_set(result);
return 0;
}
/* Arguments as runtime values (+1 each). */
static ScrDyn **ni_args_in(const ScrNodeValue *args, size_t argc, ScrDyn **small, size_t nsmall) {
ScrDyn **out = argc <= nsmall ? small : malloc(argc * sizeof *out);
if (out == NULL) ni_export_oom();
for (size_t i = 0; i < argc; i++) out[i] = scr_ni_arg_in(&args[i]);
return out;
}
static void ni_args_free(ScrDyn **args, size_t argc, ScrDyn **small) {
for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]);
if (args != small) free(args);
}
int scr_ni_host_export_call(uint32_t id, const ScrNodeValue *this_, const ScrNodeValue *args,
size_t argc, ScrNodeValue *out) {
ni_keep_set(NULL);
ScrDyn *fn = scr_ni_export_value(id);
if (fn == NULL) {
scr_throw_error_msg(SCR_ERR_TYPE, "native function is gone", 23);
return ni_threw(out);
}
scr_dyn_retain(fn);
ScrDyn *small[8];
ScrDyn **in = ni_args_in(args, argc, small, 8);
ScrDyn *self = this_ != NULL ? scr_ni_value_in(this_) : scr_dyn_retain(scr_dyn_undefined());
scr_dyn_this_push_dyn(self);
ScrDyn *result = scr_dyn_call(fn, in, argc, "callback");
scr_dyn_this_pop();
scr_dyn_release(self);
ni_args_free(in, argc, small);
scr_dyn_release(fn);
if (result == NULL || scr_exc_pending()) {
scr_dyn_release(result);
return ni_threw(out);
}
return ni_returned(result, out);
}
int scr_ni_host_export_construct(uint32_t id, const ScrNodeValue *args, size_t argc,
ScrNodeValue *out) {
ni_keep_set(NULL);
ScrDyn *ctor = scr_ni_export_value(id);
if (ctor == NULL) {
scr_throw_error_msg(SCR_ERR_TYPE, "native function is gone", 23);
return ni_threw(out);
}
scr_dyn_retain(ctor);
ScrDyn *list = scr_dyn_new_arr();
for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(list, scr_ni_value_in(&args[i]));
ScrStr *what = scr_str_new("constructor", 11);
ScrDyn *result = scr_dyn_construct(ctor, list, what);
scr_str_release(what);
scr_dyn_release(list);
scr_dyn_release(ctor);
if (result == NULL || scr_exc_pending()) {
scr_dyn_release(result);
return ni_threw(out);
}
return ni_returned(result, out);
}
/* ── class method tables ──────────────────────────────────────────────
* A class instance's capsule holds its fields; its prototype methods come
* from a table the compiler's builder makes once per class (name -> the
* unbound method value, a dyn function whose thunk checks the receiver),
* keyed by the capsule's type key. */
typedef struct {
const char *key;
size_t key_len;
ScrDyn *table; /* +1 dyn object */
} NiMethods;
static NiMethods *ni_methods = NULL;
static size_t ni_nmethods = 0, ni_methods_cap = 0;
static ScrDyn *ni_methods_of(const ScrDyn *v) {
if (v->kind != SCR_DYN_TYPED_REF || v->v.typed_ref.type_key == NULL) return NULL;
const char *key = v->v.typed_ref.type_key;
size_t len = v->v.typed_ref.type_key_len;
for (size_t i = 0; i < ni_nmethods; i++)
if (ni_methods[i].key_len == len && memcmp(ni_methods[i].key, key, len) == 0)
return ni_methods[i].table;
return NULL;
}
ScrDyn *scr_node_island_class_methods(ScrDyn *value, ScrDyn *builder) {
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key != NULL &&
ni_methods_of(value) == NULL) {
ScrDyn *table = scr_dyn_call(builder, NULL, 0, "class methods");
if (table == NULL) return NULL;
if (ni_nmethods == ni_methods_cap) {
ni_methods_cap = ni_methods_cap ? ni_methods_cap * 2 : 8;
NiMethods *grown = realloc(ni_methods, ni_methods_cap * sizeof *grown);
if (grown == NULL) ni_export_oom();
ni_methods = grown;
}
ni_methods[ni_nmethods].key = value->v.typed_ref.type_key; /* static */
ni_methods[ni_nmethods].key_len = value->v.typed_ref.type_key_len;
ni_methods[ni_nmethods].table = table;
ni_nmethods++;
}
return scr_dyn_retain(value);
}
/* A prototype method of a class capsule (+1), or NULL. An own property the
* island wrote under the method's name (kept in the capsule's materialized
* view) hides the method, as an own property hides a prototype's in Node. */
static ScrDyn *ni_class_method(const ScrDyn *target, const ScrDyn *key) {
if (key->kind != SCR_DYN_STR) return NULL;
ScrDyn *table = ni_methods_of(target);
if (table == NULL) return NULL;
const ScrDyn *view = target->v.typed_ref.materialized;
if (view != NULL && view->kind == SCR_DYN_OBJ &&
scr_dyn_obj_get(view, key->v.str->data, key->v.str->len) != NULL)
return NULL;
ScrDyn *m = scr_dyn_obj_get(table, key->v.str->data, key->v.str->len);
return m != NULL ? scr_dyn_retain(m) : NULL;
}
int scr_ni_host_export_get(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out) {
ni_keep_set(NULL);
ScrDyn *target = scr_ni_export_value(id);
if (target == NULL) return 2;
ScrDyn *k = scr_ni_value_in(key);
ScrDyn *pk = scr_dyn_property_key_value(k);
if (pk == NULL) {
scr_dyn_release(k);
return ni_threw(out);
}
int present = 0;
if (pk->kind == SCR_DYN_STR) present = scr_dyn_in(target, pk->v.str);
if (!present && !scr_exc_pending()) {
scr_dyn_release(pk);
scr_dyn_release(k);
return 2;
}
ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_reflect_get(target, pk, target);
if (result != NULL && result->kind == SCR_DYN_UNDEF) {
ScrDyn *method = ni_class_method(target, pk);
if (method != NULL) {
scr_dyn_release(result);
result = method;
}
}
scr_dyn_release(pk);
scr_dyn_release(k);
if (result == NULL || scr_exc_pending()) {
scr_dyn_release(result);
return ni_threw(out);
}
return ni_returned(result, out);
}
int scr_ni_host_export_set(uint32_t id, const ScrNodeValue *key, const ScrNodeValue *value,
ScrNodeValue *out) {
ni_keep_set(NULL);
ScrDyn *target = scr_ni_export_value(id);
if (target == NULL) return 2;
ScrDyn *k = scr_ni_value_in(key);
ScrDyn *v = scr_ni_value_in(value);
bool ok = scr_dyn_reflect_set(target, k, v, target);
scr_dyn_release(v);
scr_dyn_release(k);
if (scr_exc_pending()) return ni_threw(out);
(void)ok; /* a refused write is silent outside strict mode; Node's setter
* interceptor reports the same */
memset(out, 0, sizeof *out);
return 0;
}
int scr_ni_host_export_has(uint32_t id, const ScrNodeValue *key) {
ScrDyn *target = scr_ni_export_value(id);
if (target == NULL) return 0;
ScrDyn *k = scr_ni_value_in(key);
ScrDyn *pk = scr_dyn_property_key_value(k);
int present = 0;
if (pk != NULL && pk->kind == SCR_DYN_STR) present = scr_dyn_in(target, pk->v.str);
if (scr_exc_pending()) scr_exc_clear();
scr_dyn_release(pk);
scr_dyn_release(k);
return present;
}
int scr_ni_host_export_delete(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out) {
ni_keep_set(NULL);
ScrDyn *target = scr_ni_export_value(id);
if (target == NULL) return 2;
ScrDyn *k = scr_ni_value_in(key);
scr_dyn_key_delete_computed(target, k, false);
scr_dyn_release(k);
if (scr_exc_pending()) return ni_threw(out);
memset(out, 0, sizeof *out);
return 0;
}
/* Own enumerable string keys, in JS order, as an array value. */
int scr_ni_host_export_keys(uint32_t id, ScrNodeValue *out) {
ni_keep_set(NULL);
ScrDyn *target = scr_ni_export_value(id);
ScrDyn *keys = target != NULL ? scr_dyn_obj_keys(target) : scr_dyn_new_arr();
if (keys == NULL || scr_exc_pending()) {
scr_dyn_release(keys);
return ni_threw(out);
}
/* Keys cross as a JSON copy: an array of strings. */
ScrStr *gap = scr_str_new("", 0);
ScrDyn *json = scr_json_stringify_replacer(keys, scr_dyn_undefined(), gap);
scr_str_release(gap);
scr_dyn_release(keys);
if (json == NULL || json->kind != SCR_DYN_STR) {
scr_dyn_release(json);
if (scr_exc_pending()) return ni_threw(out);
json = scr_dyn_new_str(scr_str_new("[]", 2));
}
memset(out, 0, sizeof *out);
out->tag = SCR_NV_JSON;
out->u.str.data = json->v.str->data;
out->u.str.len = json->v.str->len;
ni_keep_set(json);
return 0;
}
void scr_ni_host_export_release(uint32_t id) {
if (id == 0 || id >= ni_exports_len || ni_exports[id].value == NULL) return;
ScrDyn *v = ni_exports[id].value;
ni_ids_del(ni_exports[id].identity);
if (v->kind == SCR_DYN_PROMISE) scr_promise_identity_drop(ni_exports[id].identity);
ni_exports[id].value = NULL;
ni_exports[id].identity = NULL;
if (ni_nfree == ni_free_cap) {
uint32_t cap = ni_free_cap ? ni_free_cap * 2 : 64;
uint32_t *grown = realloc(ni_free, cap * sizeof *grown);
if (grown == NULL) ni_export_oom();
ni_free = grown;
ni_free_cap = cap;
}
ni_free[ni_nfree++] = id;
scr_dyn_release(v);
}
/* ── promises into Node ───────────────────────────────────────────────── */
typedef struct {
uint32_t id;
int rejected;
const ScrNodeValue *value;
} NiSettle;
static void ni_settle_job(void *p) {
NiSettle *a = p;
scr_ni_bridge_loaded()->export_settle(a->id, a->rejected, a->value);
}
void scr_ni_bridge_export_settle(uint32_t id, int rejected, const ScrNodeValue *value) {
if (scr_ni_bridge_loaded() == NULL) return;
NiSettle a = {id, rejected, value};
scr_ni_on_node_stack(ni_settle_job, &a);
}
/* Settles the V8 promise of export `id` from the settled runtime promise. */
static void ni_settle_export(uint32_t id, ScrPromise *p) {
ScrNodeValue value;
ScrDyn *payload;
int rejected = scr_promise_is_rejected(p);
if (rejected) {
payload = scr_promise_reason_dyn(p);
} else if (scr_promise_payload_is_dyn(p)) {
payload = (ScrDyn *)scr_promise_payload_ref(p);
} else {
payload = NULL; /* a void fulfillment */
}
if (payload == NULL) payload = scr_dyn_retain(scr_dyn_undefined());
if (!scr_ni_value_out(payload, &value)) {
/* The payload cannot cross: the promise rejects with that error. */
scr_dyn_release(payload);
ScrCaught *caught = scr_exc_take();
payload = scr_caught_to_dyn(caught);
scr_caught_release(caught);
rejected = 1;
if (payload == NULL || !scr_ni_value_out(payload, &value)) {
scr_exc_clear();
memset(&value, 0, sizeof value);
}
}
scr_ni_bridge_export_settle(id, rejected, &value);
scr_dyn_release(payload);
}
static void ni_promise_settled(ScrPromise *dst, ScrPromise *src) {
(void)dst;
uint32_t id = ni_ids_get(scr_promise_identity_key(src));
if (id != 0) ni_settle_export(id, src);
}
/* The bridge created the V8 promise of export `id`: it settles inside the
* runtime promise's settlement, or now when that already happened. The
* runtime promise counts as handled from here on; the V8 promise carries
* Node's own unhandled-rejection tracking. */
void scr_ni_host_export_watch(uint32_t id) {
ScrDyn *v = scr_ni_export_value(id);
if (v == NULL || v->kind != SCR_DYN_PROMISE) return;
ScrPromise *p = v->v.promise;
scr_promise_mark_handled(p);
if (!scr_promise_is_pending(p)) {
ni_settle_export(id, p);
return;
}
scr_promise_on_settle_sync(p, ni_promise_settled);
}
/* At exit, once Node stopped: everything the export table and the class
* tables hold (the RC audit counts them). */
void scr_ni_exports_release_all(void) {
ni_keep_set(NULL);
for (uint32_t i = 1; i < ni_exports_len; i++) {
ScrDyn *v = ni_exports[i].value;
if (v == NULL) continue;
if (v->kind == SCR_DYN_PROMISE) scr_promise_identity_drop(ni_exports[i].identity);
ni_exports[i].value = NULL;
scr_dyn_release(v);
}
ni_exports_len = 1;
ni_nfree = 0;
free(ni_ids);
ni_ids = NULL;
ni_ids_cap = ni_ids_len = ni_ids_dead = 0;
for (size_t i = 0; i < ni_nmethods; i++) scr_dyn_release(ni_methods[i].table);
ni_nmethods = 0;
}
#endif
+136 -40
View File
@@ -330,56 +330,78 @@ static ScrDyn *nh_from_node(const ScrNodeValue *v) {
return d;
}
case SCR_NV_BYTES: return nh_bytes_box(v);
case SCR_NV_EXPORT: {
/* A native value's representative came back: the value itself. */
ScrDyn *d = scr_ni_export_value(v->u.exp.id);
return d != NULL ? scr_dyn_retain(d) : scr_dyn_undefined();
}
default: return scr_dyn_undefined();
}
}
/* The ScrError of an island Error handle (+1): the one already pinned to
* it, else a new one of the nearest intrinsic class, pinned. */
static ScrError *nh_error_of(const ScrNodeValue *v) {
ScrNodeRef *ref = nh_ref(v->u.handle, v->flags);
ScrError *pinned = nh_pinned_obj(ref, NH_PIN_ERROR);
if (pinned != NULL) {
nh_ref_release(ref);
return scr_error_retain(pinned);
}
ScrNodeErrorInfo info;
nh_bridge()->error_info(ref->id, &info);
ScrStr *message = nh_str(info.message ? info.message : "", info.message_len);
ScrError *e = scr_error_new((int)info.kind, message);
scr_str_release(message);
if (info.name != NULL) {
ScrStr *name = nh_str(info.name, info.name_len);
ScrStr *deflt = scr_error_default_name(e);
bool differs = deflt == NULL || deflt->len != name->len ||
memcmp(deflt->data, name->data, name->len) != 0;
if (deflt != NULL) scr_str_release(deflt);
if (differs) {
scr_str_release(e->name);
e->name = name;
e->name_present = true;
} else {
scr_str_release(name);
}
}
if (info.stack != NULL) {
ScrStr *stack = nh_str(info.stack, info.stack_len);
scr_error_set_stack(e, stack);
scr_str_release(stack);
}
if (info.code != NULL) {
ScrStr *code = nh_str(info.code, info.code_len);
scr_error_set_code_value(e, code);
scr_str_release(code);
}
nh_pin(e, NH_PIN_ERROR, ref);
nh_ref_release(ref);
return e;
}
/* An argument of a native callback (scr_node_island_export.c): island
* Errors arrive as their pinned ScrError, as thrown ones do, so typed code
* sees `instanceof RangeError`, `message` and `code`. +1. */
ScrDyn *scr_ni_arg_in(const ScrNodeValue *v) {
if (v->tag == SCR_NV_HANDLE && (v->flags & SCR_NVF_ERROR)) {
ScrError *e = nh_error_of(v);
ScrDyn *d = scr_dyn_from_error(e);
scr_error_release(e);
return d;
}
return scr_ni_value_in(v);
}
/* Leaves a thrown island value pending as a scriptc exception. An Error
* becomes a ScrError of the nearest intrinsic class with V8's name,
* message, stack and string code, pinned to the V8 error so that it is
* that error again if it crosses back. */
static void nh_throw_value(const ScrNodeValue *v) {
if (v->tag == SCR_NV_HANDLE && (v->flags & SCR_NVF_ERROR)) {
ScrNodeRef *ref = nh_ref(v->u.handle, v->flags);
/* An island error the program already holds is that ScrError again. */
ScrError *known = nh_pinned_obj(ref, NH_PIN_ERROR);
if (known != NULL) {
nh_ref_release(ref);
scr_throw_obj(scr_error_retain(known), &scr_error_retain_v, &scr_error_release_v,
scr_error_trace_arg());
return;
}
ScrNodeErrorInfo info;
nh_bridge()->error_info(ref->id, &info);
ScrStr *message = nh_str(info.message ? info.message : "", info.message_len);
ScrError *e = scr_error_new((int)info.kind, message);
scr_str_release(message);
if (info.name != NULL) {
ScrStr *name = nh_str(info.name, info.name_len);
ScrStr *deflt = scr_error_default_name(e);
bool differs = deflt == NULL || deflt->len != name->len ||
memcmp(deflt->data, name->data, name->len) != 0;
if (deflt != NULL) scr_str_release(deflt);
if (differs) {
scr_str_release(e->name);
e->name = name;
e->name_present = true;
} else {
scr_str_release(name);
}
}
if (info.stack != NULL) {
ScrStr *stack = nh_str(info.stack, info.stack_len);
scr_error_set_stack(e, stack);
scr_str_release(stack);
}
if (info.code != NULL) {
ScrStr *code = nh_str(info.code, info.code_len);
scr_error_set_code_value(e, code);
scr_str_release(code);
}
nh_pin(e, NH_PIN_ERROR, ref);
nh_ref_release(ref);
ScrError *e = nh_error_of(v);
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
return;
}
@@ -462,6 +484,12 @@ SCR_COLD static bool nh_refuse(const char *what) {
* native objects and arrays). Until then: plain dyn objects and arrays
* cross as a JSON copy (the NI-4 shortcut), anything else is refused. */
static bool nh_export_value(ScrDyn *v, ScrNodeValue *out, NhKeep *keep) {
/* NI-5: closures, promises, records, class instances and dyn objects
* cross by reference (scr_node_island_export.c). */
if (scr_ni_exportable(v)) {
scr_ni_export_out(v, out);
return true;
}
if (v->kind == SCR_DYN_OBJ || v->kind == SCR_DYN_ARR) {
ScrStr *gap = scr_str_new("", 0);
ScrDyn *json = scr_json_stringify_replacer(v, scr_dyn_undefined(), gap);
@@ -989,6 +1017,74 @@ ScrStr *scr_ni_inspect(const ScrDyn *value, double recurse, double depth) {
return s;
}
/* The conversions for scr_node_island_export.c: what a value_out keeps
* alive lives until the next value_out (the bridge reads it at once). */
static NhKeep nh_out_keep;
bool scr_ni_value_out(ScrDyn *v, ScrNodeValue *out) {
NhKeep old = nh_out_keep;
memset(&nh_out_keep, 0, sizeof nh_out_keep);
nh_keep_done(&old);
return nh_to_node(v, out, &nh_out_keep);
}
ScrDyn *scr_ni_value_in(const ScrNodeValue *v) {
ScrDyn *d = nh_from_node(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;
@@ -56,6 +56,65 @@ 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)) {
// Own enumerable keys, as structuredClone copies them: holes stay holes
// and named properties come along.
const out = new Array(value.length);
memo.set(value, out);
for (const key of Object.keys(value)) out[key] = plainCopy(value[key], 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) {
@@ -62,6 +62,65 @@ 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"
" // Own enumerable keys, as structuredClone copies them: holes stay holes\012"
" // and named properties come along.\012"
" const out = new Array(value.length);\012"
" memo.set(value, out);\012"
" for (const key of Object.keys(value)) out[key] = plainCopy(value[key], 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"
@@ -1,4 +1,5 @@
// @requires-libnode
// @order: race
// X { T 0 { Z { L i-zlib } } } T 2 { B 80 L h-sync }
import { callSync } from "@scriptc/node";
import { readFile, writeSync } from "node:fs";
@@ -0,0 +1,40 @@
"use strict";
const { EventEmitter } = require("node:events");
class Bus extends EventEmitter {
fire(n) {
const delivered = this.emit("data", n, `item-${n}`);
return delivered;
}
}
function each(items, fn) {
const out = [];
for (let i = 0; i < items.length; i++) out.push(fn(items[i], i));
return out;
}
function compute(n, callback) {
setTimeout(() => {
if (n < 0) callback(new RangeError(`negative: ${n}`));
else callback(null, n * n);
}, 5);
}
function guarded(fn, value) {
try {
return `ok ${fn(value)}`;
} catch (e) {
return `caught ${e.name}: ${e.message}`;
}
}
function twice(fn) {
return [fn(1), fn(2)];
}
function same(a, b) {
return a === b;
}
module.exports = { Bus, each, compute, guarded, twice, same };
@@ -0,0 +1,53 @@
// @requires-libnode
// Native closures passed into a Node island run as V8 functions: event
// listeners, synchronous callbacks, Node-style async callbacks, and
// exceptions crossing both ways.
import { requireModule } from "@scriptc/node";
const { Bus, compute, each, guarded, same, twice } = requireModule("./lib.cjs");
const bus = new Bus();
let total = 0;
const listener = (n, label) => {
total += n;
console.log("listener", n, label);
};
bus.on("data", listener);
bus.once("data", (n) => console.log("once", n));
console.log("delivered", bus.fire(3));
console.log("delivered", bus.fire(4));
console.log("count", bus.listenerCount("data"), "total", total);
bus.off("data", listener);
console.log("delivered", bus.fire(5));
console.log(each([1, 2, 3], (x, i) => x * 10 + i).join(","));
console.log(twice((n) => `n=${n}`).join(" "));
// A native throw propagates through island frames, where JS can catch it.
console.log(guarded((v) => {
if (v > 1) throw new RangeError(`too big: ${v}`);
return v;
}, 2));
console.log(guarded((v) => v + 1, 1));
// An exception thrown natively and not caught by the island reaches the caller.
bus.on("boom", () => {
throw new TypeError("native boom");
});
try {
bus.emit("boom");
} catch (e) {
console.log("caught", e instanceof TypeError, e.message);
}
// The same closure crosses as the same function.
const f = (n) => `${n}`;
console.log("same", same(f, f));
compute(6, (err, value) => {
console.log("compute", err, value);
compute(-1, (err2, value2) => {
console.log("compute", err2 instanceof RangeError, err2 && err2.message, value2);
});
});
console.log("end of main");
@@ -0,0 +1,35 @@
"use strict";
function log(s) { console.log(`island: ${s}`); }
async function later(label) {
log(`later ${label} start`);
await null;
log(`later ${label} resumed`);
return `${label}!`;
}
function resolved(v) { return Promise.resolve(v); }
function rejectsWith(message) { return Promise.reject(new TypeError(message)); }
// Awaits a promise the caller passes in, interleaving its own microtasks.
async function consume(p) {
Promise.resolve().then(() => log("tick A"));
const v = await p;
log(`consumed ${v}`);
return v * 2;
}
function chain(p) {
return p.then((v) => { log(`then ${v}`); return v + 1; });
}
function isPromise(p) { return p instanceof Promise && typeof p.then === "function"; }
function timer(ms, v) { return new Promise((r) => setTimeout(() => r(v), ms)); }
async function catches(p) {
try { await p; return "no error"; } catch (e) { return `caught ${e.name}: ${e.message}`; }
}
module.exports = { later, resolved, rejectsWith, consume, chain, isPromise, timer, catches };
@@ -0,0 +1,45 @@
// @requires-libnode
// Promises both ways on one microtask queue (scope-B NI-5): island
// promises awaited natively and native promises awaited in the island
// interleave with both sides' reactions exactly as in Node.
import { requireModule } from "@scriptc/node";
const { catches, chain, consume, isPromise, later, rejectsWith, resolved, timer } =
requireModule("./lib.cjs");
async function nativeValue(n) {
await null;
console.log("native resolving", n);
return n;
}
async function main() {
console.log("start");
Promise.resolve().then(() => console.log("native tick 1"));
const p = later("a");
Promise.resolve().then(() => console.log("native tick 2"));
console.log("awaited", await p);
console.log("resolved", await resolved(41));
try {
await rejectsWith("bad input");
} catch (e) {
console.log("rejected", e instanceof TypeError, e.message);
}
// A native promise passed into the island is a real Promise there.
const mine = nativeValue(21);
console.log("isPromise", isPromise(mine));
Promise.resolve().then(() => console.log("native tick 3"));
console.log("consume", await consume(mine));
console.log("chain", await chain(nativeValue(1)));
console.log("catches", await catches(Promise.reject(new RangeError("native rejection"))));
console.log("catches", await catches(nativeValue(2)));
const [x, y] = await Promise.all([timer(10, "slow"), timer(1, "fast")]);
console.log("all", x, y);
console.log("timer", await timer(2, "early"));
console.log("end");
}
main();
@@ -0,0 +1,26 @@
"use strict";
const kept = [];
function describe(o) {
return Object.keys(o).map((k) => `${k}=${o[k]}`).join(" ");
}
function bump(o, by) {
o.count += by;
o.label = `${o.label}+`;
return o.count;
}
function keep(o) { kept.push(o); return kept.length; }
function keptAt(i) { return kept[i]; }
function same(a, b) { return a === b; }
function has(o, k) { return k in o; }
function readThrough(o) { return `${o.greeting}, island (${Object.keys(o).join(",")})`; }
function store(o) { return { inner: o }; }
// Reads a property from a Node timer: outside any call from the program.
function readLater(o) {
return new Promise((resolve) => setTimeout(() => resolve(`${o.label}/${o.count}`), 0));
}
module.exports = { describe, bump, keep, keptAt, same, has, readThrough, store, readLater };
@@ -0,0 +1,40 @@
// @requires-libnode
// Native objects and class instances passed into a Node island are live
// objects there (scope-B NI-5): island reads and writes reach the native
// storage, and the same native object comes back as itself.
import { requireModule } from "@scriptc/node";
const { bump, describe, has, keep, keptAt, readLater, readThrough, same, store } =
requireModule("./lib.cjs");
class Greeter {
constructor(greeting) {
this.greeting = greeting;
}
greet(who) {
return `${this.greeting}, ${who}`;
}
}
const counter = { count: 1, label: "c" };
console.log(describe(counter));
console.log("bump", bump(counter, 2), bump(counter, 3));
console.log("native sees", counter.count, counter.label);
counter.count = 100;
console.log(describe(counter));
console.log("kept", keep(counter));
const back = keptAt(0);
console.log("identity", back === counter, same(counter, counter), same(counter, { ...counter }));
back.label = "via island";
console.log("label", counter.label);
console.log("has", has(counter, "count"), has(counter, "missing"));
const g = new Greeter("Hello");
console.log(readThrough(g));
g.greeting = "Hi";
console.log(readThrough(g));
const boxed = store(g);
console.log("boxed", boxed.inner === g, boxed.inner.greet("native"));
console.log("later", await readLater(counter));
@@ -0,0 +1,40 @@
"use strict";
const { EventEmitter } = require("node:events");
class Bus extends EventEmitter {
fire(n) {
const delivered = this.emit("data", n, `item-${n}`);
return delivered;
}
}
function each(items, fn) {
const out = [];
for (let i = 0; i < items.length; i++) out.push(fn(items[i], i));
return out;
}
function compute(n, callback) {
setTimeout(() => {
if (n < 0) callback(new RangeError(`negative: ${n}`));
else callback(null, n * n);
}, 5);
}
function guarded(fn, value) {
try {
return `ok ${fn(value)}`;
} catch (e) {
return `caught ${e.name}: ${e.message}`;
}
}
function twice(fn) {
return [fn(1), fn(2)];
}
function same(a, b) {
return a === b;
}
module.exports = { Bus, each, compute, guarded, twice, same };
@@ -0,0 +1,54 @@
// @requires-libnode
// Typed native closures passed into a Node island run as V8 functions
// whose arguments meet the declared parameter types (scope-B NI-5): event
// listeners, synchronous callbacks, Node-style async callbacks, and
// exceptions crossing both ways.
import { requireModule } from "@scriptc/node";
const { Bus, compute, each, guarded, same, twice } = requireModule("./lib.cjs");
const bus = new Bus();
let total = 0;
const listener = (n: number, label: string): void => {
total += n;
console.log("listener", n, label);
};
bus.on("data", listener);
bus.once("data", (n: number) => console.log("once", n));
console.log("delivered", bus.fire(3));
console.log("delivered", bus.fire(4));
console.log("count", bus.listenerCount("data"), "total", total);
bus.off("data", listener);
console.log("delivered", bus.fire(5));
console.log(each([1, 2, 3], (x: number, i: number) => x * 10 + i).join(","));
console.log(twice((n: number) => `n=${n}`).join(" "));
// A native throw propagates through island frames, where JS can catch it.
console.log(guarded((v: number) => {
if (v > 1) throw new RangeError(`too big: ${v}`);
return v;
}, 2));
console.log(guarded((v: number) => v + 1, 1));
// An exception thrown natively and not caught by the island reaches the caller.
bus.on("boom", () => {
throw new TypeError("native boom");
});
try {
bus.emit("boom");
} catch (e) {
console.log("caught", e instanceof TypeError, (e as Error).message);
}
// The same closure crosses as the same function.
const f = (n: number): string => `${n}`;
console.log("same", same(f, f));
compute(6, (err: Error | null, value?: number) => {
console.log("compute", err, value);
compute(-1, (err2: Error | null, value2?: number) => {
console.log("compute", err2 instanceof RangeError, err2?.message, value2);
});
});
console.log("end of main");
@@ -0,0 +1,51 @@
// @requires-libnode
// Native closures passed into a Node island run as V8 functions: event
// listeners, synchronous callbacks, Node-style async callbacks, and
// exceptions crossing both ways.
import { Bus, compute, each, guarded, same, twice } from "emitter-pkg";
const bus = new Bus();
let total = 0;
const listener = (n: number, label: string): void => {
total += n;
console.log("listener", n, label);
};
bus.on("data", listener);
bus.once("data", (n: number) => console.log("once", n));
console.log("delivered", bus.fire(3));
console.log("delivered", bus.fire(4));
console.log("count", bus.listenerCount("data"), "total", total);
bus.off("data", listener);
console.log("delivered", bus.fire(5));
console.log(each(3, (x: number, i: number) => x * 10 + i));
console.log(twice((n: number) => `n=${n}`));
// A native throw propagates through island frames, where JS can catch it.
console.log(guarded((v: number) => {
if (v > 1) throw new RangeError(`too big: ${v}`);
return v;
}, 2));
console.log(guarded((v: number) => v + 1, 1));
// An exception thrown natively and not caught by the island reaches the caller.
bus.on("boom", () => {
throw new TypeError("native boom");
});
try {
bus.emit("boom");
} catch (e) {
console.log("caught", e instanceof TypeError, (e as Error).message);
}
// The same closure crosses as the same function.
const f = (n: number): string => `${n}`;
console.log("same", same(f, f));
compute(6, (err: Error | null, value?: number) => {
console.log("compute", err, value);
compute(-1, (err2: Error | null, value2?: number) => {
console.log("compute", err2 instanceof RangeError, err2?.message, value2);
});
});
console.log("end of main");
@@ -0,0 +1,9 @@
import { EventEmitter } from "node:events";
export declare class Bus extends EventEmitter {
fire(n: number): boolean;
}
export declare function each(count: number, fn: (item: number, index: number) => number): string;
export declare function compute(n: number, callback: (err: Error | null, value?: number) => void): void;
export declare function guarded(fn: (value: number) => number, value: unknown): string;
export declare function twice(fn: (n: number) => string): string;
export declare function same(a: unknown, b: unknown): boolean;
@@ -0,0 +1,40 @@
"use strict";
const { EventEmitter } = require("node:events");
class Bus extends EventEmitter {
fire(n) {
const delivered = this.emit("data", n, `item-${n}`);
return delivered;
}
}
function each(count, fn) {
const out = [];
for (let i = 0; i < count; i++) out.push(fn(i + 1, i));
return out.join(",");
}
function compute(n, callback) {
setTimeout(() => {
if (n < 0) callback(new RangeError(`negative: ${n}`));
else callback(null, n * n);
}, 5);
}
function guarded(fn, value) {
try {
return `ok ${fn(value)}`;
} catch (e) {
return `caught ${e.name}: ${e.message}`;
}
}
function twice(fn) {
return [fn(1), fn(2)].join(" ");
}
function same(a, b) {
return a === b;
}
module.exports = { Bus, each, compute, guarded, twice, same };
@@ -0,0 +1 @@
{ "name": "emitter-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-callbacks", "private": true, "type": "module", "scriptc": { "packages": { "emitter-pkg": "node" } } }
@@ -0,0 +1,55 @@
// @requires-libnode
// Promises both ways on one microtask queue: island promises awaited
// natively and native promises awaited in the island interleave with
// both sides' reactions exactly as in Node.
import {
catches,
chain,
consume,
isPromise,
later,
rejectsWith,
resolved,
samePromise,
timer,
} from "async-pkg";
async function nativeValue(n: number): Promise<number> {
await null;
console.log("native resolving", n);
return n;
}
async function main(): Promise<void> {
console.log("start");
Promise.resolve().then(() => console.log("native tick 1"));
const p = later("a");
Promise.resolve().then(() => console.log("native tick 2"));
console.log("awaited", await p);
console.log("resolved", await resolved(41));
try {
await rejectsWith("bad input");
} catch (e) {
console.log("rejected", e instanceof TypeError, (e as Error).message);
}
// A native promise passed into the island is a real Promise there.
const mine = nativeValue(21);
console.log("isPromise", isPromise(mine));
Promise.resolve().then(() => console.log("native tick 3"));
console.log("consume", await consume(mine));
console.log("chain", await chain(nativeValue(1)));
console.log("catches", await catches(Promise.reject(new RangeError("native rejection"))));
console.log("catches", await catches(nativeValue(2)));
const twice = nativeValue(3);
console.log("same promise", samePromise(twice, twice));
await twice;
const [x, y] = await Promise.all([timer(10, "slow"), timer(1, "fast")]);
console.log("all", x, y);
console.log("race", await Promise.race([timer(20, "late"), timer(2, "early")]));
console.log("end");
}
main();
@@ -0,0 +1,9 @@
export declare function later(label: string): Promise<string>;
export declare function resolved<T>(v: T): Promise<T>;
export declare function rejectsWith(message: string): Promise<void>;
export declare function consume(p: Promise<number>): Promise<number>;
export declare function chain(p: Promise<number>): Promise<number>;
export declare function isPromise(p: unknown): boolean;
export declare function timer<T>(ms: number, v: T): Promise<T>;
export declare function catches(p: Promise<unknown>): Promise<string>;
export declare function samePromise(a: Promise<number>, b: Promise<number>): boolean;
@@ -0,0 +1,39 @@
"use strict";
function log(s) { console.log(`island: ${s}`); }
async function later(label) {
log(`later ${label} start`);
await null;
log(`later ${label} resumed`);
return `${label}!`;
}
function resolved(v) { return Promise.resolve(v); }
function rejectsWith(message) { return Promise.reject(new TypeError(message)); }
// Awaits a promise the caller passes in, interleaving its own microtasks.
async function consume(p) {
Promise.resolve().then(() => log("tick A"));
const v = await p;
log(`consumed ${v}`);
return v * 2;
}
function chain(p) {
return p.then((v) => { log(`then ${v}`); return v + 1; });
}
function isPromise(p) { return p instanceof Promise && typeof p.then === "function"; }
function timer(ms, v) { return new Promise((r) => setTimeout(() => r(v), ms)); }
async function catches(p) {
try { await p; return "no error"; } catch (e) { return `caught ${e.name}: ${e.message}`; }
}
function samePromise(a, b) {
return a === b;
}
module.exports = { later, resolved, rejectsWith, consume, chain, isPromise, timer, catches, samePromise };
@@ -0,0 +1 @@
{ "name": "async-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-promises", "private": true, "type": "module", "scriptc": { "packages": { "async-pkg": "node" } } }
@@ -0,0 +1,42 @@
// @requires-libnode
// Native records and class instances passed into a Node island are live
// objects there: island reads and writes reach the native storage, and the
// same native object comes back as itself.
import { bump, describe, has, keep, keptAt, readThrough, same, store } from "obj-pkg";
interface Counter {
count: number;
label: string;
}
class Greeter {
greeting: string;
constructor(greeting: string) {
this.greeting = greeting;
}
greet(who: string): string {
return `${this.greeting}, ${who}`;
}
}
const counter: Counter = { count: 1, label: "c" };
console.log(describe(counter));
console.log("bump", bump(counter, 2), bump(counter, 3));
console.log("native sees", counter.count, counter.label);
counter.count = 100;
console.log(describe(counter));
console.log("kept", keep(counter));
const back = keptAt<Counter>(0);
console.log("identity", back === counter, same(counter, counter), same(counter, { count: counter.count, label: counter.label }));
back.label = "via island";
console.log("label", counter.label);
console.log("has", has(counter, "count"), has(counter, "missing"));
const g = new Greeter("Hello");
console.log(readThrough(g));
g.greeting = "Hi";
console.log(readThrough(g));
const boxed = store(g);
console.log("boxed", boxed.inner === g, boxed.inner.greet("native"));
@@ -0,0 +1,8 @@
export declare function describe(o: object): string;
export declare function bump(o: { count: number; label: string }, by: number): number;
export declare function keep(o: object): number;
export declare function keptAt<T>(i: number): T;
export declare function same(a: unknown, b: unknown): boolean;
export declare function has(o: object, k: string): boolean;
export declare function readThrough(o: { greet(who: string): string }): string;
export declare function store<T>(o: T): { inner: T };
@@ -0,0 +1,21 @@
"use strict";
const kept = [];
function describe(o) {
return Object.keys(o).map((k) => `${k}=${o[k]}`).join(" ");
}
function bump(o, by) {
o.count += by;
o.label = `${o.label}+`;
return o.count;
}
function keep(o) { kept.push(o); return kept.length; }
function keptAt(i) { return kept[i]; }
function same(a, b) { return a === b; }
function has(o, k) { return k in o; }
function readThrough(o) { return o.greet("island"); }
function store(o) { return { inner: o }; }
module.exports = { describe, bump, keep, keptAt, same, has, readThrough, store };
@@ -0,0 +1 @@
{ "name": "obj-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-objects", "private": true, "type": "module", "scriptc": { "packages": { "obj-pkg": "node" } } }
@@ -0,0 +1,42 @@
// @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,
cloneSparse,
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]));
console.log(cloneFails());
console.log(cloneSparse({ kind: "native" }));
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,7 @@
export declare function cloneClaims(payload: object): string;
export declare function cloneNested(value: unknown): string;
export declare function cloneFails(): string;
export declare function cloneSparse(native: object): string;
export declare function listFiles(dir: string): Promise<string[]>;
export declare function names(): string[];
export declare function matrix(): Promise<number[][]>;
@@ -0,0 +1,36 @@
"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]];
}
function cloneSparse(native) {
const list = [1, , 3];
list.extra = "x";
const copy = structuredClone({ native, list });
return `sparse ${copy.list.length} ${1 in copy.list} ${copy.list.extra} ${copy.native.kind}`;
}
module.exports = { cloneClaims, cloneNested, cloneFails, cloneSparse, 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,30 @@
// @requires-libnode
// A native class instance passed into a Node island shows the island only
// its public members: its #private methods, and the names the compiler
// gives accessors and symbol-keyed methods, read as undefined, as in Node.
import { probe, shadow } from "private-pkg";
class Account {
#balance = 10;
owner: string;
constructor(owner: string) {
this.owner = owner;
}
#secret(): string {
return `secret of ${this.owner}: ${this.#balance}`;
}
get label(): string {
return `account ${this.owner}`;
}
describe(): string {
return `${this.owner} (${this.#secret().length})`;
}
*[Symbol.iterator](): Generator<string> {
yield this.owner;
}
}
const a = new Account("ada");
console.log(probe(a));
console.log(a.describe(), [...a].join(","));
console.log(shadow(new Account("bob")));
@@ -0,0 +1,2 @@
export declare function probe(o: object): string;
export declare function shadow(o: object): string;
@@ -0,0 +1,22 @@
"use strict";
// Reads a native instance's #private method and internal member names by
// string: an island sees only its public members.
function probe(o) {
const out = [typeof o["#secret"], "#secret" in o];
// A property with the private method's spelling, holding undefined.
o["#secret"] = undefined;
const m = o["#secret"];
out.push(typeof m, "#secret" in o);
if (typeof m === "function") out.push(m.call(o));
out.push(typeof o["get:label"], typeof o["sym:iterator"], typeof o.describe);
return out.join(" ");
}
// An own property holding undefined hides the prototype method.
function shadow(o) {
o.describe = undefined;
return `${typeof o.describe} ${Object.keys(o).includes("describe")}`;
}
module.exports = { probe, shadow };
@@ -0,0 +1 @@
{ "name": "private-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-private-methods", "private": true, "type": "module", "scriptc": { "packages": { "private-pkg": "node" } } }
@@ -0,0 +1,30 @@
"use strict";
const shared = { count: 0, items: [] };
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
norm() {
return Math.hypot(this.x, this.y);
}
}
exports.shared = shared;
exports.Point = Point;
exports.getShared = () => shared;
exports.bump = () => ++shared.count;
exports.read = (o, k) => o[k];
exports.same = (a, b) => a === b;
exports.makeCounter = () => {
let n = 0;
return { inc: () => ++n, get value() { return n; } };
};
exports.add = (a, b) => a + b;
exports.numbers = () => [-0, NaN, Infinity, 2 ** 53 + 2, 10n ** 30n, -7n];
exports.isNegZero = (x) => Object.is(x, -0);
exports.strings = () => ["plain", "wörld ☃ 🌍", "lone \ud800 high", "\udc00 lone low"];
exports.codeUnits = (s) => Array.from(s, (c) => c.charCodeAt(0).toString(16)).join(" ");
exports.describe = (v) => `${typeof v} ${Object.prototype.toString.call(v)}`;
exports.keysOf = (o) => Object.keys(o).join(",");
@@ -0,0 +1,13 @@
// A native class extending a class owned by a Node island is SC8011:
// native subclasses of island classes are not supported yet (NI-5b).
import { requireModule } from "@scriptc/node";
interface Lib {
Point: new (x: number, y: number) => { x: number; y: number; norm(): number };
}
const { Point } = requireModule<Lib>("./lib.cjs");
class Point3 extends Point {
z = 0;
}
console.log(new Point3(1, 2).norm());
@@ -0,0 +1,15 @@
node-island-subclass/main.ts:10:22 - error SC8011: a native class cannot extend a class owned by a Node island yet (native subclasses of island classes)
9 |
10 | class Point3 extends Point {
| ^~~~~
11 | z = 0;
hint: keep the subclass in Node: move it into the island module, or mark its module as a Node island; a registry shim of the package also lifts this
node-island-subclass/main.ts:13:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
12 | }
13 | console.log(new Point3(1, 2).norm());
| ^~~~~~~~~~~~~~~~
14 |
@@ -0,0 +1 @@
{ "plain": 2000, "san": 30000 }
@@ -0,0 +1 @@
{ "plain": 2500, "san": 3300 }
+3 -1
View File
@@ -13,7 +13,9 @@
* A program whose head says `// @order: race` races a timer against I/O
* whose duration differs between the sides (scriptc's own fs callbacks
* complete in one turn where Node's readFile takes several; module loading
* on the first call into Node takes longer than in a Node process), or two
* on the first call into Node takes longer than in a Node process; a job on
* libuv's thread pool, such as zlib's, can finish before or after a 2 ms
* timer depending on the host), or two
* equal timers armed on either side of an island call, which a millisecond
* boundary between them (likely in a sanitized build) puts in different
* loop turns, so Node permits orders it rarely produces in M runs. Its
+1 -1
View File
@@ -308,7 +308,7 @@ describe.skipIf(skip)("Node islands in-process", () => {
});
expect(result.code).toBe(1);
expect(result.stderr).toBe(
`scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 5)\n`,
`scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 6)\n`,
);
});