From e1e0a265e931637f452ac05bb7d912e07bcee574 Mon Sep 17 00:00:00 2001 From: zinc Date: Thu, 15 Oct 2026 10:11:56 +0330 Subject: lsp: borrow errors ranged on the use, with related information for where the owner ended and was declared; vscode client passes relatedInformation through --- tests/drivers/borrow_diag_test.js | 73 +++++++++++++++++++++++++++++++++++++++ tests/drivers/ext_cache_test.js | 13 +++++++ 2 files changed, 86 insertions(+) create mode 100644 tests/drivers/borrow_diag_test.js (limited to 'tests/drivers') diff --git a/tests/drivers/borrow_diag_test.js b/tests/drivers/borrow_diag_test.js new file mode 100644 index 0000000..93a7561 --- /dev/null +++ b/tests/drivers/borrow_diag_test.js @@ -0,0 +1,73 @@ +// borrow diagnostics in the editor: the server publishes the compiler's own messages +// (the same check `zy` runs), with the range of the conflicting use and related +// information for where the owner ended and where it was declared. +// usage: node tests/drivers/borrow_diag_test.js path/to/zclsp [server arguments] +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; +const assert = require('assert'); +const cp = require('child_process'); +const path = require('path'); +const uri = 'file://' + path.resolve('tests/drivers/borrow-diag-fixture.zc'); +const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); }; +function diagnose(text) { + const msgs = [ + { id: 1, method: 'initialize', params: {} }, + { method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } }, + { id: 2, method: 'shutdown' }, + { method: 'exit' } + ].map(m => ({ jsonrpc: '2.0', ...m })); + const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 30000 * TIMEOUT_SCALE, maxBuffer: 32 << 20 }); + assert.strictEqual(r.status, 0, String(r.stderr)); + let b = r.stdout, last = null; + while (b.length) { + const e = b.indexOf('\r\n\r\n'); assert.ok(e >= 0); + const n = Number(/Content-Length:\s*(\d+)/i.exec(b.subarray(0, e).toString())[1]); + const m = JSON.parse(b.subarray(e + 4, e + 4 + n)); b = b.subarray(e + 4 + n); + if (m.method === 'textDocument/publishDiagnostics') last = m.params.diagnostics; + } + assert.ok(last, 'no diagnostics published'); + return last; +} +function zy(text) { + const zyBin = process.env.ZY || path.join(path.dirname(process.argv[2]), 'zy'); + const r = cp.spawnSync(zyBin, [], { input: text, timeout: 30000 * TIMEOUT_SCALE }); + const line = String(r.stdout).split('\n')[0]; + return line.replace(/^zc: line \d+: /, ''); +} +const at = (text, needle, nth = 0) => { let o = -1; for (let k = 0; k <= nth; k++) o = text.indexOf(needle, o + 1); const before = text.slice(0, o); return { line: before.split('\n').length - 1, character: o - before.lastIndexOf('\n') - 1 }; }; +let checks = 0; +function same(text, label) { + const d = diagnose(text).filter(x => x.severity === 1); + assert.ok(d.length >= 1, label + ': no error'); + const fromZy = zy(text); + assert.ok(fromZy && d.some(x => x.message === fromZy), `${label}: editor and zy disagree: [${d.map(x => x.message)}] vs [${fromZy}]`); + checks++; + return d.find(x => x.message === fromZy); +} +// a borrow used after its owner is freed +let text = 'include rt;\nfn main() {\n\tchar* t = rt.new(4); t[0] = 65;\n\tchar* r = t + 1;\n\trt.free(t);\n\treturn r[0];\n}\n'; +let d = same(text, 'freed owner'); +assert.match(d.message, /r borrows t, and t was freed/); +assert.deepStrictEqual(d.range.start, at(text, 'r[0]'), 'the range is the conflicting use'); +assert.ok(d.relatedInformation && d.relatedInformation.length === 2, JSON.stringify(d)); +assert.strictEqual(d.relatedInformation[0].message, 'owner t was freed here'); +assert.deepStrictEqual(d.relatedInformation[0].location.range.start, at(text, 't);')); +assert.strictEqual(d.relatedInformation[0].location.uri, uri); +assert.strictEqual(d.relatedInformation[1].message, 'owner t is declared here'); +assert.deepStrictEqual(d.relatedInformation[1].location.range.start, at(text, 't =')); +// the owner itself, given to a .d parameter +text = 'include rt;\nfn take(.d p) { return p[0]; }\nfn main() { let x = rt.new(3); take(x); return x[0]; }\n'; +d = same(text, '.d parameter'); +assert.match(d.message, /x was given to take's \.d parameter/); +assert.strictEqual(d.relatedInformation[0].message, 'owner x was given to a .d parameter here'); +assert.deepStrictEqual(d.relatedInformation[0].location.range.start, at(text, 'x)')); +// a function called before its body, reported at the end of the unit at the use +text = 'include rt;\nfn main() {\n\tchar* t = rt.new(8); t[0] = 32; t[1] = 0;\n\tlet r = skip(t); rt.free(t); rt.put_c(r[0]);\n\treturn 0;\n}\nchar* skip(char* s) { return s + 1; }\n'; +d = same(text, 'forward call'); +assert.match(d.message, /skip, defined after this call, returns a borrow/); +assert.deepStrictEqual(d.range.start, at(text, 'r[0]')); +assert.strictEqual(d.relatedInformation[0].message, 'owner t was freed here'); +// a borrows clause checked against its body +text = 'include rt;\nchar* first(char* a, char* b) borrows a;\nchar* first(char* a, char* b) { return b; }\nfn main() { return 0; }\n'; +d = same(text, 'borrows clause'); +assert.match(d.message, /returns a borrow of b/); +console.log(`ok borrow diagnostics: ${checks} cases match zy, with ranges and related owner places`); diff --git a/tests/drivers/ext_cache_test.js b/tests/drivers/ext_cache_test.js index 93caf94..9bd3e3a 100644 --- a/tests/drivers/ext_cache_test.js +++ b/tests/drivers/ext_cache_test.js @@ -31,6 +31,8 @@ const stub = { EventEmitter: class { constructor() { this.event = () => ({ dispose() {} }); } fire() {} }, Diagnostic: class { constructor(range, message, severity) { Object.assign(this, { range, message, severity }); } }, Range: class { constructor(a, b, c, d) { Object.assign(this, { start: { line: a, character: b }, end: { line: c, character: d } }); } }, + Location: class { constructor(uri, range) { Object.assign(this, { uri, range }); } }, + DiagnosticRelatedInformation: class { constructor(location, message) { Object.assign(this, { location, message }); } }, DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 }, SemanticTokensLegend: class { constructor(t, m) { this.tokenTypes = t; this.tokenModifiers = m; } }, SemanticTokensBuilder: class { constructor() {} push() {} build() { return null; } }, @@ -177,6 +179,17 @@ const mainUri = 'file://' + main; const expiredList = await world.completion.provideCompletionItems(expiredDoc, { line: 3, character: 1 }, { isCancellationRequested: false }); const expiredItem = expiredList && expiredList.items.find(c => c.label === 'used'); check('completion maps consumed lifetimes to the editor strikethrough tag', expiredItem && expiredItem.tags?.[0] === stub.CompletionItemTag.Deprecated && /lifetime ended/.test(expiredItem.documentation?.value)); + // a borrow error keeps the server's related information: where the owner ended and where it was declared + const borrowPath = path.join(root, 'borrow.zc'); + const borrowDoc = doc(borrowPath, 'include rt;\nfn main() {\n\tchar* t = rt.new(4); t[0] = 65;\n\tchar* r = t + 1;\n\trt.free(t);\n\treturn r[0];\n}\n'); + fs.writeFileSync(borrowPath, borrowDoc.text); + world.textDocuments.push(borrowDoc); + if (world.listeners.onDidOpenTextDocument) world.listeners.onDidOpenTextDocument(borrowDoc); + await until(() => world.sets.some(s => s.uri === 'file://' + borrowPath && s.list.length)); + const borrowSet = world.sets.filter(s => s.uri === 'file://' + borrowPath).pop(); + const borrowDiag = borrowSet && borrowSet.list.find(d => /r borrows t/.test(d.message)); + const related = borrowDiag && borrowDiag.relatedInformation; + check('borrow diagnostics pass related information through to the editor', related && related.length === 2 && related[0] instanceof stub.DiagnosticRelatedInformation && related[0].message === 'owner t was freed here' && related[0].location.range.start.line === 4 && related[1].message === 'owner t is declared here', JSON.stringify(borrowSet && borrowSet.list)); const actionDoc = doc(path.join(root, 'imports.zc'), 'fn main() { rt.put_n(7); }'); const actions = await world.actions.provideCodeActions(actionDoc, new stub.Range(0, 15, 0, 15), {}, { isCancellationRequested: false }); check('quick fixes reach the editor as workspace edits', actions.some(a => a.kind === 'quickfix' && a.edit.edits.some(e => e.newText === 'include rt;\n'))); -- cgit v1.2.3 From c9e4c809bfd90f226363df1ffa7cb57ce3dfbdee Mon Sep 17 00:00:00 2001 From: zinc Date: Thu, 15 Oct 2026 10:12:55 +0330 Subject: mkgenome: on a header mismatch name FTOP/ZFP, print the image's and the map's values, and point at a stale src/runtime/rt.zc or map --- src/tools/mkgenome.c | 30 ++++++++++++++++++++++++------ tests/drivers/tools_contract_test.py | 5 ++++- 2 files changed, 28 insertions(+), 7 deletions(-) (limited to 'tests/drivers') diff --git a/src/tools/mkgenome.c b/src/tools/mkgenome.c index 9720760..051de62 100644 --- a/src/tools/mkgenome.c +++ b/src/tools/mkgenome.c @@ -40,15 +40,33 @@ int main(int argc, char **argv) } } if (ferror(stdin)) { fputs("mkgenome: read failed\n", stderr); return 1; } - int ok = n >= 8 * 7 && n % 8 == 0; + int ok = n >= 8 * 7 && n % 8 == 0, bad[3] = {0, 0, 0}; + uint64_t got3[3] = {0, 0, 0}, want[3] = {(uint64_t)FTOP * 2 + 1, (uint64_t)ZFP * 2 + 1, 4}; for (int i = 0; ok && i < 3; i++) { - uint64_t u = 0; - for (int k = 0; k < 8; k++) u |= (uint64_t)b[i * 8 + k] << (k * 8); - if (u != (i == 0 ? (uint64_t)FTOP * 2 + 1 : i == 1 ? (uint64_t)ZFP * 2 + 1 : 4)) ok = 0; + for (int k = 0; k < 8; k++) got3[i] |= (uint64_t)b[i * 8 + k] << (k * 8); + if (got3[i] != want[i]) bad[i] = 1; } + int text = n >= 4 && !memcmp(b, "zc: ", 4); + if (ok && (bad[0] || bad[1] || bad[2])) ok = 0; if (!ok) { - if (n >= 4 && !memcmp(b, "zc: ", 4)) fwrite(b, 1, n, stderr); - fprintf(stderr, n % 8 || n < 8 * 7 ? "mkgenome: incomplete genome\n" : "mkgenome: input is not a genome for this map\n"); + if (text) fwrite(b, 1, n, stderr); + if (n % 8 || n < 8 * 7) { fputs("mkgenome: incomplete genome\n", stderr); return 1; } + fputs("mkgenome: input is not a genome for this map\n", stderr); + if (!text) { + /* the header: FTOP and ZFP as literal words (2v+1), then 4. say which differ + and by what, since a mismatch usually means rt.zc and the map disagree */ + static const char *names[3] = {"FTOP (frame stack top)", "ZFP (zero page)", "the header's third word"}; + for (int i = 0; i < 3; i++) { + if (!bad[i]) continue; + if (i < 2 && (got3[i] & 1)) fprintf(stderr, " word %d, %s: image has 0x%llX, the map has 0x%llX\n", i, names[i], (unsigned long long)(got3[i] >> 1), (unsigned long long)(want[i] >> 1)); + else if (i < 2) fprintf(stderr, " word %d, %s: image has the raw word 0x%llX, not a literal; the map has 0x%llX\n", i, names[i], (unsigned long long)got3[i], (unsigned long long)(want[i] >> 1)); + else fprintf(stderr, " word 2, %s: image has 0x%llX, expected 4\n", names[i], (unsigned long long)got3[i]); + } + if (bad[0] || bad[1]) + fputs(" the compiler read a different layout than the one in build/core/map: check that FTOP and ZFP in\n" + " src/runtime/rt.zc match the map (a stale src/runtime/rt.zc, e.g. a zip unpacked over an older\n" + " checkout, or a stale build/core/map: run make clean, then make)\n", stderr); + } return 1; } if (fwrite(b, 1, n, stdout) != n || fflush(stdout)) { fputs("mkgenome: write failed\n", stderr); return 1; } diff --git a/tests/drivers/tools_contract_test.py b/tests/drivers/tools_contract_test.py index 6a98678..9323a47 100644 --- a/tests/drivers/tools_contract_test.py +++ b/tests/drivers/tools_contract_test.py @@ -28,9 +28,12 @@ if args.group == 'image': changed = words[:]; changed[index] ^= 2 r = command(['build/bin/mkgenome'], pack(changed)) assert r.returncode != 0 and b'not a genome' in r.stderr and not r.stdout + # the header words are named, with both values, and a layout mismatch points at rt.zc and the map + assert [b'FTOP', b'ZFP', b'third word'][index] in r.stderr, r.stderr + if index < 2: assert b'the map has 0x' in r.stderr and b'src/runtime/rt.zc' in r.stderr and b'make clean' in r.stderr, r.stderr r = command(['build/bin/mkgenome'], b'zc: line 1: something is wrong\nzc: 1 error\n') assert r.returncode != 0 and b'something is wrong' in r.stderr and not r.stdout - print('ok mkgenome roundtrip, truncation, alignment, map-header mutation and diagnostic contracts') + print('ok mkgenome roundtrip, truncation, alignment, map-header mutation (naming FTOP, ZFP and both values) and diagnostic contracts') elif args.group == 'map': for preset in ['default', 'small', 'run', 'lsp', 'fixed']: with tempfile.TemporaryDirectory(prefix='tinyvm preset ') as directory: -- cgit v1.2.3