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
|
// definite storage findings use the compiler's parser and the editor protocol.
const assert = require('assert');
const cp = require('child_process');
const server = process.argv[2];
function check(text, needle) {
const uri = 'file:///storage-analysis.zc';
const messages = [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
{ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } }];
if (needle) {
const at = text.lastIndexOf(needle), before = text.slice(0, at);
assert.ok(at >= 0);
messages.push({ jsonrpc: '2.0', id: 2, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } } });
}
messages.push({ jsonrpc: '2.0', method: 'exit' });
const input = Buffer.concat(messages.map(m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b]); }));
const r = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000, maxBuffer: 8 << 20 });
assert.strictEqual(r.status, 0, String(r.stderr));
const replies = []; let data = r.stdout;
while (data.length) {
const end = data.indexOf('\r\n\r\n'); assert.ok(end >= 0);
const n = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
replies.push(JSON.parse(data.subarray(end + 4, end + 4 + n)));
data = data.subarray(end + 4 + n);
}
const diagnostics = replies.find(m => m.method === 'textDocument/publishDiagnostics').params.diagnostics;
return { diagnostics, warnings: diagnostics.filter(d => d.severity === 2), hover: replies.find(m => m.id === 2)?.result?.contents?.value };
}
const prefix = 'unchecked fn main() {\n';
for (const [type, length] of [['int', 3], ['u8', 3], ['fixed int[?]', 3], ['u8[?]', 3]]) {
const r = check(prefix + ` let p = new(${type}, ${length});\n let alias = p;\n return alias[${length}];\n}\n`, 'alias[');
assert.strictEqual(r.warnings.length, 1, JSON.stringify(r.diagnostics));
assert.match(r.warnings[0].message, /out-of-bounds.*index 3.*length 3/);
assert.match(r.hover, /size: 3 elements.*lifetime: heap storage; live here/);
assert.strictEqual(r.warnings[0].range.start.line, 3);
}
const negative = check(prefix + ' let p = new(3); return p[-1];\n}\n');
assert.match(negative.warnings[0]?.message || '', /index -1.*length 3/);
const freed = check(prefix + ' let p = new(int, 3);\n let q = p;\n free(p);\n return q[0];\n}\n', 'q[');
assert.strictEqual(freed.warnings.length, 1, JSON.stringify(freed.diagnostics));
assert.match(freed.warnings[0].message, /use after free.*line 4/);
assert.match(freed.hover, /size: 3 elements.*lifetime: ended on line 4/);
const dereference = check(prefix + ' let p = new(int); free(p); return *p;\n}\n');
assert.match(dereference.warnings[0]?.message || '', /use after free/);
const field = check('struct item { int value; };\n' + prefix + ' let p = new(item); free(p); return p.value;\n}\n');
assert.match(field.warnings[0]?.message || '', /use after free/);
const scope = check(prefix + ' int* p;\n { let a[3]; p = a; }\n return p[0];\n}\n', 'p[');
assert.match(scope.warnings[0]?.message || '', /use after free.*line 3/);
assert.match(scope.hover, /lifetime: ended on line 3/);
const safe = [
prefix + ' let p = new(int, 3); let q = p as u8*; return q[23]; }',
prefix + ' let p = new(int, 3); let q = (u8*)p; return q[23]; }',
prefix + ' int n = 2; let p = &n; let q = n; return q[2]; }',
prefix + ' let p = new(int, 3); let ignored = 0 && free(p); return p[0]; }',
prefix + ' let p = new(int, 3); let ignored = 1 || free(p); return p[0]; }',
prefix + ' let p = new(int, 50); return p[(u8)300]; }',
prefix + ' let p = new(int, 50); return p[300 as u8]; }',
prefix + ' let p = new(int, 3); let q = p; q[2] = 7; free(p); return 0; }',
prefix + ' let p = new(3); free(p); p = new(3); return p[0]; }',
'unchecked fn unknown(int* p) {}\n' + prefix + ' let p = new(int, 3); unknown(p); return p[9]; }',
'fn maybe(int n) { return n; }\n' + prefix + ' let p = new(int, 3); return p[maybe(9)]; }',
prefix + ' let p = new(int, 3); if (1) free(p); return p[0]; }',
prefix + ' let p = new(int, 3); while (0) free(p); return p[0]; }',
prefix + ' let p = new(int, 3); let f = fn() { free(p); return 0; }; return p[0]; }',
prefix + ' let p = new(int[?], 0); push(p, 9); return p[0]; }',
];
for (const source of safe) assert.deepStrictEqual(check(source).warnings, [], source);
const preserved = check('int read(int* p) { return p[0]; }\n' + prefix + ' let p = new(int, 3); read(p); return p[9]; }');
assert.match(preserved.warnings[0]?.message || '', /index 9.*length 3/);
const unknown = check('int read(int* p) { return p[0]; }\nfn main() { int n = 1; return read(&n); }\n', 'p[');
assert.match(unknown.hover, /size: unknown.*lifetime: unknown/);
const stack = check('fn main() { let a[3]; return a[2]; }\n', 'a[');
assert.match(stack.hover, /size: 3 elements.*lifetime: stack storage/);
const replacement = check('fn main() {\n let owner = new(1), replacement = new(2);\n free(owner); owner = replacement;\n return owner[0];\n}\n', 'replacement;');
assert.ok(!replacement.diagnostics.some(d => d.severity === 1), JSON.stringify(replacement.diagnostics));
assert.match(replacement.hover, /moved: replaces a released owner in `owner` on line 3/);
console.log('ok definite bounds, free and scope lifetimes, alias hovers, and conservative unknown cases');
|