52803dda05
Add kinded parameters, capability bounds, Self, aliases and closed application identities. Check generic implementations universally and build candidate-specific codecs and descriptors from immutable schemas. Preserve lexical aliases and exact dispatch identities in package and host bindings. Add an imperative CRUD+index domain scaffold with explicit soft-deletion semantics, source/codegen regression coverage, installed CLI tests and an authoring guide. Existing Web Studio opaque props and class-level create-menu migration are separate from the implemented language core.
602 lines
26 KiB
TypeScript
602 lines
26 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
|
import { generateTypeScriptBindings } from "../src/bindings/index.js";
|
|
import { generateClientContracts } from "../src/bindings/client.js";
|
|
import {
|
|
TypeSubstitution,
|
|
appliedInterfaceId,
|
|
bindTypeParameters,
|
|
canonicalTypeArgument,
|
|
instantiateInterface,
|
|
capabilityId,
|
|
isStorableType,
|
|
valueType,
|
|
computeCapabilityClosure,
|
|
compileWorkspaceRevision,
|
|
type ClosedTypeArgument,
|
|
type GenericTypeEnvironment,
|
|
type ValueTypeExpression,
|
|
} from "../src/capability-model/index.js";
|
|
|
|
test("unused generic definitions prove symbolic bounds and storable aliases", () => {
|
|
const source = { repository: "https://example.test/bounds.git", commit: "a".repeat(40) };
|
|
const interfaces = new Map();
|
|
for (const text of [
|
|
'interface Named id "named" revision "named@1" {}',
|
|
'import interface Named; interface Detailed id "detailed" revision "detailed@1" requires Named {}',
|
|
'import interface Named; interface Requires<object T implements Named> id "requires" revision "requires@1" {}',
|
|
'interface Stored<value V : storable> id "stored" revision "stored@1" {}',
|
|
]) {
|
|
const result = compileCapabilityResourceSource(text, { source, environment: { interfaces } });
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
assert.equal(result.resource.kind, "interface");
|
|
interfaces.set(result.resource.revision.displayName, result.resource.revision);
|
|
}
|
|
const compile = (body: string) => compileCapabilityResourceSource(body, { source, environment: { interfaces } });
|
|
const bad = compile(
|
|
'import interface Requires; interface Bad<object T> id "bad" revision "bad@1" requires Requires<T> {}',
|
|
);
|
|
assert.equal(bad.ok, false);
|
|
assert.match(JSON.stringify(bad.diagnostics), /does not prove/);
|
|
const good = compile(
|
|
'import interface Detailed; import interface Requires; interface Good<object T implements Detailed> id "good" revision "good@1" requires Requires<T> {}',
|
|
);
|
|
assert.ok(good.ok, JSON.stringify(good.diagnostics));
|
|
const alias = compile(
|
|
'import interface Stored; type Values<value V : storable> = list<optional<V>>; interface Good<value T : storable> id "good" revision "good@1" requires Stored<Values<T>> {}',
|
|
);
|
|
assert.ok(alias.ok, JSON.stringify(alias.diagnostics));
|
|
const nonstorable = compile(
|
|
'import interface Stored; interface Bad<value T> id "bad" revision "bad@1" requires Stored<T> {}',
|
|
);
|
|
assert.equal(nonstorable.ok, false);
|
|
});
|
|
|
|
const note = capabilityId.atom("atom:note");
|
|
test("Self remains contextual through local aliases", () => {
|
|
const result = compileCapabilityResourceSource(
|
|
'type Me = ref<Self>; type Mine = optional<Me>; interface Identity id "identity" revision "identity@1" {value mine id "mine" : Mine {get id "mine:get";}}',
|
|
{ source: { repository: "https://example.test/self.git", commit: "a".repeat(40) } },
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
if (result.resource.kind !== "interface") throw new Error("expected interface");
|
|
assert.equal(result.resource.revision.template?.usesSelf, true);
|
|
const closed = instantiateInterface(result.resource.revision, [], { ...environment(), self: note });
|
|
assert.deepEqual(closed.members[0].operations[0].outputType, valueType.optional(valueType.atomRef(note)));
|
|
});
|
|
const named = capabilityId.interfaceRevision("interface:named@1");
|
|
const environment = (arguments_: [string, ClosedTypeArgument][] = []): GenericTypeEnvironment => ({
|
|
arguments: new Map(arguments_),
|
|
applyInterface: (id, args) => {
|
|
assert.equal(args.length, 0);
|
|
return id;
|
|
},
|
|
implementsInterface: (target, required) => target.kind === "atom" && target.atomId === note && required === named,
|
|
});
|
|
|
|
test("substitution reaches nested records, lists, optionals and object references", () => {
|
|
const substitute = new TypeSubstitution(
|
|
environment([
|
|
["scope/value", { kind: "value", type: valueType.int64 }],
|
|
["scope/object", { kind: "object", target: { kind: "atom", atomId: note } }],
|
|
]),
|
|
);
|
|
const result = substitute.value({
|
|
kind: "record",
|
|
fields: {
|
|
values: { kind: "list", value: { kind: "optional", value: { kind: "parameter", parameterId: "scope/value" } } },
|
|
target: { kind: "object-ref", expectation: { kind: "parameter", parameterId: "scope/object" } },
|
|
},
|
|
});
|
|
assert.deepEqual(result, {
|
|
kind: "record",
|
|
fields: {
|
|
values: valueType.list(valueType.optional(valueType.int64)),
|
|
target: valueType.atomRef(note),
|
|
},
|
|
});
|
|
assert.equal(isStorableType(result), false);
|
|
});
|
|
|
|
test("Self is the exact implementing atom and cannot be unbound", () => {
|
|
const expression = { kind: "object-ref", expectation: { kind: "self" } } as const;
|
|
assert.throws(() => new TypeSubstitution(environment()).value(expression), /Self requires an implementing atom/);
|
|
assert.deepEqual(new TypeSubstitution({ ...environment(), self: note }).value(expression), valueType.atomRef(note));
|
|
});
|
|
|
|
test("parameter kinds and missing parameters fail closed", () => {
|
|
const substitute = new TypeSubstitution(
|
|
environment([["T", { kind: "object", target: { kind: "atom", atomId: note } }]]),
|
|
);
|
|
assert.throws(() => substitute.value({ kind: "parameter", parameterId: "T" }), /use ref<T>/);
|
|
assert.throws(() => substitute.value({ kind: "parameter", parameterId: "unknown" }), /Unbound parameter/);
|
|
assert.throws(
|
|
() => bindTypeParameters([{ id: "T", name: "T", kind: "value" }], [], environment()),
|
|
/Expected 1 type arguments/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
bindTypeParameters(
|
|
[{ id: "T", name: "T", kind: "value" }],
|
|
[{ kind: "object", target: { kind: "atom", atomId: note } }],
|
|
environment(),
|
|
),
|
|
/Expected value, received object/,
|
|
);
|
|
});
|
|
|
|
test("bounds require evidence and storable constraints inspect nested values", () => {
|
|
const parameters = [
|
|
{ id: "T", name: "T", kind: "object", implements: [{ definitionId: named, arguments: [] }] },
|
|
] as const;
|
|
const mutableParameters = parameters.map((p) => ({
|
|
...p,
|
|
implements: p.implements.map((b) => ({ ...b, arguments: [] })),
|
|
}));
|
|
assert.equal(
|
|
bindTypeParameters(mutableParameters, [{ kind: "object", target: { kind: "atom", atomId: note } }], environment())
|
|
.size,
|
|
1,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
bindTypeParameters(
|
|
mutableParameters,
|
|
[{ kind: "object", target: { kind: "atom", atomId: capabilityId.atom("person") } }],
|
|
environment(),
|
|
),
|
|
/does not implement/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
bindTypeParameters(
|
|
[{ kind: "value", id: "V", name: "V", storable: true }],
|
|
[{ kind: "value", type: valueType.list(valueType.optional(valueType.atomRef(note))) }],
|
|
environment(),
|
|
),
|
|
/cannot contain managed references/,
|
|
);
|
|
assert.equal(isStorableType(valueType.message("opaque")), false);
|
|
assert.equal(isStorableType(valueType.watchHandle), false);
|
|
assert.equal(
|
|
isStorableType(valueType.message("checked"), (id) => id === "checked"),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("closed applications canonicalize records without erasing nominal identity or provenance", () => {
|
|
const a: ClosedTypeArgument = {
|
|
kind: "value",
|
|
type: { kind: "record", fields: { b: valueType.int64, a: valueType.string } },
|
|
};
|
|
const b: ClosedTypeArgument = {
|
|
kind: "value",
|
|
type: { kind: "record", fields: { a: valueType.string, b: valueType.int64 } },
|
|
};
|
|
assert.equal(canonicalTypeArgument(a), canonicalTypeArgument(b));
|
|
const application = {
|
|
definitionId: named,
|
|
source: { repository: "https://example.test/named.git", commit: "a".repeat(40) },
|
|
arguments: [a],
|
|
};
|
|
assert.equal(appliedInterfaceId(application), appliedInterfaceId({ ...application, arguments: [b] }));
|
|
assert.notEqual(appliedInterfaceId(application), appliedInterfaceId({ ...application, self: note }));
|
|
assert.notEqual(
|
|
appliedInterfaceId(application),
|
|
appliedInterfaceId({ ...application, source: { ...application.source, commit: "b".repeat(40) } }),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
canonicalTypeArgument({
|
|
kind: "value",
|
|
type: { kind: "parameter", parameterId: "T" },
|
|
} as unknown as ClosedTypeArgument),
|
|
/Expected a closed value type/,
|
|
);
|
|
});
|
|
|
|
test("aliases use lexical parameter identities, preserve outer bindings and reject cycles", () => {
|
|
const env = environment([["outer/T", { kind: "value", type: valueType.string }]]);
|
|
env.aliases = new Map([
|
|
[
|
|
"Box",
|
|
{
|
|
id: "Box",
|
|
parameters: [{ id: "box/T", name: "T", kind: "value" }],
|
|
body: { kind: "list", value: { kind: "parameter", parameterId: "box/T" } },
|
|
},
|
|
],
|
|
["Loop", { id: "Loop", parameters: [], body: { kind: "alias", definitionId: "Loop", arguments: [] } }],
|
|
]);
|
|
const substitute = new TypeSubstitution(env);
|
|
assert.deepEqual(
|
|
substitute.value({
|
|
kind: "alias",
|
|
definitionId: "Box",
|
|
arguments: [{ kind: "value", type: { kind: "parameter", parameterId: "outer/T" } }],
|
|
}),
|
|
valueType.list(valueType.string),
|
|
);
|
|
assert.deepEqual(substitute.value({ kind: "parameter", parameterId: "outer/T" }), valueType.string);
|
|
assert.throws(() => substitute.value({ kind: "alias", definitionId: "Loop", arguments: [] }), /Loop -> Loop/);
|
|
});
|
|
|
|
test("excessively deep types produce a bounded diagnostic", () => {
|
|
let expression: ValueTypeExpression = valueType.string;
|
|
for (let index = 0; index < 200; index++) expression = { kind: "list", value: expression };
|
|
assert.throws(() => new TypeSubstitution(environment()).value(expression), /depth or expansion budget/);
|
|
});
|
|
|
|
const source = { repository: "https://example.test/contracts.git", commit: "a".repeat(40) };
|
|
const reader = () => {
|
|
const result = compileCapabilityResourceSource(
|
|
`interface Reader<value V> id "reader" revision "reader@1" {
|
|
value values id "values" : list<optional<V>> { get id "values:get"; watch start id "values:watch" stop id "values:stop"; }
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
assert.equal(result.resource.kind, "interface");
|
|
if (result.resource.kind !== "interface") throw new Error("expected interface");
|
|
return result.resource.revision;
|
|
};
|
|
|
|
test("generic interface source retains its template and produces closed package/codegen contracts", () => {
|
|
const definition = reader();
|
|
assert.equal(definition.template?.parameters[0].name, "V");
|
|
const result = compileCapabilityResourceSource(
|
|
`import interface Reader;
|
|
package Client id "client" revision "client@1" {
|
|
function run id "run" : unit -> list<optional<string>> requires { interface reader id "reader-port" : Reader<string>; };
|
|
}`,
|
|
{ source, environment: { interfaces: new Map([["Reader", definition]]) } },
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
if (result.resource.kind !== "package") throw new Error("expected package");
|
|
assert.equal(result.resource.specializations?.length, 1);
|
|
const closed = result.resource.specializations![0];
|
|
assert.deepEqual(closed.members[0].operations[0].outputType, valueType.list(valueType.optional(valueType.string)));
|
|
assert.deepEqual(closed.members[0].operations[1].eventType, closed.members[0].operations[0].outputType);
|
|
const generated = generateTypeScriptBindings(
|
|
{ format: "quixos-bindings", version: 1, interfaces: [closed], packages: [result.resource.revision] },
|
|
"client@1",
|
|
);
|
|
assert.match(generated, /Array<\(string \| null\)>/);
|
|
assert.ok(generated.includes(closed.revisionId));
|
|
});
|
|
|
|
test("generic conformances bind state against specialized signatures", () => {
|
|
const result = compileCapabilitySource(
|
|
`workspace Demo id "ws" revision "ws@1" commit "${source.commit}" {
|
|
atom Document id "document";
|
|
import interface Reader;
|
|
conform Document as Reader<string> id "reader-conformance" {
|
|
private state Values id "values-slot" on Document : list<optional<string>> policy optimistic-register;
|
|
bind values.get to state Values.read;
|
|
bind values.watch-start to state Values.watch-start;
|
|
bind values.watch-stop to state Values.watch-stop;
|
|
}
|
|
}`,
|
|
"workspace.qx",
|
|
{ interfaces: new Map([["Reader", reader()]]) },
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
assert.equal(result.workspace.interfaceImports.length, 1);
|
|
assert.equal(result.workspace.interfaceImports[0].template, undefined);
|
|
assert.equal(result.workspace.conformances[0].interfaceRevisionId, result.workspace.interfaceImports[0].revisionId);
|
|
});
|
|
|
|
test("source rejects wrong generic arity and an unapplied interface", () => {
|
|
for (const input of [
|
|
"interface-ref<Reader>",
|
|
"interface-ref<Reader<string, int32>>",
|
|
"interface-ref<Reader<atom Note>>",
|
|
]) {
|
|
const result = compileCapabilityResourceSource(
|
|
`import interface Reader; external atom Note id "note";
|
|
package P id "p" revision "p@1" { function f id "f" : ${input} -> unit; }`,
|
|
{ source, environment: { interfaces: new Map([["Reader", reader()]]) } },
|
|
);
|
|
assert.equal(result.ok, false, input);
|
|
assert.match(result.diagnostics[0].code, /type-arity|parameter-kind/);
|
|
}
|
|
});
|
|
|
|
test("package receivers and injected dependencies select exact applications", () => {
|
|
const definition = reader();
|
|
const summary = compileCapabilityResourceSource(
|
|
`interface Summary id "summary" revision "summary@1" {
|
|
value summary id "summary-value" : list<optional<string>> { get id "summary:get"; }
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.ok(summary.ok);
|
|
if (summary.resource.kind !== "interface") throw new Error("expected interface");
|
|
const pkg = compileCapabilityResourceSource(
|
|
`import interface Reader;
|
|
package P id "p" revision "p@1" {
|
|
operation summarize id "summarize" : unit -> list<optional<string>> mode call receiver interfaces [Reader<string>]
|
|
requires { interface reader id "reader-port" : Reader<string>; };
|
|
}`,
|
|
{ source, environment: { interfaces: new Map([["Reader", definition]]) } },
|
|
);
|
|
assert.ok(pkg.ok, JSON.stringify(pkg.diagnostics));
|
|
if (pkg.resource.kind !== "package") throw new Error("expected package");
|
|
const result = compileCapabilitySource(
|
|
`workspace W id "w" revision "w@1" commit "${source.commit}" {
|
|
import interface Reader; import interface Summary; import package P;
|
|
atom Note id "note";
|
|
conform Note as Reader<string> id "reader-conformance" {
|
|
private state Values id "values" on Note : list<optional<string>> policy optimistic-register;
|
|
bind values.get to state Values.read;
|
|
bind values.watch-start to state Values.watch-start;
|
|
bind values.watch-stop to state Values.watch-stop;
|
|
}
|
|
conform Note as Summary id "summary-conformance" {
|
|
bind summary.get to package P.summarize with { reader to interface Reader<string>; };
|
|
}
|
|
}`,
|
|
"workspace.qx",
|
|
{
|
|
interfaces: new Map([
|
|
["Reader", definition],
|
|
["Summary", summary.resource.revision],
|
|
]),
|
|
interfaceClosure: pkg.resource.specializations,
|
|
packages: new Map([["P", pkg.resource.revision]]),
|
|
},
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
});
|
|
|
|
test("named generic value aliases elaborate through nested lists", () => {
|
|
const result = compileCapabilityResourceSource(
|
|
`type Page<value T> = record { items: list<T>; next: optional<string>; };
|
|
package P id "p" revision "p@1" { function f id "f" : unit -> Page<int64>; }`,
|
|
{ source },
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
if (result.resource.kind !== "package") throw new Error("expected package");
|
|
assert.deepEqual(result.resource.revision.exports[0].outputType, {
|
|
kind: "record",
|
|
fields: { items: valueType.list(valueType.int64), next: valueType.optional(valueType.string) },
|
|
});
|
|
});
|
|
|
|
test("Self specializes to the implementing atom rather than an erased interface", () => {
|
|
const definition = compileCapabilityResourceSource(
|
|
`interface Identity id "identity" revision "identity@1" {
|
|
operation identity id "identity-member" : ref<Self> -> ref<Self> { call id "identity-call"; }
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.ok(definition.ok, JSON.stringify(definition.diagnostics));
|
|
if (definition.resource.kind !== "interface") throw new Error("expected interface");
|
|
assert.equal(definition.resource.revision.template?.usesSelf, true);
|
|
const { revision } = definition.resource;
|
|
const first = instantiateInterface(revision, [], { ...environment(), self: note });
|
|
const second = instantiateInterface(revision, [], { ...environment(), self: capabilityId.atom("other") });
|
|
assert.deepEqual(first.members[0].operations[0].outputType, valueType.atomRef(note));
|
|
assert.notEqual(first.revisionId, second.revisionId);
|
|
});
|
|
|
|
test("package Self comes from an exact receiver, never a previous export", () => {
|
|
const valid = compileCapabilityResourceSource(
|
|
`type Owned<object T> = ref<T>; external atom Note id "note";
|
|
package P id "p" revision "p@1" {
|
|
operation identity id "identity" : ref<Self> -> Owned<Self> mode call receiver atom Note;
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.ok(valid.ok, JSON.stringify(valid.diagnostics));
|
|
if (valid.resource.kind !== "package") throw new Error("expected package");
|
|
assert.deepEqual(valid.resource.revision.exports[0].outputType, valueType.atomRef(capabilityId.atom("note")));
|
|
const invalid = compileCapabilityResourceSource(
|
|
`external atom Note id "note";
|
|
package P id "p" revision "p@1" {
|
|
operation identity id "identity" : ref<Self> -> ref<Self> mode call receiver atom Note;
|
|
function bad id "bad" : unit -> ref<Self>;
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.equal(invalid.ok, false);
|
|
assert.equal(invalid.diagnostics[0].code, "unbound-self");
|
|
});
|
|
|
|
test("host generation rejects unresolved definitions and operation-only dispatch ambiguity", () => {
|
|
const definition = reader();
|
|
assert.throws(() => generateClientContracts([definition], {}), /closed interface/);
|
|
const first = instantiateInterface(definition, [{ kind: "value", type: valueType.string }], environment());
|
|
const second = instantiateInterface(definition, [{ kind: "value", type: valueType.int32 }], environment());
|
|
assert.throws(() => generateClientContracts([first, second], {}), /ambiguous/);
|
|
});
|
|
|
|
test("finite recursive generic interface references share the same closed application", () => {
|
|
const definition = compileCapabilityResourceSource(
|
|
`interface Node<value V> id "node" revision "node@1" {
|
|
value next id "next" : optional<interface-ref<Node<V>>> { get id "next:get"; }
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.ok(definition.ok, JSON.stringify(definition.diagnostics));
|
|
if (definition.resource.kind !== "interface") throw new Error("expected interface");
|
|
const result = compileCapabilityResourceSource(
|
|
`import interface Node;
|
|
package P id "p" revision "p@1" { function f id "f" : interface-ref<Node<string>> -> unit; }
|
|
`,
|
|
{ source, environment: { interfaces: new Map([["Node", definition.resource.revision]]) } },
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
const [closed] = result.resource.specializations!;
|
|
assert.equal(result.resource.specializations!.length, 1);
|
|
assert.deepEqual(
|
|
closed.members[0].operations[0].outputType,
|
|
valueType.optional(valueType.interfaceRef(closed.revisionId)),
|
|
);
|
|
});
|
|
|
|
test("expanding recursive generic interfaces fail with a bounded diagnostic", () => {
|
|
const definition = compileCapabilityResourceSource(
|
|
`interface Node<value V> id "node" revision "node@1" {
|
|
value next id "next" : interface-ref<Node<list<V>>> { get id "next:get"; }
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.ok(definition.ok, JSON.stringify(definition.diagnostics));
|
|
if (definition.resource.kind !== "interface") throw new Error("expected interface");
|
|
const result = compileCapabilityResourceSource(
|
|
`import interface Node;
|
|
package P id "p" revision "p@1" { function f id "f" : interface-ref<Node<string>> -> unit; }
|
|
`,
|
|
{ source, environment: { interfaces: new Map([["Node", definition.resource.revision]]) } },
|
|
);
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.diagnostics[0].code, "recursive-application");
|
|
});
|
|
|
|
test("storable parameters do not imply support for RPC-only records", () => {
|
|
assert.equal(isStorableType({ kind: "record", fields: { name: valueType.string } }), false);
|
|
assert.equal(isStorableType(valueType.list(valueType.optional(valueType.string))), true);
|
|
});
|
|
|
|
test("generic declarations reject duplicate members before specialization", () => {
|
|
const result = compileCapabilityResourceSource(
|
|
`interface Bad<value V> id "bad" revision "bad@1" {
|
|
value first id "duplicate" : V { get id "first:get"; }
|
|
value second id "duplicate" : V { get id "second:get"; }
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.diagnostics[0].code, "duplicate-interface-member");
|
|
});
|
|
|
|
test("unused templates still check nested application arity and alias cycles", () => {
|
|
const malformed = compileCapabilityResourceSource(
|
|
`import interface Reader;
|
|
interface Bad<value V> id "bad" revision "bad@1" {
|
|
value item id "item" : interface-ref<Reader<V, V>> { get id "get"; }
|
|
}`,
|
|
{ source, environment: { interfaces: new Map([["Reader", reader()]]) } },
|
|
);
|
|
assert.equal(malformed.ok, false);
|
|
assert.equal(malformed.diagnostics[0].code, "type-arity");
|
|
const cycle = compileCapabilityResourceSource(
|
|
`type Loop<value V> = list<Loop<V>>;
|
|
interface Bad<value V> id "bad" revision "bad@1" {
|
|
value item id "item" : Loop<V> { get id "get"; }
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.equal(cycle.ok, false);
|
|
assert.equal(cycle.diagnostics[0].code, "recursive-alias");
|
|
});
|
|
|
|
test("Self in a prerequisite contributes to the outer application identity", () => {
|
|
const identity = compileCapabilityResourceSource(
|
|
`interface Identity id "identity" revision "identity@1" {
|
|
value self id "self" : ref<Self> { get id "self:get"; }
|
|
}`,
|
|
{ source },
|
|
);
|
|
assert.ok(identity.ok);
|
|
if (identity.resource.kind !== "interface") throw new Error("expected interface");
|
|
const outer = compileCapabilityResourceSource(
|
|
`import interface Identity;
|
|
interface Outer<value V> id "outer" revision "outer@1" requires Identity {}`,
|
|
{ source, environment: { interfaces: new Map([["Identity", identity.resource.revision]]) } },
|
|
);
|
|
assert.ok(outer.ok, JSON.stringify(outer.diagnostics));
|
|
if (outer.resource.kind !== "interface") throw new Error("expected interface");
|
|
assert.equal(outer.resource.revision.template?.usesSelf, true);
|
|
});
|
|
|
|
test("object bounds are discharged against the complete candidate, not source order", () => {
|
|
const namedResult = compileCapabilityResourceSource(`interface Named id "named" revision "named@1" {}`, { source });
|
|
assert.ok(namedResult.ok);
|
|
if (namedResult.resource.kind !== "interface") throw new Error("expected interface");
|
|
const named = namedResult.resource.revision;
|
|
const bounded = compileCapabilityResourceSource(
|
|
`import interface Named;
|
|
interface Container<object T implements Named> id "container" revision "container@1" {}`,
|
|
{ source, environment: { interfaces: new Map([["Named", named]]) } },
|
|
);
|
|
assert.ok(bounded.ok, JSON.stringify(bounded.diagnostics));
|
|
if (bounded.resource.kind !== "interface") throw new Error("expected interface");
|
|
const container = bounded.resource.revision;
|
|
const compile = (evidence: string) =>
|
|
compileCapabilitySource(
|
|
`workspace W id "w" revision "w@1" commit "${source.commit}" {
|
|
import interface Named; import interface Container;
|
|
atom Note id "note"; atom Index id "index";
|
|
conform Index as Container<atom Note> id "index-container" {}
|
|
${evidence}
|
|
}`,
|
|
"workspace.qx",
|
|
{
|
|
interfaces: new Map([
|
|
["Named", named],
|
|
["Container", container],
|
|
]),
|
|
},
|
|
);
|
|
const missing = compile("");
|
|
assert.equal(missing.ok, false);
|
|
assert.ok(missing.diagnostics.some((issue) => issue.code === "unsatisfied-interface"));
|
|
const valid = compile('conform Note as Named id "note-named" {}');
|
|
assert.ok(valid.ok, JSON.stringify(valid.diagnostics));
|
|
});
|
|
|
|
test("prerequisites require explicit conformances and remain in the capability closure", () => {
|
|
const baseResult = compileCapabilityResourceSource(`interface Base id "base" revision "base@1" {}`, { source });
|
|
assert.ok(baseResult.ok);
|
|
if (baseResult.resource.kind !== "interface") throw new Error("expected interface");
|
|
const base = baseResult.resource.revision;
|
|
const derivedResult = compileCapabilityResourceSource(
|
|
`import interface Base;
|
|
interface Derived<value V> id "derived" revision "derived@1" requires Base {}`,
|
|
{
|
|
source,
|
|
environment: { interfaces: new Map([["Base", base]]) },
|
|
},
|
|
);
|
|
assert.ok(derivedResult.ok, JSON.stringify(derivedResult.diagnostics));
|
|
if (derivedResult.resource.kind !== "interface") throw new Error("expected interface");
|
|
const derived = derivedResult.resource.revision;
|
|
const compile = (evidence: string) =>
|
|
compileCapabilitySource(
|
|
`workspace W id "w" revision "w@1" commit "${source.commit}" {
|
|
import interface Base; import interface Derived; atom Note id "note";
|
|
conform Note as Derived<string> id "derived-conformance" {}
|
|
${evidence}
|
|
}`,
|
|
"workspace.qx",
|
|
{
|
|
interfaces: new Map([
|
|
["Base", base],
|
|
["Derived", derived],
|
|
]),
|
|
},
|
|
);
|
|
assert.equal(compile("").ok, false);
|
|
const result = compile('conform Note as Base id "base-conformance" {}');
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
const root = result.workspace.conformances[0];
|
|
const closure = computeCapabilityClosure(result.plan, [root]);
|
|
assert.equal(closure.conformances.length, 2);
|
|
const unresolved = structuredClone(result.workspace);
|
|
unresolved.interfaceImports[0].members.push({
|
|
kind: "value",
|
|
id: capabilityId.member("unresolved"),
|
|
displayName: "unresolved",
|
|
operations: [],
|
|
valueType: { kind: "parameter", parameterId: "T" } as unknown as typeof valueType.string,
|
|
});
|
|
assert.equal(compileWorkspaceRevision(unresolved).ok, false);
|
|
const forged = structuredClone(result.workspace);
|
|
const applied = forged.interfaceImports.find((entry) => entry.application)!;
|
|
applied.application!.arguments = [{ kind: "value", type: valueType.int64 }];
|
|
assert.equal(compileWorkspaceRevision(forged).ok, false);
|
|
});
|