Merge base branch

This commit is contained in:
Malte Ubl
2026-10-25 18:38:31 -07:00
10 changed files with 183 additions and 10 deletions
+7 -7
View File
@@ -156,18 +156,20 @@ export function prepareExecutableModule(
);
}
// A failed program whose compiled packages need the runtime or a
// mutable object model (NI-2's report): try them as islands, and
// report this failure if that build fails too.
// mutable object model (NI-2's report): try them as islands. If that
// build fails too, its failure is the one reported. This build typed
// those packages from their JavaScript, where the types they only
// declare (interfaces, aliases such as `Static<T>`) are `any`, so its
// diagnostics describe a program the island build does not compile:
// there the packages' declarations type the user code.
const marked = retryFences
? fenceIslands(entryPath, opts, fe, lowered, lowerIslands, false)
: null;
if (marked !== null) {
const diagnostics = lowered.diagnostics;
const texts = fe.sourceTexts();
fe.dispose();
frontendDisposed();
disposed = true;
const islanded = prepareExecutableModule(
return prepareExecutableModule(
entryPath,
opts,
ffi,
@@ -177,8 +179,6 @@ export function prepareExecutableModule(
marked,
false,
);
if (islanded.ok) return islanded;
return { ok: false, diagnostics, sourceTexts: texts };
}
return fail(lowered.diagnostics);
}
@@ -929,11 +929,20 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
// 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":
case "refused": {
const type = lowerer.typeOf(node);
// A type parameter the instance binds to dyn (a generic function
// over island types, `<T extends TSchema>`): its values are dyn
// values, and they cross the way `any` values do.
if (
(type.flags & ts.TypeFlags.TypeParameter) !== 0 &&
lowerer.typeParamResolver(type)?.kind === "dyn"
)
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
// 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))) {
if (exportedByReference(lowerer, type)) {
const lowered = lowerer.lowerExpr(node);
const value = lowerer.coerceToExpected(lowered, DYN);
return lowered.type.kind === "object"
@@ -942,9 +951,10 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
}
return boundaryFail(
lowerer,
`a '${lowerer.checker.typeToString(lowerer.typeOf(node))}' value (a native object would cross as a copy and lose its identity)`,
`a '${lowerer.checker.typeToString(type)}' value (a native object would cross as a copy and lose its identity)`,
node,
);
}
}
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6829-node-island-declared-types/main.ts"
],
"diags": []
}
@@ -0,0 +1,39 @@
// @requires-libnode
// Typed code names an island package's declared interfaces and generic
// aliases: the package's declarations type it, while its values cross the
// boundary as handles.
import { Arr, Num, Str, check, type Static, type TArray, type TNumber, type TSchema, type TString } from "schemakit";
interface Tool<T extends TSchema = TSchema> {
name: string;
parameters: T;
execute(id: string, params: Static<T>): string;
}
const sum: Tool<TArray<TNumber>> = {
name: "sum",
parameters: Arr(Num()),
execute: (id, params) => `${id}: ${params.reduce((a, b) => a + b, 0)}`,
};
const shout: Tool<TString> = {
name: "shout",
parameters: Str(),
execute: (id, params) => `${id}: ${params.toUpperCase()}`,
};
function run<T extends TSchema>(tool: Tool<T>, input: unknown): string {
if (!check(tool.parameters, input)) return `${tool.name}: rejected ${JSON.stringify(input)}`;
return tool.execute(tool.name, input);
}
function describeAll(schemas: TSchema[]): string[] {
return schemas.map((s) => `${s.kind} ${s.describe()}`);
}
console.log(run(sum, [1, 2, 3]));
console.log(run(sum, [1, "2"]));
console.log(run(shout, "quiet"));
console.log(run(shout, 7));
console.log(describeAll([sum.parameters, shout.parameters, Arr(Arr(Str()))]).join(" | "));
const tools: Tool[] = [sum, shout];
console.log(tools.map((t) => t.name).join(","));
@@ -0,0 +1,27 @@
// Types the package only declares: its JavaScript has no interfaces and no
// conditional aliases, so code typed from the JavaScript sees `any` here.
export interface TSchema {
readonly kind: string;
describe(): string;
}
export interface TNumber extends TSchema {
readonly kind: "number";
}
export interface TString extends TSchema {
readonly kind: "string";
}
export interface TArray<T extends TSchema = TSchema> extends TSchema {
readonly kind: "array";
readonly items: T;
}
export type Static<T extends TSchema> = T extends TNumber
? number
: T extends TString
? string
: T extends TArray<infer I>
? Static<I>[]
: unknown;
export declare function Num(): TNumber;
export declare function Str(): TString;
export declare function Arr<T extends TSchema>(items: T): TArray<T>;
export declare function check<T extends TSchema>(schema: T, value: unknown): value is Static<T>;
@@ -0,0 +1,37 @@
"use strict";
// The esbuild CommonJS bundle prologue: getters over live `let` bindings
// and a constructor function with prototype methods. scriptc compiles it
// natively only with runtime fences, so it runs in a Node island.
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all) __defProp(target, name, { get: all[name], enumerable: true });
};
var src_exports = {};
__export(src_exports, {
Num: () => Num,
Str: () => Str,
Arr: () => Arr,
check: () => check,
});
module.exports = src_exports;
function Schema(kind, items) {
this.kind = kind;
if (items !== undefined) this.items = items;
}
Schema.prototype.describe = function () {
return this.items === undefined ? this.kind : this.kind + "<" + this.items.describe() + ">";
};
function Num() {
return new Schema("number");
}
function Str() {
return new Schema("string");
}
function Arr(items) {
return new Schema("array", items);
}
function check(schema, value) {
if (schema.kind === "array") return Array.isArray(value) && value.every((v) => check(schema.items, v));
return typeof value === schema.kind;
}
0 && (module.exports = { Num, Str, Arr, check });
@@ -0,0 +1 @@
{ "name": "schemakit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
+27
View File
@@ -0,0 +1,27 @@
// A failed build whose package runs in a Node island reports the island
// build's errors: there the package's declarations type the program. The
// test builds it beside corpus 6829's schemakit package.
import { Num, check, type Static, type TNumber, type TSchema } from "schemakit";
interface Tool<T extends TSchema = TSchema> {
name: string;
parameters: T;
execute(id: string, params: Static<T>): string;
}
const double: Tool<TNumber> = {
name: "double",
parameters: Num(),
execute: (id, params) => `${id}: ${params * 2}`,
};
const input: unknown = 4;
if (check(double.parameters, input)) console.log(double.execute(double.name, input));
// The build's own error: member decorators have no native lowering.
function logged(method: () => number, context: ClassMethodDecoratorContext): void {}
class Counter {
@logged
next(): number {
return 1;
}
}
console.log(new Counter().next());
@@ -0,0 +1 @@
{ "plain": 3400, "san": 62000 }
+25
View File
@@ -354,3 +354,28 @@ test("recursive JSON types cross into Node", async () => {
const ir = JSON.stringify(deserializeModule(readFileSync(outPath, "utf8")));
expect(ir).toContain("nodeIsland.callSync");
}, 300_000);
// When a build fails and the packages it compiled with runtime fences then
// fail as islands too, the island build's errors are reported. That build
// types the user code from the packages' declarations; the native one
// typed it from their JavaScript, where declared-only types (`Static<T>`)
// are `any`. Needs no libnode pack: both builds fail before linking.
test("a failed build reports the island build's errors", async () => {
const dir = join(outDir, "declared-types");
rmSync(dir, { recursive: true, force: true });
cpSync(
join(repoRoot, "tests/corpus/6829-node-island-declared-types/node_modules"),
join(dir, "node_modules"),
{ recursive: true },
);
copyFileSync(
join(repoRoot, "tests/fixtures/node-island/declared-types.ts"),
join(dir, "main.ts"),
);
const result = await compile(join(dir, "main.ts"), { outPath: join(dir, "main"), outDir: dir });
expect(result.ok).toBe(false);
if (result.ok) return;
const messages = result.diagnostics.map((d) => `${d.code}: ${d.message}`);
expect(messages.some((m) => m.startsWith("SC1090: method decorators"))).toBe(true);
expect(messages.filter((m) => /Tool|Static|'check'/.test(m))).toEqual([]);
}, 300_000);