code wiki / (root) / nx_lsp.nx

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}