Implement capability generics, checked package specializations and CRUD scaffolding

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.
This commit is contained in:
Timothy J. Aveni
2026-09-16 10:42:12 -07:00
parent 9358b5ed0e
commit 52803dda05
26 changed files with 6009 additions and 2193 deletions
+4 -1
View File
@@ -162,7 +162,10 @@ const environmentFor = (
),
),
interfaceClosure: exactRevisions(
closure.flatMap((node) => (node.resource.kind === "interface" ? [node.resource.revision] : [])),
closure.flatMap((node) => [
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
...(node.resource.specializations ?? []),
]),
),
packageClosure: exactRevisions(
closure.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
+7 -2
View File
@@ -12,7 +12,7 @@ import {
type EvolutionReview,
type WorkspaceRevision,
} from "../capability-model/index.js";
import { bindingSchema } from "../bindings/index.js";
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
import { snapshotCommit, checkoutCommit, buildCheckedPackage } from "./checked-build.js";
const execFile = promisify(execFileCallback);
const bytesDigest = (value: Uint8Array) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
@@ -219,7 +219,12 @@ export const checkWorkspaceCandidate = async (options: {
resolveResource,
});
const schema = path.join(temporary, "bindings.json");
await fs.writeFile(schema, JSON.stringify(bindingSchema(candidate)));
await fs.writeFile(
schema,
JSON.stringify(
specializeBindingSchema(bindingSchema(candidate), compiled.workspace, candidate.resource.revision.revisionId),
),
);
const artifactPath = await buildCheckedPackage(
resource.directory,
schema,
File diff suppressed because one or more lines are too long
@@ -1,213 +1,227 @@
WORKSPACE=1
FRAGMENT=2
IMPORT=3
EXTERNAL=4
ATOM=5
INTERFACE=6
INTERFACES=7
PACKAGE=8
VALUE=9
RELATION=10
OPERATION=11
FUNCTION=12
CONSTRUCTOR=13
CONSTRUCTS=14
INPUT=15
CONFORM=16
AS=17
BIND=18
TO=19
PRIVATE=20
SHARED=21
STATE=22
EDGE=23
PROJECTION=24
WITH=25
USING=26
VIA=27
MATERIALIZE=28
IF=29
ABSENT=30
ON=31
POLICY=32
DEFAULT=33
SOURCE=34
REPOSITORY=35
COMMIT=36
REVISION=37
SEMANTIC_MAJOR=38
ON_DELETE=39
RETAIN_OTHER=40
KEYED=41
PUBLIC_TRAVERSAL=42
ID=43
DOC=44
MODE=45
EMITS=46
RECEIVER=47
REQUIRES=48
ANY=49
GET=50
SET=51
WATCH=52
START=53
STOP=54
READ=55
WRITE=56
RESOLVE=57
CONNECT=58
DISCONNECT=59
CALL=60
WATCH_START=61
WATCH_STOP=62
SUBSCRIBE=63
UNSUBSCRIBE=64
OPTIMISTIC_REGISTER=65
CRDT=66
OPTIONAL_ONE=67
EXACTLY_ONE=68
MANY_UNIQUE=69
MANY=70
ORDERED=71
UNIT=72
WATCH_HANDLE=73
MESSAGE=74
ATOM_REF=75
INTERFACE_REF=76
OPTIONAL=77
LIST=78
RECORD=79
BOOL=80
BYTES=81
DOUBLE=82
INT32=83
INT64=84
STRING=85
UINT32=86
UINT64=87
TRUE=88
FALSE=89
NULL=90
ARROW=91
COLON=92
SEMI=93
COMMA=94
DOT=95
LBRACE=96
RBRACE=97
LBRACK=98
RBRACK=99
LPAREN=100
RPAREN=101
LT=102
GT=103
INTEGER=104
JSON_NUMBER=105
IDENTIFIER=106
STRING_LITERAL=107
LINE_COMMENT=108
BLOCK_COMMENT=109
WS=110
TYPE=2
OBJECT=3
STORABLE=4
IMPLEMENTS=5
REF=6
FRAGMENT=7
IMPORT=8
EXTERNAL=9
ATOM=10
INTERFACE=11
INTERFACES=12
PACKAGE=13
VALUE=14
RELATION=15
OPERATION=16
FUNCTION=17
CONSTRUCTOR=18
CONSTRUCTS=19
INPUT=20
CONFORM=21
AS=22
BIND=23
TO=24
PRIVATE=25
SHARED=26
STATE=27
EDGE=28
PROJECTION=29
WITH=30
USING=31
VIA=32
MATERIALIZE=33
IF=34
ABSENT=35
ON=36
POLICY=37
DEFAULT=38
SOURCE=39
REPOSITORY=40
COMMIT=41
REVISION=42
SEMANTIC_MAJOR=43
ON_DELETE=44
RETAIN_OTHER=45
KEYED=46
PUBLIC_TRAVERSAL=47
ID=48
DOC=49
MODE=50
EMITS=51
RECEIVER=52
REQUIRES=53
ANY=54
GET=55
SET=56
WATCH=57
START=58
STOP=59
READ=60
WRITE=61
RESOLVE=62
CONNECT=63
DISCONNECT=64
CALL=65
WATCH_START=66
WATCH_STOP=67
SUBSCRIBE=68
UNSUBSCRIBE=69
OPTIMISTIC_REGISTER=70
CRDT=71
OPTIONAL_ONE=72
EXACTLY_ONE=73
MANY_UNIQUE=74
MANY=75
ORDERED=76
UNIT=77
WATCH_HANDLE=78
MESSAGE=79
ATOM_REF=80
INTERFACE_REF=81
OPTIONAL=82
LIST=83
RECORD=84
BOOL=85
BYTES=86
DOUBLE=87
INT32=88
INT64=89
STRING=90
UINT32=91
UINT64=92
TRUE=93
FALSE=94
NULL=95
ARROW=96
COLON=97
SEMI=98
COMMA=99
DOT=100
LBRACE=101
RBRACE=102
LBRACK=103
RBRACK=104
LPAREN=105
RPAREN=106
LT=107
GT=108
AMP=109
EQUAL=110
INTEGER=111
JSON_NUMBER=112
IDENTIFIER=113
STRING_LITERAL=114
LINE_COMMENT=115
BLOCK_COMMENT=116
WS=117
'workspace'=1
'fragment'=2
'import'=3
'external'=4
'atom'=5
'interface'=6
'interfaces'=7
'package'=8
'value'=9
'relation'=10
'operation'=11
'function'=12
'constructor'=13
'constructs'=14
'input'=15
'conform'=16
'as'=17
'bind'=18
'to'=19
'private'=20
'shared'=21
'state'=22
'edge'=23
'projection'=24
'with'=25
'using'=26
'via'=27
'materialize'=28
'if'=29
'absent'=30
'on'=31
'policy'=32
'default'=33
'source'=34
'repository'=35
'commit'=36
'revision'=37
'semantic-major'=38
'on-delete'=39
'retain-other'=40
'keyed'=41
'public-traversal'=42
'id'=43
'doc'=44
'mode'=45
'emits'=46
'receiver'=47
'requires'=48
'any'=49
'get'=50
'set'=51
'watch'=52
'start'=53
'stop'=54
'read'=55
'write'=56
'resolve'=57
'connect'=58
'disconnect'=59
'call'=60
'watch-start'=61
'watch-stop'=62
'subscribe'=63
'unsubscribe'=64
'optimistic-register'=65
'crdt'=66
'optional-one'=67
'exactly-one'=68
'many-unique'=69
'many'=70
'ordered'=71
'unit'=72
'watch-handle'=73
'message'=74
'atom-ref'=75
'interface-ref'=76
'optional'=77
'list'=78
'record'=79
'bool'=80
'bytes'=81
'double'=82
'int32'=83
'int64'=84
'string'=85
'uint32'=86
'uint64'=87
'true'=88
'false'=89
'null'=90
'->'=91
':'=92
';'=93
','=94
'.'=95
'{'=96
'}'=97
'['=98
']'=99
'('=100
')'=101
'<'=102
'>'=103
'type'=2
'object'=3
'storable'=4
'implements'=5
'ref'=6
'fragment'=7
'import'=8
'external'=9
'atom'=10
'interface'=11
'interfaces'=12
'package'=13
'value'=14
'relation'=15
'operation'=16
'function'=17
'constructor'=18
'constructs'=19
'input'=20
'conform'=21
'as'=22
'bind'=23
'to'=24
'private'=25
'shared'=26
'state'=27
'edge'=28
'projection'=29
'with'=30
'using'=31
'via'=32
'materialize'=33
'if'=34
'absent'=35
'on'=36
'policy'=37
'default'=38
'source'=39
'repository'=40
'commit'=41
'revision'=42
'semantic-major'=43
'on-delete'=44
'retain-other'=45
'keyed'=46
'public-traversal'=47
'id'=48
'doc'=49
'mode'=50
'emits'=51
'receiver'=52
'requires'=53
'any'=54
'get'=55
'set'=56
'watch'=57
'start'=58
'stop'=59
'read'=60
'write'=61
'resolve'=62
'connect'=63
'disconnect'=64
'call'=65
'watch-start'=66
'watch-stop'=67
'subscribe'=68
'unsubscribe'=69
'optimistic-register'=70
'crdt'=71
'optional-one'=72
'exactly-one'=73
'many-unique'=74
'many'=75
'ordered'=76
'unit'=77
'watch-handle'=78
'message'=79
'atom-ref'=80
'interface-ref'=81
'optional'=82
'list'=83
'record'=84
'bool'=85
'bytes'=86
'double'=87
'int32'=88
'int64'=89
'string'=90
'uint32'=91
'uint64'=92
'true'=93
'false'=94
'null'=95
'->'=96
':'=97
';'=98
','=99
'.'=100
'{'=101
'}'=102
'['=103
']'=104
'('=105
')'=106
'<'=107
'>'=108
'&'=109
'='=110
File diff suppressed because one or more lines are too long
@@ -1,213 +1,227 @@
WORKSPACE=1
FRAGMENT=2
IMPORT=3
EXTERNAL=4
ATOM=5
INTERFACE=6
INTERFACES=7
PACKAGE=8
VALUE=9
RELATION=10
OPERATION=11
FUNCTION=12
CONSTRUCTOR=13
CONSTRUCTS=14
INPUT=15
CONFORM=16
AS=17
BIND=18
TO=19
PRIVATE=20
SHARED=21
STATE=22
EDGE=23
PROJECTION=24
WITH=25
USING=26
VIA=27
MATERIALIZE=28
IF=29
ABSENT=30
ON=31
POLICY=32
DEFAULT=33
SOURCE=34
REPOSITORY=35
COMMIT=36
REVISION=37
SEMANTIC_MAJOR=38
ON_DELETE=39
RETAIN_OTHER=40
KEYED=41
PUBLIC_TRAVERSAL=42
ID=43
DOC=44
MODE=45
EMITS=46
RECEIVER=47
REQUIRES=48
ANY=49
GET=50
SET=51
WATCH=52
START=53
STOP=54
READ=55
WRITE=56
RESOLVE=57
CONNECT=58
DISCONNECT=59
CALL=60
WATCH_START=61
WATCH_STOP=62
SUBSCRIBE=63
UNSUBSCRIBE=64
OPTIMISTIC_REGISTER=65
CRDT=66
OPTIONAL_ONE=67
EXACTLY_ONE=68
MANY_UNIQUE=69
MANY=70
ORDERED=71
UNIT=72
WATCH_HANDLE=73
MESSAGE=74
ATOM_REF=75
INTERFACE_REF=76
OPTIONAL=77
LIST=78
RECORD=79
BOOL=80
BYTES=81
DOUBLE=82
INT32=83
INT64=84
STRING=85
UINT32=86
UINT64=87
TRUE=88
FALSE=89
NULL=90
ARROW=91
COLON=92
SEMI=93
COMMA=94
DOT=95
LBRACE=96
RBRACE=97
LBRACK=98
RBRACK=99
LPAREN=100
RPAREN=101
LT=102
GT=103
INTEGER=104
JSON_NUMBER=105
IDENTIFIER=106
STRING_LITERAL=107
LINE_COMMENT=108
BLOCK_COMMENT=109
WS=110
TYPE=2
OBJECT=3
STORABLE=4
IMPLEMENTS=5
REF=6
FRAGMENT=7
IMPORT=8
EXTERNAL=9
ATOM=10
INTERFACE=11
INTERFACES=12
PACKAGE=13
VALUE=14
RELATION=15
OPERATION=16
FUNCTION=17
CONSTRUCTOR=18
CONSTRUCTS=19
INPUT=20
CONFORM=21
AS=22
BIND=23
TO=24
PRIVATE=25
SHARED=26
STATE=27
EDGE=28
PROJECTION=29
WITH=30
USING=31
VIA=32
MATERIALIZE=33
IF=34
ABSENT=35
ON=36
POLICY=37
DEFAULT=38
SOURCE=39
REPOSITORY=40
COMMIT=41
REVISION=42
SEMANTIC_MAJOR=43
ON_DELETE=44
RETAIN_OTHER=45
KEYED=46
PUBLIC_TRAVERSAL=47
ID=48
DOC=49
MODE=50
EMITS=51
RECEIVER=52
REQUIRES=53
ANY=54
GET=55
SET=56
WATCH=57
START=58
STOP=59
READ=60
WRITE=61
RESOLVE=62
CONNECT=63
DISCONNECT=64
CALL=65
WATCH_START=66
WATCH_STOP=67
SUBSCRIBE=68
UNSUBSCRIBE=69
OPTIMISTIC_REGISTER=70
CRDT=71
OPTIONAL_ONE=72
EXACTLY_ONE=73
MANY_UNIQUE=74
MANY=75
ORDERED=76
UNIT=77
WATCH_HANDLE=78
MESSAGE=79
ATOM_REF=80
INTERFACE_REF=81
OPTIONAL=82
LIST=83
RECORD=84
BOOL=85
BYTES=86
DOUBLE=87
INT32=88
INT64=89
STRING=90
UINT32=91
UINT64=92
TRUE=93
FALSE=94
NULL=95
ARROW=96
COLON=97
SEMI=98
COMMA=99
DOT=100
LBRACE=101
RBRACE=102
LBRACK=103
RBRACK=104
LPAREN=105
RPAREN=106
LT=107
GT=108
AMP=109
EQUAL=110
INTEGER=111
JSON_NUMBER=112
IDENTIFIER=113
STRING_LITERAL=114
LINE_COMMENT=115
BLOCK_COMMENT=116
WS=117
'workspace'=1
'fragment'=2
'import'=3
'external'=4
'atom'=5
'interface'=6
'interfaces'=7
'package'=8
'value'=9
'relation'=10
'operation'=11
'function'=12
'constructor'=13
'constructs'=14
'input'=15
'conform'=16
'as'=17
'bind'=18
'to'=19
'private'=20
'shared'=21
'state'=22
'edge'=23
'projection'=24
'with'=25
'using'=26
'via'=27
'materialize'=28
'if'=29
'absent'=30
'on'=31
'policy'=32
'default'=33
'source'=34
'repository'=35
'commit'=36
'revision'=37
'semantic-major'=38
'on-delete'=39
'retain-other'=40
'keyed'=41
'public-traversal'=42
'id'=43
'doc'=44
'mode'=45
'emits'=46
'receiver'=47
'requires'=48
'any'=49
'get'=50
'set'=51
'watch'=52
'start'=53
'stop'=54
'read'=55
'write'=56
'resolve'=57
'connect'=58
'disconnect'=59
'call'=60
'watch-start'=61
'watch-stop'=62
'subscribe'=63
'unsubscribe'=64
'optimistic-register'=65
'crdt'=66
'optional-one'=67
'exactly-one'=68
'many-unique'=69
'many'=70
'ordered'=71
'unit'=72
'watch-handle'=73
'message'=74
'atom-ref'=75
'interface-ref'=76
'optional'=77
'list'=78
'record'=79
'bool'=80
'bytes'=81
'double'=82
'int32'=83
'int64'=84
'string'=85
'uint32'=86
'uint64'=87
'true'=88
'false'=89
'null'=90
'->'=91
':'=92
';'=93
','=94
'.'=95
'{'=96
'}'=97
'['=98
']'=99
'('=100
')'=101
'<'=102
'>'=103
'type'=2
'object'=3
'storable'=4
'implements'=5
'ref'=6
'fragment'=7
'import'=8
'external'=9
'atom'=10
'interface'=11
'interfaces'=12
'package'=13
'value'=14
'relation'=15
'operation'=16
'function'=17
'constructor'=18
'constructs'=19
'input'=20
'conform'=21
'as'=22
'bind'=23
'to'=24
'private'=25
'shared'=26
'state'=27
'edge'=28
'projection'=29
'with'=30
'using'=31
'via'=32
'materialize'=33
'if'=34
'absent'=35
'on'=36
'policy'=37
'default'=38
'source'=39
'repository'=40
'commit'=41
'revision'=42
'semantic-major'=43
'on-delete'=44
'retain-other'=45
'keyed'=46
'public-traversal'=47
'id'=48
'doc'=49
'mode'=50
'emits'=51
'receiver'=52
'requires'=53
'any'=54
'get'=55
'set'=56
'watch'=57
'start'=58
'stop'=59
'read'=60
'write'=61
'resolve'=62
'connect'=63
'disconnect'=64
'call'=65
'watch-start'=66
'watch-stop'=67
'subscribe'=68
'unsubscribe'=69
'optimistic-register'=70
'crdt'=71
'optional-one'=72
'exactly-one'=73
'many-unique'=74
'many'=75
'ordered'=76
'unit'=77
'watch-handle'=78
'message'=79
'atom-ref'=80
'interface-ref'=81
'optional'=82
'list'=83
'record'=84
'bool'=85
'bytes'=86
'double'=87
'int32'=88
'int64'=89
'string'=90
'uint32'=91
'uint64'=92
'true'=93
'false'=94
'null'=95
'->'=96
':'=97
';'=98
','=99
'.'=100
'{'=101
'}'=102
'['=103
']'=104
'('=105
')'=106
'<'=107
'>'=108
'&'=109
'='=110
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,12 @@ import { ExternalInterfaceDeclContext } from "./QuixosCapabilityParser.js";
import { ResourcePreambleContext } from "./QuixosCapabilityParser.js";
import { AtomDeclContext } from "./QuixosCapabilityParser.js";
import { InterfaceResourceDeclContext } from "./QuixosCapabilityParser.js";
import { TypeParametersContext } from "./QuixosCapabilityParser.js";
import { TypeParameterContext } from "./QuixosCapabilityParser.js";
import { InterfaceTypeContext } from "./QuixosCapabilityParser.js";
import { TypeArgumentsContext } from "./QuixosCapabilityParser.js";
import { TypeArgumentContext } from "./QuixosCapabilityParser.js";
import { TypeAliasDeclContext } from "./QuixosCapabilityParser.js";
import { InterfaceMemberContext } from "./QuixosCapabilityParser.js";
import { OperationMemberContext } from "./QuixosCapabilityParser.js";
import { ValueMemberContext } from "./QuixosCapabilityParser.js";
@@ -137,6 +143,42 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitInterfaceResourceDecl?: (ctx: InterfaceResourceDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.typeParameters`.
* @param ctx the parse tree
* @return the visitor result
*/
visitTypeParameters?: (ctx: TypeParametersContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.typeParameter`.
* @param ctx the parse tree
* @return the visitor result
*/
visitTypeParameter?: (ctx: TypeParameterContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceType`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInterfaceType?: (ctx: InterfaceTypeContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.typeArguments`.
* @param ctx the parse tree
* @return the visitor result
*/
visitTypeArguments?: (ctx: TypeArgumentsContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.typeArgument`.
* @param ctx the parse tree
* @return the visitor result
*/
visitTypeArgument?: (ctx: TypeArgumentContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.typeAliasDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitTypeAliasDecl?: (ctx: TypeAliasDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceMember`.
* @param ctx the parse tree
+392
View File
@@ -0,0 +1,392 @@
import {
TypeSubstitution,
GenericTypeError,
instantiateInterface,
appliedInterfaceId,
valueType,
type AtomId,
type ClosedTypeArgument,
type GenericTypeEnvironment,
type InterfaceApplicationExpression,
type InterfaceRevision,
type ObjectTypeExpression,
type TypeArgumentExpression,
type TypeParameter,
type ValueAliasDefinition,
type ValueType,
type ValueTypeExpression,
} from "../capability-model/index.js";
import type {
InterfaceTypeContext,
TargetConstraintContext,
TypeArgumentsContext,
TypeParametersContext,
TypeAliasDeclContext,
ValueTypeContext,
} from "./generated/QuixosCapabilityParser.js";
const literal = (context: { getText(): string }) => JSON.parse(context.getText()) as string;
/** Lexical authoring scope; only `value`/`target` return installed types. */
export class GenericSourceTypes {
readonly interfaces = new Map<string, InterfaceRevision>();
readonly definitions = new Map<string, InterfaceRevision>();
readonly applications = new Map<string, InterfaceRevision>();
readonly aliases = new Map<string, ValueAliasDefinition>();
parameters = new Map<string, TypeParameter>();
self?: AtomId;
private active: string[] = [];
private applicationCount = 0;
constructor(readonly atoms: Map<string, AtomId>) {}
environment(): GenericTypeEnvironment {
return {
arguments: new Map(),
aliases: this.aliases,
self: this.self,
applyInterface: (id, args) => this.apply(id, args).revisionId,
// Obligations are retained on the application and discharged by workspace
// validation, where all atom conformances are known (not source order).
implementsInterface: () => true,
};
}
register(name: string, definition: InterfaceRevision) {
this.interfaces.set(name, definition);
this.definitions.set(definition.revisionId, definition);
}
/** Check authored applications even when nobody has instantiated this template yet. */
validateTemplate(definition: InterfaceRevision) {
const template = definition.template;
if (!template) return;
const aliases = new Map((template.aliases ?? []).map((alias) => [alias.id, alias]));
const parametersById = new Map(
[...template.parameters, ...(template.aliases ?? []).flatMap((alias) => alias.parameters)].map((parameter) => [
parameter.id,
parameter,
]),
);
const replace = (node: unknown, arguments_: Map<string, TypeArgumentExpression>): unknown => {
if (!node || typeof node !== "object") return node;
if ("kind" in node && node.kind === "parameter" && "parameterId" in node) {
const arg = arguments_.get(String(node.parameterId));
if (arg) return arg.kind === "value" ? arg.type : arg.target;
}
if (Array.isArray(node)) return node.map((entry) => replace(entry, arguments_));
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, replace(value, arguments_)]));
};
const implies = (
actual: InterfaceApplicationExpression,
required: InterfaceApplicationExpression,
seen = new Set<string>(),
): boolean => {
const key = JSON.stringify(actual);
if (key === JSON.stringify(required)) return true;
if (seen.has(key) || seen.size > 128) return false;
seen.add(key);
const contract = this.definitions.get(actual.definitionId);
if (!contract?.template)
return (contract?.requiredInterfaces ?? []).includes(required.definitionId) && required.arguments.length === 0;
const args = new Map(
contract.template.parameters.map((parameter, index) => [parameter.id, actual.arguments[index]]),
);
return contract.template.requires.some((parent) =>
implies(replace(parent, args) as InterfaceApplicationExpression, required, seen),
);
};
const storable = (type: ValueTypeExpression, depth = 0): boolean => {
if (depth > 128)
throw new GenericTypeError(
"type-complexity-limit",
definition.displayName,
"Storable alias expansion exceeds depth limit",
);
if (type.kind === "parameter") {
const parameter = parametersById.get(type.parameterId);
return parameter?.kind === "value" && Boolean(parameter.storable);
}
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
if (type.kind === "alias") {
const alias = aliases.get(type.definitionId);
if (!alias || alias.parameters.length !== type.arguments.length) return false;
return storable(
replace(
alias.body,
new Map(alias.parameters.map((parameter, index) => [parameter.id, type.arguments[index]])),
) as ValueTypeExpression,
depth + 1,
);
}
return type.kind === "scalar" || (type.kind === "builtin" && type.name === "unit");
};
let remaining = 10000;
const visit = (node: unknown, path: string, depth = 0): void => {
if (depth > 128 || --remaining < 0)
throw new GenericTypeError("type-complexity-limit", path, "Type exceeds the depth or expansion budget");
if (!node || typeof node !== "object") return;
if ("definitionId" in node && "arguments" in node) {
const application = node as InterfaceApplicationExpression | Extract<ValueTypeExpression, { kind: "alias" }>;
const alias = "kind" in application && application.kind === "alias";
const target = alias ? aliases.get(application.definitionId) : this.definitions.get(application.definitionId);
if (!target)
throw new GenericTypeError(
alias ? "unknown-alias" : "unknown-interface",
path,
`Unknown definition ${application.definitionId}`,
);
if ("template" in target && target.template?.usesSelf) template.usesSelf = true;
const parameters = "parameters" in target ? target.parameters : (target.template?.parameters ?? []);
if (parameters.length !== application.arguments.length)
throw new GenericTypeError(
"type-arity",
path,
`Expected ${parameters.length} type arguments, received ${application.arguments.length}`,
);
parameters.forEach((parameter, index) => {
if (parameter.kind !== application.arguments[index].kind)
throw new GenericTypeError(
"parameter-kind",
path,
`Expected ${parameter.kind}, received ${application.arguments[index].kind}`,
);
const argument = application.arguments[index];
if (parameter.kind === "value" && parameter.storable && argument.kind === "value" && !storable(argument.type))
throw new GenericTypeError(
"non-storable-argument",
path,
"Generic application cannot prove its value argument is storable",
);
if (parameter.kind === "object" && argument.kind === "object" && argument.target.kind === "parameter") {
const offered = parametersById.get(argument.target.parameterId);
const mapping = new Map(parameters.map((p, i) => [p.id, application.arguments[i]]));
for (const bound of parameter.implements) {
const required = replace(bound, mapping) as InterfaceApplicationExpression;
if (offered?.kind !== "object" || !offered.implements.some((evidence) => implies(evidence, required)))
throw new GenericTypeError(
"unsatisfied-bound",
path,
`Object parameter does not prove ${required.definitionId}`,
);
}
}
});
}
for (const [name, child] of Object.entries(node)) visit(child, `${path}.${name}`, depth + 1);
};
visit(template, definition.displayName);
const active = new Set<string>();
const done = new Set<string>();
const checkAlias = (id: string) => {
if (done.has(id)) return;
if (active.has(id))
throw new GenericTypeError("recursive-alias", id, `Recursive value alias: ${[...active, id].join(" -> ")}`);
active.add(id);
const walk = (node: unknown): void => {
if (!node || typeof node !== "object") return;
if ("kind" in node && node.kind === "alias")
checkAlias((node as ValueAliasDefinition["body"] & { definitionId: string }).definitionId);
Object.values(node).forEach(walk);
};
walk(aliases.get(id)?.body);
active.delete(id);
done.add(id);
};
for (const id of aliases.keys()) checkAlias(id);
}
apply(id: string, arguments_: readonly ClosedTypeArgument[]): InterfaceRevision {
const definition = this.definitions.get(id);
if (!definition) throw new GenericTypeError("unknown-interface", id, "Unknown interface definition");
const application = {
definitionId: definition.revisionId,
source: definition.source,
arguments: [...arguments_],
...(definition.template?.usesSelf ? { self: this.self } : {}),
};
const appliedId = definition.template ? appliedInterfaceId(application) : definition.revisionId;
const cached = this.applications.get(appliedId);
if (cached) return cached;
if (this.active.includes(id))
throw new GenericTypeError(
"recursive-application",
id,
`Expanding recursive interface application: ${[...this.active, id].join(" -> ")}`,
);
if (++this.applicationCount > 10000)
throw new GenericTypeError("type-complexity-limit", id, "Too many interface applications");
this.active.push(id);
if (definition.template)
this.applications.set(appliedId, {
interfaceId: definition.interfaceId,
revisionId: appliedId,
displayName: definition.displayName,
source: definition.source,
members: [],
application,
});
try {
const obligations: NonNullable<InterfaceRevision["argumentRequirements"]> = [];
const instance = instantiateInterface(definition, arguments_, {
...this.environment(),
implementsInterface: (target, required) => {
obligations.push({ target, required });
return true;
},
});
if (instance === definition) return definition;
instance.argumentRequirements = obligations;
this.applications.set(instance.revisionId, instance);
this.definitions.set(instance.revisionId, instance);
return instance;
} catch (error) {
this.applications.delete(appliedId);
throw error;
} finally {
this.active.pop();
}
}
interface(context: InterfaceTypeContext): InterfaceApplicationExpression {
return this.interfaceByName(context.identifier().getText(), context.typeArguments());
}
interfaceByName(name: string, arguments_: TypeArgumentsContext | null): InterfaceApplicationExpression {
const definition = this.interfaces.get(name);
if (!definition) throw new GenericTypeError("unknown-interface", name, "Unknown interface");
return { definitionId: definition.revisionId, arguments: this.arguments(arguments_) };
}
arguments(context: TypeArgumentsContext | null): TypeArgumentExpression[] {
return (context?.typeArgument() ?? []).map((argument): TypeArgumentExpression => {
if (argument.INTERFACE())
return {
kind: "object",
target: { kind: "application", application: this.interface(argument.interfaceType()!) },
};
if (argument.ATOM()) return { kind: "object", target: this.atom(argument.identifier()!.getText()) };
if (argument.OBJECT()) return { kind: "object", target: this.objectParameter(argument.identifier()!.getText()) };
const type = argument.valueType()!;
if (type.getText() === "Self") return { kind: "object", target: { kind: "self" } };
const parameter = type.identifier() && this.parameters.get(type.identifier()!.getText());
if (parameter?.kind === "object" && !type.typeArguments())
return { kind: "object", target: { kind: "parameter", parameterId: parameter.id } };
return { kind: "value", type: this.expression(type) };
});
}
private atom(name: string): ObjectTypeExpression {
if (name === "Self") return { kind: "self" };
const atomId = this.atoms.get(name);
if (!atomId) throw new GenericTypeError("unknown-atom", name, "Unknown atom");
return { kind: "atom", atomId };
}
private objectParameter(name: string): ObjectTypeExpression {
if (name === "Self") return { kind: "self" };
const parameter = this.parameters.get(name);
if (!parameter || parameter.kind !== "object")
throw new GenericTypeError("parameter-kind", name, "Expected an object parameter");
return { kind: "parameter", parameterId: parameter.id };
}
targetExpression(context: TargetConstraintContext): ObjectTypeExpression {
const name = context.identifier().getText();
if (context.ATOM()) return this.atom(name);
if (context.OBJECT()) return this.objectParameter(name);
return { kind: "application", application: this.interfaceByName(name, context.typeArguments()) };
}
expression(context: ValueTypeContext): ValueTypeExpression {
if (context.scalarType())
return {
kind: "scalar",
name: context.scalarType()!.getText() as Extract<ValueType, { kind: "scalar" }>["name"],
};
if (context.UNIT()) return valueType.unit;
if (context.WATCH_HANDLE()) return valueType.watchHandle;
if (context.MESSAGE()) return valueType.message(literal(context.stringLiteral()!));
if (context.OPTIONAL() || context.LIST())
return { kind: context.LIST() ? "list" : "optional", value: this.expression(context.valueType()!) };
if (context.RECORD()) {
const fields = context
.recordField()
.map((field) => [field.identifier().getText(), this.expression(field.valueType())] as const);
if (new Set(fields.map(([name]) => name)).size !== fields.length)
throw new GenericTypeError("duplicate-field", "record", "Duplicate record field");
return { kind: "record", fields: Object.fromEntries(fields) };
}
const name = context.identifier()!.getText();
if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) };
if (context.INTERFACE_REF())
return {
kind: "object-ref",
expectation: { kind: "application", application: this.interfaceByName(name, context.typeArguments()) },
};
if (context.REF()) return { kind: "object-ref", expectation: this.objectParameter(name) };
const parameter = this.parameters.get(name);
if (parameter) {
if (parameter.kind !== "value" || context.typeArguments())
throw new GenericTypeError("parameter-kind", name, "Expected a value parameter; object parameters need ref<T>");
return { kind: "parameter", parameterId: parameter.id };
}
if (!this.aliases.has(name)) throw new GenericTypeError("unknown-type", name, "Unknown value type");
return { kind: "alias", definitionId: name, arguments: this.arguments(context.typeArguments()) };
}
value(context: ValueTypeContext): ValueType {
return new TypeSubstitution(this.environment()).value(this.expression(context));
}
target(context: TargetConstraintContext) {
return new TypeSubstitution(this.environment()).object(this.targetExpression(context));
}
declareParameters(context: TypeParametersContext | null, owner: string): TypeParameter[] {
const entries = context?.typeParameter() ?? [];
const result: TypeParameter[] = entries.map((parameter, index) => {
const name = parameter.identifier().getText();
if (name === "Self" || this.parameters.has(name))
throw new GenericTypeError("duplicate-parameter", owner, `Duplicate or reserved parameter ${name}`);
const declaration: TypeParameter = parameter.VALUE()
? {
id: `${owner}/parameter/${index}`,
name,
kind: "value",
...(parameter.STORABLE() ? { storable: true } : {}),
}
: { id: `${owner}/parameter/${index}`, name, kind: "object", implements: [] };
this.parameters.set(name, declaration);
return declaration;
});
entries.forEach((entry, index) => {
const parameter = result[index];
if (parameter.kind === "object")
parameter.implements = entry.interfaceType().map((bound) => this.interface(bound));
});
return result;
}
declareAliases(contexts: readonly TypeAliasDeclContext[]) {
for (const context of contexts) {
const name = context.identifier().getText();
if (this.aliases.has(name)) throw new GenericTypeError("duplicate-alias", name, "Duplicate type alias");
this.aliases.set(name, { id: name, parameters: [], body: valueType.unit });
}
for (const context of contexts) {
const name = context.identifier().getText();
const previous = this.parameters;
this.parameters = new Map();
try {
this.aliases.set(name, {
id: name,
parameters: this.declareParameters(context.typeParameters(), JSON.stringify(["alias", name])),
body: this.expression(context.valueType()),
});
} finally {
this.parameters = previous;
}
}
}
}
+571 -32
View File
@@ -37,7 +37,15 @@ import {
type StateSlotDefinition,
type ValueType,
type WorkspaceRevision,
GenericTypeError,
TypeSubstitution,
type ValueTypeExpression,
type ObjectTypeExpression,
type GenericPackageExport,
type GenericDependencyPort,
specializePackageExport,
} from "../capability-model/index.js";
import { GenericSourceTypes } from "./generic-types.js";
import { QuixosCapabilityLexer } from "./generated/QuixosCapabilityLexer.js";
import {
QuixosCapabilityParser,
@@ -69,6 +77,17 @@ import {
export type CapabilityResourceKind = "interface" | "package";
const genericLocations = new WeakMap<GenericTypeError, { line: number; column: number }>();
const atGenericSource = <T>(context: ParserRuleContext, lower: () => T): T => {
try {
return lower();
} catch (error) {
if (error instanceof GenericTypeError && !genericLocations.has(error))
genericLocations.set(error, { line: context.start?.line ?? 0, column: context.start?.column ?? 0 });
throw error;
}
};
export type CapabilityResourceImport = {
kind: CapabilityResourceKind;
binding: string;
@@ -98,6 +117,7 @@ export type CapabilityResource =
externalAtoms: AtomDefinition[];
externalInterfaces: CapabilityExternalInterface[];
revision: InterfaceRevision;
specializations?: InterfaceRevision[];
}
| {
kind: "package";
@@ -105,6 +125,7 @@ export type CapabilityResource =
externalAtoms: AtomDefinition[];
externalInterfaces: CapabilityExternalInterface[];
revision: PackageRevision;
specializations?: InterfaceRevision[];
};
export type CapabilityResourceCompileResult =
@@ -137,6 +158,7 @@ export type CapabilitySourceCompileResult =
};
interface InterfaceSymbol {
definition?: InterfaceRevision;
revisionId: InterfaceRevision["revisionId"];
contractAvailable: boolean;
members: Map<
@@ -154,6 +176,7 @@ interface PackageExportSymbol {
}
interface PackageSymbol {
definition?: PackageRevision;
revisionId: PackageRevision["revisionId"];
exports: Map<string, PackageExportSymbol>;
}
@@ -164,6 +187,7 @@ interface AttachmentSymbol {
}
interface LoweringState {
types: GenericSourceTypes;
fileName: string;
diagnostics: CapabilitySourceDiagnostic[];
atoms: Map<string, AtomId>;
@@ -263,6 +287,8 @@ const lowerConstraint = (
throw new Error("Missing target constraint after a successful parse");
}
const name = identifier(context.identifier());
if (context.OBJECT() || context.typeArguments() || name === "Self" || state.types.interfaces.get(name)?.template)
return state.types.target(context);
if (context.ATOM()) {
const atomId = requireSymbol(state, state.atoms, name, context, "atom");
return atomId ? { kind: "atom", atomId } : undefined;
@@ -275,6 +301,16 @@ const lowerValueType = (state: LoweringState, context: ValueTypeContext | null):
if (!context) {
throw new Error("Missing value type after a successful parse");
}
if (
context.REF() ||
context.typeArguments() ||
(context.identifier() && !context.ATOM_REF() && !context.INTERFACE_REF())
)
return state.types.value(context);
if (context.INTERFACE_REF() && state.types.interfaces.get(context.identifier()!.getText())?.template)
return state.types.value(context);
if ((context.ATOM_REF() || context.INTERFACE_REF()) && context.identifier()?.getText() === "Self")
return state.types.value(context);
const scalar = context.scalarType();
if (scalar) {
return { kind: "scalar", name: text(scalar) as never };
@@ -559,6 +595,188 @@ const lowerInterface = (
displayName: alias,
source,
members,
...(context.interfaceType().length
? {
requiredInterfaces: context
.interfaceType()
.map((entry) => new TypeSubstitution(state.types.environment()).application(state.types.interface(entry))),
}
: {}),
};
};
const hasSelfIdentifier = (context: ParserRuleContext, state: LoweringState): boolean => {
if (context.ruleIndex === QuixosCapabilityParser.RULE_identifier && context.getText() === "Self") return true;
if (context.ruleIndex === QuixosCapabilityParser.RULE_valueType) {
const aliasName = context.children
.find((child) => "ruleIndex" in child && child.ruleIndex === QuixosCapabilityParser.RULE_identifier)
?.getText();
const seen = new Set<string>();
const containsSelf = (value: unknown): boolean => {
if (!value || typeof value !== "object") return false;
if ("kind" in value && value.kind === "self") return true;
if ("kind" in value && value.kind === "alias" && "definitionId" in value) {
const id = String(value.definitionId);
if (!seen.has(id)) {
seen.add(id);
if (containsSelf(state.types.aliases.get(id)?.body)) return true;
}
}
return Object.values(value).some(containsSelf);
};
if (aliasName && containsSelf(state.types.aliases.get(aliasName)?.body)) return true;
}
if (
context.ruleIndex === QuixosCapabilityParser.RULE_interfaceType ||
((context.ruleIndex === QuixosCapabilityParser.RULE_valueType ||
context.ruleIndex === QuixosCapabilityParser.RULE_targetConstraint) &&
["interface", "interface-ref"].includes(context.getChild(0)?.getText() ?? ""))
) {
const name = context.children
.find((child) => "ruleIndex" in child && child.ruleIndex === QuixosCapabilityParser.RULE_identifier)
?.getText();
if (name && state.types.interfaces.get(name)?.template?.usesSelf) return true;
}
return context.children.some((child) => "ruleIndex" in child && hasSelfIdentifier(child as ParserRuleContext, state));
};
const lowerInterfaceTemplate = (
state: LoweringState,
context: InterfaceResourceDeclContext,
source: SourceRevision,
): InterfaceRevision => {
const types = state.types;
const revisionId = capabilityId.interfaceRevision(stringValue(context.stringLiteral(1)));
const parameters = types.declareParameters(context.typeParameters(), JSON.stringify(["interface", revisionId]));
const signature = (
name: string,
id: InterfaceOperation["id"],
input: ValueTypeExpression,
output: ValueTypeExpression,
mode: InterfaceOperationMode = "call",
eventType?: ValueTypeExpression,
): InterfaceOperation<ValueTypeExpression> => ({
id,
displayName: name,
inputType: input,
outputType: output,
mode,
...(eventType ? { eventType } : {}),
});
const members: InterfaceMember<ValueTypeExpression, ObjectTypeExpression>[] = context
.interfaceMember()
.map((entry) => {
const value = entry.valueMember();
if (value) {
const type = types.expression(value.valueType());
return {
kind: "value",
id: capabilityId.member(stringValue(value.stringLiteral())),
displayName: identifier(value.identifier()),
valueType: type,
operations: value.valueMemberOperation().flatMap((operation) => {
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
if (operation.GET()) return [signature("get", id, valueType.unit, type)];
if (operation.SET()) return [signature("set", id, type, valueType.unit)];
return [
signature("watch-start", id, valueType.unit, valueType.watchHandle, "watch-start", type),
signature(
"watch-stop",
capabilityId.operation(stringValue(operation.stringLiteral(1))),
valueType.watchHandle,
valueType.unit,
"watch-stop",
),
];
}),
};
}
const operation = entry.operationMember();
if (operation) {
const inputType = types.expression(operation.valueType(0)!);
const outputType = types.expression(operation.valueType(1)!);
return {
kind: "operation",
id: capabilityId.member(stringValue(operation.stringLiteral(0))),
displayName: identifier(operation.identifier()),
inputType,
outputType,
operations: [
signature("call", capabilityId.operation(stringValue(operation.stringLiteral(1))), inputType, outputType),
],
};
}
const relationship = entry.relationshipMember()!;
const target = types.targetExpression(relationship.targetConstraint());
const targetType: ValueTypeExpression = { kind: "object-ref", expectation: target };
const cardinality = lowerCardinality(relationship.cardinality());
const resolved: ValueTypeExpression =
cardinality === "exactly-one"
? targetType
: { kind: cardinality === "optional-one" ? "optional" : "list", value: targetType };
return {
kind: "relationship",
id: capabilityId.member(stringValue(relationship.stringLiteral())),
displayName: identifier(relationship.identifier()),
target,
cardinality,
ordered: Boolean(relationship.ORDERED()),
operations: relationship.relationshipOperation().flatMap((operation) => {
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
if (operation.RESOLVE()) return [signature("resolve", id, valueType.unit, resolved)];
if (operation.CONNECT() || operation.DISCONNECT())
return [signature(operation.CONNECT() ? "connect" : "disconnect", id, targetType, valueType.unit)];
return [
signature("watch-start", id, valueType.unit, valueType.watchHandle, "watch-start", resolved),
signature(
"watch-stop",
capabilityId.operation(stringValue(operation.stringLiteral(1))),
valueType.watchHandle,
valueType.unit,
"watch-stop",
),
];
}),
};
});
const requires = context.interfaceType().map((requirement) => types.interface(requirement));
const memberIds = new Set<string>();
const memberNames = new Set<string>();
const operationIds = new Set<string>();
for (const member of members) {
if (!member.id || memberIds.has(member.id) || memberNames.has(member.displayName))
throw new GenericTypeError(
"duplicate-interface-member",
revisionId,
`Invalid or duplicate member ${member.displayName}`,
);
memberIds.add(member.id);
memberNames.add(member.displayName);
const operationNames = new Set<string>();
for (const operation of member.operations) {
if (!operation.id || operationIds.has(operation.id) || operationNames.has(operation.displayName))
throw new GenericTypeError(
"duplicate-interface-operation",
revisionId,
`Invalid or duplicate operation ${member.displayName}.${operation.displayName}`,
);
operationIds.add(operation.id);
operationNames.add(operation.displayName);
}
}
const hasSelf = (value: unknown): boolean =>
value !== null &&
typeof value === "object" &&
(("kind" in value && value.kind === "self") || Object.values(value).some(hasSelf));
const aliases = [...types.aliases.values()];
types.parameters = new Map();
return {
interfaceId: capabilityId.interface(stringValue(context.stringLiteral(0))),
revisionId,
displayName: identifier(context.identifier()),
source,
members: [],
template: { parameters, members, requires, aliases, usesSelf: hasSelf([parameters, members, requires, aliases]) },
};
};
@@ -612,7 +830,12 @@ const lowerDependencyPort = (state: LoweringState, context: DependencyPortContex
}
const targetName = identifier(context.identifier(1));
if (context.INTERFACE()) {
const target = requireSymbol(state, state.interfaces, targetName, context, "interface");
let target = requireSymbol(state, state.interfaces, targetName, context, "interface");
if (target && (context.typeArguments() || target.definition?.template)) {
const expression = state.types.interfaceByName(targetName, context.typeArguments());
const id = new TypeSubstitution(state.types.environment()).application(expression);
target = interfaceSymbolFor(state.types.definitions.get(id)!);
}
if (target && !target.contractAvailable) {
loweringIssue(
state,
@@ -665,6 +888,12 @@ const lowerReceiver = (
state: LoweringState,
context: PackageOperationExportContext["receiverRequirement"] extends () => infer Result ? Result : never,
): PackageReceiverRequirement => {
if (context.OBJECT())
throw new GenericTypeError(
"unbound-parameter",
context.getText(),
"An object-parameter receiver requires a generic operation declaration",
);
if (context.ANY()) {
return { kind: "any-object" };
}
@@ -675,11 +904,12 @@ const lowerReceiver = (
atomId: requireSymbol(state, state.atoms, name, context, "atom") ?? capabilityId.atom(`unresolved:${name}`),
};
}
const list = context.identifierList();
return {
kind: "all-interfaces",
interfaceRevisionIds: (list?.identifier() ?? []).map((entry) => {
const name = identifier(entry);
interfaceRevisionIds: context.interfaceType().map((entry) => {
const name = identifier(entry.identifier());
if (entry.typeArguments() || state.types.interfaces.get(name)?.template)
return new TypeSubstitution(state.types.environment()).application(state.types.interface(entry));
return (
requireSymbol(state, state.interfaces, name, entry, "interface")?.revisionId ??
capabilityId.interfaceRevision(`unresolved:${name}`)
@@ -773,22 +1003,148 @@ const lowerPackageConstructor = (
return entry;
};
const lowerGenericPackageExport = (
state: LoweringState,
context: PackageOperationExportContext | PackageFunctionExportContext,
): GenericPackageExport => {
const previous = state.types.parameters;
state.types.parameters = new Map();
const id = capabilityId.packageExport(stringValue(context.stringLiteral()));
try {
const parameters = state.types.declareParameters(context.typeParameters(), JSON.stringify(["export", id]));
const dependencyPorts = (context.dependencyBlock()?.dependencyPort() ?? []).map((port): GenericDependencyPort => {
const base = {
id: capabilityId.dependencyPort(stringValue(port.stringLiteral())),
displayName: identifier(port.identifier(0)),
};
const primitives =
port
.primitiveList()
?.primitive()
.map((item) => text(item)) ?? [];
if (port.STATE())
return {
...base,
requirement: {
kind: "state",
valueType: state.types.expression(port.valueType()!),
primitives: primitives as StatePrimitive[],
},
};
if (port.EDGE())
return {
...base,
requirement: {
kind: "edge",
target: state.types.targetExpression(port.targetConstraint()!),
cardinality: lowerCardinality(port.cardinality()!),
primitives: primitives as EdgePrimitive[],
},
};
if (port.INTERFACE())
return {
...base,
requirement: {
kind: "interface",
application: state.types.interfaceByName(identifier(port.identifier(1)), port.typeArguments()),
},
};
const name = identifier(port.identifier(1)),
parameter = state.types.parameters.get(name);
const target: ObjectTypeExpression =
parameter?.kind === "object"
? { kind: "parameter", parameterId: parameter.id }
: { kind: "atom", atomId: requireSymbol(state, state.atoms, name, port, "atom")! };
if (!port.valueType())
throw new GenericTypeError("missing-constructor-input", id, "Constructor ports require an explicit input type");
return {
...base,
requirement: { kind: "constructor", target, inputType: state.types.expression(port.valueType()!) },
};
});
const operation = "receiverRequirement" in context ? context : undefined;
const receiver = operation?.receiverRequirement();
let receiverRequirement: GenericPackageExport["receiverRequirement"] = { kind: "any-object" };
if (receiver?.ATOM())
receiverRequirement = {
kind: "target",
target: {
kind: "atom",
atomId: requireSymbol(state, state.atoms, identifier(receiver.identifier()), receiver, "atom")!,
},
};
if (receiver?.OBJECT()) {
const name = identifier(receiver.identifier());
const parameter = state.types.parameters.get(name);
if (parameter?.kind !== "object")
throw new GenericTypeError("parameter-kind", id, "Receiver needs an object parameter");
receiverRequirement = { kind: "target", target: { kind: "parameter", parameterId: parameter.id } };
}
if (receiver?.INTERFACES())
receiverRequirement = {
kind: "interfaces",
interfaces: receiver.interfaceType().map((item) => state.types.interface(item)),
};
const definition: GenericPackageExport = {
id,
displayName: identifier(context.identifier()),
parameters,
kind: operation ? "operation" : "function",
inputType: state.types.expression(context.valueType(0)!),
outputType: state.types.expression(context.valueType(1)!),
dependencyPorts,
receiverRequirement,
...(operation ? { mode: text(operation.operationMode()) as InterfaceOperationMode } : {}),
...(operation?.eventClause() ? { eventType: state.types.expression(operation.eventClause()!.valueType()) } : {}),
aliases: [...state.types.aliases.values()],
};
const bindSelf = (value: unknown): unknown => {
if (!value || typeof value !== "object") return value;
if ("kind" in value && value.kind === "self") {
if (receiverRequirement.kind !== "target")
throw new GenericTypeError("unbound-self", id, "Generic Self requires an exact or object-parameter receiver");
return receiverRequirement.target;
}
if (Array.isArray(value)) return value.map(bindSelf);
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, bindSelf(child)]));
};
return bindSelf(definition) as GenericPackageExport;
} finally {
state.types.parameters = previous;
}
};
const lowerPackage = (
state: LoweringState,
context: PackageResourceDeclContext,
source: SourceRevision,
): PackageRevision => {
const alias = identifier(context.identifier());
const exports = context.packageExport().map((exportContext) => {
const genericExports: GenericPackageExport[] = [];
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
const operation = exportContext.packageOperationExport();
if (operation) {
return lowerPackageOperation(state, alias, operation);
const generic = operation ?? exportContext.packageFunctionExport();
if (generic?.typeParameters()) {
genericExports.push(atGenericSource(generic, () => lowerGenericPackageExport(state, generic)));
return [];
}
const fn = exportContext.packageFunctionExport();
if (fn) {
return lowerPackageFunction(state, alias, fn);
const constructor = exportContext.packageConstructorExport();
const receiver = operation?.receiverRequirement();
const selfName = constructor
? identifier(constructor.identifier(1))
: receiver?.ATOM()
? identifier(receiver.identifier())
: undefined;
const previousSelf = state.types.self;
state.types.self = selfName ? state.atoms.get(selfName) : undefined;
try {
if (operation) return [lowerPackageOperation(state, alias, operation)];
const fn = exportContext.packageFunctionExport();
if (fn) return [lowerPackageFunction(state, alias, fn)];
return [lowerPackageConstructor(state, alias, constructor!)];
} finally {
state.types.self = previousSelf;
}
return lowerPackageConstructor(state, alias, exportContext.packageConstructorExport()!);
});
return {
packageId: capabilityId.package(stringValue(context.stringLiteral(0))),
@@ -797,6 +1153,7 @@ const lowerPackage = (
displayName: alias,
source,
exports,
...(genericExports.length ? { genericExports } : {}),
};
};
@@ -978,7 +1335,13 @@ const lowerBoundDependencies = (
}
if (entry.INTERFACE()) {
const interfaceName = identifier(entry.identifier(1));
const interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, entry, "interface");
let interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, entry, "interface");
if (interfaceSymbol && (entry.typeArguments() || interfaceSymbol.definition?.template)) {
const id = new TypeSubstitution(state.types.environment()).application(
state.types.interfaceByName(interfaceName, entry.typeArguments()),
);
interfaceSymbol = interfaceSymbolFor(state.types.definitions.get(id)!);
}
const via = entry.VIA() ? traversal(identifier(entry.identifier(2)), identifier(entry.identifier(3))) : undefined;
if (entry.VIA() && !via) return [];
return interfaceSymbol
@@ -1047,10 +1410,23 @@ const lowerConformance = (
const atomName = identifier(context.identifier(0));
const interfaceName = identifier(context.identifier(1));
const atomId = requireSymbol(state, state.atoms, atomName, context, "atom");
const interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, context, "interface");
let interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, context, "interface");
if (!atomId || !interfaceSymbol) {
return undefined;
}
const originalSymbol = interfaceSymbol;
if (context.typeArguments() || interfaceSymbol.definition?.template) {
const previousSelf = state.types.self;
state.types.self = atomId;
try {
const expression = state.types.interfaceByName(interfaceName, context.typeArguments());
const id = new TypeSubstitution(state.types.environment()).application(expression);
interfaceSymbol = interfaceSymbolFor(state.types.definitions.get(id)!);
state.interfaces.set(interfaceName, interfaceSymbol);
} finally {
state.types.self = previousSelf;
}
}
const operationBindings = context
.conformanceItem()
.flatMap<WorkspaceRevision["conformances"][number]["operationBindings"][number]>((item) => {
@@ -1119,9 +1495,30 @@ const lowerConformance = (
const packageName = identifier(provider.identifier(0));
const exportName = identifier(provider.identifier(1));
const packageSymbol = requireSymbol(state, state.packages, packageName, provider, "package");
const exportSymbol = packageSymbol
let exportSymbol = packageSymbol
? requireSymbol(state, packageSymbol.exports, exportName, provider, `export on ${packageName}`)
: undefined;
const generic = packageSymbol?.definition?.genericExports?.find((entry) => entry.displayName === exportName);
if (packageSymbol?.definition && generic) {
const arguments_ = state.types
.arguments(provider.typeArguments())
.map((entry) => new TypeSubstitution({ ...state.types.environment(), self: atomId }).argument(entry));
const pkg = packageSymbol.definition;
const specialized = specializePackageExport(pkg, generic, arguments_, {
...state.types.environment(),
implementsInterface: (target, required) => {
(pkg.argumentRequirements ??= []).push({ target, required });
return true;
},
});
if (!pkg.exports.some((entry) => entry.id === specialized.id)) pkg.exports.push(specialized);
exportSymbol = {
exportId: specialized.id,
ports: new Map(specialized.dependencyPorts.map((entry) => [entry.displayName, entry.id])),
};
} else if (provider.typeArguments()) {
throw new GenericTypeError("type-arity", exportName, "Non-generic export takes no type arguments");
}
return packageSymbol && exportSymbol
? [
{
@@ -1146,6 +1543,7 @@ const lowerConformance = (
const lowered = lowerRelationshipMaterialization(state, interfaceName, materialization);
return lowered ? [lowered] : [];
});
state.interfaces.set(interfaceName, originalSymbol);
return {
atomId,
interfaceRevisionId: interfaceSymbol.revisionId,
@@ -1158,6 +1556,7 @@ const lowerConformance = (
};
const interfaceSymbolFor = (revision: InterfaceRevision): InterfaceSymbol => ({
definition: revision,
revisionId: revision.revisionId,
contractAvailable: true,
members: new Map(
@@ -1172,9 +1571,10 @@ const interfaceSymbolFor = (revision: InterfaceRevision): InterfaceSymbol => ({
});
const packageSymbolFor = (revision: PackageRevision): PackageSymbol => ({
definition: structuredClone(revision),
revisionId: revision.revisionId,
exports: new Map(
revision.exports.map((entry) => [
[...revision.exports, ...(revision.genericExports ?? [])].map((entry) => [
entry.displayName,
{
exportId: entry.id,
@@ -1193,8 +1593,10 @@ const registerImports = (
state: LoweringState,
contexts: readonly ResourceImportDeclContext[],
environment: CapabilityImportEnvironment,
) =>
contexts.map((context) => {
) => {
for (const definition of environment.interfaceClosure ?? [])
state.types.definitions.set(definition.revisionId, definition);
return contexts.map((context) => {
const imported = resourceImport(context);
if (imported.kind === "interface") {
const revision = environment.interfaces?.get(imported.binding);
@@ -1207,6 +1609,7 @@ const registerImports = (
);
} else {
declareSymbol(state, state.interfaces, imported.binding, interfaceSymbolFor(revision), context, "interface");
state.types.register(imported.binding, revision);
}
} else {
const revision = environment.packages?.get(imported.binding);
@@ -1223,6 +1626,7 @@ const registerImports = (
}
return imported;
});
};
const uniqueExactRevisions = <
Revision extends {
@@ -1295,6 +1699,8 @@ const lowerWorkspace = (
: [];
});
state.types.declareAliases(items.flatMap((item) => item.typeAliasDecl() ?? []));
const interfaceImports = uniqueExactRevisions([
...(environment.interfaceClosure ?? []),
...[...(environment.interfaces?.values() ?? [])],
@@ -1319,6 +1725,7 @@ const lowerWorkspace = (
}
const conformance = item.conformanceDecl();
if (conformance) {
state.types.self = state.atoms.get(identifier(conformance.identifier(0)));
const attachments = conformance.conformanceItem().flatMap((entry) => {
const declaration = entry.attachmentDecl();
if (!declaration) {
@@ -1332,6 +1739,7 @@ const lowerWorkspace = (
return [attachment];
});
privateAttachments.set(conformance, attachments);
state.types.self = undefined;
}
}
@@ -1340,7 +1748,9 @@ const lowerWorkspace = (
if (!context) {
return [];
}
const conformance = lowerConformance(state, context, privateAttachments.get(context) ?? []);
const conformance = atGenericSource(context, () =>
lowerConformance(state, context, privateAttachments.get(context) ?? []),
);
return conformance ? [conformance] : [];
});
@@ -1382,8 +1792,15 @@ const lowerWorkspace = (
sourceRootCommit: stringValue(context.stringLiteral(2)),
atoms,
sharedAttachments,
interfaceImports,
packageImports,
interfaceImports: uniqueExactRevisions([
...interfaceImports.filter((entry) => !entry.template),
...state.types.applications.values(),
]),
packageImports: packageImports.map(
(revision) =>
[...state.packages.values()].find((entry) => entry.revisionId === revision.revisionId)?.definition ??
revision,
),
conformances,
constructors,
},
@@ -1408,14 +1825,18 @@ export const parseDocument = (
return { tree, tokens, diagnostics };
};
const newLoweringState = (fileName: string, diagnostics: CapabilitySourceDiagnostic[]): LoweringState => ({
fileName,
diagnostics,
atoms: new Map(),
interfaces: new Map(),
packages: new Map(),
attachments: new Map(),
});
const newLoweringState = (fileName: string, diagnostics: CapabilitySourceDiagnostic[]): LoweringState => {
const atoms = new Map<string, AtomId>();
return {
fileName,
diagnostics,
atoms,
types: new GenericSourceTypes(atoms),
interfaces: new Map(),
packages: new Map(),
attachments: new Map(),
};
};
const validationDiagnostics = (
fileName: string,
@@ -1471,6 +1892,7 @@ const resourcePreambleParts = (contexts: readonly ResourcePreambleContext[]) =>
imports: contexts.flatMap((context) => context.resourceImportDecl() ?? []),
atoms: contexts.flatMap((context) => context.externalAtomDecl() ?? []),
interfaces: contexts.flatMap((context) => context.externalInterfaceDecl() ?? []),
aliases: contexts.flatMap((context) => context.typeAliasDecl() ?? []),
});
const resourceValidationWorkspace = (
@@ -1493,6 +1915,7 @@ const resourceValidationWorkspace = (
...(environment.interfaceClosure ?? []),
...[...(environment.interfaces?.values() ?? [])],
...(resource.kind === "interface" ? [resource.revision] : []),
...(resource.specializations ?? []),
...resource.externalInterfaces
.filter((requirement) => !resolvedInterfaceIds.has(requirement.revisionId))
.map(
@@ -1507,7 +1930,9 @@ const resourceValidationWorkspace = (
members: [],
}),
),
]),
])
.filter((entry) => !entry.template)
.map((entry) => ({ ...entry, argumentRequirements: [] })),
packageImports: uniqueExactRevisions([
...(environment.packageClosure ?? []),
...[...(environment.packages?.values() ?? [])],
@@ -1518,7 +1943,7 @@ const resourceValidationWorkspace = (
};
};
export const compileCapabilityResourceSource = (
const compileCapabilityResourceSourceInternal = (
sourceText: string,
options: {
source: SourceRevision;
@@ -1556,10 +1981,18 @@ export const compileCapabilityResourceSource = (
const externalAtoms = externalAtomsFrom(state, preamble.atoms);
const imports = registerImports(state, preamble.imports, environment);
const externalInterfaces = externalInterfacesFrom(state, preamble.interfaces);
state.types.declareAliases(preamble.aliases);
let resource: CapabilityResource;
if (interfaceContext) {
const alias = identifier(interfaceContext.identifier());
state.types.register(alias, {
interfaceId: capabilityId.interface(stringValue(interfaceContext.stringLiteral(0))),
revisionId: capabilityId.interfaceRevision(stringValue(interfaceContext.stringLiteral(1))),
displayName: alias,
source: options.source,
members: [],
});
declareSymbol(
state,
state.interfaces,
@@ -1577,7 +2010,10 @@ export const compileCapabilityResourceSource = (
imports,
externalAtoms,
externalInterfaces,
revision: lowerInterface(state, interfaceContext, options.source),
revision:
interfaceContext.typeParameters() || hasSelfIdentifier(interfaceContext, state)
? atGenericSource(interfaceContext, () => lowerInterfaceTemplate(state, interfaceContext, options.source))
: lowerInterface(state, interfaceContext, options.source),
};
} else {
const context = packageContext!;
@@ -1602,6 +2038,77 @@ export const compileCapabilityResourceSource = (
};
}
if (resource.kind === "interface") {
state.types.register(identifier(interfaceContext!.identifier()), resource.revision);
atGenericSource(interfaceContext!, () => state.types.validateTemplate(resource.revision));
} else {
const names = new Set(resource.revision.exports.map((entry) => entry.displayName));
const ids = new Set(resource.revision.exports.map((entry) => entry.id));
for (const entry of resource.revision.genericExports ?? []) {
if (names.has(entry.displayName) || ids.has(entry.id))
throw new GenericTypeError("duplicate-package-export", entry.displayName, "Duplicate generic export");
names.add(entry.displayName);
ids.add(entry.id);
if (
entry.kind === "operation" &&
(entry.mode === "watch-start" || entry.mode === "subscribe") !== Boolean(entry.eventType)
)
throw new GenericTypeError(
"invalid-operation",
entry.displayName,
"Only watch-start/subscribe operations must declare an event type",
);
const portIds = new Set<string>(),
portNames = new Set<string>();
const types: ValueTypeExpression[] = [
entry.inputType,
entry.outputType,
...(entry.eventType ? [entry.eventType] : []),
];
const requires = entry.parameters.flatMap((parameter) =>
parameter.kind === "object" ? parameter.implements : [],
);
for (const port of entry.dependencyPorts) {
if (portIds.has(port.id) || portNames.has(port.displayName))
throw new GenericTypeError("duplicate-dependency-port", entry.displayName, "Duplicate port");
portIds.add(port.id);
portNames.add(port.displayName);
const requirement = port.requirement;
if (requirement.kind === "state") {
if (requirement.primitives.some((p) => !["read", "write", "watch-start", "watch-stop"].includes(p)))
throw new GenericTypeError("invalid-port-primitive", port.displayName, "Invalid state primitive");
types.push(requirement.valueType);
}
if (requirement.kind === "edge") types.push({ kind: "object-ref", expectation: requirement.target });
if (requirement.kind === "constructor")
types.push(requirement.inputType, { kind: "object-ref", expectation: requirement.target });
if (requirement.kind === "interface") requires.push(requirement.application);
}
if (entry.receiverRequirement.kind === "interfaces") requires.push(...entry.receiverRequirement.interfaces);
state.types.validateTemplate({
interfaceId: capabilityId.interface(entry.id),
revisionId: capabilityId.interfaceRevision(entry.id),
displayName: entry.displayName,
source: resource.revision.source,
members: [],
template: {
parameters: entry.parameters,
aliases: entry.aliases,
usesSelf: false,
requires,
members: types.map((type, index) => ({
kind: "value",
id: capabilityId.member(String(index)),
displayName: String(index),
valueType: type,
operations: [],
})),
},
});
}
}
resource.specializations = [...state.types.applications.values()];
if (diagnostics.length > 0) {
return { ok: false, diagnostics };
}
@@ -1615,7 +2122,7 @@ export const compileCapabilityResourceSource = (
return { ok: true, resource, diagnostics: [] };
};
export const compileCapabilitySource = (
const compileCapabilitySourceInternal = (
source: string,
fileName = "<memory>",
environment: CapabilityImportEnvironment = {},
@@ -1666,3 +2173,35 @@ export const compileCapabilitySource = (
diagnostics: [],
};
};
const genericDiagnostic = (error: GenericTypeError, fileName: string): CapabilitySourceDiagnostic => ({
phase: "lowering",
code: error.code,
message: error.message,
fileName,
line: genericLocations.get(error)?.line ?? 0,
column: genericLocations.get(error)?.column ?? 0,
path: error.path,
});
export const compileCapabilityResourceSource = (
...args: Parameters<typeof compileCapabilityResourceSourceInternal>
): CapabilityResourceCompileResult => {
try {
return compileCapabilityResourceSourceInternal(...args);
} catch (error) {
if (!(error instanceof GenericTypeError)) throw error;
return { ok: false, diagnostics: [genericDiagnostic(error, args[1].fileName ?? "<memory>")] };
}
};
export const compileCapabilitySource = (
...args: Parameters<typeof compileCapabilitySourceInternal>
): CapabilitySourceCompileResult => {
try {
return compileCapabilitySourceInternal(...args);
} catch (error) {
if (!(error instanceof GenericTypeError)) throw error;
return { ok: false, diagnostics: [genericDiagnostic(error, args[1] ?? "<memory>")] };
}
};
+1 -1
View File
@@ -28,7 +28,7 @@ type Change = { file: string; before: string | null; after: string; mode: number
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
const safeFile = (file: string) => {
if (
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|lock|ts|tsx|css|mjs|json|nix|txtpb))$/.test(
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
file,
) ||
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))
+21 -1
View File
@@ -4,6 +4,7 @@ import { readFile, writeFile } from "node:fs/promises";
import process from "node:process";
import { compileWorkspaceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
import {
planEvolution,
runtimeContracts,
@@ -14,7 +15,7 @@ import {
const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY
[--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID]
[--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH]
[--reviews REVIEW_JSON]
[--reviews REVIEW_JSON] [--schemas-out PATH]
Resolves a workspace's recursive resource-lock graph, clones every exact
resource revision, validates standalone interface/package manifests, and emits
@@ -35,6 +36,7 @@ const parseArgs = (args: string[]) => {
rootDirectory,
checkoutRoot,
graphOut: values.get("--graph-out"),
schemasOut: values.get("--schemas-out"),
snapshotMap: values.get("--snapshot-map"),
workspaceId: values.get("--workspace-id"),
workspaceRevisionId: values.get("--workspace-revision-id"),
@@ -96,6 +98,24 @@ const main = async () => {
);
}
const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) };
if (options.schemasOut) {
const schemas: Record<string, unknown> = {};
for (const node of assembled.resources.filter((entry) => entry.kind === "package")) {
const closure = new Map<string, typeof node>();
const visit = (entry: typeof node) => {
if (closure.has(entry.key)) return;
closure.set(entry.key, entry);
entry.dependencies.forEach(visit);
};
visit(node);
schemas[node.resource.revision.revisionId] = specializeBindingSchema(
bindingSchema({ resources: [...closure.values()] }),
candidate,
node.resource.revision.revisionId,
);
}
await writeFile(options.schemasOut, JSON.stringify(schemas));
}
if (options.evolutionOut) {
const baseline = options.baseline
? (JSON.parse(await readFile(options.baseline, "utf8")) as WorkspaceRevision)