nx_lsp_gate.nx source
↩ module page · 573 lines · 25971 B
1// ============================================================================================
2// nx_lsp_gate.nx -- the referee for LN11 (nx_lsp, the NishiLang language server).
3//
4// WHAT IT PROVES, and why each half is load-bearing:
5// A CLEAN document must publish ZERO diagnostics and a BROKEN one must publish exactly ONE at the
6// right line and column. Either half alone is trivially passable: a server that always reports an
7// error passes the broken case, and a server that never reports one passes the clean case. Only
8// the PAIR discriminates, so the pairing is a gv_bite cell and not two independent teeth.
9//
10// The UTF-16 column is checked against a byte column that is DIFFERENT, on a line built at runtime
11// with multi-byte characters before the offending name. A server that passed byte columns through
12// would satisfy "character is a plausible number" and fail this. That is the anti-vacuity tooth.
13//
14// The transcript is a REAL client: a recorded byte stream fed to `nx_lsp serve` on stdin with
15// stdout captured. Nothing is stubbed, and the framing validator re-derives every Content-Length
16// from the bytes that followed it -- an escaping bug that shortens a body cannot hide behind a
17// self-consistent header.
18//
19// DELIBERATE DUPLICATION, DECLARED: this gate carries its OWN substring search, its OWN JSON string
20// escaper and its OWN UTF-16 counter. That is not the duplicate-ruler defect -- it is the opposite.
21// A test that measures the subject with the subject's own helpers cannot catch a defect IN those
22// helpers, and the escaper and the column arithmetic are exactly where the interesting defects live.
23//
24// FIXTURES ARE ASSEMBLED AT RUNTIME in /tmp/nx_lsp_gate/ -- never checked in, never shared with a
25// production beat, and never written into the estate's own source tree where a detector that scans
26// sources would find this gate's own bad input and report it as a real defect. The multi-byte
27// characters are CONSTRUCTED FROM BYTES rather than written as literals: a source file crosses
28// several transports before it reaches a compiler and each of them can re-encode a literal.
29// license_tier: ORIGINAL No hardware writes (Rule 26).
30// ============================================================================================
31
32import "nx_syscalls.nx"
33import "nx_gate_verdict.nx"
34
35const GL_LF: i64 = 10
36const GL_CR: i64 = 13
37const GL_QUOTE: i64 = 34
38const GL_D0: i64 = 48
39const GL_D9: i64 = 57
40const GL_BSLASH: i64 = 92
41const GL_B10: i64 = 10
42const GL_WORD: i64 = 8
43const GL_U8_2: i64 = 192
44const GL_U8_3: i64 = 224
45const GL_U8_4: i64 = 240
46// The two bytes of U+00E9 in UTF-8. Named so the fixture's intent survives any transport.
47const GL_E9_HI: i64 = 195
48const GL_E9_LO: i64 = 169
49const GL_LBRACE: i64 = 123
50const GL_RBRACE: i64 = 125
51const GL_RBRACK: i64 = 93
52const GL_MINUS: i64 = 45
53const GL_ESC_N: i64 = 110
54const GL_ESC_R: i64 = 114
55const GL_ESC_T: i64 = 116
56const GL_TAB: i64 = 9
57const GL_SEPLEN: i64 = 4
58const GL_DIAGS_KEYLEN: i64 = 15
59// Scratch sizes for THIS gate's own buffers. Fixtures are a few hundred bytes and the capture is a
60// handful of LSP frames; these are generous by orders of magnitude and every writer is bounded.
61const GL_BUF: i64 = 262144
62const GL_PATH: i64 = 1024
63const GL_SMALL: i64 = 4096
64
65func gl_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
66
67func gl_find(b: *u8, n: i64, start: i64, pat: *u8) -> i64 {
68 let pl: i64 = gl_len(pat)
69 if pl == 0 { return 0 - 1 }
70 var i: i64 = start
71 if i < 0 { i = 0 }
72 while i + pl <= n {
73 var j: i64 = 0
74 var ok: i64 = 1
75 while j < pl {
76 if b[i + j] != pat[j] { ok = 0; j = pl } else { j = j + 1 }
77 }
78 if ok == 1 { return i }
79 i = i + 1
80 }
81 return 0 - 1
82}
83
84func gl_count(b: *u8, n: i64, pat: *u8) -> i64 {
85 var c: i64 = 0
86 var at: i64 = 0
87 var run: i64 = 1
88 while run == 1 {
89 let f: i64 = gl_find(b, n, at, pat)
90 if f < 0 { run = 0 } else { c = c + 1; at = f + 1 }
91 }
92 return c
93}
94
95// first decimal integer following `key` at/after `start`; -1 when the key is absent.
96func gl_int_after(b: *u8, n: i64, start: i64, key: *u8) -> i64 {
97 let at: i64 = gl_find(b, n, start, key)
98 if at < 0 { return 0 - 1 }
99 var p: i64 = at + gl_len(key)
100 var neg: i64 = 0
101 if p < n { if b[p] == (GL_MINUS as u8) { neg = 1; p = p + 1 } }
102 var v: i64 = 0
103 var got: i64 = 0
104 var run: i64 = 1
105 while run == 1 {
106 if p >= n { run = 0 } else {
107 let c: i64 = b[p] as i64
108 if c < GL_D0 { run = 0 } else {
109 if c > GL_D9 { run = 0 } else { v = v * GL_B10 + (c - GL_D0); got = got + 1; p = p + 1 } }
110 }
111 }
112 if got == 0 { return 0 - 1 }
113 if neg == 1 { return 0 - v }
114 return v
115}
116
117// THE GATE'S OWN UTF-16 COUNTER. Independent of the subject's, on purpose.
118func gl_u16(b: *u8, n: i64, upto: i64) -> i64 {
119 var u: i64 = 0
120 var i: i64 = 0
121 var run: i64 = 1
122 while run == 1 {
123 if i >= upto { run = 0 } else {
124 if i >= n { run = 0 } else {
125 let c: i64 = b[i] as i64
126 var adv: i64 = 1
127 var units: i64 = 1
128 if c >= GL_U8_4 { adv = 4; units = 2 } else {
129 if c >= GL_U8_3 { adv = 3 } else {
130 if c >= GL_U8_2 { adv = 2 } } }
131 u = u + units
132 i = i + adv
133 }
134 }
135 }
136 return u
137}
138
139func gl_write_file(path: *u8, buf: *u8, n: i64) -> i64 {
140 let fd: i64 = sys_openat_wr(path, MODE_0644)
141 if fd < 0 { return 0 - 1 }
142 var off: i64 = 0
143 var run: i64 = 1
144 while run == 1 {
145 if off >= n { run = 0 } else {
146 let w: i64 = sys_write(fd, ((buf as i64) + off) as *u8, n - off)
147 if w <= 0 { run = 0 } else { off = off + w }
148 }
149 }
150 sys_close(fd)
151 if off != n { return 0 - 1 }
152 return 0
153}
154
155func gl_exists(path: *u8) -> i64 {
156 let fd: i64 = sys_openat_rd(path)
157 if fd < 0 { return 0 }
158 sys_close(fd)
159 return 1
160}
161
162// THE GATE'S OWN JSON STRING ESCAPER (client side).
163func gl_jesc(d: *u8, o: i64, s: *u8, n: i64) -> i64 {
164 var p: i64 = o
165 var i: i64 = 0
166 while i < n {
167 let c: i64 = s[i] as i64
168 if c == GL_QUOTE { d[p] = GL_BSLASH as u8; p = p + 1; d[p] = GL_QUOTE as u8; p = p + 1 } else {
169 if c == GL_BSLASH { d[p] = GL_BSLASH as u8; p = p + 1; d[p] = GL_BSLASH as u8; p = p + 1 } else {
170 if c == GL_LF { d[p] = GL_BSLASH as u8; p = p + 1; d[p] = GL_ESC_N as u8; p = p + 1 } else {
171 if c == GL_CR { d[p] = GL_BSLASH as u8; p = p + 1; d[p] = GL_ESC_R as u8; p = p + 1 } else {
172 if c == GL_TAB { d[p] = GL_BSLASH as u8; p = p + 1; d[p] = GL_ESC_T as u8; p = p + 1 } else {
173 d[p] = s[i]; p = p + 1
174 } } } } }
175 i = i + 1
176 }
177 return p
178}
179
180// Content-Length framing, written the way a real client writes it.
181func gl_frame(d: *u8, o: i64, body: *u8, bn: i64) -> i64 {
182 var p: i64 = gv_cat(d, o, "Content-Length: " as *u8)
183 p = gv_catn(d, p, bn)
184 d[p] = GL_CR as u8; p = p + 1
185 d[p] = GL_LF as u8; p = p + 1
186 d[p] = GL_CR as u8; p = p + 1
187 d[p] = GL_LF as u8; p = p + 1
188 var i: i64 = 0
189 while i < bn { d[p] = body[i]; p = p + 1; i = i + 1 }
190 return p
191}
192
193func gl_spawn(elf: *u8, argv: *i64, in_path: *u8, out_path: *u8) -> i64 {
194 let pid: i64 = sys_fork()
195 if pid < 0 { return 0 - 1 }
196 if pid == 0 {
197 if (in_path as i64) != 0 {
198 let ifd: i64 = sys_openat_rd(in_path)
199 if ifd >= 0 { sys_dup3(ifd, 0, 0); sys_close(ifd) }
200 }
201 if (out_path as i64) != 0 {
202 let ofd: i64 = sys_openat_wr(out_path, MODE_0644)
203 if ofd >= 0 { sys_dup3(ofd, 1, 0); sys_close(ofd) }
204 }
205 let envp: *i64 = sys_mmap(GL_WORD * 2) as *i64
206 envp[0] = 0
207 sys_execve(elf, argv, envp)
208 sys_exit(127)
209 }
210 let st: *i64 = sys_mmap(GL_WORD * 2) as *i64
211 st[0] = 0
212 sys_wait4(pid, st, 0)
213 return wait_status_rc(st[0])
214}
215
216func gl_slurp(path: *u8, len_out: *i64) -> *u8 {
217 len_out[0] = 0
218 let b: *u8 = sys_read_file(path, len_out)
219 if (b as i64) == 0 { len_out[0] = 0 }
220 return b
221}
222
223// EVERY Content-Length in the capture must be followed by exactly that many body bytes, and the next
224// thing after a body must be another header or end-of-stream. Returns the frame count, or -1 when
225// the framing does not hold. A self-consistent-but-wrong length is precisely what an escaping bug
226// produces, so re-deriving from the bytes is the only check worth having.
227func gl_frames_valid(b: *u8, n: i64) -> i64 {
228 var pos: i64 = 0
229 var frames: i64 = 0
230 var run: i64 = 1
231 while run == 1 {
232 if pos >= n { run = 0 } else {
233 let at: i64 = gl_find(b, n, pos, "Content-Length: " as *u8)
234 if at != pos { run = 0; frames = 0 - 1 } else {
235 let clen: i64 = gl_int_after(b, n, pos, "Content-Length: " as *u8)
236 if clen <= 0 { run = 0; frames = 0 - 1 } else {
237 let sep: i64 = gl_find(b, n, pos, "\r\n\r\n" as *u8)
238 if sep < 0 { run = 0; frames = 0 - 1 } else {
239 let bs: i64 = sep + GL_SEPLEN
240 if bs + clen > n { run = 0; frames = 0 - 1 } else {
241 var okb: i64 = 1
242 if b[bs] != (GL_LBRACE as u8) { okb = 0 }
243 if b[bs + clen - 1] != (GL_RBRACE as u8) { okb = 0 }
244 if okb == 0 { run = 0; frames = 0 - 1 } else {
245 frames = frames + 1
246 pos = bs + clen
247 }
248 }
249 }
250 }
251 }
252 }
253 }
254 return frames
255}
256
257func main() -> i64 {
258 let ctr: *i64 = gv_ctr()
259 gv_head("nx_lsp_gate -- LN11: LSP initialize plus a diagnostics round-trip against a recorded client transcript, with goto-definition and hover over the compiler's own code graph" as *u8)
260
261 // ---- SETUP. Scratch is /tmp/<gate>/, created here: a teardown does not run when a run crashes,
262 // ---- and a gate that shares a fixture with a production beat measures the beat, not the code.
263 let dir: *u8 = sys_mmap(GL_PATH)
264 dir[gv_cat(dir, 0, "/tmp/nx_lsp_gate" as *u8)] = 0 as u8
265 sys_mkdir(dir, MODE_0755)
266
267 let p_helper: *u8 = sys_mmap(GL_PATH)
268 let p_ok: *u8 = sys_mmap(GL_PATH)
269 let p_bad: *u8 = sys_mmap(GL_PATH)
270 let p_u16: *u8 = sys_mmap(GL_PATH)
271 let p_tr: *u8 = sys_mmap(GL_PATH)
272 let p_cap: *u8 = sys_mmap(GL_PATH)
273 let p_out: *u8 = sys_mmap(GL_PATH)
274 p_helper[gv_cat(p_helper, 0, "/tmp/nx_lsp_gate/helper.nx" as *u8)] = 0 as u8
275 p_ok[gv_cat(p_ok, 0, "/tmp/nx_lsp_gate/main_ok.nx" as *u8)] = 0 as u8
276 p_bad[gv_cat(p_bad, 0, "/tmp/nx_lsp_gate/main_bad.nx" as *u8)] = 0 as u8
277 p_u16[gv_cat(p_u16, 0, "/tmp/nx_lsp_gate/main_u16.nx" as *u8)] = 0 as u8
278 p_tr[gv_cat(p_tr, 0, "/tmp/nx_lsp_gate/client.transcript" as *u8)] = 0 as u8
279 p_cap[gv_cat(p_cap, 0, "/tmp/nx_lsp_gate/server.capture" as *u8)] = 0 as u8
280 p_out[gv_cat(p_out, 0, "/tmp/nx_lsp_gate/oneshot.out" as *u8)] = 0 as u8
281
282 // SUBJECT RESOLUTION, PRINTED NOT ASSUMED. Runners fork _offc/, /api/promote writes the serving
283 // root, and a mutation bite refreshes both -- so prefer the twin the runners use and fall back to
284 // the promoted binary. Either way the path is on the record: a gate that does not say WHICH
285 // binary it measured cannot be re-run against the same subject by the next reader.
286 let elf: *u8 = sys_mmap(GL_PATH)
287 elf[gv_cat(elf, 0, "_offc/nx_lsp.elf" as *u8)] = 0 as u8
288 if gl_exists(elf) == 0 { elf[gv_cat(elf, 0, "nx_lsp.elf" as *u8)] = 0 as u8 }
289 gv_puts(" subject: " as *u8) gv_puts(elf) gv_puts("\n" as *u8)
290
291 // ---- FIXTURES, assembled here so no bad input is ever a checked-in source byte.
292 let fx: *u8 = sys_mmap(GL_BUF)
293 let fn: i64 = gv_cat(fx, 0, "func helper_add(a: i64, b: i64) -> i64 {\n return a + b\n}\n" as *u8)
294 gl_write_file(p_helper, fx, fn)
295
296 let ok_txt: *u8 = sys_mmap(GL_BUF)
297 let ok_n: i64 = gv_cat(ok_txt, 0, "import \"helper.nx\"\nfunc main() -> i64 {\n let x: i64 = helper_add(1, 2)\n return x\n}\n" as *u8)
298 ok_txt[ok_n] = 0 as u8
299 gl_write_file(p_ok, ok_txt, ok_n)
300
301 let bad_txt: *u8 = sys_mmap(GL_BUF)
302 let bad_n: i64 = gv_cat(bad_txt, 0, "import \"helper.nx\"\nfunc main() -> i64 {\n let x: i64 = helper_addd(1, 2)\n return x\n}\n" as *u8)
303 bad_txt[bad_n] = 0 as u8
304 gl_write_file(p_bad, bad_txt, bad_n)
305
306 // ONE LINE, with three two-byte characters before the undefined name, so the UTF-16 column and
307 // the byte column are provably different numbers. Bytes, not literals -- see the header.
308 let u16_txt: *u8 = sys_mmap(GL_BUF)
309 var un0: i64 = gv_cat(u16_txt, 0, "func main() -> i64 { let s: *u8 = \"" as *u8)
310 var e: i64 = 0
311 while e < 3 {
312 u16_txt[un0] = GL_E9_HI as u8; un0 = un0 + 1
313 u16_txt[un0] = GL_E9_LO as u8; un0 = un0 + 1
314 e = e + 1
315 }
316 let u16_n: i64 = gv_cat(u16_txt, un0, "\" return zzz(1) }\n" as *u8)
317 u16_txt[u16_n] = 0 as u8
318 gl_write_file(p_u16, u16_txt, u16_n)
319
320 var setup_ok: i64 = 0
321 if gl_exists(p_helper) == 1 { if gl_exists(p_ok) == 1 { if gl_exists(p_bad) == 1 {
322 if gl_exists(p_u16) == 1 { setup_ok = 1 } } } }
323 gv_need("fixtures written to /tmp/nx_lsp_gate" as *u8, setup_ok, ctr)
324 gv_need("subject binary _offc/nx_lsp.elf is present" as *u8, gl_exists(elf), ctr)
325
326 // ---- expected positions, DERIVED from the fixture bytes rather than written down.
327 let bad_line: i64 = 2
328 let bad_at: i64 = gl_find(bad_txt, bad_n, 0, "helper_addd" as *u8)
329 var bad_ls: i64 = 0
330 var q: i64 = 0
331 while q < bad_at { if bad_txt[q] == (GL_LF as u8) { bad_ls = q + 1 } q = q + 1 }
332 let bad_col_bytes: i64 = bad_at - bad_ls
333 let bad_name_len: i64 = gl_len("helper_addd" as *u8)
334
335 let u16_at: i64 = gl_find(u16_txt, u16_n, 0, "zzz" as *u8)
336 let u16_expect: i64 = gl_u16(u16_txt, u16_n, u16_at)
337
338 let ok_at: i64 = gl_find(ok_txt, ok_n, 0, "helper_add(1" as *u8)
339 var ok_ls: i64 = 0
340 q = 0
341 while q < ok_at { if ok_txt[q] == (GL_LF as u8) { ok_ls = q + 1 } q = q + 1 }
342 let ok_col: i64 = ok_at - ok_ls
343
344 gv_puts(" derived: bad_line0=" as *u8) gv_num(bad_line)
345 gv_puts(" bad_col_bytes=" as *u8) gv_num(bad_col_bytes)
346 gv_puts(" u16_byte_col=" as *u8) gv_num(u16_at)
347 gv_puts(" u16_expect=" as *u8) gv_num(u16_expect)
348 gv_puts(" def_col=" as *u8) gv_num(ok_col)
349 gv_puts("\n" as *u8)
350
351 // ---- ONE-SHOT DIAGNOSTICS -----------------------------------------------------------------
352 let argv3: *i64 = sys_mmap(GL_WORD * 8) as *i64
353 let a_diag: *u8 = sys_mmap(GL_SMALL)
354 a_diag[gv_cat(a_diag, 0, "diag" as *u8)] = 0 as u8
355 let a_syms: *u8 = sys_mmap(GL_SMALL)
356 a_syms[gv_cat(a_syms, 0, "symbols" as *u8)] = 0 as u8
357 let a_serve: *u8 = sys_mmap(GL_SMALL)
358 a_serve[gv_cat(a_serve, 0, "serve" as *u8)] = 0 as u8
359
360 let lenp: *i64 = sys_mmap(GL_WORD * 2) as *i64
361
362 argv3[0] = elf as i64
363 argv3[1] = a_diag as i64
364 argv3[2] = p_ok as i64
365 argv3[3] = 0
366 gl_spawn(elf, argv3, 0 as *u8, p_out)
367 let cb: *u8 = gl_slurp(p_out, lenp)
368 let cn: i64 = lenp[0]
369 let clean_diags: i64 = gl_count(cb, cn, "\"severity\":" as *u8)
370
371 argv3[2] = p_bad as i64
372 gl_spawn(elf, argv3, 0 as *u8, p_out)
373 let bb: *u8 = gl_slurp(p_out, lenp)
374 let bn2: i64 = lenp[0]
375 let bad_diags: i64 = gl_count(bb, bn2, "\"severity\":" as *u8)
376 let bad_start: i64 = gl_find(bb, bn2, 0, "\"start\":" as *u8)
377 var got_line: i64 = 0 - 1
378 var got_char: i64 = 0 - 1
379 if bad_start >= 0 {
380 got_line = gl_int_after(bb, bn2, bad_start, "\"line\":" as *u8)
381 got_char = gl_int_after(bb, bn2, bad_start, "\"character\":" as *u8)
382 }
383 let msg_names_it: i64 = gl_find(bb, bn2, 0, "helper_addd" as *u8)
384
385 gv_puts(" measured: clean_diags=" as *u8) gv_num(clean_diags)
386 gv_puts(" bad_diags=" as *u8) gv_num(bad_diags)
387 gv_puts(" line=" as *u8) gv_num(got_line)
388 gv_puts(" character=" as *u8) gv_num(got_char)
389 gv_puts("\n" as *u8)
390
391 var bad_fires: i64 = 0
392 if bad_diags == 1 { bad_fires = 1 }
393 gv_bite("neg-control-clean-document-silent-and-broken-document-fires (either half alone is passable by a server that always, or never, reports an error)" as *u8, bad_fires, clean_diags, ctr)
394
395 var line_ok: i64 = 0
396 if got_line == bad_line { line_ok = 1 }
397 gv_check("T1 the diagnostic lands on the offending line, mapped back through the compiler LineMap from the import-expanded unit" as *u8, line_ok, ctr)
398
399 var col_ok: i64 = 0
400 if got_char >= bad_col_bytes { if got_char <= bad_col_bytes + bad_name_len { col_ok = 1 } }
401 gv_check("T2 the diagnostic column falls inside the offending identifier, derived from the caret the compiler drew" as *u8, col_ok, ctr)
402
403 var msg_ok: i64 = 0
404 if msg_names_it >= 0 { msg_ok = 1 }
405 gv_check("T3 the published message is the compiler's own text and names the offending symbol" as *u8, msg_ok, ctr)
406
407 var one_ok: i64 = 0
408 if bad_diags == 1 { one_ok = 1 }
409 gv_check("T4 exactly ONE diagnostic for one error -- an import-expanded unit must not attribute the imported file's contents to this document" as *u8, one_ok, ctr)
410
411 // ---- UTF-16 COLUMN -------------------------------------------------------------------------
412 argv3[2] = p_u16 as i64
413 gl_spawn(elf, argv3, 0 as *u8, p_out)
414 let ub: *u8 = gl_slurp(p_out, lenp)
415 let un: i64 = lenp[0]
416 let u_start: i64 = gl_find(ub, un, 0, "\"start\":" as *u8)
417 var u_char: i64 = 0 - 1
418 if u_start >= 0 { u_char = gl_int_after(ub, un, u_start, "\"character\":" as *u8) }
419 gv_puts(" measured: utf16_character=" as *u8) gv_num(u_char)
420 gv_puts(" byte_column=" as *u8) gv_num(u16_at)
421 gv_puts(" expected_utf16=" as *u8) gv_num(u16_expect)
422 gv_puts("\n" as *u8)
423
424 var u_ok: i64 = 0
425 if u_char == u16_expect { u_ok = 1 }
426 gv_check("T5 the published character is the UTF-16 code-unit column LSP requires" as *u8, u_ok, ctr)
427
428 var u_not_bytes: i64 = 0
429 if u16_expect < u16_at { if u_char != u16_at { u_not_bytes = 1 } }
430 gv_check("neg-control-utf16-column-is-not-the-byte-column (three two-byte characters precede the name, so a server passing bytes through cannot satisfy this)" as *u8, u_not_bytes, ctr)
431
432 // ---- THE RECORDED CLIENT TRANSCRIPT --------------------------------------------------------
433 let tr: *u8 = sys_mmap(GL_BUF)
434 let bodyb: *u8 = sys_mmap(GL_BUF)
435 var t: i64 = 0
436 var bo: i64 = 0
437
438 bo = gv_cat(bodyb, 0, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"processId\":null,\"capabilities\":{}}}" as *u8)
439 t = gl_frame(tr, t, bodyb, bo)
440
441 bo = gv_cat(bodyb, 0, "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}" as *u8)
442 t = gl_frame(tr, t, bodyb, bo)
443
444 bo = gv_cat(bodyb, 0, "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"file:///tmp/nx_lsp_gate/main_ok.nx\",\"languageId\":\"nishi\",\"version\":1,\"text\":\"" as *u8)
445 bo = gl_jesc(bodyb, bo, ok_txt, ok_n)
446 bo = gv_cat(bodyb, bo, "\"}}}" as *u8)
447 t = gl_frame(tr, t, bodyb, bo)
448
449 bo = gv_cat(bodyb, 0, "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"file:///tmp/nx_lsp_gate/main_ok.nx\"},\"position\":{\"line\":2,\"character\":" as *u8)
450 bo = gv_catn(bodyb, bo, ok_col)
451 bo = gv_cat(bodyb, bo, "}}}" as *u8)
452 t = gl_frame(tr, t, bodyb, bo)
453
454 bo = gv_cat(bodyb, 0, "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"file:///tmp/nx_lsp_gate/main_ok.nx\"},\"position\":{\"line\":2,\"character\":" as *u8)
455 bo = gv_catn(bodyb, bo, ok_col)
456 bo = gv_cat(bodyb, bo, "}}}" as *u8)
457 t = gl_frame(tr, t, bodyb, bo)
458
459 bo = gv_cat(bodyb, 0, "{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"textDocument/completion\",\"params\":{}}" as *u8)
460 t = gl_frame(tr, t, bodyb, bo)
461
462 bo = gv_cat(bodyb, 0, "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"shutdown\",\"params\":null}" as *u8)
463 t = gl_frame(tr, t, bodyb, bo)
464
465 bo = gv_cat(bodyb, 0, "{\"jsonrpc\":\"2.0\",\"method\":\"exit\",\"params\":null}" as *u8)
466 t = gl_frame(tr, t, bodyb, bo)
467
468 gl_write_file(p_tr, tr, t)
469
470 argv3[1] = a_serve as i64
471 argv3[2] = 0
472 let srv_rc: i64 = gl_spawn(elf, argv3, p_tr, p_cap)
473 let sb: *u8 = gl_slurp(p_cap, lenp)
474 let sn: i64 = lenp[0]
475 let frames: i64 = gl_frames_valid(sb, sn)
476
477 gv_puts(" measured: transcript_bytes=" as *u8) gv_num(t)
478 gv_puts(" capture_bytes=" as *u8) gv_num(sn)
479 gv_puts(" frames=" as *u8) gv_num(frames)
480 gv_puts(" server_exit=" as *u8) gv_num(srv_rc)
481 gv_puts("\n" as *u8)
482
483 // BIND EVERY AGGREGATE ASSERTION TO ITS DENOMINATOR. Zero answered frames is not a pass.
484 gv_subjects("LSP frames the server actually emitted" as *u8, frames, ctr)
485
486 var frame_ok: i64 = 0
487 if frames > 0 { frame_ok = 1 }
488 gv_check("T6 every Content-Length in the capture is followed by exactly that many bytes of a complete JSON object, re-derived from the bytes rather than trusted" as *u8, frame_ok, ctr)
489
490 var init_ok: i64 = 0
491 if gl_find(sb, sn, 0, "\"textDocumentSync\":1" as *u8) >= 0 {
492 if gl_find(sb, sn, 0, "\"definitionProvider\":true" as *u8) >= 0 {
493 if gl_find(sb, sn, 0, "\"hoverProvider\":true" as *u8) >= 0 { init_ok = 1 } } }
494 gv_check("T7 initialize returns ServerCapabilities advertising exactly the three surfaces this server implements" as *u8, init_ok, ctr)
495
496 var no_lies: i64 = 1
497 if gl_find(sb, sn, 0, "completionProvider" as *u8) >= 0 { no_lies = 0 }
498 if gl_find(sb, sn, 0, "renameProvider" as *u8) >= 0 { no_lies = 0 }
499 if gl_find(sb, sn, 0, "documentSymbolProvider" as *u8) >= 0 { no_lies = 0 }
500 if gl_find(sb, sn, 0, "referencesProvider" as *u8) >= 0 { no_lies = 0 }
501 gv_check("neg-control-initialize-advertises-nothing-it-does-not-implement (a client greys out its own fallback on a false capability, so the user gets silence instead of a feature)" as *u8, no_lies, ctr)
502
503 var pub_ok: i64 = 0
504 if gl_find(sb, sn, 0, "\"method\":\"textDocument/publishDiagnostics\"" as *u8) >= 0 {
505 if gl_find(sb, sn, 0, "main_ok.nx" as *u8) >= 0 { pub_ok = 1 }
506 }
507 gv_check("T8 didOpen drove an unsolicited publishDiagnostics for the opened document -- the round trip this rung names" as *u8, pub_ok, ctr)
508
509 let pub_at: i64 = gl_find(sb, sn, 0, "\"diagnostics\":[" as *u8)
510 var pub_empty: i64 = 0
511 if pub_at >= 0 {
512 if sb[pub_at + GL_DIAGS_KEYLEN] == (GL_RBRACK as u8) { pub_empty = 1 }
513 }
514 gv_check("T9 the clean document that IMPORTS A SIBLING publishes an EMPTY diagnostic list -- proving the dirty-buffer shadow resolves the same import set a real build would" as *u8, pub_empty, ctr)
515
516 var def_ok: i64 = 0
517 let def_at: i64 = gl_find(sb, sn, 0, "\"id\":2" as *u8)
518 if def_at >= 0 {
519 if gl_find(sb, sn, def_at, "helper.nx" as *u8) >= 0 { def_ok = 1 }
520 }
521 gv_check("T10 goto-definition resolved ACROSS A FILE BOUNDARY to the declaring file, over the LineMap the compiler itself built" as *u8, def_ok, ctr)
522
523 var hov_ok: i64 = 0
524 let hov_at: i64 = gl_find(sb, sn, 0, "\"id\":3" as *u8)
525 if hov_at >= 0 {
526 if gl_find(sb, sn, hov_at, "helper_add" as *u8) >= 0 { hov_ok = 1 }
527 }
528 gv_check("T11 hover returned the declaration of the symbol under the cursor" as *u8, hov_ok, ctr)
529
530 var unknown_ok: i64 = 0
531 let unk_at: i64 = gl_find(sb, sn, 0, "\"id\":9" as *u8)
532 if unk_at >= 0 {
533 if gl_find(sb, sn, unk_at, "-32601" as *u8) >= 0 { unknown_ok = 1 }
534 }
535 gv_check("neg-control-an-unimplemented-request-is-answered-with-an-error-not-a-fabricated-result" as *u8, unknown_ok, ctr)
536
537 var shut_ok: i64 = 0
538 if srv_rc == 0 {
539 if gl_find(sb, sn, 0, "\"id\":4,\"result\":null" as *u8) >= 0 { shut_ok = 1 }
540 }
541 gv_check("T12 shutdown then exit leaves the process at LSP exit code 0 -- exit without a prior shutdown must be 1, so the code carries the handshake" as *u8, shut_ok, ctr)
542
543 var no_shadow_leak: i64 = 1
544 if gl_find(sb, sn, 0, ".nxlsp" as *u8) >= 0 { no_shadow_leak = 0 }
545 gv_check("neg-control-no-shadow-path-reaches-the-client (a Location under the dirty-buffer shadow would open a file the user does not have)" as *u8, no_shadow_leak, ctr)
546
547 // ---- THE SYMBOL INDEX ARTIFACT -------------------------------------------------------------
548 argv3[1] = a_syms as i64
549 argv3[2] = p_ok as i64
550 argv3[3] = 0
551 gl_spawn(elf, argv3, 0 as *u8, p_out)
552 let yb: *u8 = gl_slurp(p_out, lenp)
553 let yn: i64 = lenp[0]
554 let nsyms: i64 = gl_int_after(yb, yn, 0, "syms=" as *u8)
555 let cov: i64 = gl_int_after(yb, yn, 0, "coverage_complete=" as *u8)
556 let unmapped: i64 = gl_int_after(yb, yn, 0, "unmapped=" as *u8)
557 gv_puts(" measured: syms=" as *u8) gv_num(nsyms)
558 gv_puts(" unmapped=" as *u8) gv_num(unmapped)
559 gv_puts(" coverage_complete=" as *u8) gv_num(cov)
560 gv_puts("\n" as *u8)
561
562 var cov_ok: i64 = 0
563 if cov == 1 { if nsyms > 0 { cov_ok = 1 } }
564 gv_check("T13 the symbol index declares complete coverage AND a non-zero population -- a coverage flag over an empty index is the empty-set pass" as *u8, cov_ok, ctr)
565
566 var xfile_ok: i64 = 0
567 if gl_find(yb, yn, 0, "helper_add" as *u8) >= 0 {
568 if gl_find(yb, yn, 0, "helper.nx" as *u8) >= 0 { xfile_ok = 1 }
569 }
570 gv_check("T14 the index carries the imported file's declarations with THEIR OWN path and line, not the expanded unit's" as *u8, xfile_ok, ctr)
571
572 return gv_verdict("LSP-GATE" as *u8, ctr, "LN11 language server" as *u8)
573}