1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
// 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/);
// a borrow that outlives the block its owner was freed at the end of
text = 'include rt;\nstruct node { int v; node* next; };\nfn main() {\n\tnode* second;\n\t{\n\t\tnode* head = rt.new(node);\n\t\thead.next = rt.new(node);\n\t\tsecond = head.next;\n\t}\n\treturn second.v;\n}\n';
d = same(text, 'block end');
assert.match(d.message, /^second: borrows from head, which was freed at the end of its block \(line 9\)$/);
assert.deepStrictEqual(d.range.start, at(text, 'second.v'), 'the range is the conflicting use');
assert.ok(d.relatedInformation && d.relatedInformation.length === 2, JSON.stringify(d));
assert.strictEqual(d.relatedInformation[0].message, 'owner head was freed at the end of this block');
assert.strictEqual(d.relatedInformation[1].message, 'owner head is declared here');
assert.deepStrictEqual(d.relatedInformation[1].location.range.start, at(text, 'head ='));
console.log(`ok borrow diagnostics: ${checks} cases match zy, with ranges and related owner places`);
|