nx_lsp.nx source
↩ module page · 1756 lines · 71816 B
1// ============================================================================================
2// nx_lsp.nx -- LN11: THE NISHILANG LANGUAGE SERVER. LSP 3.17 over stdio, JSON-RPC 2.0,
3// Content-Length framed.
4//
5// THE ARCHITECTURE BAR (lang.plan, rust-analyzer 2020 architecture note): the front end is ONE
6// query surface serving the IDE, the CLI and agents -- a language server that ships a SECOND parser
7// is a second source of truth that drifts from the compiler, and every drift is a diagnostic the
8// editor shows and the build does not (or worse, the reverse). So this organ contains NO lexer, NO
9// parser and NO diagnostic text of its own. It calls, verbatim:
10// nx_import.nx expand_imports / expand_ctx_enable_linemap (the import closure + LineMap)
11// nx_tokenizer.nx lex_source (the tokens)
12// nx_parse.nx parse_module / nx_diag_set_source / nx_diag_set_linemap
13// (the caret + did-you-mean
14// diagnostics, live since 08-05)
15// Every message a user sees in their editor is a byte the compiler wrote.
16//
17// WHY A FORK PER ANALYSIS. The compiler's diagnostic sites sys_exit(2) by design -- recovery stops
18// at NX_DIAG_MAX_ERRS and every parser DESYNC site is fatal on purpose (past a desync each further
19// diagnostic points at innocent code). Calling parse_module in-process would therefore let one bad
20// keystroke KILL THE LANGUAGE SERVER. The analysis runs in a forked child with fd 2 redirected to a
21// per-document artifact; the parent survives sys_exit, SIGSEGV and SIGALRM alike and reports WHICH.
22// This is the same isolation rust-analyzer gives flycheck, reached for the same reason.
23//
24// WHY A SHADOW FILE. expand_imports resolves an import by walking UP from the IMPORTING file's own
25// directory. An unsaved buffer analysed from anywhere else would resolve a DIFFERENT import set --
26// the measured-a-different-subject defect, and it would show up as a flood of phantom "I do not know
27// the name" errors for every symbol the real build resolves fine. So the dirty buffer is written to
28// <document-directory>/<shadow-dir-name>/<basename>: exactly one hop below the document, so the
29// resolver walks the identical chain. When the directory cannot be created the server analyses the
30// SAVED file and SAYS SO over window/logMessage rather than silently reporting stale diagnostics.
31//
32// TRUTHFUL CAPABILITIES. initialize advertises textDocumentSync=Full, definitionProvider and
33// hoverProvider and NOTHING ELSE. A server that advertises a capability it does not serve is the
34// stub-carrying-the-contract-name defect wearing an editor's clothes: the client greys out its own
35// working fallback and the user gets silence instead of a feature.
36//
37// POSITIONS ARE UTF-16 CODE UNITS, as LSP requires by default. The compiler counts BYTES, so every
38// column crosses lsp_u16_of_bytes / lsp_bytes_of_u16. Getting this wrong is invisible in ASCII and
39// silently off-by-N in every file with an accent in a comment.
40//
41// DECLARED IMPRECISION (read this before trusting a column):
42// 1. lex_source pre-expands @macro bodies BEFORE tokenising, while the caret is drawn from the
43// pre-expansion buffer. In a document that USES an arg macro the caret column can therefore be
44// the compiler's own -- this organ inherits that behaviour rather than inventing a second
45// answer, and a fix belongs in nx_tokenizer, not here.
46// 2. Diagnostics whose location the LineMap cannot resolve to THIS document are COUNTED and
47// logged, never published. A diagnostic attributed to the wrong file is worse than none --
48// nx_parse's own nx_diag_loc says exactly this and falls back rather than guessing.
49// 3. Imports are read from DISK. An unsaved edit in file A is not seen while analysing file B.
50//
51// usage: nx_lsp serve LSP over stdin/stdout (the editor entry point)
52// nx_lsp diag <file.nx> one-shot: the publishDiagnostics payload, unframed, to stdout
53// nx_lsp symbols <file.nx> one-shot: the symbol index artifact, to stdout
54// exit: 0 ok | 1 exit-without-shutdown | 3 usage | 4 config refusal
55// license_tier: ORIGINAL No hardware writes (Rule 26).
56// ============================================================================================
57
58import "nx_syscalls.nx"
59import "nx_types.nx"
60import "nx_lex_kinds.nx"
61import "nx_ir.nx"
62import "nx_linemap.nx"
63import "nx_tokenizer.nx"
64import "nx_parse.nx"
65import "nx_import.nx"
66import "nx_json_lib.nx"
67import "nx_lineconf_lib.nx"
68
69// ---- byte values. NishiLang has no character literal, so every byte this file compares against is
70// ---- named once here. These are FACTS about ASCII, not thresholds -- nothing here is tunable.
71const LSP_BS: i64 = 8
72const LSP_TAB: i64 = 9
73const LSP_LF: i64 = 10
74const LSP_FF: i64 = 12
75const LSP_CR: i64 = 13
76const LSP_SPACE: i64 = 32
77const LSP_QUOTE: i64 = 34
78const LSP_PCT: i64 = 37
79const LSP_COMMA: i64 = 44
80const LSP_MINUS: i64 = 45
81const LSP_DOT: i64 = 46
82const LSP_SLASH: i64 = 47
83const LSP_D0: i64 = 48
84const LSP_D9: i64 = 57
85const LSP_COLON: i64 = 58
86const LSP_UA: i64 = 65
87const LSP_UF: i64 = 70
88const LSP_UZ: i64 = 90
89const LSP_BSLASH: i64 = 92
90const LSP_CARET: i64 = 94
91const LSP_USCORE: i64 = 95
92const LSP_LA: i64 = 97
93const LSP_LF_HEX: i64 = 102
94const LSP_LZ: i64 = 122
95const LSP_RBRACE: i64 = 125
96const LSP_TILDE: i64 = 126
97
98const LSP_B10: i64 = 10
99const LSP_B16: i64 = 16
100const LSP_WORD: i64 = 8
101const LSP_NUMBUF: i64 = 32
102
103// UTF-8 lead-byte boundaries, and the one codepoint class that costs TWO UTF-16 code units.
104const LSP_U8_2: i64 = 192
105const LSP_U8_3: i64 = 224
106const LSP_U8_4: i64 = 240
107
108// LSP wire enums (specification values, not choices).
109const LSP_SYNC_FULL: i64 = 1
110const LSP_SEV_ERROR: i64 = 1
111const LSP_SEV_WARN: i64 = 2
112const LSP_MSG_ERROR: i64 = 1
113const LSP_MSG_WARN: i64 = 2
114const LSP_MSG_INFO: i64 = 3
115const LSP_RPC_METHOD_NOT_FOUND: i64 = 0 - 32601
116
117const LSP_EXIT_OK: i64 = 0
118const LSP_EXIT_NO_SHUTDOWN: i64 = 1
119const LSP_EXIT_USAGE: i64 = 3
120const LSP_EXIT_CONF: i64 = 4
121
122// A JSON escape of ONE byte is at most six bytes -- backslash u 0 0 X X. That is arithmetic about
123// the JSON grammar, so every response buffer below is sized as (input bytes * this) + scaffolding
124// rather than guessed. LSP_SCAFFOLD_BYTES / LSP_PER_DIAG_BYTES bound the FIXED literal text this
125// file emits around the payload; they are countable by reading the emitters, and an underestimate
126// cannot truncate silently because every appender sets g_ovf and the send path REFUSES on it.
127const LSP_JSON_ESC_MAX: i64 = 6
128const LSP_SCAFFOLD_BYTES: i64 = 2048
129const LSP_PER_DIAG_BYTES: i64 = 256
130const LSP_URI_ESC_MAX: i64 = 3
131// "Content-Length: " + 20 decimal digits + CRLFCRLF fits well inside this. Structural.
132const LSP_OUTHDR_BYTES: i64 = 64
133const LSP_ID_BYTES: i64 = 256
134const LSP_SYMROW_BYTES: i64 = 192
135const LSP_KINDBUF: i64 = 32
136const LSP_NAMEBUF: i64 = 128
137
138// ---- configuration. Every one of these is a row in knowledge/nx_lsp.conf and a MISSING ROW IS A
139// ---- REFUSAL (exit 4) naming the row -- there is no default to silently inherit.
140static g_hdr_max: i64
141static g_timeout_s: i64
142static g_max_docs: i64
143static g_uri_bytes: i64
144static g_expand_reserve: i64
145static g_scratch: *u8
146static g_shadow_name: *u8
147
148// ---- document store
149static g_ndocs: i64
150static g_doc_uri: *u8
151static g_doc_path: *u8
152static g_doc_shadow: *u8
153static g_doc_txt: *i64
154static g_doc_len: *i64
155static g_doc_live: *i64
156
157static g_shutdown: i64
158static g_ovf: i64
159
160// ============================================================================================
161// SECTION 1 -- bytes out, bytes in
162// ============================================================================================
163
164func lsp_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
165
166// FORWARD DECLARATIONS. Both are used above their definition; the estate's own front end declares
167// forward like this (nx_parse parse_module_const, nx_import expand_imports_inner) rather than
168// reordering a file to satisfy a one-pass reader.
169func lsp_dec_at(b: *u8, s: i64, e: i64) -> i64;
170func lsp_slen_bounded(b: *u8, cap: i64) -> i64;
171
172func lsp_streq(a: *u8, b: *u8) -> i64 {
173 var i: i64 = 0
174 while a[i] != (0 as u8) {
175 if a[i] != b[i] { return 0 }
176 i = i + 1
177 }
178 if b[i] != (0 as u8) { return 0 }
179 return 1
180}
181
182// A short write on a pipe is normal, not an error. Loop or lose the tail of every large payload.
183func lsp_write_all(fd: i64, buf: *u8, n: i64) -> i64 {
184 var off: i64 = 0
185 var run: i64 = 1
186 while run == 1 {
187 if off >= n { run = 0 } else {
188 let w: i64 = sys_write(fd, ((buf as i64) + off) as *u8, n - off)
189 if w <= 0 { run = 0 } else { off = off + w }
190 }
191 }
192 return off
193}
194
195func lsp_log(s: *u8) -> i64 { lsp_write_all(2, s, lsp_slen(s)); return 0 }
196
197func lsp_lognum(v: i64) -> i64 {
198 let b: *u8 = sys_mmap(LSP_NUMBUF)
199 let t: *u8 = sys_mmap(LSP_NUMBUF)
200 var m: i64 = v
201 var p: i64 = 0
202 if m < 0 { b[p] = LSP_MINUS as u8; p = p + 1; m = 0 - m }
203 var k: i64 = 0
204 if m == 0 { t[0] = LSP_D0 as u8; k = 1 }
205 while m > 0 { t[k] = (LSP_D0 + (m % LSP_B10)) as u8; m = m / LSP_B10; k = k + 1 }
206 var i: i64 = 0
207 while i < k { b[p] = t[k - 1 - i]; p = p + 1; i = i + 1 }
208 lsp_write_all(2, b, p)
209 return 0
210}
211
212// ---- bounded appenders. EVERY clip sets g_ovf; nothing that overflowed is ever sent, because a
213// ---- truncated JSON message is not a smaller message, it is a protocol violation the client
214// ---- reports as a server crash.
215func lsp_put(d: *u8, o: i64, cap: i64, c: i64) -> i64 {
216 if o < cap { d[o] = c as u8; return o + 1 }
217 g_ovf = 1
218 return o
219}
220
221func lsp_str_into(d: *u8, o: i64, cap: i64, s: *u8) -> i64 {
222 var p: i64 = o
223 var i: i64 = 0
224 while s[i] != (0 as u8) { p = lsp_put(d, p, cap, s[i] as i64); i = i + 1 }
225 return p
226}
227
228func lsp_bytes_into(d: *u8, o: i64, cap: i64, s: *u8, n: i64) -> i64 {
229 var p: i64 = o
230 var i: i64 = 0
231 while i < n { p = lsp_put(d, p, cap, s[i] as i64); i = i + 1 }
232 return p
233}
234
235func lsp_num_into(d: *u8, o: i64, cap: i64, v: i64) -> i64 {
236 var p: i64 = o
237 var m: i64 = v
238 if m < 0 { p = lsp_put(d, p, cap, LSP_MINUS); m = 0 - m }
239 let t: *u8 = sys_mmap(LSP_NUMBUF)
240 var k: i64 = 0
241 if m == 0 { t[0] = LSP_D0 as u8; k = 1 }
242 while m > 0 { t[k] = (LSP_D0 + (m % LSP_B10)) as u8; m = m / LSP_B10; k = k + 1 }
243 var i: i64 = 0
244 while i < k { p = lsp_put(d, p, cap, t[k - 1 - i] as i64); i = i + 1 }
245 return p
246}
247
248func lsp_hexdig(v: i64) -> i64 {
249 if v < LSP_B10 { return LSP_D0 + v }
250 return LSP_LA + (v - LSP_B10)
251}
252
253// JSON string CONTENT (callers supply the quotes). UTF-8 above 0x1F passes through untouched, which
254// is what the spec wants: JSON is UTF-8 and escaping non-ASCII would only cost bytes.
255func lsp_jstr_into(d: *u8, o: i64, cap: i64, s: *u8, n: i64) -> i64 {
256 var p: i64 = o
257 var i: i64 = 0
258 while i < n {
259 let c: i64 = s[i] as i64
260 if c == LSP_QUOTE { p = lsp_str_into(d, p, cap, "\\\"" as *u8) } else {
261 if c == LSP_BSLASH { p = lsp_str_into(d, p, cap, "\\\\" as *u8) } else {
262 if c == LSP_LF { p = lsp_str_into(d, p, cap, "\\n" as *u8) } else {
263 if c == LSP_CR { p = lsp_str_into(d, p, cap, "\\r" as *u8) } else {
264 if c == LSP_TAB { p = lsp_str_into(d, p, cap, "\\t" as *u8) } else {
265 if c < LSP_SPACE {
266 p = lsp_str_into(d, p, cap, "\\u00" as *u8)
267 p = lsp_put(d, p, cap, lsp_hexdig(c / LSP_B16))
268 p = lsp_put(d, p, cap, lsp_hexdig(c % LSP_B16))
269 } else {
270 p = lsp_put(d, p, cap, c)
271 } } } } } }
272 i = i + 1
273 }
274 return p
275}
276
277func lsp_jstrz_into(d: *u8, o: i64, cap: i64, s: *u8) -> i64 {
278 return lsp_jstr_into(d, o, cap, s, lsp_slen(s))
279}
280
281// ============================================================================================
282// SECTION 2 -- configuration (Rule 20: validate at startup, never on first request)
283// ============================================================================================
284
285func lsp_conf_int(key: *u8) -> i64 {
286 let v: i64 = lcf_int_of("knowledge/nx_lsp.conf" as *u8, key)
287 if v == LCF_MISS {
288 lsp_log("nx_lsp: REFUSED at startup -- knowledge/nx_lsp.conf has no numeric row named " as *u8)
289 lsp_log(key)
290 lsp_log(". A missing bound is a refusal, never a default.\n" as *u8)
291 sys_exit(LSP_EXIT_CONF)
292 }
293 return v
294}
295
296func lsp_conf_str(key: *u8) -> *u8 {
297 let out: *u8 = sys_mmap(SYS_PATH_MAX)
298 let r: i64 = lcf_str_of("knowledge/nx_lsp.conf" as *u8, key, out, SYS_PATH_MAX)
299 if r <= 0 {
300 lsp_log("nx_lsp: REFUSED at startup -- knowledge/nx_lsp.conf has no string row named " as *u8)
301 lsp_log(key)
302 lsp_log(". A missing path is a refusal, never a default.\n" as *u8)
303 sys_exit(LSP_EXIT_CONF)
304 }
305 return out
306}
307
308func lsp_cfg_load() -> i64 {
309 g_hdr_max = lsp_conf_int("header-bytes-max" as *u8)
310 g_timeout_s = lsp_conf_int("analysis-timeout-s" as *u8)
311 g_max_docs = lsp_conf_int("max-open-docs" as *u8)
312 g_uri_bytes = lsp_conf_int("doc-uri-bytes" as *u8)
313 g_expand_reserve = lsp_conf_int("expand-reserve-bytes" as *u8)
314 g_scratch = lsp_conf_str("scratch-dir" as *u8)
315 g_shadow_name = lsp_conf_str("shadow-dir-name" as *u8)
316 sys_mkdir(g_scratch, MODE_0755)
317 g_doc_uri = sys_mmap(g_max_docs * g_uri_bytes + LSP_WORD)
318 g_doc_path = sys_mmap(g_max_docs * SYS_PATH_MAX + LSP_WORD)
319 g_doc_shadow = sys_mmap(g_max_docs * SYS_PATH_MAX + LSP_WORD)
320 g_doc_txt = sys_mmap(g_max_docs * LSP_WORD + LSP_WORD) as *i64
321 g_doc_len = sys_mmap(g_max_docs * LSP_WORD + LSP_WORD) as *i64
322 g_doc_live = sys_mmap(g_max_docs * LSP_WORD + LSP_WORD) as *i64
323 g_ndocs = 0
324 g_shutdown = 0
325 g_ovf = 0
326 return 0
327}
328
329// ============================================================================================
330// SECTION 3 -- positions. LSP characters are UTF-16 code units; the compiler counts bytes.
331// ============================================================================================
332
333// byte offset of the start of 0-based line `line0`, or -1 when the document has no such line.
334func lsp_line_start(t: *u8, n: i64, line0: i64) -> i64 {
335 if line0 <= 0 { return 0 }
336 var cur: i64 = 0
337 var i: i64 = 0
338 var found: i64 = 0 - 1
339 var run: i64 = 1
340 while run == 1 {
341 if i >= n { run = 0 } else {
342 if t[i] == (LSP_LF as u8) {
343 cur = cur + 1
344 if cur == line0 { found = i + 1; run = 0 }
345 }
346 i = i + 1
347 }
348 }
349 return found
350}
351
352func lsp_line_end(t: *u8, n: i64, start: i64) -> i64 {
353 var e: i64 = start
354 var run: i64 = 1
355 while run == 1 {
356 if e >= n { run = 0 } else {
357 if t[e] == (LSP_LF as u8) { run = 0 } else { e = e + 1 }
358 }
359 }
360 return e
361}
362
363// bytes -> UTF-16 code units within one line. A 4-byte UTF-8 sequence is a surrogate PAIR: 2 units.
364func lsp_u16_of_bytes(lp: *u8, n: i64, byte_off: i64) -> i64 {
365 var u: i64 = 0
366 var i: i64 = 0
367 var run: i64 = 1
368 while run == 1 {
369 if i >= byte_off { run = 0 } else {
370 if i >= n { run = 0 } else {
371 let c: i64 = lp[i] as i64
372 var adv: i64 = 1
373 var units: i64 = 1
374 if c >= LSP_U8_4 { adv = 4; units = 2 } else {
375 if c >= LSP_U8_3 { adv = 3 } else {
376 if c >= LSP_U8_2 { adv = 2 } } }
377 u = u + units
378 i = i + adv
379 }
380 }
381 }
382 return u
383}
384
385func lsp_bytes_of_u16(lp: *u8, n: i64, u16col: i64) -> i64 {
386 var u: i64 = 0
387 var i: i64 = 0
388 var run: i64 = 1
389 while run == 1 {
390 if u >= u16col { run = 0 } else {
391 if i >= n { run = 0 } else {
392 let c: i64 = lp[i] as i64
393 var adv: i64 = 1
394 var units: i64 = 1
395 if c >= LSP_U8_4 { adv = 4; units = 2 } else {
396 if c >= LSP_U8_3 { adv = 3 } else {
397 if c >= LSP_U8_2 { adv = 2 } } }
398 u = u + units
399 i = i + adv
400 }
401 }
402 }
403 return i
404}
405
406func lsp_is_word(c: i64) -> i64 {
407 if c >= LSP_D0 { if c <= LSP_D9 { return 1 } }
408 if c >= LSP_UA { if c <= LSP_UZ { return 1 } }
409 if c >= LSP_LA { if c <= LSP_LZ { return 1 } }
410 if c == LSP_USCORE { return 1 }
411 return 0
412}
413
414// identifier extent containing (or immediately ending at) byte offset `off`.
415func lsp_word_at(t: *u8, n: i64, off: i64, s_out: *i64, e_out: *i64) -> i64 {
416 s_out[0] = off
417 e_out[0] = off
418 if off < 0 { return 0 }
419 if off > n { return 0 }
420 var a: i64 = off
421 if a >= n { a = n - 1 }
422 if a < 0 { return 0 }
423 if lsp_is_word(t[a] as i64) == 0 {
424 if a > 0 { a = a - 1 } else { return 0 }
425 if lsp_is_word(t[a] as i64) == 0 { return 0 }
426 }
427 var s: i64 = a
428 var run: i64 = 1
429 while run == 1 {
430 if s <= 0 { run = 0 } else {
431 if lsp_is_word(t[s - 1] as i64) == 1 { s = s - 1 } else { run = 0 }
432 }
433 }
434 var e: i64 = a + 1
435 run = 1
436 while run == 1 {
437 if e >= n { run = 0 } else {
438 if lsp_is_word(t[e] as i64) == 1 { e = e + 1 } else { run = 0 }
439 }
440 }
441 s_out[0] = s
442 e_out[0] = e
443 return 1
444}
445
446// ============================================================================================
447// SECTION 4 -- URI <-> path
448// ============================================================================================
449
450func lsp_hexval(c: i64) -> i64 {
451 if c >= LSP_D0 { if c <= LSP_D9 { return c - LSP_D0 } }
452 if c >= LSP_LA { if c <= LSP_LF_HEX { return c - LSP_LA + LSP_B10 } }
453 if c >= LSP_UA { if c <= LSP_UF { return c - LSP_UA + LSP_B10 } }
454 return 0 - 1
455}
456
457// file:///a/b%20c.nx -> /a/b c.nx . A URI with no scheme is taken as a plain path (the CLI modes
458// and the gate use plain paths; being lenient here costs nothing and refusing would cost a mode).
459func lsp_uri_to_path(uri: *u8, out: *u8, cap: i64) -> i64 {
460 let n: i64 = lsp_slen(uri)
461 var s: i64 = 0
462 if jx_find(uri, n, 0, "file://" as *u8) == 0 { s = 7 }
463 var o: i64 = 0
464 var i: i64 = s
465 while i < n {
466 let c: i64 = uri[i] as i64
467 var taken: i64 = 0
468 if c == LSP_PCT {
469 if i + 2 < n {
470 let h1: i64 = lsp_hexval(uri[i + 1] as i64)
471 let h2: i64 = lsp_hexval(uri[i + 2] as i64)
472 if h1 >= 0 { if h2 >= 0 {
473 if o < cap - 1 { out[o] = (h1 * LSP_B16 + h2) as u8; o = o + 1 }
474 i = i + 3
475 taken = 1
476 } }
477 }
478 }
479 if taken == 0 {
480 if o < cap - 1 { out[o] = uri[i]; o = o + 1 }
481 i = i + 1
482 }
483 }
484 out[o] = 0 as u8
485 return o
486}
487
488func lsp_uri_unreserved(c: i64) -> i64 {
489 if lsp_is_word(c) == 1 { return 1 }
490 if c == LSP_MINUS { return 1 }
491 if c == LSP_DOT { return 1 }
492 if c == LSP_TILDE { return 1 }
493 if c == LSP_SLASH { return 1 }
494 return 0
495}
496
497// absolute path -> file:// URI. A relative path is made absolute against the CWD first, because a
498// Location the client cannot open is indistinguishable from no answer at all.
499func lsp_path_to_uri(path: *u8, out: *u8, cap: i64) -> i64 {
500 let abs: *u8 = sys_mmap(SYS_PATH_MAX * 2)
501 var an: i64 = 0
502 if path[0] != (LSP_SLASH as u8) {
503 let cwd: *u8 = sys_mmap(SYS_PATH_MAX)
504 let r: i64 = sys_getcwd(cwd, SYS_PATH_MAX)
505 if r > 0 {
506 an = lsp_str_into(abs, 0, SYS_PATH_MAX * 2, cwd)
507 an = lsp_put(abs, an, SYS_PATH_MAX * 2, LSP_SLASH)
508 }
509 }
510 an = lsp_str_into(abs, an, SYS_PATH_MAX * 2, path)
511 var o: i64 = lsp_str_into(out, 0, cap, "file://" as *u8)
512 var i: i64 = 0
513 while i < an {
514 let c: i64 = abs[i] as i64
515 if lsp_uri_unreserved(c) == 1 { o = lsp_put(out, o, cap, c) } else {
516 o = lsp_put(out, o, cap, LSP_PCT)
517 o = lsp_put(out, o, cap, lsp_hexdig(c / LSP_B16))
518 o = lsp_put(out, o, cap, lsp_hexdig(c % LSP_B16))
519 }
520 i = i + 1
521 }
522 if o < cap { out[o] = 0 as u8 }
523 return o
524}
525
526func lsp_basename(path: *u8) -> *u8 {
527 let n: i64 = lsp_slen(path)
528 var last: i64 = 0 - 1
529 var i: i64 = 0
530 while i < n {
531 if path[i] == (LSP_SLASH as u8) { last = i }
532 i = i + 1
533 }
534 return ((path as i64) + last + 1) as *u8
535}
536
537// ============================================================================================
538// SECTION 5 -- document store
539// ============================================================================================
540
541func lsp_uri_slot(i: i64) -> *u8 { return ((g_doc_uri as i64) + i * g_uri_bytes) as *u8 }
542func lsp_path_slot(i: i64) -> *u8 { return ((g_doc_path as i64) + i * SYS_PATH_MAX) as *u8 }
543func lsp_shadow_slot(i: i64) -> *u8 { return ((g_doc_shadow as i64) + i * SYS_PATH_MAX) as *u8 }
544
545func lsp_doc_find(uri: *u8) -> i64 {
546 var i: i64 = 0
547 while i < g_ndocs {
548 if g_doc_live[i] == 1 {
549 if lsp_streq(lsp_uri_slot(i), uri) == 1 { return i }
550 }
551 i = i + 1
552 }
553 return 0 - 1
554}
555
556// Returns the slot, or -1 when the store is full or the URI does not fit. BOTH refusals are the
557// caller's to announce: silently evicting a document the editor still tracks would make every later
558// request answer about nothing, with no error anywhere.
559func lsp_doc_put(uri: *u8, txt: *u8, n: i64) -> i64 {
560 if lsp_slen(uri) >= g_uri_bytes { return 0 - 1 }
561 var slot: i64 = lsp_doc_find(uri)
562 if slot < 0 {
563 var i: i64 = 0
564 while i < g_ndocs {
565 if g_doc_live[i] == 0 { if slot < 0 { slot = i } }
566 i = i + 1
567 }
568 }
569 if slot < 0 {
570 if g_ndocs >= g_max_docs { return 0 - 1 }
571 slot = g_ndocs
572 g_ndocs = g_ndocs + 1
573 }
574 let up: *u8 = lsp_uri_slot(slot)
575 var k: i64 = 0
576 while uri[k] != (0 as u8) { up[k] = uri[k]; k = k + 1 }
577 up[k] = 0 as u8
578 lsp_uri_to_path(uri, lsp_path_slot(slot), SYS_PATH_MAX)
579 g_doc_txt[slot] = txt as i64
580 g_doc_len[slot] = n
581 g_doc_live[slot] = 1
582 return slot
583}
584
585// ============================================================================================
586// SECTION 6 -- the analysis child: the compiler's own front end, crash-isolated
587// ============================================================================================
588
589func lsp_scratch_path(slot: i64, ext: *u8, out: *u8, cap: i64) -> i64 {
590 var o: i64 = lsp_str_into(out, 0, cap, g_scratch)
591 o = lsp_str_into(out, o, cap, "/doc" as *u8)
592 o = lsp_num_into(out, o, cap, slot)
593 o = lsp_str_into(out, o, cap, ext)
594 if o < cap { out[o] = 0 as u8 }
595 return o
596}
597
598func lsp_write_file(path: *u8, buf: *u8, n: i64) -> i64 {
599 let fd: i64 = sys_openat_wr(path, MODE_0644)
600 if fd < 0 { return 0 - 1 }
601 let w: i64 = lsp_write_all(fd, buf, n)
602 sys_close(fd)
603 if w != n { return 0 - 1 }
604 return 0
605}
606
607func lsp_truncate_file(path: *u8) -> i64 {
608 let fd: i64 = sys_openat_wr(path, MODE_0644)
609 if fd >= 0 { sys_close(fd) }
610 return 0
611}
612
613// Materialise the unsaved buffer ONE directory below the document so try_resolve_import walks the
614// identical chain. Returns 1 when the shadow exists, 0 when we must fall back to the saved file.
615func lsp_shadow_make(path: *u8, txt: *u8, n: i64, out: *u8, cap: i64) -> i64 {
616 let dir: *u8 = sys_mmap(SYS_PATH_MAX)
617 path_dir(path, dir)
618 if dir[0] == (0 as u8) { dir[0] = LSP_DOT as u8; dir[1] = 0 as u8 }
619 let sdir: *u8 = sys_mmap(SYS_PATH_MAX)
620 var o: i64 = lsp_str_into(sdir, 0, SYS_PATH_MAX, dir)
621 o = lsp_put(sdir, o, SYS_PATH_MAX, LSP_SLASH)
622 o = lsp_str_into(sdir, o, SYS_PATH_MAX, g_shadow_name)
623 sdir[o] = 0 as u8
624 sys_mkdir(sdir, MODE_0755)
625 var p: i64 = lsp_str_into(out, 0, cap, sdir)
626 p = lsp_put(out, p, cap, LSP_SLASH)
627 p = lsp_str_into(out, p, cap, lsp_basename(path))
628 if p < cap { out[p] = 0 as u8 }
629 if lsp_write_file(out, txt, n) != 0 { out[0] = 0 as u8; return 0 }
630 return 1
631}
632
633// The symbol artifact. Deliberately a readable text file, not an in-memory-only structure: it is
634// what `nx_lsp symbols` prints, so the index a query answers from is the index a human can audit.
635// V1 expand=<OK|FAIL> rc=<n> syms=<n> unmapped=<n> files=<n> coverage_complete=<0|1>
636// F<TAB><idx><TAB><path>
637// S<TAB><kind><TAB><line><TAB><col><TAB><fileidx><TAB><name>
638func lsp_child_analyze(path: *u8, symp: *u8) -> i64 {
639 let ebuf: *u8 = sys_mmap(g_expand_reserve + LSP_WORD)
640 let ctx: *ExpandCtx = expand_ctx_new(ebuf, g_expand_reserve)
641 let lm: *LineMap = expand_ctx_enable_linemap(ctx)
642 let rc: i64 = expand_imports(ctx, path)
643
644 let hdr: *u8 = sys_mmap(LSP_SCAFFOLD_BYTES)
645 if rc < 0 {
646 var ho: i64 = lsp_str_into(hdr, 0, LSP_SCAFFOLD_BYTES, "V1 expand=FAIL rc=" as *u8)
647 ho = lsp_num_into(hdr, ho, LSP_SCAFFOLD_BYTES, rc)
648 ho = lsp_str_into(hdr, ho, LSP_SCAFFOLD_BYTES, " syms=0 unmapped=0 files=0 coverage_complete=0\n" as *u8)
649 lsp_write_file(symp, hdr, ho)
650 return 0
651 }
652
653 let op: *i64 = ctx.out_pos
654 let endp: i64 = *op
655 ebuf[endp] = 0 as u8
656
657 let toks: *Tok = lex_source(ebuf, 0)
658
659 // count tokens so the row buffer is DERIVED from the input rather than guessed
660 var ntok: i64 = 0
661 var run: i64 = 1
662 while run == 1 {
663 let t: *Tok = ((toks as i64) + ntok * TOK_BYTES) as *Tok
664 if t.kind == TK_EOF { run = 0 } else { ntok = ntok + 1 }
665 }
666
667 let rowcap: i64 = ntok * LSP_SYMROW_BYTES + LSP_SCAFFOLD_BYTES
668 let rows: *u8 = sys_mmap(rowcap + LSP_WORD)
669 let files: *i64 = sys_mmap(MAX_IMPORTS * LSP_WORD + LSP_WORD) as *i64
670 var nfiles: i64 = 0
671 let lp: *i64 = sys_mmap(LSP_WORD * 4) as *i64
672 let fp: *i64 = lp
673 let sp: *i64 = ((lp as i64) + LSP_WORD) as *i64
674
675 var ro: i64 = 0
676 var nsym: i64 = 0
677 var unmapped: i64 = 0
678 var i: i64 = 0
679 while i < ntok {
680 let t: *Tok = ((toks as i64) + i * TOK_BYTES) as *Tok
681 var kn: *u8 = 0 as *u8
682 if t.kind == TK_FUNC { kn = "func" as *u8 }
683 if t.kind == TK_STRUCT { kn = "struct" as *u8 }
684 if t.kind == TK_ENUM { kn = "enum" as *u8 }
685 if t.kind == TK_CONST { kn = "const" as *u8 }
686 if t.kind == TK_STATIC { kn = "static" as *u8 }
687 if (kn as i64) != 0 {
688 let nt: *Tok = ((toks as i64) + (i + 1) * TOK_BYTES) as *Tok
689 if nt.kind == TK_IDENT {
690 fp[0] = 0
691 sp[0] = 0
692 if lm_lookup(lm, nt.line, fp, sp) == 1 {
693 let fidx: i64 = fp[0]
694 var seen: i64 = 0
695 var k: i64 = 0
696 while k < nfiles { if files[k] == fidx { seen = 1 } k = k + 1 }
697 if seen == 0 { if nfiles < MAX_IMPORTS { files[nfiles] = fidx; nfiles = nfiles + 1 } }
698 ro = lsp_str_into(rows, ro, rowcap, "S\t" as *u8)
699 ro = lsp_str_into(rows, ro, rowcap, kn)
700 ro = lsp_put(rows, ro, rowcap, LSP_TAB)
701 ro = lsp_num_into(rows, ro, rowcap, sp[0])
702 ro = lsp_put(rows, ro, rowcap, LSP_TAB)
703 ro = lsp_num_into(rows, ro, rowcap, nt.col)
704 ro = lsp_put(rows, ro, rowcap, LSP_TAB)
705 ro = lsp_num_into(rows, ro, rowcap, fidx)
706 ro = lsp_put(rows, ro, rowcap, LSP_TAB)
707 ro = lsp_str_into(rows, ro, rowcap, tok_text_ptr(nt))
708 ro = lsp_put(rows, ro, rowcap, LSP_LF)
709 nsym = nsym + 1
710 } else { unmapped = unmapped + 1 }
711 }
712 }
713 i = i + 1
714 }
715
716 var cov: i64 = 1
717 if unmapped > 0 { cov = 0 }
718 if g_ovf == 1 { cov = 0 }
719 var ho: i64 = lsp_str_into(hdr, 0, LSP_SCAFFOLD_BYTES, "V1 expand=OK rc=0 syms=" as *u8)
720 ho = lsp_num_into(hdr, ho, LSP_SCAFFOLD_BYTES, nsym)
721 ho = lsp_str_into(hdr, ho, LSP_SCAFFOLD_BYTES, " unmapped=" as *u8)
722 ho = lsp_num_into(hdr, ho, LSP_SCAFFOLD_BYTES, unmapped)
723 ho = lsp_str_into(hdr, ho, LSP_SCAFFOLD_BYTES, " files=" as *u8)
724 ho = lsp_num_into(hdr, ho, LSP_SCAFFOLD_BYTES, nfiles)
725 ho = lsp_str_into(hdr, ho, LSP_SCAFFOLD_BYTES, " coverage_complete=" as *u8)
726 ho = lsp_num_into(hdr, ho, LSP_SCAFFOLD_BYTES, cov)
727 ho = lsp_put(hdr, ho, LSP_SCAFFOLD_BYTES, LSP_LF)
728
729 let sfd: i64 = sys_openat_wr(symp, MODE_0644)
730 if sfd >= 0 {
731 lsp_write_all(sfd, hdr, ho)
732 let frow: *u8 = sys_mmap(SYS_PATH_MAX + LSP_SCAFFOLD_BYTES)
733 var k: i64 = 0
734 while k < nfiles {
735 var fo: i64 = lsp_str_into(frow, 0, SYS_PATH_MAX, "F\t" as *u8)
736 fo = lsp_num_into(frow, fo, SYS_PATH_MAX, files[k])
737 fo = lsp_put(frow, fo, SYS_PATH_MAX, LSP_TAB)
738 fo = lsp_str_into(frow, fo, SYS_PATH_MAX, lm_file_path(lm, files[k]))
739 fo = lsp_put(frow, fo, SYS_PATH_MAX, LSP_LF)
740 lsp_write_all(sfd, frow, fo)
741 k = k + 1
742 }
743 lsp_write_all(sfd, rows, ro)
744 sys_close(sfd)
745 }
746
747 // THE DIAGNOSTICS. Source + linemap plumbed exactly as nx_compile_x86 plumbs them, so the caret,
748 // the did-you-mean note and the file:line attribution are the compiler's, byte for byte.
749 nx_diag_set_source(ebuf, endp)
750 nx_diag_set_linemap(lm)
751 parse_module(toks, 0 as *Module)
752 return 0
753}
754
755// Runs one analysis. Returns the child's exit code; *sig_out carries the terminating signal (0 when
756// it exited normally) so a TIMEOUT is never reported in the same word as a CRASH.
757func lsp_analyze(slot: i64, sig_out: *i64) -> i64 {
758 let errp: *u8 = sys_mmap(SYS_PATH_MAX)
759 let symp: *u8 = sys_mmap(SYS_PATH_MAX)
760 lsp_scratch_path(slot, ".err" as *u8, errp, SYS_PATH_MAX)
761 lsp_scratch_path(slot, ".sym" as *u8, symp, SYS_PATH_MAX)
762 // Truncate BOTH before the fork: a child that dies before writing must leave an EMPTY artifact,
763 // never the previous run's. A stale fixture silences a gate; a stale artifact lies to an editor.
764 lsp_truncate_file(errp)
765 lsp_truncate_file(symp)
766
767 let path: *u8 = lsp_path_slot(slot)
768 let shadow: *u8 = lsp_shadow_slot(slot)
769 let txt: *u8 = g_doc_txt[slot] as *u8
770 let have: i64 = lsp_shadow_make(path, txt, g_doc_len[slot], shadow, SYS_PATH_MAX)
771 var target: *u8 = path
772 if have == 1 { target = shadow } else {
773 lsp_log("nx_lsp: shadow=UNAVAILABLE for " as *u8)
774 lsp_log(path)
775 lsp_log(" -- diagnostics reflect the SAVED file, not your buffer\n" as *u8)
776 }
777
778 sig_out[0] = 0
779 let pid: i64 = sys_fork()
780 if pid < 0 { return 0 - 1 }
781 if pid == 0 {
782 let efd: i64 = sys_openat_wr(errp, MODE_0644)
783 if efd >= 0 { sys_dup3(efd, 2, 0) }
784 sys_alarm(g_timeout_s)
785 lsp_child_analyze(target, symp)
786 sys_exit(0)
787 }
788 let st: *i64 = sys_mmap(LSP_WORD * 2) as *i64
789 st[0] = 0
790 sys_wait4(pid, st, 0)
791 sig_out[0] = wait_term_signal(st[0])
792 return wait_status_rc(st[0])
793}
794
795// ============================================================================================
796// SECTION 7 -- reading the compiler's diagnostics back out
797// ============================================================================================
798
799func lsp_starts(b: *u8, n: i64, at: i64, s: *u8) -> i64 {
800 let sl: i64 = lsp_slen(s)
801 if at + sl > n { return 0 }
802 var i: i64 = 0
803 while i < sl {
804 if b[at + i] != s[i] { return 0 }
805 i = i + 1
806 }
807 return 1
808}
809
810// A caret line is ALL space/tab and then a single '^'. It always immediately follows the echoed
811// source line, so the pair is what we match -- a source line that happens to be indented XOR would
812// otherwise read as a caret. Returns the 1-based COLUMN, or 0.
813func lsp_caret_col(b: *u8, ls: i64, le: i64) -> i64 {
814 var i: i64 = ls
815 var seen: i64 = 0
816 while i < le {
817 let c: i64 = b[i] as i64
818 if c == LSP_CARET {
819 if seen == 0 { return 0 }
820 if i + 1 != le { return 0 }
821 return i - ls - 1
822 }
823 if c != LSP_SPACE { if c != LSP_TAB { return 0 } }
824 seen = 1
825 i = i + 1
826 }
827 return 0
828}
829
830// THE COMPILER'S STDERR IS A STREAM, NOT A SET OF LINES. nx_parse fixes the contract in its own
831// note: the literal "error at " is a strict prefix of BOTH the mapped and unmapped location forms
832// precisely so a reader can anchor on it, and nx_mgmt_api already anchors its build-log window on
833// exactly that string. A LINE anchor is therefore the wrong reader -- and MEASURED 2026-08-25 it is
834// wrong in the worst available direction: nx_parse.nx:6047 parse_function writes "<name>:" to fd 2
835// for EVERY function with no terminator, so the FIRST diagnostic of every compile is glued to the end
836// of that trace, and a line-anchored reader reports ZERO PROBLEMS in a file that has one. A detector
837// whose failure mode is silence about a real defect is worse than absent. Anchoring on the marker is
838// matching the documented contract, not widening the reader: a false anchor still publishes nothing,
839// because the location grammar behind it must parse before any diagnostic is emitted.
840func lsp_next_marker(b: *u8, n: i64, from: i64, kind_out: *i64) -> i64 {
841 kind_out[0] = 0
842 let a: i64 = jx_find(b, n, from, "error at " as *u8)
843 let c: i64 = jx_find(b, n, from, "nx_parse: " as *u8)
844 let d: i64 = jx_find(b, n, from, "nx_tokenizer: " as *u8)
845 var best: i64 = 0 - 1
846 var kind: i64 = 0
847 if a >= 0 { best = a; kind = 1 }
848 if c >= 0 {
849 if best < 0 { best = c; kind = 2 } else { if c < best { best = c; kind = 2 } }
850 }
851 if d >= 0 {
852 if best < 0 { best = d; kind = 3 } else { if d < best { best = d; kind = 3 } }
853 }
854 kind_out[0] = kind
855 return best
856}
857
858// Parses the child's stderr into positional diagnostics for THIS document. Everything it cannot
859// attribute truthfully is counted into *dropped and reported, never published against a guess.
860func lsp_parse_diags(b: *u8, n: i64, base: *u8, cap: i64,
861 dline: *i64, dcol: *i64, dsev: *i64, dms: *i64, dml: *i64,
862 dropped: *i64) -> i64 {
863 dropped[0] = 0
864 var count: i64 = 0
865 var ls: i64 = 0
866 let kp: *i64 = sys_mmap(LSP_WORD * 2) as *i64
867 var scanning: i64 = 1
868 while scanning == 1 {
869 kp[0] = 0
870 let m: i64 = lsp_next_marker(b, n, ls, kp)
871 if m < 0 { scanning = 0 } else {
872 let le: i64 = lsp_line_end(b, n, m)
873 var pp: i64 = m + 9
874 var sev: i64 = LSP_SEV_ERROR
875 var positional: i64 = 1
876 if kp[0] == 2 { pp = m + 10 }
877 if kp[0] == 3 {
878 pp = m
879 sev = LSP_SEV_WARN
880 positional = 0
881 }
882 if pp >= 0 {
883 var ok: i64 = 0
884 var srcline: i64 = 1
885 var mstart: i64 = pp
886 if positional == 0 {
887 ok = 1
888 mstart = pp
889 } else {
890 // <basename>:<line>: <message> (the mapped form; "line <n>" means UNMAPPED)
891 var q: i64 = pp
892 var colon: i64 = 0 - 1
893 while q < le {
894 if b[q] == (LSP_COLON as u8) { if colon < 0 { colon = q } }
895 q = q + 1
896 }
897 if colon > pp {
898 let bl: i64 = colon - pp
899 var same: i64 = 1
900 if lsp_slen(base) != bl { same = 0 }
901 var k: i64 = 0
902 while k < bl { if b[pp + k] != base[k] { same = 0 } k = k + 1 }
903 if same == 1 {
904 var d: i64 = colon + 1
905 var got: i64 = 0
906 var v: i64 = 0
907 var run: i64 = 1
908 while run == 1 {
909 if d >= le { run = 0 } else {
910 let c: i64 = b[d] as i64
911 if c < LSP_D0 { run = 0 } else {
912 if c > LSP_D9 { run = 0 } else {
913 v = v * LSP_B10 + (c - LSP_D0)
914 got = got + 1
915 d = d + 1
916 } }
917 }
918 }
919 if got > 0 {
920 srcline = v
921 ok = 1
922 mstart = d
923 if mstart < le { if b[mstart] == (LSP_COLON as u8) { mstart = mstart + 1 } }
924 if mstart < le { if b[mstart] == (LSP_SPACE as u8) { mstart = mstart + 1 } }
925 }
926 }
927 }
928 }
929 if ok == 0 { dropped[0] = dropped[0] + 1 } else {
930 var col: i64 = 1
931 if positional == 1 {
932 // the echo+caret pair, before the next diagnostic starts
933 let k2: *i64 = sys_mmap(LSP_WORD * 2) as *i64
934 k2[0] = 0
935 let nxt: i64 = lsp_next_marker(b, n, le + 1, k2)
936 var limit: i64 = n
937 if nxt >= 0 { limit = nxt }
938 var es: i64 = le + 1
939 var scan: i64 = 1
940 while scan == 1 {
941 if es >= limit { scan = 0 } else {
942 let ee: i64 = lsp_line_end(b, n, es)
943 if ee > limit { scan = 0 } else {
944 let cc: i64 = lsp_caret_col(b, es, ee)
945 if cc > 0 { col = cc; scan = 0 } else { es = ee + 1 }
946 }
947 }
948 }
949 }
950 if count < cap {
951 dline[count] = srcline
952 dcol[count] = col
953 dsev[count] = sev
954 dms[count] = mstart
955 dml[count] = le - mstart
956 count = count + 1
957 } else { dropped[0] = dropped[0] + 1 }
958 }
959 }
960 ls = le + 1
961 if ls <= m { ls = m + 1 }
962 }
963 }
964 return count
965}
966
967// ============================================================================================
968// SECTION 8 -- the symbol index, read back
969// ============================================================================================
970
971// Finds `name` in the slot's symbol artifact. Returns 1 on a hit, filling kind/line/col/path.
972func lsp_sym_find(slot: i64, name: *u8, kind_out: *u8, kcap: i64,
973 line_out: *i64, col_out: *i64, path_out: *u8, pcap: i64) -> i64 {
974 let symp: *u8 = sys_mmap(SYS_PATH_MAX)
975 lsp_scratch_path(slot, ".sym" as *u8, symp, SYS_PATH_MAX)
976 let lp: *i64 = sys_mmap(LSP_WORD * 2) as *i64
977 lp[0] = 0
978 let b: *u8 = sys_read_file(symp, lp)
979 if (b as i64) == 0 { return 0 }
980 let n: i64 = lp[0]
981 if n <= 0 { return 0 }
982
983 var found: i64 = 0
984 var want_file: i64 = 0 - 1
985 var ls: i64 = 0
986 while ls < n {
987 let le: i64 = lsp_line_end(b, n, ls)
988 if found == 0 {
989 if lsp_starts(b, n, ls, "S\t" as *u8) == 1 {
990 // S <kind> <line> <col> <fileidx> <name>
991 var f: i64 = 0
992 var fs: i64 = ls + 2
993 var kstart: i64 = fs
994 var klen: i64 = 0
995 var vline: i64 = 0
996 var vcol: i64 = 0
997 var vfile: i64 = 0
998 var nstart: i64 = 0 - 1
999 var i: i64 = fs
1000 var cur: i64 = 0
1001 while i <= le {
1002 var brk: i64 = 0
1003 if i == le { brk = 1 }
1004 if brk == 0 { if b[i] == (LSP_TAB as u8) { brk = 1 } }
1005 if brk == 1 {
1006 if f == 0 { kstart = fs; klen = i - fs }
1007 if f == 1 { vline = lsp_dec_at(b, fs, i) }
1008 if f == 2 { vcol = lsp_dec_at(b, fs, i) }
1009 if f == 3 { vfile = lsp_dec_at(b, fs, i) }
1010 if f == 4 { nstart = fs }
1011 f = f + 1
1012 fs = i + 1
1013 }
1014 i = i + 1
1015 }
1016 if nstart >= 0 {
1017 let nlen: i64 = le - nstart
1018 var same: i64 = 1
1019 if lsp_slen(name) != nlen { same = 0 }
1020 var k: i64 = 0
1021 while k < nlen { if b[nstart + k] != name[k] { same = 0 } k = k + 1 }
1022 if same == 1 {
1023 var ko: i64 = 0
1024 while ko < klen { if ko < kcap - 1 { kind_out[ko] = b[kstart + ko] } ko = ko + 1 }
1025 kind_out[klen] = 0 as u8
1026 line_out[0] = vline
1027 col_out[0] = vcol
1028 want_file = vfile
1029 found = 1
1030 }
1031 }
1032 }
1033 }
1034 ls = le + 1
1035 }
1036 if found == 0 { return 0 }
1037
1038 path_out[0] = 0 as u8
1039 ls = 0
1040 while ls < n {
1041 let le: i64 = lsp_line_end(b, n, ls)
1042 if lsp_starts(b, n, ls, "F\t" as *u8) == 1 {
1043 var i: i64 = ls + 2
1044 var t1: i64 = 0 - 1
1045 while i < le { if b[i] == (LSP_TAB as u8) { if t1 < 0 { t1 = i } } i = i + 1 }
1046 if t1 > 0 {
1047 if lsp_dec_at(b, ls + 2, t1) == want_file {
1048 var po: i64 = 0
1049 var k: i64 = t1 + 1
1050 while k < le { if po < pcap - 1 { path_out[po] = b[k]; po = po + 1 } k = k + 1 }
1051 path_out[po] = 0 as u8
1052 }
1053 }
1054 }
1055 ls = le + 1
1056 }
1057 return 1
1058}
1059
1060func lsp_dec_at(b: *u8, s: i64, e: i64) -> i64 {
1061 var v: i64 = 0
1062 var i: i64 = s
1063 while i < e {
1064 let c: i64 = b[i] as i64
1065 if c >= LSP_D0 { if c <= LSP_D9 { v = v * LSP_B10 + (c - LSP_D0) } }
1066 i = i + 1
1067 }
1068 return v
1069}
1070
1071// ============================================================================================
1072// SECTION 9 -- LSP wire emission
1073// ============================================================================================
1074
1075func lsp_send(body: *u8, n: i64) -> i64 {
1076 if g_ovf == 1 {
1077 lsp_log("nx_lsp: REFUSED to send -- a response buffer overflowed and a truncated JSON message\n" as *u8)
1078 lsp_log(" is a protocol violation, not a shorter answer. Nothing was written.\n" as *u8)
1079 g_ovf = 0
1080 return 0 - 1
1081 }
1082 let h: *u8 = sys_mmap(LSP_OUTHDR_BYTES)
1083 var o: i64 = lsp_str_into(h, 0, LSP_OUTHDR_BYTES, "Content-Length: " as *u8)
1084 o = lsp_num_into(h, o, LSP_OUTHDR_BYTES, n)
1085 o = lsp_put(h, o, LSP_OUTHDR_BYTES, LSP_CR)
1086 o = lsp_put(h, o, LSP_OUTHDR_BYTES, LSP_LF)
1087 o = lsp_put(h, o, LSP_OUTHDR_BYTES, LSP_CR)
1088 o = lsp_put(h, o, LSP_OUTHDR_BYTES, LSP_LF)
1089 lsp_write_all(1, h, o)
1090 lsp_write_all(1, body, n)
1091 return 0
1092}
1093
1094func lsp_window_log(kind: i64, msg: *u8) -> i64 {
1095 let cap: i64 = lsp_slen(msg) * LSP_JSON_ESC_MAX + LSP_SCAFFOLD_BYTES
1096 let b: *u8 = sys_mmap(cap + LSP_WORD)
1097 var o: i64 = lsp_str_into(b, 0, cap, "{\"jsonrpc\":\"2.0\",\"method\":\"window/logMessage\",\"params\":{\"type\":" as *u8)
1098 o = lsp_num_into(b, o, cap, kind)
1099 o = lsp_str_into(b, o, cap, ",\"message\":\"" as *u8)
1100 o = lsp_jstrz_into(b, o, cap, msg)
1101 o = lsp_str_into(b, o, cap, "\"}}" as *u8)
1102 return lsp_send(b, o)
1103}
1104
1105// TRUTHFUL CAPABILITIES ONLY. textDocumentSync 1 = Full: we do not implement incremental sync, so we
1106// do not claim it, and a client that honours this never sends us a range we would have to guess at.
1107func lsp_reply_initialize(id: *u8) -> i64 {
1108 let cap: i64 = LSP_SCAFFOLD_BYTES + LSP_ID_BYTES
1109 let b: *u8 = sys_mmap(cap + LSP_WORD)
1110 var o: i64 = lsp_str_into(b, 0, cap, "{\"jsonrpc\":\"2.0\",\"id\":" as *u8)
1111 o = lsp_str_into(b, o, cap, id)
1112 o = lsp_str_into(b, o, cap, ",\"result\":{\"capabilities\":{\"textDocumentSync\":" as *u8)
1113 o = lsp_num_into(b, o, cap, LSP_SYNC_FULL)
1114 o = lsp_str_into(b, o, cap, ",\"definitionProvider\":true,\"hoverProvider\":true}," as *u8)
1115 o = lsp_str_into(b, o, cap, "\"serverInfo\":{\"name\":\"nx_lsp\",\"version\":\"LN11\"}}}" as *u8)
1116 return lsp_send(b, o)
1117}
1118
1119func lsp_reply_null(id: *u8) -> i64 {
1120 let cap: i64 = LSP_SCAFFOLD_BYTES + LSP_ID_BYTES
1121 let b: *u8 = sys_mmap(cap + LSP_WORD)
1122 var o: i64 = lsp_str_into(b, 0, cap, "{\"jsonrpc\":\"2.0\",\"id\":" as *u8)
1123 o = lsp_str_into(b, o, cap, id)
1124 o = lsp_str_into(b, o, cap, ",\"result\":null}" as *u8)
1125 return lsp_send(b, o)
1126}
1127
1128func lsp_reply_error(id: *u8, code: i64, msg: *u8) -> i64 {
1129 let cap: i64 = LSP_SCAFFOLD_BYTES + LSP_ID_BYTES + lsp_slen(msg) * LSP_JSON_ESC_MAX
1130 let b: *u8 = sys_mmap(cap + LSP_WORD)
1131 var o: i64 = lsp_str_into(b, 0, cap, "{\"jsonrpc\":\"2.0\",\"id\":" as *u8)
1132 o = lsp_str_into(b, o, cap, id)
1133 o = lsp_str_into(b, o, cap, ",\"error\":{\"code\":" as *u8)
1134 o = lsp_num_into(b, o, cap, code)
1135 o = lsp_str_into(b, o, cap, ",\"message\":\"" as *u8)
1136 o = lsp_jstrz_into(b, o, cap, msg)
1137 o = lsp_str_into(b, o, cap, "\"}}" as *u8)
1138 return lsp_send(b, o)
1139}
1140
1141// Builds the publishDiagnostics payload for a slot. Returns the byte length; *nd_out carries the
1142// number of diagnostics so the caller can report it without re-deriving it.
1143func lsp_build_diags(slot: i64, out_cap_out: *i64, nd_out: *i64) -> *u8 {
1144 let sig: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1145 sig[0] = 0
1146 lsp_analyze(slot, sig)
1147
1148 let errp: *u8 = sys_mmap(SYS_PATH_MAX)
1149 lsp_scratch_path(slot, ".err" as *u8, errp, SYS_PATH_MAX)
1150 let lp: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1151 lp[0] = 0
1152 var eb: *u8 = sys_read_file(errp, lp)
1153 var en: i64 = lp[0]
1154 if (eb as i64) == 0 { en = 0 }
1155 if en < 0 { en = 0 }
1156
1157 // one diagnostic can never be shorter than its "error at " prefix, so the stderr byte count is a
1158 // sound upper bound on how many there are -- derived, not guessed.
1159 var dcap: i64 = en / 9 + 2
1160 let dline: *i64 = sys_mmap(dcap * LSP_WORD + LSP_WORD) as *i64
1161 let dcol: *i64 = sys_mmap(dcap * LSP_WORD + LSP_WORD) as *i64
1162 let dsev: *i64 = sys_mmap(dcap * LSP_WORD + LSP_WORD) as *i64
1163 let dms: *i64 = sys_mmap(dcap * LSP_WORD + LSP_WORD) as *i64
1164 let dml: *i64 = sys_mmap(dcap * LSP_WORD + LSP_WORD) as *i64
1165 let drop: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1166 drop[0] = 0
1167
1168 var nd: i64 = 0
1169 if en > 0 {
1170 nd = lsp_parse_diags(eb, en, lsp_basename(lsp_path_slot(slot)), dcap,
1171 dline, dcol, dsev, dms, dml, drop)
1172 }
1173
1174 let uri: *u8 = lsp_uri_slot(slot)
1175 let txt: *u8 = g_doc_txt[slot] as *u8
1176 let tn: i64 = g_doc_len[slot]
1177
1178 let uricost: i64 = lsp_slen(uri) * LSP_JSON_ESC_MAX
1179 let cap: i64 = en * LSP_JSON_ESC_MAX + (nd + 2) * LSP_PER_DIAG_BYTES + uricost + LSP_SCAFFOLD_BYTES
1180 let b: *u8 = sys_mmap(cap + LSP_WORD)
1181 var o: i64 = lsp_str_into(b, 0, cap, "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/publishDiagnostics\",\"params\":{\"uri\":\"" as *u8)
1182 o = lsp_jstrz_into(b, o, cap, uri)
1183 o = lsp_str_into(b, o, cap, "\",\"diagnostics\":[" as *u8)
1184
1185 var emitted: i64 = 0
1186 var i: i64 = 0
1187 while i < nd {
1188 let l0: i64 = dline[i] - 1
1189 let lstart: i64 = lsp_line_start(txt, tn, l0)
1190 if lstart >= 0 {
1191 let lend: i64 = lsp_line_end(txt, tn, lstart)
1192 let llen: i64 = lend - lstart
1193 let lptr: *u8 = ((txt as i64) + lstart) as *u8
1194 var boff: i64 = dcol[i] - 1
1195 if boff < 0 { boff = 0 }
1196 if boff > llen { boff = llen }
1197 let c0: i64 = lsp_u16_of_bytes(lptr, llen, boff)
1198 let ws: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1199 let we: *i64 = ((ws as i64) + LSP_WORD) as *i64
1200 var c1: i64 = c0
1201 if lsp_word_at(lptr, llen, boff, ws, we) == 1 {
1202 c1 = lsp_u16_of_bytes(lptr, llen, we[0])
1203 }
1204 if c1 <= c0 { c1 = c0 + 1 }
1205 if emitted > 0 { o = lsp_put(b, o, cap, LSP_COMMA) }
1206 o = lsp_str_into(b, o, cap, "{\"range\":{\"start\":{\"line\":" as *u8)
1207 o = lsp_num_into(b, o, cap, l0)
1208 o = lsp_str_into(b, o, cap, ",\"character\":" as *u8)
1209 o = lsp_num_into(b, o, cap, c0)
1210 o = lsp_str_into(b, o, cap, "},\"end\":{\"line\":" as *u8)
1211 o = lsp_num_into(b, o, cap, l0)
1212 o = lsp_str_into(b, o, cap, ",\"character\":" as *u8)
1213 o = lsp_num_into(b, o, cap, c1)
1214 o = lsp_str_into(b, o, cap, "}},\"severity\":" as *u8)
1215 o = lsp_num_into(b, o, cap, dsev[i])
1216 o = lsp_str_into(b, o, cap, ",\"source\":\"nx_lsp\",\"message\":\"" as *u8)
1217 o = lsp_jstr_into(b, o, cap, ((eb as i64) + dms[i]) as *u8, dml[i])
1218 o = lsp_str_into(b, o, cap, "\"}" as *u8)
1219 emitted = emitted + 1
1220 } else { drop[0] = drop[0] + 1 }
1221 i = i + 1
1222 }
1223
1224 // A CHILD KILLED BY A SIGNAL PRODUCED NO VERDICT -- say which signal, at 0:0, rather than
1225 // publishing an empty list that reads exactly like a clean file.
1226 if sig[0] != 0 {
1227 if emitted > 0 { o = lsp_put(b, o, cap, LSP_COMMA) }
1228 o = lsp_str_into(b, o, cap, "{\"range\":{\"start\":{\"line\":0,\"character\":0},\"end\":{\"line\":0,\"character\":1}},\"severity\":" as *u8)
1229 o = lsp_num_into(b, o, cap, LSP_SEV_WARN)
1230 o = lsp_str_into(b, o, cap, ",\"source\":\"nx_lsp\",\"message\":\"analysis did not complete: the front end was killed by signal " as *u8)
1231 o = lsp_num_into(b, o, cap, sig[0])
1232 o = lsp_str_into(b, o, cap, " (14 is the analysis-timeout-s bound in knowledge/nx_lsp.conf; anything else is a front-end crash). This file has NOT been checked.\"}" as *u8)
1233 emitted = emitted + 1
1234 }
1235
1236 o = lsp_str_into(b, o, cap, "]}}" as *u8)
1237
1238 if drop[0] > 0 {
1239 lsp_log("nx_lsp: " as *u8)
1240 lsp_lognum(drop[0])
1241 lsp_log(" diagnostic(s) not published -- located outside this document or unmappable; a diagnostic attributed to the wrong file is worse than none\n" as *u8)
1242 }
1243 out_cap_out[0] = cap
1244 nd_out[0] = emitted
1245 return b
1246}
1247
1248func lsp_publish(slot: i64) -> i64 {
1249 let capo: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1250 let ndo: *i64 = ((capo as i64) + LSP_WORD) as *i64
1251 capo[0] = 0
1252 ndo[0] = 0
1253 let b: *u8 = lsp_build_diags(slot, capo, ndo)
1254 return lsp_send(b, lsp_slen_bounded(b, capo[0]))
1255}
1256
1257func lsp_slen_bounded(b: *u8, cap: i64) -> i64 {
1258 var n: i64 = 0
1259 while n < cap { if b[n] == (0 as u8) { return n } n = n + 1 }
1260 return cap
1261}
1262
1263// ============================================================================================
1264// SECTION 10 -- definition and hover, over the symbol index
1265// ============================================================================================
1266
1267// Resolves the identifier under (line0, char0) to its declaration. Returns 1 on a hit.
1268func lsp_resolve(slot: i64, line0: i64, char0: i64,
1269 name_out: *u8, ncap: i64, kind_out: *u8, kcap: i64,
1270 dline: *i64, dcol: *i64, dpath: *u8, pcap: i64,
1271 wc0: *i64, wc1: *i64) -> i64 {
1272 let txt: *u8 = g_doc_txt[slot] as *u8
1273 let tn: i64 = g_doc_len[slot]
1274 let lstart: i64 = lsp_line_start(txt, tn, line0)
1275 if lstart < 0 { return 0 }
1276 let lend: i64 = lsp_line_end(txt, tn, lstart)
1277 let llen: i64 = lend - lstart
1278 let lptr: *u8 = ((txt as i64) + lstart) as *u8
1279 let boff: i64 = lsp_bytes_of_u16(lptr, llen, char0)
1280 let ws: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1281 let we: *i64 = ((ws as i64) + LSP_WORD) as *i64
1282 if lsp_word_at(lptr, llen, boff, ws, we) == 0 { return 0 }
1283 let wl: i64 = we[0] - ws[0]
1284 if wl <= 0 { return 0 }
1285 var k: i64 = 0
1286 while k < wl { if k < ncap - 1 { name_out[k] = lptr[ws[0] + k] } k = k + 1 }
1287 name_out[wl] = 0 as u8
1288 wc0[0] = lsp_u16_of_bytes(lptr, llen, ws[0])
1289 wc1[0] = lsp_u16_of_bytes(lptr, llen, we[0])
1290 if lsp_sym_find(slot, name_out, kind_out, kcap, dline, dcol, dpath, pcap) == 0 { return 0 }
1291 // The root file was analysed through its shadow, so a hit in it must be spoken about as the
1292 // document the editor actually has open -- never as a path under the shadow directory.
1293 if lsp_streq(dpath, lsp_shadow_slot(slot)) == 1 {
1294 var p: i64 = 0
1295 let real: *u8 = lsp_path_slot(slot)
1296 while real[p] != (0 as u8) { if p < pcap - 1 { dpath[p] = real[p] } p = p + 1 }
1297 dpath[p] = 0 as u8
1298 }
1299 return 1
1300}
1301
1302// The declaration's own source line, for hover. Read from the declaring file; the open document is
1303// answered from the live buffer so an unsaved edit shows the text the user is looking at.
1304// trim=1 strips leading indentation (hover DISPLAY); trim=0 keeps the line byte-exact (COLUMN
1305// ARITHMETIC). One reader, two contracts: converting a byte column against a trimmed line would be
1306// off by exactly the indentation, and every .nx declaration inside a block is indented.
1307func lsp_decl_line(slot: i64, path: *u8, line: i64, out: *u8, cap: i64, trim: i64) -> i64 {
1308 var txt: *u8 = 0 as *u8
1309 var tn: i64 = 0
1310 if lsp_streq(path, lsp_path_slot(slot)) == 1 {
1311 txt = g_doc_txt[slot] as *u8
1312 tn = g_doc_len[slot]
1313 } else {
1314 let lp: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1315 lp[0] = 0
1316 txt = sys_read_file(path, lp)
1317 tn = lp[0]
1318 }
1319 out[0] = 0 as u8
1320 if (txt as i64) == 0 { return 0 }
1321 if tn <= 0 { return 0 }
1322 let s: i64 = lsp_line_start(txt, tn, line - 1)
1323 if s < 0 { return 0 }
1324 let e: i64 = lsp_line_end(txt, tn, s)
1325 var o: i64 = 0
1326 var i: i64 = s
1327 while i < e {
1328 var c: i64 = txt[i] as i64
1329 var skip: i64 = 0
1330 if trim == 1 {
1331 if o == 0 { if c == LSP_SPACE { skip = 1 } }
1332 if o == 0 { if c == LSP_TAB { skip = 1 } }
1333 }
1334 if skip == 0 { if o < cap - 1 { out[o] = txt[i]; o = o + 1 } }
1335 i = i + 1
1336 }
1337 out[o] = 0 as u8
1338 return o
1339}
1340
1341func lsp_handle_definition(id: *u8, slot: i64, line0: i64, char0: i64) -> i64 {
1342 let name: *u8 = sys_mmap(LSP_NAMEBUF)
1343 let kind: *u8 = sys_mmap(LSP_KINDBUF)
1344 let dpath: *u8 = sys_mmap(SYS_PATH_MAX)
1345 let sl: *i64 = sys_mmap(LSP_WORD * 8) as *i64
1346 let dl: *i64 = sl
1347 let dc: *i64 = ((sl as i64) + LSP_WORD) as *i64
1348 let w0: *i64 = ((sl as i64) + LSP_WORD * 2) as *i64
1349 let w1: *i64 = ((sl as i64) + LSP_WORD * 3) as *i64
1350 dl[0] = 0
1351 dc[0] = 0
1352 if lsp_resolve(slot, line0, char0, name, LSP_NAMEBUF, kind, LSP_KINDBUF,
1353 dl, dc, dpath, SYS_PATH_MAX, w0, w1) == 0 {
1354 return lsp_reply_null(id)
1355 }
1356 let uri: *u8 = sys_mmap(SYS_PATH_MAX * LSP_URI_ESC_MAX + LSP_SCAFFOLD_BYTES)
1357 lsp_path_to_uri(dpath, uri, SYS_PATH_MAX * LSP_URI_ESC_MAX + LSP_SCAFFOLD_BYTES)
1358
1359 // The declaration column is a BYTE column from the tokenizer and LSP wants UTF-16 code units.
1360 // Convert against the declaring line's OWN UNTRIMMED bytes -- taking the byte value directly
1361 // would be silently wrong on any line carrying a multi-byte character before the name, which is
1362 // exactly the class of error nobody notices until a user with an accented comment reports that
1363 // goto-def lands two columns off.
1364 let dtxt: *u8 = sys_mmap(SYS_PATH_MAX)
1365 let dlen: i64 = lsp_decl_line(slot, dpath, dl[0], dtxt, SYS_PATH_MAX, 0)
1366 var dboff: i64 = dc[0] - 1
1367 if dboff < 0 { dboff = 0 }
1368 let nlen: i64 = lsp_slen(name)
1369 let dc0: i64 = lsp_u16_of_bytes(dtxt, dlen, dboff)
1370 let dc1: i64 = lsp_u16_of_bytes(dtxt, dlen, dboff + nlen)
1371
1372 let cap: i64 = lsp_slen(uri) * LSP_JSON_ESC_MAX + LSP_SCAFFOLD_BYTES + LSP_ID_BYTES
1373 let b: *u8 = sys_mmap(cap + LSP_WORD)
1374 var o: i64 = lsp_str_into(b, 0, cap, "{\"jsonrpc\":\"2.0\",\"id\":" as *u8)
1375 o = lsp_str_into(b, o, cap, id)
1376 o = lsp_str_into(b, o, cap, ",\"result\":{\"uri\":\"" as *u8)
1377 o = lsp_jstrz_into(b, o, cap, uri)
1378 o = lsp_str_into(b, o, cap, "\",\"range\":{\"start\":{\"line\":" as *u8)
1379 o = lsp_num_into(b, o, cap, dl[0] - 1)
1380 o = lsp_str_into(b, o, cap, ",\"character\":" as *u8)
1381 o = lsp_num_into(b, o, cap, dc0)
1382 o = lsp_str_into(b, o, cap, "},\"end\":{\"line\":" as *u8)
1383 o = lsp_num_into(b, o, cap, dl[0] - 1)
1384 o = lsp_str_into(b, o, cap, ",\"character\":" as *u8)
1385 o = lsp_num_into(b, o, cap, dc1)
1386 o = lsp_str_into(b, o, cap, "}}}}" as *u8)
1387 return lsp_send(b, o)
1388}
1389
1390func lsp_handle_hover(id: *u8, slot: i64, line0: i64, char0: i64) -> i64 {
1391 let name: *u8 = sys_mmap(LSP_NAMEBUF)
1392 let kind: *u8 = sys_mmap(LSP_KINDBUF)
1393 let dpath: *u8 = sys_mmap(SYS_PATH_MAX)
1394 let sl: *i64 = sys_mmap(LSP_WORD * 8) as *i64
1395 let dl: *i64 = sl
1396 let dc: *i64 = ((sl as i64) + LSP_WORD) as *i64
1397 let w0: *i64 = ((sl as i64) + LSP_WORD * 2) as *i64
1398 let w1: *i64 = ((sl as i64) + LSP_WORD * 3) as *i64
1399 dl[0] = 0
1400 dc[0] = 0
1401 w0[0] = 0
1402 w1[0] = 0
1403 if lsp_resolve(slot, line0, char0, name, LSP_NAMEBUF, kind, LSP_KINDBUF,
1404 dl, dc, dpath, SYS_PATH_MAX, w0, w1) == 0 {
1405 return lsp_reply_null(id)
1406 }
1407 let decl: *u8 = sys_mmap(SYS_PATH_MAX)
1408 lsp_decl_line(slot, dpath, dl[0], decl, SYS_PATH_MAX, 1)
1409
1410 let textcost: i64 = (lsp_slen(decl) + lsp_slen(dpath) + LSP_NAMEBUF) * LSP_JSON_ESC_MAX
1411 let cap: i64 = textcost + LSP_SCAFFOLD_BYTES + LSP_ID_BYTES
1412 let b: *u8 = sys_mmap(cap + LSP_WORD)
1413 var o: i64 = lsp_str_into(b, 0, cap, "{\"jsonrpc\":\"2.0\",\"id\":" as *u8)
1414 o = lsp_str_into(b, o, cap, id)
1415 o = lsp_str_into(b, o, cap, ",\"result\":{\"contents\":{\"kind\":\"plaintext\",\"value\":\"" as *u8)
1416 o = lsp_jstrz_into(b, o, cap, kind)
1417 o = lsp_jstr_into(b, o, cap, " " as *u8, 1)
1418 o = lsp_jstrz_into(b, o, cap, name)
1419 o = lsp_str_into(b, o, cap, "\\n" as *u8)
1420 o = lsp_jstrz_into(b, o, cap, decl)
1421 o = lsp_str_into(b, o, cap, "\\n" as *u8)
1422 o = lsp_jstrz_into(b, o, cap, lsp_basename(dpath))
1423 o = lsp_jstr_into(b, o, cap, ":" as *u8, 1)
1424 o = lsp_num_into(b, o, cap, dl[0])
1425 o = lsp_str_into(b, o, cap, "\"},\"range\":{\"start\":{\"line\":" as *u8)
1426 o = lsp_num_into(b, o, cap, line0)
1427 o = lsp_str_into(b, o, cap, ",\"character\":" as *u8)
1428 o = lsp_num_into(b, o, cap, w0[0])
1429 o = lsp_str_into(b, o, cap, "},\"end\":{\"line\":" as *u8)
1430 o = lsp_num_into(b, o, cap, line0)
1431 o = lsp_str_into(b, o, cap, ",\"character\":" as *u8)
1432 o = lsp_num_into(b, o, cap, w1[0])
1433 o = lsp_str_into(b, o, cap, "}}}}" as *u8)
1434 return lsp_send(b, o)
1435}
1436
1437// ============================================================================================
1438// SECTION 11 -- the message loop
1439// ============================================================================================
1440
1441// The id is copied VERBATIM (quotes and all when it is a string), so a client using string ids gets
1442// its own id back with its own type -- a number echoed for a string id is a spec violation clients
1443// report as a hang.
1444func lsp_get_id_raw(b: *u8, n: i64, out: *u8, cap: i64) -> i64 {
1445 let at: i64 = jx_find(b, n, 0, "\"id\":" as *u8)
1446 if at < 0 { out[0] = 0 as u8; return 0 }
1447 var p: i64 = jx_skip_ws(b, n, at + 5)
1448 var o: i64 = 0
1449 if p < n {
1450 if b[p] == (LSP_QUOTE as u8) {
1451 if o < cap - 1 { out[o] = b[p]; o = o + 1 }
1452 p = p + 1
1453 var run: i64 = 1
1454 while run == 1 {
1455 if p >= n { run = 0 } else {
1456 if b[p] == (LSP_BSLASH as u8) {
1457 if o < cap - 2 { out[o] = b[p]; o = o + 1; out[o] = b[p + 1]; o = o + 1 }
1458 p = p + 2
1459 } else {
1460 if o < cap - 1 { out[o] = b[p]; o = o + 1 }
1461 if b[p] == (LSP_QUOTE as u8) { run = 0 }
1462 p = p + 1
1463 }
1464 }
1465 }
1466 } else {
1467 var run: i64 = 1
1468 while run == 1 {
1469 if p >= n { run = 0 } else {
1470 let c: i64 = b[p] as i64
1471 var stop: i64 = 0
1472 if c == LSP_COMMA { stop = 1 }
1473 if c == LSP_RBRACE { stop = 1 }
1474 if c == LSP_SPACE { stop = 1 }
1475 if stop == 1 { run = 0 } else {
1476 if o < cap - 1 { out[o] = b[p]; o = o + 1 }
1477 p = p + 1
1478 }
1479 }
1480 }
1481 }
1482 }
1483 out[o] = 0 as u8
1484 return o
1485}
1486
1487func lsp_read_header(clen_out: *i64) -> i64 {
1488 clen_out[0] = 0
1489 let hb: *u8 = sys_mmap(g_hdr_max + LSP_WORD)
1490 let one: *u8 = sys_mmap(LSP_WORD)
1491 var o: i64 = 0
1492 var run: i64 = 1
1493 var result: i64 = 0
1494 while run == 1 {
1495 let r: i64 = sys_read(0, one, 1)
1496 if r <= 0 { run = 0 } else {
1497 if o >= g_hdr_max {
1498 lsp_log("nx_lsp: LSP-HEADER-OVERRUN -- the message header passed header-bytes-max in knowledge/nx_lsp.conf without a blank line. Refusing rather than reading forever.\n" as *u8)
1499 run = 0
1500 result = 0 - 1
1501 } else {
1502 hb[o] = one[0]
1503 o = o + 1
1504 var done: i64 = 0
1505 if o >= 4 {
1506 if hb[o-4] == (LSP_CR as u8) { if hb[o-3] == (LSP_LF as u8) {
1507 if hb[o-2] == (LSP_CR as u8) { if hb[o-1] == (LSP_LF as u8) { done = 1 } } } }
1508 }
1509 if o >= 2 {
1510 if hb[o-2] == (LSP_LF as u8) { if hb[o-1] == (LSP_LF as u8) { done = 1 } }
1511 }
1512 if done == 1 { run = 0; result = 1 }
1513 }
1514 }
1515 }
1516 if result != 1 { return result }
1517 var at: i64 = jx_find(hb, o, 0, "Content-Length:" as *u8)
1518 if at < 0 { at = jx_find(hb, o, 0, "content-length:" as *u8) }
1519 if at < 0 {
1520 lsp_log("nx_lsp: message header carried no Content-Length -- refusing the frame\n" as *u8)
1521 return 0 - 1
1522 }
1523 var p: i64 = jx_skip_ws(hb, o, at + 15)
1524 var v: i64 = 0
1525 var got: i64 = 0
1526 var run2: i64 = 1
1527 while run2 == 1 {
1528 if p >= o { run2 = 0 } else {
1529 let c: i64 = hb[p] as i64
1530 if c < LSP_D0 { run2 = 0 } else {
1531 if c > LSP_D9 { run2 = 0 } else { v = v * LSP_B10 + (c - LSP_D0); got = got + 1; p = p + 1 } }
1532 }
1533 }
1534 if got == 0 { return 0 - 1 }
1535 clen_out[0] = v
1536 return 1
1537}
1538
1539func lsp_read_body(n: i64) -> *u8 {
1540 let b: *u8 = sys_mmap(n + LSP_WORD)
1541 var off: i64 = 0
1542 var run: i64 = 1
1543 while run == 1 {
1544 if off >= n { run = 0 } else {
1545 let r: i64 = sys_read(0, ((b as i64) + off) as *u8, n - off)
1546 if r <= 0 { run = 0 } else { off = off + r }
1547 }
1548 }
1549 if off < n { return 0 as *u8 }
1550 b[n] = 0 as u8
1551 return b
1552}
1553
1554func lsp_params_pos(b: *u8, n: i64, l_out: *i64, c_out: *i64) -> i64 {
1555 l_out[0] = 0
1556 c_out[0] = 0
1557 let at: i64 = jx_find(b, n, 0, "\"position\"" as *u8)
1558 if at < 0 { return 0 }
1559 let lv: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1560 let cv: *i64 = ((lv as i64) + LSP_WORD) as *i64
1561 if jx_get_int(b, n, at, "line" as *u8, lv) < 0 { return 0 }
1562 if jx_get_int(b, n, at, "character" as *u8, cv) < 0 { return 0 }
1563 l_out[0] = lv[0]
1564 c_out[0] = cv[0]
1565 return 1
1566}
1567
1568func lsp_dispatch(b: *u8, n: i64) -> i64 {
1569 let method: *u8 = sys_mmap(LSP_NAMEBUF)
1570 method[0] = 0 as u8
1571 jx_get_str(b, n, 0, "method" as *u8, method, LSP_NAMEBUF)
1572 let id: *u8 = sys_mmap(LSP_ID_BYTES)
1573 let has_id: i64 = lsp_get_id_raw(b, n, id, LSP_ID_BYTES)
1574
1575 if lsp_streq(method, "initialize" as *u8) == 1 { return lsp_reply_initialize(id) }
1576 if lsp_streq(method, "initialized" as *u8) == 1 { return 0 }
1577 if lsp_streq(method, "shutdown" as *u8) == 1 { g_shutdown = 1; return lsp_reply_null(id) }
1578 if lsp_streq(method, "exit" as *u8) == 1 {
1579 if g_shutdown == 1 { sys_exit(LSP_EXIT_OK) }
1580 sys_exit(LSP_EXIT_NO_SHUTDOWN)
1581 }
1582
1583 let uri: *u8 = sys_mmap(g_uri_bytes + LSP_WORD)
1584 uri[0] = 0 as u8
1585 jx_get_str(b, n, 0, "uri" as *u8, uri, g_uri_bytes)
1586
1587 if lsp_streq(method, "textDocument/didOpen" as *u8) == 1 {
1588 let txt: *u8 = sys_mmap(n + LSP_WORD)
1589 txt[0] = 0 as u8
1590 jx_get_str_raw(b, n, 0, "text" as *u8, txt, n + 1)
1591 let slot: i64 = lsp_doc_put(uri, txt, lsp_slen(txt))
1592 if slot < 0 {
1593 lsp_window_log(LSP_MSG_ERROR, "nx_lsp REFUSED didOpen: the document store is full or the URI exceeds doc-uri-bytes. Raise max-open-docs / doc-uri-bytes in knowledge/nx_lsp.conf. This file is NOT being checked." as *u8)
1594 return 0
1595 }
1596 return lsp_publish(slot)
1597 }
1598
1599 if lsp_streq(method, "textDocument/didChange" as *u8) == 1 {
1600 let slot0: i64 = lsp_doc_find(uri)
1601 if slot0 < 0 {
1602 lsp_window_log(LSP_MSG_WARN, "nx_lsp: didChange for a document that was never opened -- ignored" as *u8)
1603 return 0
1604 }
1605 let cc: i64 = jx_find(b, n, 0, "\"contentChanges\"" as *u8)
1606 var from: i64 = 0
1607 if cc >= 0 { from = cc }
1608 if jx_find(b, n, from, "\"range\"" as *u8) >= 0 {
1609 lsp_window_log(LSP_MSG_ERROR, "nx_lsp REFUSED an incremental didChange: this server advertises textDocumentSync=Full and does not implement ranges. Applying it as a full replacement would silently corrupt the buffer." as *u8)
1610 return 0
1611 }
1612 let txt: *u8 = sys_mmap(n + LSP_WORD)
1613 txt[0] = 0 as u8
1614 jx_get_str_raw(b, n, from, "text" as *u8, txt, n + 1)
1615 let slot: i64 = lsp_doc_put(uri, txt, lsp_slen(txt))
1616 if slot < 0 { return 0 }
1617 return lsp_publish(slot)
1618 }
1619
1620 if lsp_streq(method, "textDocument/didClose" as *u8) == 1 {
1621 let slot: i64 = lsp_doc_find(uri)
1622 if slot >= 0 { g_doc_live[slot] = 0 }
1623 // The spec requires clearing: a diagnostic left behind on a closed file outlives its subject.
1624 let cap: i64 = lsp_slen(uri) * LSP_JSON_ESC_MAX + LSP_SCAFFOLD_BYTES
1625 let ob: *u8 = sys_mmap(cap + LSP_WORD)
1626 var o: i64 = lsp_str_into(ob, 0, cap, "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/publishDiagnostics\",\"params\":{\"uri\":\"" as *u8)
1627 o = lsp_jstrz_into(ob, o, cap, uri)
1628 o = lsp_str_into(ob, o, cap, "\",\"diagnostics\":[]}}" as *u8)
1629 return lsp_send(ob, o)
1630 }
1631
1632 if lsp_streq(method, "textDocument/definition" as *u8) == 1 {
1633 let slot: i64 = lsp_doc_find(uri)
1634 if slot < 0 { return lsp_reply_null(id) }
1635 let pv: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1636 let pc: *i64 = ((pv as i64) + LSP_WORD) as *i64
1637 if lsp_params_pos(b, n, pv, pc) == 0 { return lsp_reply_null(id) }
1638 return lsp_handle_definition(id, slot, pv[0], pc[0])
1639 }
1640
1641 if lsp_streq(method, "textDocument/hover" as *u8) == 1 {
1642 let slot: i64 = lsp_doc_find(uri)
1643 if slot < 0 { return lsp_reply_null(id) }
1644 let pv: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1645 let pc: *i64 = ((pv as i64) + LSP_WORD) as *i64
1646 if lsp_params_pos(b, n, pv, pc) == 0 { return lsp_reply_null(id) }
1647 return lsp_handle_hover(id, slot, pv[0], pc[0])
1648 }
1649
1650 // An unknown REQUEST must be answered or the client waits forever; an unknown NOTIFICATION must
1651 // be ignored in silence. Telling them apart is the presence of an id, and nothing else.
1652 if has_id > 0 { return lsp_reply_error(id, LSP_RPC_METHOD_NOT_FOUND, method) }
1653 return 0
1654}
1655
1656func lsp_serve() -> i64 {
1657 let cl: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1658 var run: i64 = 1
1659 while run == 1 {
1660 cl[0] = 0
1661 let h: i64 = lsp_read_header(cl)
1662 if h != 1 { run = 0 } else {
1663 if cl[0] <= 0 { run = 0 } else {
1664 let b: *u8 = lsp_read_body(cl[0])
1665 if (b as i64) == 0 {
1666 lsp_log("nx_lsp: stdin closed mid-body -- the client went away\n" as *u8)
1667 run = 0
1668 } else {
1669 g_ovf = 0
1670 lsp_dispatch(b, cl[0])
1671 }
1672 }
1673 }
1674 }
1675 if g_shutdown == 1 { return LSP_EXIT_OK }
1676 return LSP_EXIT_NO_SHUTDOWN
1677}
1678
1679// ============================================================================================
1680// SECTION 12 -- one-shot modes (the auditable surface: same code path, no framing)
1681// ============================================================================================
1682
1683func lsp_oneshot_load(path: *u8) -> i64 {
1684 let lp: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1685 lp[0] = 0
1686 let t: *u8 = sys_read_file(path, lp)
1687 if (t as i64) == 0 {
1688 lsp_log("nx_lsp: cannot read " as *u8)
1689 lsp_log(path)
1690 lsp_log("\n" as *u8)
1691 return 0 - 1
1692 }
1693 return lsp_doc_put(path, t, lp[0])
1694}
1695
1696func lsp_mode_diag(path: *u8) -> i64 {
1697 let slot: i64 = lsp_oneshot_load(path)
1698 if slot < 0 { return 1 }
1699 let capo: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1700 let ndo: *i64 = ((capo as i64) + LSP_WORD) as *i64
1701 capo[0] = 0
1702 ndo[0] = 0
1703 let b: *u8 = lsp_build_diags(slot, capo, ndo)
1704 let n: i64 = lsp_slen_bounded(b, capo[0])
1705 lsp_write_all(1, b, n)
1706 lsp_write_all(1, "\n" as *u8, 1)
1707 return 0
1708}
1709
1710func lsp_mode_symbols(path: *u8) -> i64 {
1711 let slot: i64 = lsp_oneshot_load(path)
1712 if slot < 0 { return 1 }
1713 let sig: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1714 sig[0] = 0
1715 lsp_analyze(slot, sig)
1716 let symp: *u8 = sys_mmap(SYS_PATH_MAX)
1717 lsp_scratch_path(slot, ".sym" as *u8, symp, SYS_PATH_MAX)
1718 let lp: *i64 = sys_mmap(LSP_WORD * 2) as *i64
1719 lp[0] = 0
1720 let b: *u8 = sys_read_file(symp, lp)
1721 if (b as i64) == 0 { return 1 }
1722 lsp_write_all(1, b, lp[0])
1723 if sig[0] != 0 {
1724 lsp_log("nx_lsp: the analysis child was killed by signal " as *u8)
1725 lsp_lognum(sig[0])
1726 lsp_log(" -- this index is PARTIAL\n" as *u8)
1727 }
1728 return 0
1729}
1730
1731func lsp_usage() -> i64 {
1732 lsp_log("nx_lsp -- LN11 NishiLang language server (LSP 3.17 over stdio).\n" as *u8)
1733 lsp_log("usage: nx_lsp serve speak LSP on stdin/stdout (the editor entry point)\n" as *u8)
1734 lsp_log(" nx_lsp diag <file.nx> the publishDiagnostics payload for one file, unframed\n" as *u8)
1735 lsp_log(" nx_lsp symbols <file.nx> the symbol index artifact for one file\n" as *u8)
1736 lsp_log("Diagnostics, goto-definition and hover are served from the compiler's own front end\n" as *u8)
1737 lsp_log("(nx_import expand_imports + LineMap, nx_tokenizer lex_source, nx_parse parse_module).\n" as *u8)
1738 lsp_log("Every bound it uses is a row in knowledge/nx_lsp.conf; a missing row is a refusal.\n" as *u8)
1739 return LSP_EXIT_USAGE
1740}
1741
1742func main(argc: i64, argv: *i64) -> i64 {
1743 if argc < 2 { lsp_cfg_load(); return lsp_usage() }
1744 lsp_cfg_load()
1745 let a1: *u8 = argv[1] as *u8
1746 if lsp_streq(a1, "serve" as *u8) == 1 { return lsp_serve() }
1747 if lsp_streq(a1, "diag" as *u8) == 1 {
1748 if argc < 3 { return lsp_usage() }
1749 return lsp_mode_diag(argv[2] as *u8)
1750 }
1751 if lsp_streq(a1, "symbols" as *u8) == 1 {
1752 if argc < 3 { return lsp_usage() }
1753 return lsp_mode_symbols(argv[2] as *u8)
1754 }
1755 return lsp_usage()
1756}