// 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');