Support checked closed generic portable bindings

This commit is contained in:
Timothy J. Aveni
2026-09-20 19:47:10 -07:00
parent 74a16d3646
commit d701464312
5 changed files with 135 additions and 5 deletions
+4 -5
View File
@@ -210,11 +210,10 @@ export function generatePortableTypeScriptBindings(
adapters: string[] = [], adapters: string[] = [],
descriptors: unknown[] = []; descriptors: unknown[] = [];
for (const entry of entries) { for (const entry of entries) {
if (entry.application || entry.kind === "constructor") // The checked binding schema already contains concrete generic applications.
throw new PortableBindingError( // Each has its own identity, value codecs and specialized dependency ports.
entry.id, if (entry.kind === "constructor")
"Portable generic adapters/legacy constructors are not implemented; declare an ordinary factory", throw new PortableBindingError(entry.id, "Legacy constructors are not supported; declare an ordinary factory");
);
if (names.has(entry.displayName)) throw new PortableBindingError(entry.id, "Duplicate handler name"); if (names.has(entry.displayName)) throw new PortableBindingError(entry.id, "Duplicate handler name");
names.add(entry.displayName); names.add(entry.displayName);
const portNames = new Set<string>(); const portNames = new Set<string>();
+10
View File
@@ -103,3 +103,13 @@ against both generated targets without casts. The root
`camino-portable-bindings` qualification compares Node, Window, Worker and `camino-portable-bindings` qualification compares Node, Window, Worker and
serialized-port behavior. Its simulated channel tests adapters, not installed serialized-port behavior. Its simulated channel tests adapters, not installed
operation acquisition or process isolation; those require the coordinator. operation acquisition or process isolation; those require the coordinator.
Closed generic package applications use the same portable adapters. The checked
workspace specializes their input/output types and dependency ports before code
generation. Each application has its own export ID and generated handler name
(`name$<specialization digest>`); handlers are concrete, so a string application
and a list-of-int64 application cannot accidentally share an untyped codec.
Common implementation helpers can still be ordinary generic TypeScript functions.
This does not implement universal runtime type application or legacy constructors.
The `camino-portable-bindings` check compiles positive/negative typed consumers and
executes both specializations in Node, Chromium Window, and Worker hosts.
+36
View File
@@ -51,3 +51,39 @@ for (const target of ["browser", "server"])
`dist/portable-reader/composite/${name}-${target}.ts`, `dist/portable-reader/composite/${name}-${target}.ts`,
generatePortableTypeScriptBindings(composite.definition.bindingSchema, revision, { target }), generatePortableTypeScriptBindings(composite.definition.bindingSchema, revision, { target }),
); );
const { portableGenericPlan } = await import("./portable-generic-fixture.js");
const generic = portableGenericPlan();
const genericPackage = generic.definition.bindingSchema.packages[0]!;
await mkdir("dist/portable-reader/generic", { recursive: true });
for (const target of ["browser", "server"]) {
await writeFile(
`dist/portable-reader/generic/${target}.ts`,
generatePortableTypeScriptBindings(generic.definition.bindingSchema, genericPackage.revisionId, { target }),
);
}
const stringEntry = genericPackage.exports.find((e) => e.inputType.kind === "scalar")!;
const listEntry = genericPackage.exports.find((e) => e.inputType.kind === "list")!;
await writeFile(
"dist/portable-reader/generic/implementation.ts",
`
import { createRegistry, type PortableImplementation } from "./gen/qx.js";
export const registry = createRegistry({
${JSON.stringify(stringEntry.displayName)}: context => context.input.toUpperCase(),
${JSON.stringify(listEntry.displayName)}: context => context.input.map(value => value + 1n),
});
// The specialization must retain its actual contract, not become unknown or any.
const negative: PortableImplementation = {
// @ts-expect-error a string specialization cannot return a number
${JSON.stringify(stringEntry.displayName)}: context => context.input.length,
${JSON.stringify(listEntry.displayName)}: context => {
// @ts-expect-error input collections are read-only
context.input.push(0n);
// @ts-expect-error integer values are exact bigint, not number
const lostPrecision: number = context.input[0]!;
return context.input;
},
};
void negative;
`,
);
+43
View File
@@ -36,3 +36,46 @@ test("generation rejects remote dependencies, wrong effects and unsupported shap
entry.dependencyPorts[0]!.requirement = { kind: "reflection", selectors: ["active"] }; entry.dependencyPorts[0]!.requirement = { kind: "reflection", selectors: ["active"] };
assert.throws(generate, /Unsupported reflection port/); assert.throws(generate, /Unsupported reflection port/);
}); });
test("closed generic portable handlers have distinct concrete codecs and no universal casts", async () => {
const { portableGenericPlan } = await import("./portable-generic-fixture.js");
const plan = portableGenericPlan();
const schema = plan.definition.bindingSchema;
const pkg = schema.packages[0]!;
assert.equal(pkg.exports.length, 2);
const generated = generatePortableTypeScriptBindings(schema, pkg.revisionId, { target: "browser" });
assert.match(generated, /readonly input: string/);
assert.match(generated, /readonly input: ReadonlyArray<bigint>/);
for (const entry of pkg.exports) {
assert.ok(entry.application);
assert.ok(generated.includes(JSON.stringify(entry.id)));
assert.ok(generated.includes(JSON.stringify(entry.displayName)));
}
assert.notEqual(pkg.exports[0]!.displayName, pkg.exports[1]!.displayName);
assert.equal(
generated.replace('target = "browser"', 'target = "server"'),
generatePortableTypeScriptBindings(schema, pkg.revisionId, { target: "server" }),
);
});
test("a portable subset of a mixed interface excludes remote dependencies", async () => {
const schema = compileExecutionPlan(await loadReaderWorkspace()).definition.bindingSchema;
const entry = schema.packages.flatMap((p) => p.exports).find((e) => e.displayName === "addHighlight")!;
const iface = schema.interfaces.find((i) => i.revisionId === "interface:reader:document-data@1")!;
const selected = entry.dependencyPorts.find((p) => p.requirement.kind === "interface")!.requirement;
assert.equal(selected.kind, "interface");
if (selected.kind !== "interface") throw Error("interface required");
const operation = structuredClone(iface.members[0]!.operations[0]!);
operation.id = "document:remote:get" as typeof operation.id;
operation.execution = { kind: "remote" };
iface.members.push({
...structuredClone(iface.members[0]!),
id: "document:remote" as (typeof iface.members)[0]["id"],
displayName: "remote",
operations: [operation],
});
const generate = () => generatePortableTypeScriptBindings(schema, "package:reader-runtime@1", { target: "browser" });
assert.doesNotMatch(generate(), /readonly "remote.get"/);
selected.operations!.push(operation.id);
assert.throws(generate, /select a portable subset/);
});
+42
View File
@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
import { compileExecutionPlan } from "../src/execution-world/plan.js";
/** Independently versioned generic interface and implementation, closed by a workspace. */
export function portableGenericPlan() {
const source = { repository: "https://example.test/portable-generics.git", commit: "a".repeat(40) };
const iface = compileCapabilityResourceSource(
`
interface Echo<value T> id "echo-interface" revision "echo-interface@2" {
operation echo id "echo-member" : T -> T {
call id "echo-call" execution snapshot targets [browser, server];
}
}`,
{ source },
);
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
const pkg = compileCapabilityResourceSource(
`
package Generic id "generic" revision "generic@3" {
operation echo<value T> id "echo" : T -> T mode call
execution snapshot targets [browser, server] receiver any;
}`,
{ source: { ...source, commit: "b".repeat(40) } },
);
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
const workspace = compileCapabilitySource(
`
workspace W id "w" revision "w@1" commit "${source.commit}" {
import interface Echo; import package Generic; atom Thing id "thing";
conform Thing as Echo<string> id "string-echo" { bind echo.call to package Generic.echo<string>; }
conform Thing as Echo<list<int64>> id "list-echo" { bind echo.call to package Generic.echo<list<int64>>; }
}`,
"workspace.qx",
{
interfaces: new Map([["Echo", iface.resource.revision]]),
packages: new Map([["Generic", pkg.resource.revision]]),
},
);
assert.ok(workspace.ok, JSON.stringify(workspace.diagnostics));
return compileExecutionPlan(workspace.workspace);
}