@ldclabs/kip-lang 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/dist/budget.d.ts +6 -5
- package/dist/budget.d.ts.map +1 -1
- package/dist/budget.js +10 -6
- package/dist/budget.js.map +1 -1
- package/dist/formatter.js +57 -4
- package/dist/formatter.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +90 -18
- package/dist/lower.js.map +1 -1
- package/dist/semantics.d.ts.map +1 -1
- package/dist/semantics.js +59 -10
- package/dist/semantics.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/budget.ts +10 -6
- package/src/formatter.ts +58 -4
- package/src/lower.ts +108 -25
- package/src/semantics.ts +64 -11
- package/src/version.ts +1 -1
package/src/lower.ts
CHANGED
|
@@ -50,7 +50,8 @@ import type {
|
|
|
50
50
|
HistoryStatement,
|
|
51
51
|
ChangesStatement,
|
|
52
52
|
SnapshotStatement,
|
|
53
|
-
ExportCapsuleStatement
|
|
53
|
+
ExportCapsuleStatement,
|
|
54
|
+
NumberLiteral
|
|
54
55
|
} from './ast.js'
|
|
55
56
|
import { invalidSyntax } from './errors.js'
|
|
56
57
|
import type {
|
|
@@ -955,22 +956,44 @@ function lowerCreateConcept(stmt: CreateConceptStatement): ConceptCreate {
|
|
|
955
956
|
}
|
|
956
957
|
}
|
|
957
958
|
|
|
959
|
+
/**
|
|
960
|
+
* Whether a matcher pins exactly one Concept.
|
|
961
|
+
*
|
|
962
|
+
* Only `id` and `key` identify, and only when they carry a value that is one
|
|
963
|
+
* value: a literal, or a parameter the runtime binds to one. Anything else —
|
|
964
|
+
* a variable, a nested pattern, a list — describes candidates.
|
|
965
|
+
*/
|
|
966
|
+
function hasStableIdentity(match: ObjectMatcher): boolean {
|
|
967
|
+
for (const field of ['id', 'key']) {
|
|
968
|
+
const value = match[field]
|
|
969
|
+
if (value && ('Literal' in value || 'Param' in value)) return true
|
|
970
|
+
}
|
|
971
|
+
return false
|
|
972
|
+
}
|
|
973
|
+
|
|
958
974
|
function lowerUpsertConcept(stmt: UpsertConceptStatement): ConceptUpsert {
|
|
959
975
|
const match = stmt.match ? lowerObjectMatcher(stmt.match.pattern) : null
|
|
960
976
|
|
|
961
|
-
// Identity for an upsert is `id` or `key
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
977
|
+
// Identity for an upsert is `id` or `key`, spelled as a literal or a
|
|
978
|
+
// parameter. Three things are refused here, and they are the same mistake at
|
|
979
|
+
// different depths:
|
|
980
|
+
//
|
|
981
|
+
// - no MATCH at all, which would make UPSERT mean "create, always";
|
|
982
|
+
// - a name-only match, because names are mutable grounding state with
|
|
983
|
+
// duplicates allowed, so "the Concept named X" can silently address a
|
|
984
|
+
// different node over time;
|
|
985
|
+
// - an identity whose value is a variable, which is a *set* of candidates
|
|
986
|
+
// rather than one element — an upsert resolving it would pick a winner.
|
|
987
|
+
//
|
|
988
|
+
// A match may carry other fields beside the identity; they narrow, they do
|
|
989
|
+
// not identify.
|
|
990
|
+
if (!match || !hasStableIdentity(match)) {
|
|
991
|
+
throw invalidSyntax(
|
|
992
|
+
'UPSERT CONCEPT requires a MATCH on a stable identity: {id: <literal-or-parameter>} ' +
|
|
993
|
+
'or {key: <literal-or-parameter>} — name is mutable grounding state and never ' +
|
|
994
|
+
'identifies a Concept, and a variable names a set rather than an element',
|
|
995
|
+
stmt.match ? stmt.match.range : stmt.range
|
|
996
|
+
)
|
|
974
997
|
}
|
|
975
998
|
|
|
976
999
|
return {
|
|
@@ -1607,11 +1630,11 @@ function lowerUpdateExpr(
|
|
|
1607
1630
|
return { Param: paramName(expr.name) }
|
|
1608
1631
|
|
|
1609
1632
|
case 'NumberLiteral':
|
|
1610
|
-
return { Number: expr
|
|
1633
|
+
return { Number: numberValue(expr) }
|
|
1611
1634
|
|
|
1612
1635
|
case 'UnaryExpression':
|
|
1613
1636
|
if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
|
|
1614
|
-
return { Number: -expr.operand
|
|
1637
|
+
return { Number: -numberValue(expr.operand) }
|
|
1615
1638
|
}
|
|
1616
1639
|
throw invalidSyntax(
|
|
1617
1640
|
`expected a number, a parameter, the target's own field or a registered function, found ${describeExpression(expr)}`,
|
|
@@ -1869,9 +1892,19 @@ function lowerHistory(stmt: HistoryStatement): HistoryCommand {
|
|
|
1869
1892
|
}
|
|
1870
1893
|
|
|
1871
1894
|
function lowerExport(stmt: ExportCapsuleStatement) {
|
|
1895
|
+
const where_clauses = lowerWhere(stmt.where)
|
|
1896
|
+
// A Capsule is a bounded, self-contained excerpt. `WHERE { }` selects the
|
|
1897
|
+
// whole Space, which is not a smaller thing to hand somebody — it is the
|
|
1898
|
+
// Brain, exported by accident.
|
|
1899
|
+
if (where_clauses.length === 0) {
|
|
1900
|
+
throw invalidSyntax(
|
|
1901
|
+
'expected at least one selection pattern: an unbounded EXPORT is not a Capsule',
|
|
1902
|
+
stmt.where.range
|
|
1903
|
+
)
|
|
1904
|
+
}
|
|
1872
1905
|
return {
|
|
1873
1906
|
target: lowerElementRef(stmt.target),
|
|
1874
|
-
where_clauses
|
|
1907
|
+
where_clauses,
|
|
1875
1908
|
options: stmt.options ? lowerBoundObject(stmt.options) : null,
|
|
1876
1909
|
as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null
|
|
1877
1910
|
}
|
|
@@ -1924,18 +1957,68 @@ function lowerElementRef(ref: TargetRef): ElementRef {
|
|
|
1924
1957
|
}
|
|
1925
1958
|
}
|
|
1926
1959
|
|
|
1960
|
+
// ---------------------------------------------------------------------------
|
|
1961
|
+
// Numeric literals
|
|
1962
|
+
// ---------------------------------------------------------------------------
|
|
1963
|
+
|
|
1964
|
+
/** `i64::MIN` — the most negative integer a KIP number literal may spell. */
|
|
1965
|
+
const INT_MIN = -(2n ** 63n)
|
|
1966
|
+
/** `u64::MAX` — the largest. */
|
|
1967
|
+
const INT_MAX = 2n ** 64n - 1n
|
|
1968
|
+
|
|
1969
|
+
/** An integer literal: no fraction, no exponent, so it is read as an integer. */
|
|
1970
|
+
const INTEGER_FORM = /^-?\d+$/
|
|
1971
|
+
|
|
1972
|
+
/**
|
|
1973
|
+
* The value of a number literal, refusing the ones that cannot survive being
|
|
1974
|
+
* one.
|
|
1975
|
+
*
|
|
1976
|
+
* A JavaScript number is a double, so `18446744073709551617` silently becomes
|
|
1977
|
+
* `18446744073709551616` on the way in. Accepting that would be the worst
|
|
1978
|
+
* possible outcome: the command does not fail, it *executes with a different
|
|
1979
|
+
* number than it says*, and no engine downstream can detect it — by the time
|
|
1980
|
+
* an executable AST exists the digits are gone. So the check happens here,
|
|
1981
|
+
* against the raw text, which is the only place the original is still around.
|
|
1982
|
+
*
|
|
1983
|
+
* The bounds are the reference grammar's: an integer literal is read as an
|
|
1984
|
+
* `i64` or a `u64` and must fit one of them, and any other form must parse to a
|
|
1985
|
+
* finite double. `18446744073709551616.0` is therefore accepted where
|
|
1986
|
+
* `18446744073709551616` is not — the float form is claiming an approximation,
|
|
1987
|
+
* and the integer form is claiming an exact value it cannot deliver.
|
|
1988
|
+
*
|
|
1989
|
+
* Integers above 2^53 still lose precision in this implementation's `value`
|
|
1990
|
+
* even though they are accepted, because a double cannot hold them. That is a
|
|
1991
|
+
* property of the host, not a disagreement about the language: both engines
|
|
1992
|
+
* agree the command is legal, and a runtime that needs the exact digits has
|
|
1993
|
+
* `raw`.
|
|
1994
|
+
*/
|
|
1995
|
+
function numberValue(node: NumberLiteral): number {
|
|
1996
|
+
if (INTEGER_FORM.test(node.raw)) {
|
|
1997
|
+
const exact = BigInt(node.raw)
|
|
1998
|
+
if (exact < INT_MIN || exact > INT_MAX) {
|
|
1999
|
+
throw invalidSyntax(
|
|
2000
|
+
`${node.raw} is outside the range a KIP integer literal can represent ` +
|
|
2001
|
+
`(${INT_MIN} to ${INT_MAX})`,
|
|
2002
|
+
node.range
|
|
2003
|
+
)
|
|
2004
|
+
}
|
|
2005
|
+
return node.value
|
|
2006
|
+
}
|
|
2007
|
+
if (!Number.isFinite(node.value)) {
|
|
2008
|
+
throw invalidSyntax(
|
|
2009
|
+
`only finite numbers are valid KIP literals, found ${node.raw}`,
|
|
2010
|
+
node.range
|
|
2011
|
+
)
|
|
2012
|
+
}
|
|
2013
|
+
return node.value
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1927
2016
|
function lowerKipValue(expr: Expression): KipValue {
|
|
1928
2017
|
switch (expr.kind) {
|
|
1929
2018
|
case 'StringLiteral':
|
|
1930
2019
|
return { String: expr.parsed }
|
|
1931
2020
|
case 'NumberLiteral':
|
|
1932
|
-
|
|
1933
|
-
throw invalidSyntax(
|
|
1934
|
-
`only finite numbers are valid KIP literals, found ${expr.raw}`,
|
|
1935
|
-
expr.range
|
|
1936
|
-
)
|
|
1937
|
-
}
|
|
1938
|
-
return { Number: expr.value }
|
|
2021
|
+
return { Number: numberValue(expr) }
|
|
1939
2022
|
case 'BooleanLiteral':
|
|
1940
2023
|
return { Bool: expr.value }
|
|
1941
2024
|
case 'NullLiteral':
|
|
@@ -1954,7 +2037,7 @@ function lowerKipValue(expr: Expression): KipValue {
|
|
|
1954
2037
|
}
|
|
1955
2038
|
case 'UnaryExpression':
|
|
1956
2039
|
if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
|
|
1957
|
-
return { Number: -expr.operand
|
|
2040
|
+
return { Number: -numberValue(expr.operand) }
|
|
1958
2041
|
}
|
|
1959
2042
|
throw invalidSyntax(
|
|
1960
2043
|
`expected a value, found ${describeExpression(expr)}`,
|
package/src/semantics.ts
CHANGED
|
@@ -11,7 +11,8 @@ import type {
|
|
|
11
11
|
ScalarValue,
|
|
12
12
|
MutateStatement,
|
|
13
13
|
AssertStatement,
|
|
14
|
-
SetFacetClause
|
|
14
|
+
SetFacetClause,
|
|
15
|
+
SetStructuralClause
|
|
15
16
|
} from './ast.js'
|
|
16
17
|
import type { Range } from './token.js'
|
|
17
18
|
|
|
@@ -54,6 +55,7 @@ const ASSERTION_STATES = new Set([
|
|
|
54
55
|
'expired'
|
|
55
56
|
])
|
|
56
57
|
const ACTIVITY_TERMINAL = new Set(['completed', 'failed', 'cancelled'])
|
|
58
|
+
const EVIDENCE_ROLES = new Set(['support', 'challenge', 'context'])
|
|
57
59
|
|
|
58
60
|
/** Signals the Profile fixes to `[0,1]`; none of them is truth. */
|
|
59
61
|
const UNIT_INTERVAL_FIELDS = new Set([
|
|
@@ -96,11 +98,11 @@ function analyzeMutationClause(stmt: Statement, diags: Diagnostic[]): void {
|
|
|
96
98
|
break
|
|
97
99
|
|
|
98
100
|
case 'CreateConceptStatement':
|
|
99
|
-
for (const facet of stmt.setFacets) checkFacet(facet, diags)
|
|
100
|
-
break
|
|
101
|
-
|
|
102
101
|
case 'UpsertConceptStatement':
|
|
102
|
+
case 'CreateEvidenceStatement':
|
|
103
|
+
case 'CreateActivityStatement':
|
|
103
104
|
for (const facet of stmt.setFacets) checkFacet(facet, diags)
|
|
105
|
+
checkStructural(stmt.setStructural, diags)
|
|
104
106
|
break
|
|
105
107
|
|
|
106
108
|
case 'CreateAssertionStatement':
|
|
@@ -108,30 +110,61 @@ function analyzeMutationClause(stmt: Statement, diags: Diagnostic[]): void {
|
|
|
108
110
|
checkAssignmentValues(stmt.setFields.assignments, diags)
|
|
109
111
|
}
|
|
110
112
|
for (const facet of stmt.setFacets) checkFacet(facet, diags)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
case 'CreateEvidenceStatement':
|
|
114
|
-
case 'CreateActivityStatement':
|
|
115
|
-
for (const facet of stmt.setFacets) checkFacet(facet, diags)
|
|
113
|
+
checkStructural(stmt.setStructural, diags)
|
|
116
114
|
break
|
|
117
115
|
|
|
118
116
|
case 'UpdateStatement':
|
|
119
117
|
for (const action of stmt.actions) {
|
|
120
118
|
if (action.kind === 'SetFacetClause') checkFacet(action, diags)
|
|
119
|
+
// Core fields are Core-typed wherever they are written, so an UPDATE
|
|
120
|
+
// that sets one gets the same check `CREATE ASSERTION` already gets.
|
|
121
|
+
else if (action.kind === 'SetFieldsClause') {
|
|
122
|
+
checkAssignmentValues(action.assignments, diags)
|
|
123
|
+
} else if (action.kind === 'SetStructuralClause') {
|
|
124
|
+
checkStructural(action, diags)
|
|
125
|
+
}
|
|
121
126
|
}
|
|
122
127
|
if (stmt.where) checkWhere(stmt.where, !!stmt.limit, diags)
|
|
123
128
|
break
|
|
124
129
|
|
|
130
|
+
// `EXPECT STATE` is checkable only where the target's kind is fixed by the
|
|
131
|
+
// statement itself. RETRACT and SUPERSEDE always name an Assertion, so the
|
|
132
|
+
// Assertion lifecycle registry applies. ARCHIVE and TOMBSTONE take any
|
|
133
|
+
// element — a Concept is not `retracted` and may well be `archived` — and
|
|
134
|
+
// Core registers no element-lifecycle vocabulary, so checking them here
|
|
135
|
+
// rejected commands the Specification admits.
|
|
125
136
|
case 'RetractAssertionStatement':
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
if (stmt.expectState) {
|
|
138
|
+
checkEnum(stmt.expectState.value, ASSERTION_STATES, 'EXPECT STATE', diags)
|
|
139
|
+
}
|
|
140
|
+
if (stmt.where) checkWhere(stmt.where, !!stmt.limit, diags)
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
case 'SupersedeAssertionStatement':
|
|
128
144
|
if (stmt.expectState) {
|
|
129
145
|
checkEnum(stmt.expectState.value, ASSERTION_STATES, 'EXPECT STATE', diags)
|
|
130
146
|
}
|
|
131
147
|
break
|
|
132
148
|
|
|
149
|
+
case 'ArchiveStatement':
|
|
150
|
+
case 'TombstoneStatement':
|
|
151
|
+
if (stmt.where) checkWhere(stmt.where, !!stmt.limit, diags)
|
|
152
|
+
break
|
|
153
|
+
|
|
133
154
|
case 'TransitionActivityStatement':
|
|
134
155
|
checkEnum(stmt.to, ACTIVITY_TERMINAL, 'TRANSITION ACTIVITY TO', diags)
|
|
156
|
+
checkStructural(stmt.finalize.find(
|
|
157
|
+
(c): c is SetStructuralClause => c.kind === 'SetStructuralClause'
|
|
158
|
+
), diags)
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
// Spec §52.7 names six statements whose WHERE can select an unbounded set
|
|
162
|
+
// and which SHOULD therefore carry a LIMIT. Warning on the read and on
|
|
163
|
+
// UPDATE while staying silent on the removal ladder had it backwards: an
|
|
164
|
+
// over-broad PURGE is the one that cannot be undone.
|
|
165
|
+
case 'PurgeStatement':
|
|
166
|
+
case 'SetRetentionStatement':
|
|
167
|
+
if (stmt.where) checkWhere(stmt.where, !!stmt.limit, diags)
|
|
135
168
|
break
|
|
136
169
|
}
|
|
137
170
|
}
|
|
@@ -176,6 +209,26 @@ function checkFacet(clause: SetFacetClause, diags: Diagnostic[]): void {
|
|
|
176
209
|
checkAssignmentValues(clause.assignments, diags)
|
|
177
210
|
}
|
|
178
211
|
|
|
212
|
+
/**
|
|
213
|
+
* `role` on an `("evidence", ...)` citation comes from the Core registry
|
|
214
|
+
* (Spec §20.13, §56.2), so a misspelling is as checkable here as a bad
|
|
215
|
+
* `stance`. Other structural fields carry engine- or package-defined options,
|
|
216
|
+
* which only the Schema Environment can judge.
|
|
217
|
+
*/
|
|
218
|
+
function checkStructural(
|
|
219
|
+
clause: SetStructuralClause | undefined,
|
|
220
|
+
diags: Diagnostic[]
|
|
221
|
+
): void {
|
|
222
|
+
if (!clause) return
|
|
223
|
+
for (const edge of clause.assignments) {
|
|
224
|
+
if (edge.field.kind === 'ParameterRef' || edge.field.parsed !== 'evidence') {
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
const role = edge.options?.entries.find((e) => e.key === 'role')
|
|
228
|
+
if (role) checkEnum(role.value, EVIDENCE_ROLES, 'Evidence role', diags)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
179
232
|
function checkAssignmentValues(
|
|
180
233
|
object: ObjectLiteral,
|
|
181
234
|
diags: Diagnostic[]
|
package/src/version.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* has to load in runtimes with no filesystem; `test/lower.test.mjs` asserts the
|
|
8
8
|
* two stay in step.
|
|
9
9
|
*/
|
|
10
|
-
export const PARSER_VERSION = '2.0.
|
|
10
|
+
export const PARSER_VERSION = '2.0.2'
|
|
11
11
|
|
|
12
12
|
/** The KIP specification revision this grammar targets. */
|
|
13
13
|
export const KIP_SPEC_REVISION = '2.0-draft'
|