@ldclabs/kip-lang 0.4.0 → 2.0.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/README.md +89 -64
  4. package/dist/ast.d.ts +511 -145
  5. package/dist/ast.d.ts.map +1 -1
  6. package/dist/diagnostics.d.ts +8 -2
  7. package/dist/diagnostics.d.ts.map +1 -1
  8. package/dist/diagnostics.js +32 -3
  9. package/dist/diagnostics.js.map +1 -1
  10. package/dist/exec-ast.d.ts +514 -149
  11. package/dist/exec-ast.d.ts.map +1 -1
  12. package/dist/exec-ast.js +8 -7
  13. package/dist/exec-ast.js.map +1 -1
  14. package/dist/formatter.d.ts.map +1 -1
  15. package/dist/formatter.js +870 -479
  16. package/dist/formatter.js.map +1 -1
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/lexer.d.ts.map +1 -1
  22. package/dist/lexer.js +12 -30
  23. package/dist/lexer.js.map +1 -1
  24. package/dist/lower.d.ts +1 -2
  25. package/dist/lower.d.ts.map +1 -1
  26. package/dist/lower.js +1355 -594
  27. package/dist/lower.js.map +1 -1
  28. package/dist/parser.d.ts.map +1 -1
  29. package/dist/parser.js +2410 -1300
  30. package/dist/parser.js.map +1 -1
  31. package/dist/semantics.d.ts +11 -7
  32. package/dist/semantics.d.ts.map +1 -1
  33. package/dist/semantics.js +295 -180
  34. package/dist/semantics.js.map +1 -1
  35. package/dist/token.d.ts +130 -40
  36. package/dist/token.d.ts.map +1 -1
  37. package/dist/token.js +264 -83
  38. package/dist/token.js.map +1 -1
  39. package/dist/version.d.ts +2 -2
  40. package/dist/version.js +2 -2
  41. package/package.json +35 -5
  42. package/src/ast.ts +914 -0
  43. package/src/budget.ts +108 -0
  44. package/src/diagnostics.ts +182 -0
  45. package/src/errors.ts +42 -0
  46. package/src/exec-ast.ts +614 -0
  47. package/src/formatter.ts +1339 -0
  48. package/src/index.ts +226 -0
  49. package/src/lexer.ts +459 -0
  50. package/src/lower.ts +2094 -0
  51. package/src/parser.ts +3506 -0
  52. package/src/semantics.ts +392 -0
  53. package/src/token.ts +408 -0
  54. package/src/version.ts +13 -0
package/src/budget.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { resourceExhausted } from './errors.js'
2
+
3
+ /**
4
+ * Parser budgets.
5
+ *
6
+ * KIP source reaches a server as agent-generated text, so the parser is an
7
+ * attack surface before it is a convenience. `parse` is recursive descent:
8
+ * without a depth ceiling, `[[[[...` recurses until the JavaScript stack
9
+ * overflows, and in an environment that shares one stack across requests
10
+ * (a Cloudflare Worker isolate, say) that takes the whole runtime down rather
11
+ * than failing the one request. The check runs on raw text, before a single
12
+ * token is produced, so a hostile input costs one linear scan.
13
+ *
14
+ * The values mirror `MAX_KIP_*` in `anda_kip`'s parser: a command rejected by
15
+ * one KIP engine's budget must be rejected by every other engine's, or the
16
+ * same command succeeds on one deployment and fails on another.
17
+ */
18
+ export const MAX_KIP_INPUT_LEN = 256 * 1024
19
+ export const MAX_KIP_NESTING_DEPTH = 64
20
+ export const MAX_KIP_BATCH_COMMANDS = 256
21
+
22
+ /**
23
+ * Rejects source that exceeds a parser budget.
24
+ *
25
+ * The bracket scan must skip line comments exactly the way the lexer does.
26
+ * Counting a `"` inside a comment would latch the scanner into string mode
27
+ * for the rest of the input, after which every bracket goes uncounted and the
28
+ * depth ceiling silently stops existing — the failure mode is a guard that
29
+ * looks present and defends nothing.
30
+ *
31
+ * @throws {KipSyntaxError} `KIP_4002` when a budget is exceeded.
32
+ */
33
+ export function checkBudget(source: string): void {
34
+ if (source.length > MAX_KIP_INPUT_LEN) {
35
+ throw resourceExhausted(
36
+ `KIP input length ${source.length} exceeds maximum ${MAX_KIP_INPUT_LEN}`
37
+ )
38
+ }
39
+
40
+ let depth = 0
41
+ const stack: string[] = []
42
+ let inString = false
43
+ let escaped = false
44
+ let inLineComment = false
45
+ let prevSlash = false
46
+
47
+ for (let i = 0; i < source.length; i++) {
48
+ const ch = source[i]!
49
+
50
+ if (inLineComment) {
51
+ if (ch === '\n') inLineComment = false
52
+ continue
53
+ }
54
+
55
+ if (inString) {
56
+ prevSlash = false
57
+ if (escaped) {
58
+ escaped = false
59
+ continue
60
+ }
61
+ if (ch === '\\') escaped = true
62
+ else if (ch === '"') inString = false
63
+ continue
64
+ }
65
+
66
+ if (ch === '/') {
67
+ if (prevSlash) {
68
+ inLineComment = true
69
+ prevSlash = false
70
+ } else {
71
+ prevSlash = true
72
+ }
73
+ continue
74
+ }
75
+ prevSlash = false
76
+
77
+ if (ch === '"') {
78
+ inString = true
79
+ } else if (ch === '(' || ch === '[' || ch === '{') {
80
+ stack.push(ch)
81
+ depth = stack.length
82
+ if (depth > MAX_KIP_NESTING_DEPTH) {
83
+ throw resourceExhausted(
84
+ `KIP input nesting exceeds maximum ${MAX_KIP_NESTING_DEPTH}`
85
+ )
86
+ }
87
+ } else if (ch === ')') {
88
+ if (stack[stack.length - 1] === '(') stack.pop()
89
+ } else if (ch === ']') {
90
+ if (stack[stack.length - 1] === '[') stack.pop()
91
+ } else if (ch === '}') {
92
+ if (stack[stack.length - 1] === '{') stack.pop()
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Rejects an over-long batch of commands.
99
+ *
100
+ * @throws {KipSyntaxError} `KIP_4002` when the batch is too large.
101
+ */
102
+ export function checkBatchBudget(count: number): void {
103
+ if (count > MAX_KIP_BATCH_COMMANDS) {
104
+ throw resourceExhausted(
105
+ `KIP batch of ${count} commands exceeds maximum ${MAX_KIP_BATCH_COMMANDS}`
106
+ )
107
+ }
108
+ }
@@ -0,0 +1,182 @@
1
+ import type { Range } from './token.js'
2
+ import { tokenize } from './lexer.js'
3
+ import { parse } from './parser.js'
4
+ import { analyzeSemantics } from './semantics.js'
5
+ import { lowerStatement } from './lower.js'
6
+ import { KipSyntaxError } from './errors.js'
7
+ import type { Program } from './ast.js'
8
+ import { TokenType } from './token.js'
9
+
10
+ export interface Diagnostic {
11
+ range: Range
12
+ severity: 'error' | 'warning' | 'info'
13
+ message: string
14
+ code: string
15
+ }
16
+
17
+ /**
18
+ * Runs diagnostics on KIP source code.
19
+ * Combines lexer and parser errors with schema-independent semantic and
20
+ * executable-AST validation.
21
+ */
22
+ export function diagnose(source: string): Diagnostic[] {
23
+ const diagnostics: Diagnostic[] = []
24
+
25
+ // Phase 1: Lexer-level diagnostics
26
+ const tokens = tokenize(source)
27
+ for (const tok of tokens) {
28
+ if (tok.type === TokenType.Unknown) {
29
+ diagnostics.push({
30
+ range: {
31
+ start: { line: tok.line, column: tok.column },
32
+ end: { line: tok.line, column: tok.column + tok.value.length }
33
+ },
34
+ severity: 'error',
35
+ message: `Unexpected character '${tok.value}'`,
36
+ code: 'KIP_LEX_UNKNOWN'
37
+ })
38
+ }
39
+
40
+ // Check for unterminated strings
41
+ if (tok.type === TokenType.String) {
42
+ if (!tok.value.endsWith('"') || tok.value.length < 2) {
43
+ diagnostics.push({
44
+ range: {
45
+ start: { line: tok.line, column: tok.column },
46
+ end: { line: tok.line, column: tok.column + tok.value.length }
47
+ },
48
+ severity: 'error',
49
+ message: 'Unterminated string literal',
50
+ code: 'KIP_LEX_UNTERMINATED_STRING'
51
+ })
52
+ } else {
53
+ try {
54
+ JSON.parse(tok.value)
55
+ } catch {
56
+ diagnostics.push({
57
+ range: {
58
+ start: { line: tok.line, column: tok.column },
59
+ end: { line: tok.line, column: tok.column + tok.value.length }
60
+ },
61
+ severity: 'error',
62
+ message: 'Invalid JSON string literal escape sequence',
63
+ code: 'KIP_LEX_INVALID_STRING'
64
+ })
65
+ }
66
+ }
67
+ }
68
+
69
+ if (tok.type === TokenType.Number && !isJsonNumberLiteral(tok.value)) {
70
+ diagnostics.push({
71
+ range: {
72
+ start: { line: tok.line, column: tok.column },
73
+ end: { line: tok.line, column: tok.column + tok.value.length }
74
+ },
75
+ severity: 'error',
76
+ message: 'Invalid JSON number literal',
77
+ code: 'KIP_LEX_INVALID_NUMBER'
78
+ })
79
+ }
80
+ }
81
+
82
+ // Phase 2: Bracket/paren matching
83
+ const bracketStack: { type: string; line: number; column: number }[] = []
84
+ const openers: Record<string, string> = { '{': '}', '(': ')', '[': ']' }
85
+ const closers: Record<string, string> = { '}': '{', ')': '(', ']': '[' }
86
+
87
+ for (const tok of tokens) {
88
+ if (
89
+ tok.type === TokenType.LBrace ||
90
+ tok.type === TokenType.LParen ||
91
+ tok.type === TokenType.LBracket
92
+ ) {
93
+ bracketStack.push({ type: tok.value, line: tok.line, column: tok.column })
94
+ } else if (
95
+ tok.type === TokenType.RBrace ||
96
+ tok.type === TokenType.RParen ||
97
+ tok.type === TokenType.RBracket
98
+ ) {
99
+ const expected = closers[tok.value]
100
+ if (bracketStack.length === 0) {
101
+ diagnostics.push({
102
+ range: {
103
+ start: { line: tok.line, column: tok.column },
104
+ end: { line: tok.line, column: tok.column + 1 }
105
+ },
106
+ severity: 'error',
107
+ message: `Unmatched closing '${tok.value}'`,
108
+ code: 'KIP_LEX_UNMATCHED_BRACKET'
109
+ })
110
+ } else {
111
+ const top = bracketStack[bracketStack.length - 1]!
112
+ if (top.type !== expected) {
113
+ diagnostics.push({
114
+ range: {
115
+ start: { line: tok.line, column: tok.column },
116
+ end: { line: tok.line, column: tok.column + 1 }
117
+ },
118
+ severity: 'error',
119
+ message: `Mismatched bracket: expected '${openers[top.type]}' but got '${tok.value}'`,
120
+ code: 'KIP_LEX_MISMATCHED_BRACKET'
121
+ })
122
+ }
123
+ bracketStack.pop()
124
+ }
125
+ }
126
+ }
127
+
128
+ // Report unclosed brackets
129
+ for (const unclosed of bracketStack) {
130
+ diagnostics.push({
131
+ range: {
132
+ start: { line: unclosed.line, column: unclosed.column },
133
+ end: { line: unclosed.line, column: unclosed.column + 1 }
134
+ },
135
+ severity: 'error',
136
+ message: `Unclosed '${unclosed.type}'`,
137
+ code: 'KIP_LEX_UNCLOSED_BRACKET'
138
+ })
139
+ }
140
+
141
+ // Phase 3: Parser diagnostics
142
+ const { ast, diagnostics: parseDiags } = parse(source)
143
+ diagnostics.push(...parseDiags)
144
+
145
+ // Phase 4: Semantic checks (only when the AST parsed cleanly, to avoid
146
+ // cascading false positives from error-recovery placeholders).
147
+ if (!parseDiags.some((d) => d.severity === 'error')) {
148
+ const semanticDiags = analyzeSemantics(ast)
149
+ diagnostics.push(...semanticDiags)
150
+ if (!semanticDiags.some((d) => d.severity === 'error')) {
151
+ diagnostics.push(...validateExecutable(ast))
152
+ }
153
+ }
154
+
155
+ return diagnostics
156
+ }
157
+
158
+ /**
159
+ * Runs the schema-independent checks needed for every parsed statement to
160
+ * lower into the closed executable AST used by a KIP engine.
161
+ */
162
+ export function validateExecutable(program: Program): Diagnostic[] {
163
+ const diagnostics: Diagnostic[] = []
164
+ for (const statement of program.statements) {
165
+ try {
166
+ lowerStatement(statement)
167
+ } catch (error) {
168
+ if (!(error instanceof KipSyntaxError)) throw error
169
+ diagnostics.push({
170
+ range: error.range ?? statement.range,
171
+ severity: 'error',
172
+ message: error.message,
173
+ code: error.code
174
+ })
175
+ }
176
+ }
177
+ return diagnostics
178
+ }
179
+
180
+ function isJsonNumberLiteral(value: string): boolean {
181
+ return /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value)
182
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { Range } from './token.js'
2
+
3
+ /**
4
+ * The subset of the KIP error taxonomy this package can produce.
5
+ *
6
+ * A KIP engine owns the full taxonomy (schema, logic and execution codes);
7
+ * a language toolkit only ever reaches the parse-time codes. The `hint` that
8
+ * accompanies each code on the wire is deliberately *not* duplicated here —
9
+ * it is engine-side wire contract, and a second copy that drifts is worse
10
+ * than no copy at all.
11
+ */
12
+ export type KipSyntaxCode = 'KIP_1001' | 'KIP_1002' | 'KIP_4002'
13
+
14
+ /**
15
+ * A fatal, single-error view of a KIP source problem.
16
+ *
17
+ * `parse` reports every problem it can recover from as a `Diagnostic`, which
18
+ * is what an editor wants. An engine wants the opposite: the first thing that
19
+ * makes the command unexecutable, thrown, with a code it can put on the wire.
20
+ * `lower` and `checkBudget` therefore throw this instead of accumulating.
21
+ */
22
+ export class KipSyntaxError extends Error {
23
+ readonly code: KipSyntaxCode
24
+ readonly range: Range | undefined
25
+
26
+ constructor(code: KipSyntaxCode, message: string, range?: Range) {
27
+ super(message)
28
+ this.name = 'KipSyntaxError'
29
+ this.code = code
30
+ this.range = range
31
+ }
32
+ }
33
+
34
+ /** `KIP_1001 InvalidSyntax` — the command does not parse, or violates a grammar rule. */
35
+ export function invalidSyntax(message: string, range?: Range): KipSyntaxError {
36
+ return new KipSyntaxError('KIP_1001', message, range)
37
+ }
38
+
39
+ /** `KIP_4002 ResourceExhausted` — the input exceeds a parser budget. */
40
+ export function resourceExhausted(message: string): KipSyntaxError {
41
+ return new KipSyntaxError('KIP_4002', message)
42
+ }