@ldclabs/kip-lang 2.2.0 → 2.3.1
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 +39 -0
- package/README.md +32 -1
- package/dist/ast.d.ts +1 -1
- package/dist/ast.d.ts.map +1 -1
- package/dist/canonical.d.ts +11 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +156 -0
- package/dist/canonical.js.map +1 -0
- package/dist/exec-ast.d.ts +1 -1
- package/dist/exec-ast.d.ts.map +1 -1
- package/dist/formatter.js +1 -3
- package/dist/formatter.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +13 -54
- package/dist/lower.js.map +1 -1
- package/dist/parser.js +0 -8
- package/dist/parser.js.map +1 -1
- package/dist/token.d.ts +0 -2
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +0 -4
- package/dist/token.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/src/ast.ts +0 -2
- package/src/canonical.ts +117 -0
- package/src/exec-ast.ts +0 -2
- package/src/formatter.ts +1 -3
- package/src/index.ts +1 -0
- package/src/lower.ts +17 -63
- package/src/parser.ts +0 -8
- package/src/token.ts +0 -4
- package/src/version.ts +1 -1
package/src/canonical.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Portable KIP JSON: RFC 8785 serialization with safe integral values. */
|
|
2
|
+
export function portableNumber(value: number, raw?: string): number {
|
|
3
|
+
if (!Number.isFinite(value)) throw new Error('only finite numbers are valid KIP values')
|
|
4
|
+
if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
5
|
+
throw new Error('number is outside the range of portable exact integers; use a schema-defined string value')
|
|
6
|
+
}
|
|
7
|
+
// Inspect the significand, not the exponent: 0e-400 is still exactly zero.
|
|
8
|
+
if (value === 0 && raw && /[1-9]/.test(raw.split(/[eE]/)[0])) {
|
|
9
|
+
throw new Error('nonzero number underflows to zero')
|
|
10
|
+
}
|
|
11
|
+
return Object.is(value, -0) ? 0 : value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function scalarString(value: string): string {
|
|
15
|
+
for (let i = 0; i < value.length; i++) {
|
|
16
|
+
const c = value.charCodeAt(i)
|
|
17
|
+
if (c >= 0xd800 && c <= 0xdbff) {
|
|
18
|
+
const next = value.charCodeAt(++i)
|
|
19
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) throw new Error('unpaired Unicode surrogate')
|
|
20
|
+
} else if (c >= 0xdc00 && c <= 0xdfff) throw new Error('unpaired Unicode surrogate')
|
|
21
|
+
}
|
|
22
|
+
return JSON.stringify(value)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Serialize JSON values using JCS (UTF-16 key order, ECMAScript numbers,
|
|
27
|
+
* no Unicode normalization). KIP narrows integral values to safe integers.
|
|
28
|
+
* Semantic NFC normalization belongs to Literal construction, not hashing.
|
|
29
|
+
*/
|
|
30
|
+
export function canonicalize(value: unknown): string {
|
|
31
|
+
const ancestors = new Set<object>()
|
|
32
|
+
function emit(item: unknown, depth: number): string {
|
|
33
|
+
if (depth > 128) throw new Error('JSON nesting limit exceeded')
|
|
34
|
+
if (item === null) return 'null'
|
|
35
|
+
if (typeof item === 'string') return scalarString(item)
|
|
36
|
+
if (typeof item === 'number') return JSON.stringify(portableNumber(item))
|
|
37
|
+
if (typeof item === 'boolean') return item ? 'true' : 'false'
|
|
38
|
+
if (typeof item !== 'object') throw new Error('not a JSON value')
|
|
39
|
+
if (ancestors.has(item)) throw new Error('cyclic JSON value')
|
|
40
|
+
ancestors.add(item)
|
|
41
|
+
let result: string
|
|
42
|
+
if (Array.isArray(item)) {
|
|
43
|
+
result = '[' + Array.from(item, v => emit(v, depth + 1)).join(',') + ']'
|
|
44
|
+
} else {
|
|
45
|
+
const proto = Object.getPrototypeOf(item)
|
|
46
|
+
if (proto !== Object.prototype && proto !== null) throw new Error('not a plain JSON object')
|
|
47
|
+
const object = item as Record<string, unknown>
|
|
48
|
+
result = '{' + Object.keys(object).sort().map(key =>
|
|
49
|
+
scalarString(key) + ':' + emit(object[key], depth + 1)
|
|
50
|
+
).join(',') + '}'
|
|
51
|
+
}
|
|
52
|
+
ancestors.delete(item)
|
|
53
|
+
return result
|
|
54
|
+
}
|
|
55
|
+
return emit(value, 0)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Decode before hashing/binding, rejecting duplicate decoded keys and loss. */
|
|
59
|
+
export function parseCanonicalJson(source: string): unknown {
|
|
60
|
+
let pos = 0
|
|
61
|
+
function whitespace() { while (/[\x20\t\n\r]/.test(source[pos] ?? '\0')) pos++ }
|
|
62
|
+
function string(): string {
|
|
63
|
+
const start = pos++
|
|
64
|
+
while (pos < source.length) {
|
|
65
|
+
const c = source[pos++]
|
|
66
|
+
if (c === '\\') { pos++; continue }
|
|
67
|
+
if (c === '"') {
|
|
68
|
+
const result: string = JSON.parse(source.slice(start, pos))
|
|
69
|
+
scalarString(result)
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw new Error('unterminated JSON string')
|
|
74
|
+
}
|
|
75
|
+
function value(depth: number): unknown {
|
|
76
|
+
if (depth > 128) throw new Error('JSON nesting limit exceeded')
|
|
77
|
+
whitespace()
|
|
78
|
+
if (source[pos] === '"') return string()
|
|
79
|
+
if (source[pos] === '{') {
|
|
80
|
+
pos++; whitespace()
|
|
81
|
+
const result: Record<string, unknown> = Object.create(null)
|
|
82
|
+
if (source[pos] === '}') { pos++; return result }
|
|
83
|
+
while (true) {
|
|
84
|
+
whitespace()
|
|
85
|
+
if (source[pos] !== '"') throw new Error('expected JSON property name')
|
|
86
|
+
const key = string(); whitespace()
|
|
87
|
+
if (Object.hasOwn(result, key)) throw new Error('duplicate JSON property: ' + key)
|
|
88
|
+
if (source[pos++] !== ':') throw new Error('expected colon')
|
|
89
|
+
result[key] = value(depth + 1); whitespace()
|
|
90
|
+
const end = source[pos++]
|
|
91
|
+
if (end === '}') return result
|
|
92
|
+
if (end !== ',') throw new Error('expected comma or closing brace')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (source[pos] === '[') {
|
|
96
|
+
pos++; whitespace()
|
|
97
|
+
const result: unknown[] = []
|
|
98
|
+
if (source[pos] === ']') { pos++; return result }
|
|
99
|
+
while (true) {
|
|
100
|
+
result.push(value(depth + 1)); whitespace()
|
|
101
|
+
const end = source[pos++]
|
|
102
|
+
if (end === ']') return result
|
|
103
|
+
if (end !== ',') throw new Error('expected comma or closing bracket')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const [token, result] of [['true', true], ['false', false], ['null', null]] as const) {
|
|
107
|
+
if (source.startsWith(token, pos)) { pos += token.length; return result }
|
|
108
|
+
}
|
|
109
|
+
const match = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(source.slice(pos))
|
|
110
|
+
if (!match) throw new Error('invalid JSON value')
|
|
111
|
+
pos += match[0].length
|
|
112
|
+
return portableNumber(Number(match[0]), match[0])
|
|
113
|
+
}
|
|
114
|
+
const result = value(0); whitespace()
|
|
115
|
+
if (pos !== source.length) throw new Error('trailing JSON content')
|
|
116
|
+
return result
|
|
117
|
+
}
|
package/src/exec-ast.ts
CHANGED
package/src/formatter.ts
CHANGED
|
@@ -1000,9 +1000,7 @@ class Formatter {
|
|
|
1000
1000
|
const words: Record<VerifyStatement['target'], string> = {
|
|
1001
1001
|
CAPSULE: 'CAPSULE',
|
|
1002
1002
|
SCHEMA_PACKAGE: 'SCHEMA PACKAGE',
|
|
1003
|
-
RECEIPT: 'RECEIPT'
|
|
1004
|
-
BLOB: 'BLOB',
|
|
1005
|
-
CHECKPOINT: 'CHECKPOINT'
|
|
1003
|
+
RECEIPT: 'RECEIPT'
|
|
1006
1004
|
}
|
|
1007
1005
|
this.writeIndent()
|
|
1008
1006
|
this.write(`VERIFY ${words[stmt.target]} ${this.scalar(stmt.value)}`)
|
package/src/index.ts
CHANGED
package/src/lower.ts
CHANGED
|
@@ -50,6 +50,7 @@ import type {
|
|
|
50
50
|
NumberLiteral
|
|
51
51
|
} from './ast.js'
|
|
52
52
|
import { invalidSyntax } from './errors.js'
|
|
53
|
+
import { portableNumber } from './canonical.js'
|
|
53
54
|
import type {
|
|
54
55
|
AggregationFunction,
|
|
55
56
|
Assignments,
|
|
@@ -161,7 +162,8 @@ const PROTECTED_FIELDS = new Set([
|
|
|
161
162
|
'_system',
|
|
162
163
|
'governance',
|
|
163
164
|
'space_id',
|
|
164
|
-
'space_seq'
|
|
165
|
+
'space_seq',
|
|
166
|
+
'merged_into'
|
|
165
167
|
])
|
|
166
168
|
|
|
167
169
|
/**
|
|
@@ -1657,7 +1659,7 @@ function lowerUpdateExpr(
|
|
|
1657
1659
|
|
|
1658
1660
|
case 'UnaryExpression':
|
|
1659
1661
|
if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
|
|
1660
|
-
return { Number: -numberValue(expr.operand) }
|
|
1662
|
+
return { Number: portableNumber(-numberValue(expr.operand)) }
|
|
1661
1663
|
}
|
|
1662
1664
|
throw invalidSyntax(
|
|
1663
1665
|
`expected a number, a parameter, the target's own field or a registered function, found ${describeExpression(expr)}`,
|
|
@@ -1770,9 +1772,7 @@ function lowerMeta(stmt: Statement): MetaCommand {
|
|
|
1770
1772
|
const VERIFY_TARGETS: Record<VerifyStatement['target'], VerifyTarget> = {
|
|
1771
1773
|
CAPSULE: 'Capsule',
|
|
1772
1774
|
SCHEMA_PACKAGE: 'SchemaPackage',
|
|
1773
|
-
RECEIPT: 'Receipt'
|
|
1774
|
-
BLOB: 'Blob',
|
|
1775
|
-
CHECKPOINT: 'Checkpoint'
|
|
1775
|
+
RECEIPT: 'Receipt'
|
|
1776
1776
|
}
|
|
1777
1777
|
|
|
1778
1778
|
const VALIDATE_TARGETS: Record<ValidateStatement['target'], ValidateTarget> = {
|
|
@@ -1989,56 +1989,13 @@ function lowerElementRef(ref: TargetRef): ElementRef {
|
|
|
1989
1989
|
// Numeric literals
|
|
1990
1990
|
// ---------------------------------------------------------------------------
|
|
1991
1991
|
|
|
1992
|
-
/**
|
|
1993
|
-
const INT_MIN = -(2n ** 63n)
|
|
1994
|
-
/** `u64::MAX` — the largest. */
|
|
1995
|
-
const INT_MAX = 2n ** 64n - 1n
|
|
1996
|
-
|
|
1997
|
-
/** An integer literal: no fraction, no exponent, so it is read as an integer. */
|
|
1998
|
-
const INTEGER_FORM = /^-?\d+$/
|
|
1999
|
-
|
|
2000
|
-
/**
|
|
2001
|
-
* The value of a number literal, refusing the ones that cannot survive being
|
|
2002
|
-
* one.
|
|
2003
|
-
*
|
|
2004
|
-
* A JavaScript number is a double, so `18446744073709551617` silently becomes
|
|
2005
|
-
* `18446744073709551616` on the way in. Accepting that would be the worst
|
|
2006
|
-
* possible outcome: the command does not fail, it *executes with a different
|
|
2007
|
-
* number than it says*, and no engine downstream can detect it — by the time
|
|
2008
|
-
* an executable AST exists the digits are gone. So the check happens here,
|
|
2009
|
-
* against the raw text, which is the only place the original is still around.
|
|
2010
|
-
*
|
|
2011
|
-
* The bounds are the reference grammar's: an integer literal is read as an
|
|
2012
|
-
* `i64` or a `u64` and must fit one of them, and any other form must parse to a
|
|
2013
|
-
* finite double. `18446744073709551616.0` is therefore accepted where
|
|
2014
|
-
* `18446744073709551616` is not — the float form is claiming an approximation,
|
|
2015
|
-
* and the integer form is claiming an exact value it cannot deliver.
|
|
2016
|
-
*
|
|
2017
|
-
* Integers above 2^53 still lose precision in this implementation's `value`
|
|
2018
|
-
* even though they are accepted, because a double cannot hold them. That is a
|
|
2019
|
-
* property of the host, not a disagreement about the language: both engines
|
|
2020
|
-
* agree the command is legal, and a runtime that needs the exact digits has
|
|
2021
|
-
* `raw`.
|
|
2022
|
-
*/
|
|
1992
|
+
/** Never discard exact integer digits or silently turn a nonzero value into zero. */
|
|
2023
1993
|
function numberValue(node: NumberLiteral): number {
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
`${node.raw} is outside the range a KIP integer literal can represent ` +
|
|
2029
|
-
`(${INT_MIN} to ${INT_MAX})`,
|
|
2030
|
-
node.range
|
|
2031
|
-
)
|
|
2032
|
-
}
|
|
2033
|
-
return node.value
|
|
2034
|
-
}
|
|
2035
|
-
if (!Number.isFinite(node.value)) {
|
|
2036
|
-
throw invalidSyntax(
|
|
2037
|
-
`only finite numbers are valid KIP literals, found ${node.raw}`,
|
|
2038
|
-
node.range
|
|
2039
|
-
)
|
|
1994
|
+
try {
|
|
1995
|
+
return portableNumber(node.value, node.raw)
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
throw invalidSyntax(`${(error as Error).message}: ${node.raw}`, node.range)
|
|
2040
1998
|
}
|
|
2041
|
-
return node.value
|
|
2042
1999
|
}
|
|
2043
2000
|
|
|
2044
2001
|
function lowerKipValue(expr: Expression): KipValue {
|
|
@@ -2057,11 +2014,10 @@ function lowerKipValue(expr: Expression): KipValue {
|
|
|
2057
2014
|
case 'ObjectPattern': {
|
|
2058
2015
|
const entries =
|
|
2059
2016
|
expr.kind === 'ObjectLiteral' ? expr.entries : expr.members
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
}
|
|
2064
|
-
return { Object: out }
|
|
2017
|
+
// Create data properties even for names such as __proto__.
|
|
2018
|
+
return { Object: Object.fromEntries(entries.map(entry =>
|
|
2019
|
+
[entry.key, lowerKipValue(entry.value)]
|
|
2020
|
+
)) }
|
|
2065
2021
|
}
|
|
2066
2022
|
case 'UnaryExpression':
|
|
2067
2023
|
if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
|
|
@@ -2084,11 +2040,9 @@ function lowerKipValue(expr: Expression): KipValue {
|
|
|
2084
2040
|
* lets a parameter stand anywhere inside them.
|
|
2085
2041
|
*/
|
|
2086
2042
|
function lowerBoundObject(object: ObjectLiteral): Record<string, BoundValue> {
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
}
|
|
2091
|
-
return out
|
|
2043
|
+
return Object.fromEntries(object.entries.map(entry =>
|
|
2044
|
+
[entry.key, lowerBoundValue(entry.value, null)]
|
|
2045
|
+
))
|
|
2092
2046
|
}
|
|
2093
2047
|
|
|
2094
2048
|
/** Strips the `?` sigil; the executable form carries bare names. */
|
package/src/parser.ts
CHANGED
|
@@ -2235,14 +2235,6 @@ class Parser {
|
|
|
2235
2235
|
this.expectSecondWord(TokenType.Receipt, verify)
|
|
2236
2236
|
target = 'RECEIPT'
|
|
2237
2237
|
break
|
|
2238
|
-
case TokenType.Blob:
|
|
2239
|
-
this.expectSecondWord(TokenType.Blob, verify)
|
|
2240
|
-
target = 'BLOB'
|
|
2241
|
-
break
|
|
2242
|
-
case TokenType.Checkpoint:
|
|
2243
|
-
this.expectSecondWord(TokenType.Checkpoint, verify)
|
|
2244
|
-
target = 'CHECKPOINT'
|
|
2245
|
-
break
|
|
2246
2238
|
default:
|
|
2247
2239
|
this.error(`Unknown VERIFY target '${tok.value}'`, tok)
|
|
2248
2240
|
throw new ParseAbort()
|
package/src/token.ts
CHANGED
|
@@ -120,8 +120,6 @@ export enum TokenType {
|
|
|
120
120
|
Cognition = 'COGNITION',
|
|
121
121
|
Threshold = 'THRESHOLD',
|
|
122
122
|
Receipt = 'RECEIPT',
|
|
123
|
-
Blob = 'BLOB',
|
|
124
|
-
Checkpoint = 'CHECKPOINT',
|
|
125
123
|
Kql = 'KQL',
|
|
126
124
|
Kml = 'KML',
|
|
127
125
|
Import = 'IMPORT',
|
|
@@ -293,8 +291,6 @@ export const KEYWORDS: ReadonlyMap<string, TokenType> = new Map([
|
|
|
293
291
|
['COGNITION', TokenType.Cognition],
|
|
294
292
|
['THRESHOLD', TokenType.Threshold],
|
|
295
293
|
['RECEIPT', TokenType.Receipt],
|
|
296
|
-
['BLOB', TokenType.Blob],
|
|
297
|
-
['CHECKPOINT', TokenType.Checkpoint],
|
|
298
294
|
['KQL', TokenType.Kql],
|
|
299
295
|
['KML', TokenType.Kml],
|
|
300
296
|
['IMPORT', TokenType.Import],
|
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.
|
|
10
|
+
export const PARSER_VERSION = '2.3.1'
|
|
11
11
|
|
|
12
12
|
/** The KIP specification revision this grammar targets. */
|
|
13
13
|
export const KIP_SPEC_REVISION = '2.0-draft'
|