aontu 0.46.0 → 0.48.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/dist/aontu.js +5 -0
- package/dist/aontu.js.map +1 -1
- package/dist/err.js +1 -1
- package/dist/err.js.map +1 -1
- package/dist/hints.js +1 -0
- package/dist/hints.js.map +1 -1
- package/dist/lang.d.ts +1 -1
- package/dist/lang.js +45 -18
- package/dist/lang.js.map +1 -1
- package/dist/lsp-server.d.ts +14 -0
- package/dist/lsp-server.js +87 -0
- package/dist/lsp-server.js.map +1 -0
- package/dist/lsp.d.ts +71 -0
- package/dist/lsp.js +375 -0
- package/dist/lsp.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type.d.ts +1 -1
- package/dist/unify.js +31 -4
- package/dist/unify.js.map +1 -1
- package/dist/utility.js +9 -2
- package/dist/utility.js.map +1 -1
- package/dist/val/BagVal.js +20 -2
- package/dist/val/BagVal.js.map +1 -1
- package/dist/val/DisjunctVal.js +25 -16
- package/dist/val/DisjunctVal.js.map +1 -1
- package/dist/val/IntegerVal.js +1 -0
- package/dist/val/IntegerVal.js.map +1 -1
- package/dist/val/LowerFuncVal.d.ts +1 -1
- package/dist/val/LowerFuncVal.js +1 -1
- package/dist/val/LowerFuncVal.js.map +1 -1
- package/dist/val/MapVal.js +127 -16
- package/dist/val/MapVal.js.map +1 -1
- package/dist/val/NumberVal.js +7 -2
- package/dist/val/NumberVal.js.map +1 -1
- package/dist/val/OpBaseVal.d.ts +1 -1
- package/dist/val/OpBaseVal.js +1 -1
- package/dist/val/OpBaseVal.js.map +1 -1
- package/dist/val/ScalarVal.js +3 -1
- package/dist/val/ScalarVal.js.map +1 -1
- package/dist/val/Val.d.ts +2 -0
- package/dist/val/Val.js +16 -1
- package/dist/val/Val.js.map +1 -1
- package/dist/val/VarVal.js +5 -3
- package/dist/val/VarVal.js.map +1 -1
- package/package.json +22 -14
- package/src/aontu.ts +5 -0
- package/src/err.ts +1 -1
- package/src/hints.ts +2 -0
- package/src/lang.ts +46 -18
- package/src/lsp-server.ts +109 -0
- package/src/lsp.ts +486 -0
- package/src/type.ts +1 -1
- package/src/unify.ts +33 -4
- package/src/utility.ts +11 -2
- package/src/val/BagVal.ts +22 -2
- package/src/val/DisjunctVal.ts +27 -18
- package/src/val/IntegerVal.ts +1 -0
- package/src/val/LowerFuncVal.ts +1 -1
- package/src/val/MapVal.ts +138 -23
- package/src/val/NumberVal.ts +7 -2
- package/src/val/OpBaseVal.ts +1 -1
- package/src/val/ScalarVal.ts +3 -1
- package/src/val/Val.ts +18 -1
- package/src/val/VarVal.ts +6 -4
package/src/lsp.ts
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// Aontu Language Server library.
|
|
4
|
+
//
|
|
5
|
+
// This module is the reusable, transport-free core of the language
|
|
6
|
+
// server, deliberately split from the process that serves it:
|
|
7
|
+
//
|
|
8
|
+
// - `computeDiagnostics(src)` turns Aontu source into LSP diagnostics.
|
|
9
|
+
// - `LspHandler` implements the LSP message flow (document sync ->
|
|
10
|
+
// publishDiagnostics) over decoded JSON-RPC *objects*, with no
|
|
11
|
+
// stdin/stdout. It is fully unit-testable and embeddable.
|
|
12
|
+
// - the server (`lsp-server.ts`) is a thin stdio JSON-RPC loop that
|
|
13
|
+
// frames bytes and feeds decoded messages to an `LspHandler`.
|
|
14
|
+
//
|
|
15
|
+
// The Go port mirrors this split in go/lsp (library) and
|
|
16
|
+
// go/cmd/aontu-lsp (server). Diagnostic messages are built the same way
|
|
17
|
+
// in both so editors see identical text regardless of implementation.
|
|
18
|
+
|
|
19
|
+
import type { Val } from './type'
|
|
20
|
+
|
|
21
|
+
import { Aontu } from './aontu'
|
|
22
|
+
import { getHint } from './err'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// LSP DiagnosticSeverity subset.
|
|
26
|
+
const SEVERITY_ERROR = 1
|
|
27
|
+
const SEVERITY_WARNING = 2
|
|
28
|
+
const SEVERITY_INFORMATION = 3
|
|
29
|
+
const SEVERITY_HINT = 4
|
|
30
|
+
|
|
31
|
+
// Reported to the client in the initialize response.
|
|
32
|
+
const LSP_VERSION = '0.1.0'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// Zero-based line / UTF-16 character offset (LSP Position).
|
|
36
|
+
type Position = { line: number, character: number }
|
|
37
|
+
|
|
38
|
+
// Inclusive-start, exclusive-end span (LSP Range).
|
|
39
|
+
type Range = { start: Position, end: Position }
|
|
40
|
+
|
|
41
|
+
// A single LSP diagnostic.
|
|
42
|
+
type Diagnostic = {
|
|
43
|
+
range: Range
|
|
44
|
+
severity: number
|
|
45
|
+
code?: string
|
|
46
|
+
source: string
|
|
47
|
+
message: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
// Incoming JSON-RPC message (request or notification).
|
|
52
|
+
type Message = {
|
|
53
|
+
jsonrpc?: string
|
|
54
|
+
id?: number | string | null
|
|
55
|
+
method?: string
|
|
56
|
+
params?: any
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Outgoing JSON-RPC message (response or notification).
|
|
60
|
+
type OutMessage = {
|
|
61
|
+
jsonrpc: string
|
|
62
|
+
id?: number | string | null
|
|
63
|
+
method?: string
|
|
64
|
+
params?: any
|
|
65
|
+
result?: any
|
|
66
|
+
error?: { code: number, message: string }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
// Compute LSP diagnostics for a unit of Aontu source. A valid document —
|
|
71
|
+
// including a non-concrete schema such as `a:string` — returns an empty
|
|
72
|
+
// array; only genuine errors (conflicts, unresolved references, unknown
|
|
73
|
+
// functions, syntax errors) produce diagnostics.
|
|
74
|
+
function computeDiagnostics(
|
|
75
|
+
src: string,
|
|
76
|
+
opts?: { vars?: Record<string, Val> }
|
|
77
|
+
): Diagnostic[] {
|
|
78
|
+
const aontu = new Aontu()
|
|
79
|
+
|
|
80
|
+
let root: any
|
|
81
|
+
try {
|
|
82
|
+
const ac = aontu.ctx({ collect: true })
|
|
83
|
+
if (opts?.vars) {
|
|
84
|
+
Object.assign(ac.vars, opts.vars)
|
|
85
|
+
}
|
|
86
|
+
root = aontu.unify(src, { collect: true }, ac)
|
|
87
|
+
}
|
|
88
|
+
catch (err: any) {
|
|
89
|
+
// Hard parse/syntax failure: report a single diagnostic. jsonic
|
|
90
|
+
// errors may carry 1-based line/col; fall back to the document start.
|
|
91
|
+
return [parseErrorDiagnostic(err)]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const nils: any[] = []
|
|
95
|
+
walkNils(root, nils, new Set())
|
|
96
|
+
|
|
97
|
+
return nils.map(nilToDiagnostic)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
// Walk a unified Val tree collecting every NilVal exactly once. A NilVal
|
|
102
|
+
// in the result always represents an error; valid non-concrete values
|
|
103
|
+
// (scalar kinds, refs, conjuncts) are never NilVals.
|
|
104
|
+
function walkNils(v: any, out: any[], seen: Set<any>) {
|
|
105
|
+
if (null == v || 'object' !== typeof v || true !== v.isVal) return
|
|
106
|
+
if (seen.has(v)) return
|
|
107
|
+
seen.add(v)
|
|
108
|
+
|
|
109
|
+
if (v.isNil) {
|
|
110
|
+
out.push(v)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const peg = v.peg
|
|
115
|
+
if (Array.isArray(peg)) {
|
|
116
|
+
for (const c of peg) walkNils(c, out, seen)
|
|
117
|
+
}
|
|
118
|
+
else if (null != peg && 'object' === typeof peg) {
|
|
119
|
+
for (const k in peg) walkNils(peg[k], out, seen)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Spread constraints live off-peg on Map/List Vals.
|
|
123
|
+
const spreadCj = v.spread?.cj
|
|
124
|
+
if (spreadCj) walkNils(spreadCj, out, seen)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
// Convert a NilVal (1-based site row/col) to an LSP diagnostic (0-based
|
|
129
|
+
// line/character).
|
|
130
|
+
function nilToDiagnostic(nil: any): Diagnostic {
|
|
131
|
+
const row = nil.site?.row ?? -1
|
|
132
|
+
const col = nil.site?.col ?? -1
|
|
133
|
+
|
|
134
|
+
let start: Position
|
|
135
|
+
let end: Position
|
|
136
|
+
if (row >= 1 && col >= 1) {
|
|
137
|
+
start = { line: row - 1, character: col - 1 }
|
|
138
|
+
const len = labelLength(nil)
|
|
139
|
+
end = { line: row - 1, character: col - 1 + len }
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
start = { line: 0, character: 0 }
|
|
143
|
+
end = { line: 0, character: 1 }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
range: { start, end },
|
|
148
|
+
severity: SEVERITY_ERROR,
|
|
149
|
+
code: nil.why,
|
|
150
|
+
source: 'aontu',
|
|
151
|
+
message: nilMessage(nil),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
// Length (UTF-16 units, like LSP characters) of the offending value's
|
|
157
|
+
// canonical form, used to size the diagnostic range (minimum 1).
|
|
158
|
+
function labelLength(nil: any): number {
|
|
159
|
+
const c = nil.primary?.canon
|
|
160
|
+
return 'string' === typeof c && c.length > 0 ? c.length : 1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// Build the human-readable message. Kept identical to the Go port's
|
|
165
|
+
// NilVal.Message() (go/val.go) so diagnostics match across
|
|
166
|
+
// implementations: "Cannot <attempt> value: X with value: Y\n<hint>".
|
|
167
|
+
function nilMessage(nil: any): string {
|
|
168
|
+
if (nil.msg) return nil.msg
|
|
169
|
+
|
|
170
|
+
const attempt = nil.attempt ?? (null == nil.secondary ? 'resolve' : 'unify')
|
|
171
|
+
let msg = 'Cannot ' + attempt + ' value'
|
|
172
|
+
if (null != nil.primary) {
|
|
173
|
+
msg += ': ' + nil.primary.canon
|
|
174
|
+
if (null != nil.secondary) {
|
|
175
|
+
msg += ' with value: ' + nil.secondary.canon
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const hint = getHint(nil.why, nil.details)
|
|
179
|
+
if (hint) {
|
|
180
|
+
msg += '\n' + hint
|
|
181
|
+
}
|
|
182
|
+
return msg
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
function parseErrorDiagnostic(err: any): Diagnostic {
|
|
187
|
+
// jsonic/AontuError may expose 1-based line/col.
|
|
188
|
+
const row = err?.lineNumber ?? err?.row ?? err?.line ?? -1
|
|
189
|
+
const col = err?.column ?? err?.col ?? -1
|
|
190
|
+
const start: Position = row >= 1 && col >= 1
|
|
191
|
+
? { line: row - 1, character: col - 1 }
|
|
192
|
+
: { line: 0, character: 0 }
|
|
193
|
+
return {
|
|
194
|
+
range: { start, end: { line: start.line, character: start.character + 1 } },
|
|
195
|
+
severity: SEVERITY_ERROR,
|
|
196
|
+
code: 'parse',
|
|
197
|
+
source: 'aontu',
|
|
198
|
+
message: ('string' === typeof err?.message ? err.message : String(err)),
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
// initialize result: advertise full-text document sync feeding diagnostics.
|
|
204
|
+
function initializeResult() {
|
|
205
|
+
return {
|
|
206
|
+
capabilities: {
|
|
207
|
+
// 1 = TextDocumentSyncKind.Full
|
|
208
|
+
textDocumentSync: 1,
|
|
209
|
+
hoverProvider: true,
|
|
210
|
+
completionProvider: {},
|
|
211
|
+
},
|
|
212
|
+
serverInfo: {
|
|
213
|
+
name: 'aontu-lsp',
|
|
214
|
+
version: LSP_VERSION,
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
function publishDiagnosticsMsg(uri: string, diagnostics: Diagnostic[]): OutMessage {
|
|
221
|
+
return {
|
|
222
|
+
jsonrpc: '2.0',
|
|
223
|
+
method: 'textDocument/publishDiagnostics',
|
|
224
|
+
params: { uri, diagnostics },
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
// --- Hover ------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
type MarkupContent = { kind: 'markdown' | 'plaintext', value: string }
|
|
232
|
+
type Hover = { contents: MarkupContent, range?: Range }
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
// Resolve the value under the cursor and describe it. Returns null when
|
|
236
|
+
// the position is not over a value with a known source location. Because
|
|
237
|
+
// hover reads the *unified* tree, a literal shows its resolved value and
|
|
238
|
+
// kind (e.g. a reference target resolves to the value it points at).
|
|
239
|
+
function computeHover(src: string, position: Position): Hover | null {
|
|
240
|
+
let root: any
|
|
241
|
+
try {
|
|
242
|
+
root = new Aontu().unify(src, { collect: true })
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const cands: { val: any, line: number, start: number, end: number }[] = []
|
|
249
|
+
collectHoverCandidates(root, cands, new Set())
|
|
250
|
+
|
|
251
|
+
let best: { val: any, line: number, start: number, end: number } | null = null
|
|
252
|
+
for (const c of cands) {
|
|
253
|
+
if (c.line === position.line &&
|
|
254
|
+
c.start <= position.character && position.character < c.end) {
|
|
255
|
+
// Most specific (smallest) span wins.
|
|
256
|
+
if (null == best || (c.end - c.start) < (best.end - best.start)) best = c
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (null == best) return null
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
contents: { kind: 'markdown', value: hoverMarkdown(best.val) },
|
|
263
|
+
range: {
|
|
264
|
+
start: { line: best.line, character: best.start },
|
|
265
|
+
end: { line: best.line, character: best.end },
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
function collectHoverCandidates(
|
|
272
|
+
v: any,
|
|
273
|
+
out: { val: any, line: number, start: number, end: number }[],
|
|
274
|
+
seen: Set<any>,
|
|
275
|
+
) {
|
|
276
|
+
if (null == v || 'object' !== typeof v || true !== v.isVal) return
|
|
277
|
+
if (seen.has(v)) return
|
|
278
|
+
seen.add(v)
|
|
279
|
+
|
|
280
|
+
const row = v.site?.row ?? -1
|
|
281
|
+
const col = v.site?.col ?? -1
|
|
282
|
+
let canon = ''
|
|
283
|
+
try { canon = v.canon } catch { canon = '' }
|
|
284
|
+
// Hover targets concrete values (scalars, kinds, refs, …), not
|
|
285
|
+
// containers: a map/list source span is not reliably reconstructable
|
|
286
|
+
// from a single site, and the same restriction in the Go port keeps
|
|
287
|
+
// hover behaviour identical across implementations. The walk still
|
|
288
|
+
// recurses into containers below to reach their leaf values. Canon is
|
|
289
|
+
// single-line, so its length approximates the on-line source span.
|
|
290
|
+
if (row >= 1 && col >= 1 && canon.length > 0 && !canon.includes('\n') &&
|
|
291
|
+
!v.isMap && !v.isList) {
|
|
292
|
+
out.push({ val: v, line: row - 1, start: col - 1, end: col - 1 + canon.length })
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const peg = v.peg
|
|
296
|
+
if (Array.isArray(peg)) {
|
|
297
|
+
for (const c of peg) collectHoverCandidates(c, out, seen)
|
|
298
|
+
}
|
|
299
|
+
else if (null != peg && 'object' === typeof peg) {
|
|
300
|
+
for (const k in peg) collectHoverCandidates(peg[k], out, seen)
|
|
301
|
+
}
|
|
302
|
+
const spreadCj = v.spread?.cj
|
|
303
|
+
if (spreadCj) collectHoverCandidates(spreadCj, out, seen)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
function hoverMarkdown(val: any): string {
|
|
308
|
+
let canon = ''
|
|
309
|
+
try { canon = val.canon } catch { canon = '' }
|
|
310
|
+
return '```aontu\n' + canon + '\n```\n\n' + '*' + valKind(val) + '*'
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
// A short human description of a Val's kind, shown under the hover canon.
|
|
315
|
+
function valKind(val: any): string {
|
|
316
|
+
if (val.isNil) return 'error'
|
|
317
|
+
if (val.isScalarKind) return 'type'
|
|
318
|
+
if (val.isMap) return 'map'
|
|
319
|
+
if (val.isList) return 'list'
|
|
320
|
+
if (val.isRef) return 'reference'
|
|
321
|
+
if (val.isInteger) return 'integer'
|
|
322
|
+
if (val.isNumber) return 'number'
|
|
323
|
+
if (val.isString) return 'string'
|
|
324
|
+
if (val.isBoolean) return 'boolean'
|
|
325
|
+
if (val.isScalar) return 'scalar'
|
|
326
|
+
return val.constructor.name.replace(/Val$/, '').toLowerCase()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
// --- Completion -------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
type CompletionItem = {
|
|
333
|
+
label: string
|
|
334
|
+
kind?: number // LSP CompletionItemKind
|
|
335
|
+
detail?: string
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// LSP CompletionItemKind subset.
|
|
339
|
+
const COMPLETION_FUNCTION = 3
|
|
340
|
+
const COMPLETION_KEYWORD = 14
|
|
341
|
+
|
|
342
|
+
// The twelve built-in functions. Kept in sync with the engine by
|
|
343
|
+
// `lsp.test.ts`, which asserts each is recognised and no others are.
|
|
344
|
+
const BUILTIN_FUNCS = [
|
|
345
|
+
'close', 'copy', 'hide', 'key', 'lower', 'move',
|
|
346
|
+
'open', 'path', 'pref', 'super', 'type', 'upper',
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
// Scalar-kind and literal keywords.
|
|
350
|
+
const KIND_KEYWORDS = ['string', 'number', 'integer', 'boolean']
|
|
351
|
+
const LITERAL_KEYWORDS = ['true', 'false', 'null', 'top']
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
// Context-free completion: the built-in functions, scalar-kind keywords
|
|
355
|
+
// and literals. Clients filter by the typed prefix.
|
|
356
|
+
function computeCompletions(): CompletionItem[] {
|
|
357
|
+
const out: CompletionItem[] = []
|
|
358
|
+
for (const f of BUILTIN_FUNCS) {
|
|
359
|
+
out.push({ label: f, kind: COMPLETION_FUNCTION, detail: 'Aontu built-in function' })
|
|
360
|
+
}
|
|
361
|
+
for (const k of KIND_KEYWORDS) {
|
|
362
|
+
out.push({ label: k, kind: COMPLETION_KEYWORD, detail: 'scalar kind' })
|
|
363
|
+
}
|
|
364
|
+
for (const k of LITERAL_KEYWORDS) {
|
|
365
|
+
out.push({ label: k, kind: COMPLETION_KEYWORD, detail: 'keyword' })
|
|
366
|
+
}
|
|
367
|
+
return out
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
// Transport-agnostic LSP message dispatcher. Consumes decoded JSON-RPC
|
|
372
|
+
// messages and returns the messages to send back, tracking open document
|
|
373
|
+
// text and recomputing diagnostics on open/change/close. Not safe for
|
|
374
|
+
// concurrent use; drive it from a single loop (as the stdio server does).
|
|
375
|
+
class LspHandler {
|
|
376
|
+
private docs = new Map<string, string>()
|
|
377
|
+
private shutdownOK = false
|
|
378
|
+
private exited = false
|
|
379
|
+
|
|
380
|
+
// True once an `exit` notification has been received.
|
|
381
|
+
get shouldExit(): boolean { return this.exited }
|
|
382
|
+
|
|
383
|
+
// Process exit code per the LSP spec: 0 if `shutdown` preceded `exit`,
|
|
384
|
+
// else 1.
|
|
385
|
+
get exitCode(): number { return this.shutdownOK ? 0 : 1 }
|
|
386
|
+
|
|
387
|
+
// Current text of an open document, or undefined.
|
|
388
|
+
doc(uri: string): string | undefined { return this.docs.get(uri) }
|
|
389
|
+
|
|
390
|
+
// Process one incoming message, returning zero or more to send.
|
|
391
|
+
handle(msg: Message): OutMessage[] {
|
|
392
|
+
switch (msg.method) {
|
|
393
|
+
case 'initialize':
|
|
394
|
+
return [{ jsonrpc: '2.0', id: msg.id, result: initializeResult() }]
|
|
395
|
+
|
|
396
|
+
case 'initialized':
|
|
397
|
+
return []
|
|
398
|
+
|
|
399
|
+
case 'shutdown':
|
|
400
|
+
this.shutdownOK = true
|
|
401
|
+
return [{ jsonrpc: '2.0', id: msg.id, result: null }]
|
|
402
|
+
|
|
403
|
+
case 'exit':
|
|
404
|
+
this.exited = true
|
|
405
|
+
return []
|
|
406
|
+
|
|
407
|
+
case 'textDocument/didOpen': {
|
|
408
|
+
const td = msg.params?.textDocument
|
|
409
|
+
if (null == td?.uri) return []
|
|
410
|
+
this.docs.set(td.uri, td.text ?? '')
|
|
411
|
+
return [this.publish(td.uri)]
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
case 'textDocument/didChange': {
|
|
415
|
+
const uri = msg.params?.textDocument?.uri
|
|
416
|
+
const changes = msg.params?.contentChanges
|
|
417
|
+
if (null == uri || !Array.isArray(changes) || 0 === changes.length) return []
|
|
418
|
+
// Full document sync: the last change holds the entire new text.
|
|
419
|
+
this.docs.set(uri, changes[changes.length - 1].text ?? '')
|
|
420
|
+
return [this.publish(uri)]
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case 'textDocument/didClose': {
|
|
424
|
+
const uri = msg.params?.textDocument?.uri
|
|
425
|
+
if (null == uri) return []
|
|
426
|
+
this.docs.delete(uri)
|
|
427
|
+
// Clear diagnostics for the closed document.
|
|
428
|
+
return [publishDiagnosticsMsg(uri, [])]
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
case 'textDocument/hover': {
|
|
432
|
+
const uri = msg.params?.textDocument?.uri
|
|
433
|
+
const pos = msg.params?.position
|
|
434
|
+
const text = null != uri ? this.docs.get(uri) : undefined
|
|
435
|
+
const hover = (null != text && null != pos) ? computeHover(text, pos) : null
|
|
436
|
+
return [{ jsonrpc: '2.0', id: msg.id, result: hover }]
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
case 'textDocument/completion':
|
|
440
|
+
return [{ jsonrpc: '2.0', id: msg.id, result: computeCompletions() }]
|
|
441
|
+
|
|
442
|
+
default:
|
|
443
|
+
// Unknown request (has an id): reply method-not-found. Unknown
|
|
444
|
+
// notification: ignore.
|
|
445
|
+
if (null != msg.id) {
|
|
446
|
+
return [{
|
|
447
|
+
jsonrpc: '2.0',
|
|
448
|
+
id: msg.id,
|
|
449
|
+
error: { code: -32601, message: 'method not found: ' + msg.method },
|
|
450
|
+
}]
|
|
451
|
+
}
|
|
452
|
+
return []
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private publish(uri: string): OutMessage {
|
|
457
|
+
return publishDiagnosticsMsg(uri, computeDiagnostics(this.docs.get(uri) ?? ''))
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
export {
|
|
463
|
+
computeDiagnostics,
|
|
464
|
+
computeHover,
|
|
465
|
+
computeCompletions,
|
|
466
|
+
LspHandler,
|
|
467
|
+
LSP_VERSION,
|
|
468
|
+
BUILTIN_FUNCS,
|
|
469
|
+
SEVERITY_ERROR,
|
|
470
|
+
SEVERITY_WARNING,
|
|
471
|
+
SEVERITY_INFORMATION,
|
|
472
|
+
SEVERITY_HINT,
|
|
473
|
+
COMPLETION_FUNCTION,
|
|
474
|
+
COMPLETION_KEYWORD,
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export type {
|
|
478
|
+
Position,
|
|
479
|
+
Range,
|
|
480
|
+
Diagnostic,
|
|
481
|
+
Message,
|
|
482
|
+
OutMessage,
|
|
483
|
+
Hover,
|
|
484
|
+
MarkupContent,
|
|
485
|
+
CompletionItem,
|
|
486
|
+
}
|
package/src/type.ts
CHANGED
package/src/unify.ts
CHANGED
|
@@ -157,13 +157,38 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
|
|
|
157
157
|
// because the branch that set `out = X.unify(Y, ctx)` already
|
|
158
158
|
// ran that Val's own unify logic.
|
|
159
159
|
if (!out.done && !unified) {
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
// Once per pass per (Val, path): within a single fixpoint pass
|
|
161
|
+
// nothing external to the subtree changes, so repeating the TOP
|
|
162
|
+
// self-unify — which conjunct folds otherwise trigger once per
|
|
163
|
+
// fold term — is pure re-work, and on large models (hundreds of
|
|
164
|
+
// sibling terms) the repeats trip the MAXCYCLE guard as a false
|
|
165
|
+
// positive. The path is part of the key: a shared Val can
|
|
166
|
+
// resolve path-dependent content differently per location.
|
|
167
|
+
if (undefined !== ctx.cc
|
|
168
|
+
&& (out as any)._tcc === ctx.cc && (out as any)._tpi === ctx.pathidx) {
|
|
169
|
+
why += 't'
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
out = out.unify(top(), te ? ctx.clone({ explain: ec(te, 'ND') }) : ctx)
|
|
173
|
+
if (!out.done && undefined !== ctx.cc) {
|
|
174
|
+
; (out as any)._tcc = ctx.cc
|
|
175
|
+
; (out as any)._tpi = ctx.pathidx
|
|
176
|
+
}
|
|
177
|
+
why += 'T'
|
|
178
|
+
}
|
|
162
179
|
}
|
|
163
180
|
}
|
|
164
181
|
catch (err: any) {
|
|
165
|
-
//
|
|
166
|
-
|
|
182
|
+
// This catch-all converts an unexpected exception into an 'internal'
|
|
183
|
+
// Nil so one bad node doesn't crash a whole unify. To avoid fully
|
|
184
|
+
// masking regressions, preserve the original error message (and a
|
|
185
|
+
// RangeError flag — i.e. stack overflow from runaway recursion) on
|
|
186
|
+
// the Nil's details so it surfaces in the formatted error rather
|
|
187
|
+
// than vanishing. See err.ts (descErr) for how details render.
|
|
188
|
+
out = makeNilErr(ctx, 'internal', a, b, undefined, {
|
|
189
|
+
error: String(err?.message ?? err),
|
|
190
|
+
...(err instanceof RangeError ? { overflow: true } : {}),
|
|
191
|
+
})
|
|
167
192
|
}
|
|
168
193
|
}
|
|
169
194
|
|
|
@@ -229,6 +254,10 @@ class Unify {
|
|
|
229
254
|
uctx.err = this.err
|
|
230
255
|
uctx.explain = this.explain
|
|
231
256
|
|
|
257
|
+
// Ref-spread snapshot store (see snapshotRefSpread in MapVal):
|
|
258
|
+
// keyed by ref canon + source site, shared across all passes.
|
|
259
|
+
; (uctx as any).snapmap = new Map()
|
|
260
|
+
|
|
232
261
|
const explain = null == ctx?.explain ? undefined : ctx?.explain
|
|
233
262
|
const te = explain && explainOpen(uctx, explain, 'root', res)
|
|
234
263
|
|
package/src/utility.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
import type { Val } from './type'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
// Default walk() depth limit. High enough that real configs are never
|
|
9
|
+
// silently truncated (the old default of 32 dropped marks on deeply
|
|
10
|
+
// nested refs/funcs → wrong output), while still bounding runaway or
|
|
11
|
+
// accidentally-cyclic walks (walk has no cycle detection). Pass null for
|
|
12
|
+
// truly unbounded.
|
|
13
|
+
const WALK_DEFAULT_MAXDEPTH = 9999
|
|
14
|
+
|
|
15
|
+
|
|
8
16
|
// Mark value in source is propagated to target (true ratchets).
|
|
9
17
|
function propagateMarks(source: Val, target: Val): void {
|
|
10
18
|
// Don't infect top!
|
|
@@ -53,7 +61,8 @@ function walk(
|
|
|
53
61
|
// After descending into a node.
|
|
54
62
|
after?: WalkApply,
|
|
55
63
|
|
|
56
|
-
// Maximum recursive depth, default:
|
|
64
|
+
// Maximum recursive depth, default: WALK_DEFAULT_MAXDEPTH. Use null for
|
|
65
|
+
// infinite depth.
|
|
57
66
|
maxdepth?: number | null,
|
|
58
67
|
|
|
59
68
|
// These arguments are used for recursive state.
|
|
@@ -63,7 +72,7 @@ function walk(
|
|
|
63
72
|
): Val {
|
|
64
73
|
let out = null == before ? val : before(key, val, parent, path || [])
|
|
65
74
|
|
|
66
|
-
maxdepth = null != maxdepth && 0 <= maxdepth ? maxdepth :
|
|
75
|
+
maxdepth = null != maxdepth && 0 <= maxdepth ? maxdepth : WALK_DEFAULT_MAXDEPTH
|
|
67
76
|
if (null != maxdepth && 0 === maxdepth) {
|
|
68
77
|
return out
|
|
69
78
|
}
|
package/src/val/BagVal.ts
CHANGED
|
@@ -65,7 +65,18 @@ abstract class BagVal extends FeatureVal {
|
|
|
65
65
|
return undefined
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
// Maps emit their keys alphabetically so the generated output is
|
|
69
|
+
// independent of insertion/unification order (and matches the Go
|
|
70
|
+
// port, whose JSON marshaling also sorts map keys). Lists keep their
|
|
71
|
+
// numeric index order.
|
|
72
|
+
let entries = items(this.peg)
|
|
73
|
+
if (this.isMap) {
|
|
74
|
+
entries = entries
|
|
75
|
+
.slice()
|
|
76
|
+
.sort((a: any, b: any) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (let item of entries) {
|
|
69
80
|
const p = item[0]
|
|
70
81
|
const child = item[1]
|
|
71
82
|
|
|
@@ -96,7 +107,16 @@ abstract class BagVal extends FeatureVal {
|
|
|
96
107
|
|| child.isDisjunct
|
|
97
108
|
|| child.isNil
|
|
98
109
|
) {
|
|
99
|
-
|
|
110
|
+
// An optional child is generated in an isolated collect context so an
|
|
111
|
+
// unresolved inner value (a bare type that survived unification, e.g.
|
|
112
|
+
// an absent optional sub-map's `field: string`) is dropped rather than
|
|
113
|
+
// raised: the optional subtree is simply omitted. Without isolation
|
|
114
|
+
// such inner errors pollute the shared ctx.err and make generate()
|
|
115
|
+
// throw even though the key is skipped below. Mirrors the
|
|
116
|
+
// optional-disjunct path above.
|
|
117
|
+
const cctx = optional ? ctx.clone({ err: [], collect: true }) : ctx
|
|
118
|
+
|
|
119
|
+
let cval = child.gen(cctx)
|
|
100
120
|
|
|
101
121
|
if (optional && (undefined === cval || empty(cval))) {
|
|
102
122
|
continue
|
package/src/val/DisjunctVal.ts
CHANGED
|
@@ -98,27 +98,33 @@ class DisjunctVal extends JunctionVal {
|
|
|
98
98
|
// C1-inner: tell `makeNilErr` to return TRIAL_NIL in this scope
|
|
99
99
|
// instead of allocating per-failure NilVals. Save/restore so
|
|
100
100
|
// nested DisjunctVal trials (and the outer non-trial code) are
|
|
101
|
-
// not affected.
|
|
101
|
+
// not affected. The restore lives in `finally`: if a trial throws,
|
|
102
|
+
// leaving ctx._trialMode=true would collapse every subsequent real
|
|
103
|
+
// error in this ctx to the shared TRIAL_NIL sentinel.
|
|
102
104
|
ctx._trialMode = true
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
try {
|
|
106
|
+
for (let vI = 0; vI < this.peg.length; vI++) {
|
|
107
|
+
const v = this.peg[vI]
|
|
108
|
+
const trialErr: any[] = []
|
|
109
|
+
ctx.err = trialErr
|
|
110
|
+
|
|
111
|
+
oval[vI] = unite(te ? ctx.clone({ explain: ec(te, 'DIST:' + vI) }) : ctx, v, peer, 'dj-peer')
|
|
112
|
+
|
|
113
|
+
if (0 < trialErr.length) {
|
|
114
|
+
// C1: failed-trial marker is never user-visible — it just
|
|
115
|
+
// signals "this disjunct member doesn't match" and is
|
|
116
|
+
// filtered out before the result is built. Use the shared
|
|
117
|
+
// sentinel instead of allocating a fresh NilVal per trial.
|
|
118
|
+
oval[vI] = TRIAL_NIL
|
|
119
|
+
}
|
|
109
120
|
|
|
110
|
-
|
|
111
|
-
// C1: failed-trial marker is never user-visible — it just
|
|
112
|
-
// signals "this disjunct member doesn't match" and is
|
|
113
|
-
// filtered out before the result is built. Use the shared
|
|
114
|
-
// sentinel instead of allocating a fresh NilVal per trial.
|
|
115
|
-
oval[vI] = TRIAL_NIL
|
|
121
|
+
done = done && DONE === oval[vI].dc
|
|
116
122
|
}
|
|
117
|
-
|
|
118
|
-
done = done && DONE === oval[vI].dc
|
|
119
123
|
}
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
finally {
|
|
125
|
+
ctx._trialMode = savedTrialMode
|
|
126
|
+
ctx.err = savedErr
|
|
127
|
+
}
|
|
122
128
|
|
|
123
129
|
// // // console.log('DISJUNCT-unify-B', this.id, oval.map(v => v.canon))
|
|
124
130
|
|
|
@@ -257,7 +263,10 @@ class DisjunctVal extends JunctionVal {
|
|
|
257
263
|
// TODO: over unifies complex types like maps
|
|
258
264
|
// ({x:1}|{y:2})&{z:3} should be {"x":1,"z":3}|{"y":2,"z":3} not { x:1, z:3, y:2 }
|
|
259
265
|
for (let vI = 1; vI < vals.length; vI++) {
|
|
260
|
-
|
|
266
|
+
// Index `vals`, not `this.peg`: when the pref members are not the
|
|
267
|
+
// leading entries the two arrays differ, and folding against
|
|
268
|
+
// this.peg[vI] would unify the wrong (or a repeated) member.
|
|
269
|
+
let valnext = val.unify(vals[vI], ctx)
|
|
261
270
|
// // // console.log('DJ-GEN-VALS-NEXT', valnext.canon)
|
|
262
271
|
val = valnext
|
|
263
272
|
}
|