Generate typed QX bindings and add source editing tools

This commit is contained in:
Timothy J. Aveni
2026-09-08 14:27:24 -07:00
parent ce793cc54f
commit 4e17693d82
30 changed files with 2806 additions and 1939 deletions
+30
View File
@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import test from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/parser.js";
import { generateTypeScriptBindings, type BindingSchema } from "../src/bindings/index.js";
const source = { repository: "https://example.test/p.git", commit: "1".repeat(40) };
const schemaFor = (body: string): BindingSchema => {
const compiled = compileCapabilityResourceSource(`external atom Thing id "thing"; package Demo id "demo" revision "demo@1" { ${body} }`, { source });
assert.equal(compiled.ok, true, JSON.stringify(compiled.diagnostics));
if (!compiled.ok || compiled.resource.kind !== "package") throw new Error("expected package");
return { format: "quixos-bindings", version: 1, interfaces: [], packages: [compiled.resource.revision] };
};
test("generator uses exact IDs, restricted ports, nominal references, and lossless scalar types", () => {
const schema = schemaFor('operation run id "run-id" : list<int64> -> optional<atom-ref<Thing>> mode call receiver atom Thing requires { state payload id "payload-id" : bytes [read]; };');
const generated = generateTypeScriptBindings(schema, "demo@1");
assert.match(generated, /Array<bigint>/);
assert.match(generated, /Promise<Uint8Array>/);
assert.match(generated, /QxObjectRef<"atom:thing">/);
assert.doesNotMatch(generated, /"set":/);
assert.match(generated, /"run-id": bindQxHandler/);
assert.equal(generateTypeScriptBindings(schema, "demo@1"), generated);
});
test("missing external types and constructor signatures fail generation", () => {
const schema = schemaFor('function run id "run" : unit -> message "example.Payload";');
assert.throws(() => generateTypeScriptBindings(schema, "demo@1"), /Missing TypeScript message binding/);
assert.match(generateTypeScriptBindings(schema, "demo@1", { messages: { "example.Payload": { module: "./payload.js", export: "payload" } } }), /BindingValue<typeof message0>/);
const constructor = schemaFor('function run id "run" : unit -> unit requires { constructor thing id "ctor" : Thing; };');
assert.throws(() => generateTypeScriptBindings(constructor, "demo@1"), /explicit input contract/);
const typed = schemaFor('function run id "run" : unit -> unit requires { constructor thing id "ctor" : Thing input string; };');
assert.match(generateTypeScriptBindings(typed, "demo@1"), /construct.*input: string/);
});
+12
View File
@@ -28,6 +28,18 @@ const expectIssue = (
assert.equal(compileWorkspaceRevision(workspace).ok, false);
};
test("constructor dependency input contracts match the selected constructor", () => {
const workspace = makeValidCapabilityWorkspace();
const port = workspace.packageImports.flatMap((pkg) => pkg.exports).flatMap((entry) => entry.dependencyPorts)
.find((port) => port.requirement.kind === "constructor")!;
assert.equal(port.requirement.kind, "constructor");
if (port.requirement.kind !== "constructor") return;
port.requirement.inputType = valueType.unit;
assert.deepEqual(validateWorkspaceRevision(workspace), []);
port.requirement.inputType = valueType.string;
expectIssue(workspace, "invalid-dependency-binding");
});
const conformance = (
workspace: WorkspaceRevision,
identity: Pick<(typeof workspace.conformances)[number], "atomId" | "interfaceRevisionId">,
+8
View File
@@ -0,0 +1,8 @@
external atom Thing id "atom:example:thing";
package TypedExample id "package:typed-example" revision "package:typed-example@1" {
constructor createThing id "export:typed-example:create" constructs Thing : unit;
operation payloadGet id "export:typed-example:payload" : unit -> bytes mode call receiver atom Thing requires {
state payload id "port:typed-example:payload" : bytes [read];
};
}
+6
View File
@@ -0,0 +1,6 @@
quixos-lock version 1 {
quixos source {
repository "https://example.test/quixos.git";
commit "1111111111111111111111111111111111111111";
}
}
+84
View File
@@ -0,0 +1,84 @@
import assert from "node:assert/strict";
import test from "node:test";
import { mkdtemp, writeFile, readFile, rm, symlink } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { parseQx, formatQx, lintQx, applySourceEdits, addWorkspaceImport, walkSyntax,
resolveQxSources, readQxSource, compileCapabilitySource, scaffoldAtom, compileWorkspaceRepository } from "../src/capability-language/index.js";
const workspace = `workspace Test id "w" revision "w@1" commit "${"1".repeat(40)}" {\n// keep 🐈 comment\n}\n`;
test("lint reports invalid and redundant imports without resolving repositories", () => {
const source = workspace.replace("}\n", 'import "a.qx"; import "a.qx"; import "../bad.qx";\n}\n');
assert.deepEqual(lintQx(source).map((entry) => [entry.code, entry.severity]), [
["duplicate-source-import", "warning"], ["invalid-source-import", "error"],
]);
});
test("lossless tokens, UTF-16 ranges, and safe source edits", () => {
const text = workspace.replace("}\n", 'atom Cat id "🐈";\n}\n');
const parsed = parseQx(text);
assert.deepEqual(parsed.diagnostics, []);
assert.equal(parsed.tokens.map((token) => token.text).join(""), text);
const atom = [...walkSyntax(parsed.root)].find((node) => node.kind === "atomDecl")!;
assert.equal(text.slice(atom.start, atom.end), 'atom Cat id "🐈";');
assert.equal(applySourceEdits(text, [{ ...atom, text: 'atom Dog id "dog";' }]), text.replace('atom Cat id "🐈";', 'atom Dog id "dog";'));
assert.throws(() => applySourceEdits(text, [{ start: 0, end: 4, text: "" }, { start: 3, end: 7, text: "" }]), /overlapping/);
assert.throws(() => applySourceEdits(text, [{ start: -1, end: 0, text: "" }]), /Invalid/);
});
test("formatting preserves comments and strings, is idempotent, and rejects invalid source", () => {
const text = workspace.replace("}\n", 'atom Cat id "{ cat }"; /* multiline\n unchanged */\n}\n');
const formatted = formatQx(text);
assert.match(formatted, / atom Cat id "\{ cat \}"; \/\* multiline\n unchanged \*\//);
assert.equal(formatQx(formatted), formatted);
assert.deepEqual(parseQx(formatted).diagnostics, []);
assert.throws(() => formatQx("workspace {"), /syntax errors/);
});
test("imports preserve root text, deduplicate diamonds, reject cycles, and compile together", async () => {
const root = addWorkspaceImport(addWorkspaceImport(workspace, "a.qx"), "b.qx");
assert.equal(addWorkspaceImport(root, "a.qx"), root);
assert.match(root, /keep 🐈 comment/);
const files: Record<string, string> = { "workspace.qx": root,
"a.qx": 'fragment { import "shared.qx"; atom A id "a"; }',
"b.qx": 'fragment { import "shared.qx"; atom B id "b"; }',
"shared.qx": 'fragment { atom Shared id "shared"; }' };
const result = await resolveQxSources(async (name) => files[name]!);
assert.equal(result.sourceFiles.length, 4);
const compiled = compileCapabilitySource(result.source);
assert.equal(compiled.ok, true, JSON.stringify(compiled.diagnostics));
if (compiled.ok) assert.equal(compiled.workspace.atoms.length, 3);
const unresolved = compileCapabilitySource(root);
assert.equal(unresolved.ok, false);
assert.match(JSON.stringify(unresolved.diagnostics), /unresolved-source-import/);
files["shared.qx"] = 'fragment { import "a.qx"; }';
await assert.rejects(resolveQxSources(async (name) => files[name]!), /cycle/);
assert.throws(() => addWorkspaceImport(workspace, "../x.qx"), /Invalid/);
});
test("repository scaffolding validates before writing and refuses duplicates and symlinks", async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), "qx-scaffold-"));
t.after(() => rm(root, { recursive: true, force: true }));
await writeFile(path.join(root, "workspace.qx"), workspace);
await writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/q.git"; commit "${"1".repeat(40)}"; } }`);
const resolveResource = async (): Promise<never> => { throw new Error("unexpected resource"); };
const options = { root, name: "Cat", id: "cat", write: false, resolveResource };
await scaffoldAtom(options);
assert.equal(await readFile(path.join(root, "workspace.qx"), "utf8"), workspace);
await assert.rejects(readFile(path.join(root, "Cat.qx")), /ENOENT/);
await scaffoldAtom({ ...options, write: true });
const compiled = await compileWorkspaceRepository({ rootDirectory: root, resolveResource });
assert.equal(compiled.workspace.atoms[0]!.id, "cat");
await assert.rejects(scaffoldAtom({ ...options, write: true }), /already exists/);
await assert.rejects(scaffoldAtom({ ...options, name: "Other", write: true }), /Duplicate|duplicate/);
await assert.rejects(readFile(path.join(root, "Other.qx")), /ENOENT/);
await symlink(path.join(root, "Cat.qx"), path.join(root, "link.qx"));
await assert.rejects(readQxSource(root, "link.qx"), /ordinary files/);
});
test("lowering diagnostics map to the imported file", async () => {
const files: Record<string, string> = { "workspace.qx": addWorkspaceImport(workspace, "bad.qx"),
"bad.qx": 'fragment {\n conform Missing as Nope {}\n}' };
const sources = await resolveQxSources(async (name) => files[name]!);
const compiled = compileCapabilitySource(sources.source);
assert.equal(compiled.ok, false);
const diagnostic = compiled.diagnostics[0]!;
const position = sources.originalPosition(diagnostic.line, diagnostic.column);
assert.equal(position.fileName, "bad.qx");
assert.equal(position.line, 2);
});