// pending requirements have explicit protocol oracles, selectable in small groups. const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); const path = require('path'); const os = require('os'); const host = process.argv[2], group = process.env.CONTRACT_GROUP; const frame = m => { const b = Buffer.from(JSON.stringify({ jsonrpc: '2.0', ...m })); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); }; function run(text, requests = [], uri = 'file:///contracts.zc', files = []) { const messages = [{ id: 1, method: 'initialize', params: {} }, ...files.map(([file, content]) => ({ method: 'zc/file', params: { path: file, text: content } })), { method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } }, ...requests.map((r, i) => { const at = text.indexOf(r.needle) + (r.offset || 0); assert.ok(at >= 0, r.needle); const before = text.slice(0, at); return { id: i + 2, method: r.method || 'textDocument/hover', params: { textDocument: { uri }, ...(r.method === 'textDocument/codeAction' ? {} : { position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } }), ...(r.params || {}) } }; }), { method: 'exit' }]; let output; if (host === '--wasm' || host === '--vm') { const wasm = host === '--wasm'; const bridge = require(path.resolve('editors/vscode/client', wasm ? 'wasmproc.js' : 'vmproc.js')); const proc = (wasm ? bridge.wasmProcess : bridge.vmProcess)(path.resolve(process.argv[3]), { log: () => {} }); const chunks = []; proc.stdout.on('data', b => chunks.push(b)); for (const message of messages) proc.stdin.write(frame(message)); output = Buffer.concat(chunks); } else { const r = cp.spawnSync(host, process.argv.slice(3), { input: Buffer.concat(messages.map(frame)), timeout: 60000, maxBuffer: 32 << 20 }); assert.strictEqual(r.status, 0, String(r.stderr)); output = r.stdout; } const replies = []; let data = output; 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]); assert.ok(data.length >= end + 4 + n); replies.push(JSON.parse(data.subarray(end + 4, end + 4 + n))); data = data.subarray(end + 4 + n); } const ds = replies.filter(r => r.method === 'textDocument/publishDiagnostics').at(-1)?.params.diagnostics || []; return { ds, result: id => replies.find(r => r.id === id)?.result }; } let failures = 0, checks = 0; function test(g, name, f) { if (group && group !== g) return; checks++; try { f(); console.log('ok ' + g + ': ' + name); } catch (e) { failures++; console.error('FAIL ' + g + ': ' + name + '\n' + e.message); } } for (const [uri, prefix] of [['file:///contracts.zc', ''], ['tinyvm:/fixture.zc', '']]) { test('hovers', 'word-string global at declaration and use ' + uri, () => { const text = prefix + 'let hello = "hi"; fn main() { return hello[0]; }'; const r = run(text, [{ needle: 'hello =' }, { needle: 'hello[0]' }], uri); for (const id of [2, 3]) { const md = r.result(id)?.contents.value || ''; assert.match(md, /\(char\*\) let hello \/\/ global;/); assert.ok(!md.includes('; type ')); } }); test('hovers', 'fixed global array at declaration and use ' + uri, () => { const r = run('let prog[20000]; fn main() { return prog[0]; }', [{ needle: 'prog[20000]' }, { needle: 'prog[0]' }], uri); for (const id of [2, 3]) assert.match(r.result(id)?.contents.value || '', /\(word\[20000\]\) let prog \/\/ global;/); }); } test('strings', 'packed declaration, parameter, return and uses keep bstr', () => { const r = run('fn echo(s) { return s; } fn main() { let hello = b"hi"; let other = echo(hello); return other[0]; }', [{ needle: 'hello =' }, { needle: 'hello);' }, { needle: 'other[0]' }, { needle: 'echo(hello)' }, { needle: 'return s;', offset: 7 }]); assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds)); for (const id of [2, 3, 4, 6]) { const md = r.result(id)?.contents.value || ''; assert.match(md, /\(bstr\) let/); assert.ok(!/\(word\)/.test(md)); } assert.match(r.result(5)?.contents.value || '', /bstr echo\(bstr s\)/); }); test('strings', 'grouped word-string local stays char*', () => { const r = run('fn main() { let n = 1, text = "hi"; return text[0] + n; }', [{ needle: 'text[0]' }]); assert.match(r.result(2)?.contents.value || '', /\(char\*\) let text/); }); test('strings', 'inferred packed return is shown in hover', () => { const text = 'fn text() { return b"hi"; } fn main() { let result = text(); return result[0]; }'; const r = run(text, [{ needle: 'text()' }, { needle: 'result = ' }, { needle: 'result[0]' }]); assert.match(r.result(2)?.contents.value || '', /bstr text\(\)/); for (const id of [3, 4]) assert.match(r.result(id)?.contents.value || '', /\(bstr\) let result/); }); test('strings', 'inferred word-string return is shown in hover', () => { const text = 'fn text() { return "hi"; } fn main() { let result = text(); return result[0]; }'; const r = run(text, [{ needle: 'text()' }, { needle: 'result = ' }, { needle: 'result[0]' }]); assert.match(r.result(2)?.contents.value || '', /char\* text\(\)/); for (const id of [3, 4]) assert.match(r.result(id)?.contents.value || '', /\(char\*\) let result/); }); for (const [name, src, expected, severity = 3] of [ ['unused parameter', 'fn ignore(int unused) { return 0; }', true], ['used parameter', 'fn use(int used) { return used; }', false], ['explicit discard parameter', 'fn ignore(int _) { return 0; }', false], ['duplicate discard parameters', 'fn ignore(int _, int _) { return 0; }', false], ['write-only parameter', 'fn write(int unused) { unused = 2; return 0; }', true], ['shadowed parameter', 'fn shadow(int unused) { { int unused = 2; return unused; } }', true], ['unused method argument', 'struct point { fn f(int unused) { return 0; } }', true], ['unused closure argument', 'fn main() { let f = fn:int(int unused) { return 0; }; return f(2); }', true], ['unused generic argument', 'T first(T used, T unused) { return used; } fn main() { return first(1, 2); }', true], ['on directive', 'hints on; fn ignore(int unused) { return 0; }', true, 3], ['small directive', 'hints small; fn ignore(int unused) { return 0; }', true, 4], ['off directive', 'hints off; fn ignore(int unused) { return 0; }', false] ]) test('arguments', name, () => { const r = run(src + '\nfn marker() { return 0; }'); const hints = r.ds.filter(d => /unused.*(argument|parameter)|(?:argument|parameter).*unused/.test(d.message)); assert.strictEqual(hints.length > 0, expected, JSON.stringify(r.ds)); if (expected) { assert.strictEqual(hints[0].severity, severity); assert.strictEqual(src.split('\n')[hints[0].range.start.line].slice(hints[0].range.start.character, hints[0].range.end.character), 'unused'); } }); for (const [name, src, warning] of [ ['discard read warns', 'fn f(int _) { return _; }', true], ['discard assignment is not a read', 'fn f(int _) { _ = 2; return 0; }', false], ['duplicate payload discards', 'include rt; enum pair { item { int x; int y; }, other }; fn main() { let p = pair.item(1, 2); switch(p) { case item(_, _): return 0; case _: return 1; } }', false], ['bound payload discard read', 'include rt; enum pair { item { int x; int y; }, other }; fn main() { let p = pair.item(1, 2); switch(p) { case item(_, _): return _; case _: return 1; } }', true], ['numeric catch-all', 'fn f(int n) { switch(n) { case 1: return 1; case _: return 2; } }', false] ]) test('discards', name, () => { const r = run(src); assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds)); const ds = r.ds.filter(d => /discard.*read|read.*discard/.test(d.message)); assert.strictEqual(ds.length > 0, warning, JSON.stringify(r.ds)); for (const d of ds) { assert.strictEqual(d.severity, 2); assert.strictEqual(src.slice(d.range.start.character, d.range.end.character), '_'); } }); for (const [name, src, count, severity] of [ ['uncommented literal', 'fn f(int n) { return n + 0x40ff; }', 1, 3], ['commented literal', 'fn f(int n) { return n + /* offset in memory */ 0x40ff; }', 0, 3], ['empty comment escape', 'fn f(int n) { return n + /**/ 0x40ff; }', 0, 3], ['nearby comment does not hide it', 'fn f(int n) { return /* explanation of n */ n + 0x40ff; }', 1, 3], ['compact dots', 'hints small; fn f(int n) { return n + 0x40ff; }', 1, 4], ['off directive', 'hints off; fn f(int n) { return n + 0x40ff; }', 0, 3] ]) test('magic', name, () => { const ds = run(src).ds.filter(d => /magic|numeric literal|number .*has no name/i.test(d.message)); assert.strictEqual(ds.length, count, JSON.stringify(ds)); if (count) { assert.strictEqual(ds[0].severity, severity); assert.match(ds[0].message, /inline block comment/); assert.ok(!ds[0].message.includes('a const says what it means')); } }); test('navigation', 's_to_n points to its runtime declaration', () => { const r = run('include rt; fn main() { return rt.s_to_n(b"42"); }', [{ method: 'textDocument/definition', needle: 's_to_n' }]); const loc = r.result(2)?.[0]; assert.ok(loc, 'missing definition'); assert.strictEqual(loc.uri, 'tinyvm:/rt.zc'); const line = fs.readFileSync('src/runtime/rt.zc', 'utf8').split('\n')[loc.range.start.line]; assert.strictEqual(line.slice(loc.range.start.character, loc.range.end.character), 's_to_n'); }); for (const [module, file] of [['core', 'core.zc'], ['rt', 'rt.zc']]) test('navigation', 'qualified duplicate names open the correct module: ' + module, () => { const source = 'include rt, core; fn main() { return ' + module + '.syscall(9,0,0,0); }'; const r = run(source, [{ needle: 'syscall(', method: 'textDocument/definition' }, { needle: 'syscall(' }]); const location = r.result(2)?.[0]; assert.strictEqual(location?.uri, 'tinyvm:/' + file); const sourcePath = module === 'core' ? 'src/core/core.zc' : 'src/runtime/rt.zc'; const line = fs.readFileSync(sourcePath, 'utf8').split('\n')[location.range.start.line]; assert.strictEqual(line.slice(location.range.start.character, location.range.end.character), 'syscall'); if (module === 'core') assert.match(r.result(3)?.contents.value || '', /int syscall\(int operation/); }); test('navigation', 'qualified references retain their resolved module identity', () => { const source = 'include rt, core; fn main() { core.syscall(9,0,0,0); rt.syscall(9,0,0,0); return core.syscall(9,0,0,0); }'; const r = run(source, [{ needle: 'core.syscall', offset: 5, method: 'textDocument/references', params: { context: { includeDeclaration: false } } }]); const references = r.result(2) || []; assert.strictEqual(references.length, 2); for (const location of references) assert.ok(source.slice(location.range.start.character, location.range.end.character).includes('syscall')); }); test('modules', 'qualified completion exports only imported module', () => { const r = run('include rt; fn main() { rt.put_', [{ method: 'textDocument/completion', needle: 'rt.put_', offset: 7 }]); const items = r.result(2)?.items || r.result(2) || []; assert.ok(items.some(i => i.label === 'put_s')); assert.ok(!items.some(i => i.label === 'evals')); }); test('modules', 'lsp exports require an explicit include and qualified name', () => { const accepted = run('include lsp; fn main() { return lsp.hid(97); }'); assert.ok(!accepted.ds.some(d => d.severity === 1), JSON.stringify(accepted.ds)); const missing = run('fn main() { return lsp.hid(97); }'); assert.ok(missing.ds.some(d => d.severity === 1 && /include lsp/.test(d.message)), JSON.stringify(missing.ds)); const bare = run('include lsp; fn main() { return hid(97); }'); assert.ok(bare.ds.some(d => d.severity === 1), JSON.stringify(bare.ds)); }); test('modules', 'missing include reports error at qualified member', () => { const r = run('fn main() { rt.put_s("hi"); return 0; }'); assert.ok(r.ds.some(d => d.severity === 1 && /include rt/.test(d.message)), JSON.stringify(r.ds)); }); for (const [name, description] of [['rt', /runtime/], ['run', /resident compiler/], ['core', /vm host/], ['lsp', /language server/], ['std', /standard library/], ['icon', /syntax transformations/]]) { test('modulehover', 'module documentation in comma-separated include: ' + name, () => { const text = 'include rt, run, core, lsp, std, icon; fn main() { return 0; }'; const r = run(text, [{ needle: name + (name === 'icon' ? ';' : ',') }]); const hover = r.result(2)?.contents; assert.strictEqual(hover?.kind, 'markdown'); assert.match(hover.value, new RegExp('```zc\\ninclude ' + name + ';\\n```')); assert.match(hover.value, description); assert.match(hover.value, new RegExp('`' + name + '\\.[^`]+`')); }); } test('modulehover', 'a local with the same name retains its variable hover', () => { const r = run('fn main() { int rt = 3; return rt; }', [{ needle: 'rt;' }]); assert.match(r.result(2)?.contents.value || '', /\(int\) let rt/); assert.ok(!r.result(2).contents.value.includes('include rt;')); }); test('modulehover', 'comments and newlines preserve the include list', () => { const r = run('include /* runtime */ rt,\n /* compiler */ run; fn main() { return 0; }', [{ needle: 'run;' }]); assert.match(r.result(2)?.contents.value || '', /include run;[\s\S]*resident compiler/); }); for (const [name, member] of [['rt', 'put_s'], ['run', 'evals'], ['core', 'syscall'], ['lsp', 'hid'], ['std', 'array_map'], ['icon', 'find_replace']]) { test('namespacecompletion', 'qualified members, signatures and documentation: ' + name, () => { const r = run('include rt, run, core, lsp, std, icon; fn main() { ' + name + '.', [{ needle: name + '.', offset: name.length + 1, method: 'textDocument/completion' }]); const items = r.result(2)?.items || []; const item = items.find(i => i.label === member); assert.ok(item, JSON.stringify(items.map(i => i.label))); assert.ok(item.detail && item.documentation?.value, JSON.stringify(item)); assert.ok(!items.some(i => i.label === 'main')); assert.strictEqual(new Set(items.map(i => i.label)).size, items.length); }); } function applyWorkspaceEdit(text, edit, uri) { const edits = edit.documentChanges?.flatMap(c => c.textDocument.uri === uri ? c.edits : []) || edit.changes?.[uri] || []; const offset = p => text.split('\n').slice(0, p.line).reduce((n, l) => n + l.length + 1, 0) + p.character; for (const e of edits.sort((a, b) => offset(b.range.start) - offset(a.range.start))) text = text.slice(0, offset(e.range.start)) + e.newText + text.slice(offset(e.range.end)); return text; } for (const [name, source, needle] of [ ['qualified runtime import', 'fn main() { rt.put_n(7); }', 'put_n'], ['qualified compiler import', 'fn main() { run.evals("return 0;"); }', 'evals'], ['qualified host import', 'fn main() { return core.syscall(9,0,0,0); }', 'syscall'], ['bare runtime call needs qualification too', 'fn main() { put_n(7); }', 'put_n'], ['imported module still needs a qualified name', 'include rt; fn main() { put_n(7); }', 'put_n'], ['namespace and header are preserved', '// file overview\nnamespace demo;\n\n// prints the result\nfn main() { rt.put_n(7); }', 'put_n'], ['crlf stays crlf', 'namespace demo;\r\nfn main() { rt.put_n(7); }\r\n', 'put_n'], ]) test('quickfix', name, () => { const uri = 'file:///contracts.zc', before = source.slice(0, source.indexOf(needle)); const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 }; const r = run(source, [{ needle, method: 'textDocument/codeAction', params: { range: { start: position, end: position }, context: { diagnostics: [] } } }], uri); assert.ok(r.result(1)?.capabilities.codeActionProvider); const action = r.result(2)?.find(a => a.kind === 'quickfix' && a.edit); assert.ok(action, JSON.stringify(r.result(2))); const changed = applyWorkspaceEdit(source, action.edit, uri); assert.notStrictEqual(changed, source); if (source.includes('\r\n')) assert.ok(!/(? d.severity === 1), JSON.stringify({ changed, ds: checked.ds })); }); for (const source of ['include rt; fn main() { rt.put_n(7); }', 'fn main() { int put_n = 7; return put_n; }', 'fn main() { return unknown_name; }']) test('quickfix', 'no unrelated import edits: ' + source, () => { const needle = source.includes('put_n') ? 'put_n' : 'unknown_name'; const r = run(source, [{ needle, method: 'textDocument/codeAction' }]); assert.deepStrictEqual(r.result(2), []); }); test('qualifiers', 'fixed const array hover preserves both qualifiers and shape', () => { const r = run('fn main() { fixed const int[2] data = {1,2}; return data[0]; }', [{ needle: 'data[0]' }]); assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds)); assert.match(r.result(2)?.contents.value || '', /\(const int\[2\]\) let data/); }); test('qualifiers', 'const fixed global hover preserves its type', () => { const r = run('const fixed int[2] data = {1,2}; fn main() { return data[0]; }', [{ needle: 'data[0]' }]); assert.match(r.result(2)?.contents.value || '', /\(const int\[2\]\) let data \/\/ global;/); }); test('qualifiers', 'readonly qualifiers reject aliasing without optional type checking', () => { const r = run('typecheck off; fn mutate(int* p) { p[0] = 9; } fn main() { fixed const int[2] data = {1,2}; mutate(data); }'); assert.ok(r.ds.some(d => d.severity === 1 && /const storage cannot be converted/.test(d.message)), JSON.stringify(r.ds)); }); test('inference', 'typed callee constrains caller arguments and returned values', () => { const source = 'int seed(int cap) { return cap + 1; } fn squares(n) { let l = seed(n); return l; } fn main() { return squares(3); }'; const r = run(source, [{ needle: 'squares(n)' }, { needle: 'seed(n)', offset: 5 }, { needle: 'return l;', offset: 7 }]); assert.match(r.result(2)?.contents.value || '', /int squares\(int n\)/); assert.match(r.result(3)?.contents.value || '', /\(int\) let n/); assert.match(r.result(4)?.contents.value || '', /\(int\) let l/); }); test('inference', 'twelve caller layers converge in reverse declaration order', () => { let source = ''; for (let i = 12; i > 0; i--) source += `fn layer${i}(n) { return layer${i - 1}(n); }\n`; source += 'int layer0(int n) { return n + 1; } fn main() { return layer12(3); }'; const r = run(source, [{ needle: 'layer12(n)' }, { needle: 'layer7(n)' }, { needle: 'layer1(n)' }]); for (const [id, name] of [[2, 'layer12'], [3, 'layer7'], [4, 'layer1']]) assert.match(r.result(id)?.contents.value || '', new RegExp('int ' + name + '\\(int n\\)')); }); test('inference', 'recursive calls retain an anchored integer return and argument type', () => { const source = 'int seed(int n) { return n; } fn recursive(n) { if (n <= 0) return seed(n); return recursive(n - 1); } fn main() { return recursive(3); }'; const r = run(source, [{ needle: 'recursive(n)' }]); assert.match(r.result(2)?.contents.value || '', /int recursive\(int n\)/); }); test('inference', 'uncalled caller parameters inherit typed callee requirements', () => { const r = run('int seed(int n) { return n; } fn wrapper(n) { return seed(n); }', [{ needle: 'wrapper(n)' }]); assert.match(r.result(2)?.contents.value || '', /int wrapper\(int n\)/); }); test('inference', 'conflicting caller types do not become a single inferred signature', () => { const r = run('fn identity(value) { return value; } fn main() { identity(3); return identity("text")[0]; }', [{ needle: 'identity(value)' }]); assert.match(r.result(2)?.contents.value || '', /fn identity\(value\)/); }); test('inference', 'unknown reassignment invalidates the previous initializer flow', () => { const r = run('int seed(int n) { return n; } fn external(); fn changed(n) { let result = seed(n); result = external(); return result; }', [{ needle: 'changed(n)' }]); assert.ok(!/int changed/.test(r.result(2)?.contents.value || ''), JSON.stringify(r.result(2))); }); test('inference', 'owned list allocation constrains capacity and caller types', () => { const text = 'include rt; .u mklist(cap) { let l = rt.new(cap + 1); l[0] = 0; return l; } .u squares(n) { let l = mklist(n); return l; }'; const r = run(text, [{ needle: 'mklist(cap)' }, { needle: 'squares(n)' }, { needle: 'mklist(n)', offset: 7 }]); assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds)); assert.match(r.result(2)?.contents.value || '', /word\.u mklist\(int cap\)/); assert.match(r.result(3)?.contents.value || '', /word\.u squares\(int n\)/); assert.match(r.result(4)?.contents.value || '', /\(int\) let n/); }); test('inference', 'array indices do not constrain the type of an element result', () => { const r = run('include rt; fn lookup(index) { let data = rt.new(4); return data[index]; } fn main() { return lookup(1); }', [{ needle: 'lookup(index)' }]); assert.ok(!/int lookup/.test(r.result(2)?.contents.value || ''), JSON.stringify(r.result(2))); }); test('inference', 'typed fields do not constrain the containing parameter to a scalar', () => { const r = run('struct point { int x; } fn coordinate(p) { return p.x; }', [{ needle: 'coordinate(p)' }]); assert.ok(!/coordinate\(int p\)/.test(r.result(2)?.contents.value || ''), JSON.stringify(r.result(2))); }); test('quickfix', 'an entire-line request finds the compiler error inside the range', () => { const source = 'fn main() { rt.put_n(7); }'; const r = run(source, [{ needle: 'fn', method: 'textDocument/codeAction', params: { range: { start: { line: 0, character: 0 }, end: { line: 0, character: source.length } }, context: { diagnostics: [{ range: { start: { line: 99, character: 99 }, end: { line: 99, character: 99 } }, message: 'stale' }] } } }]); assert.ok(r.result(2)?.some(a => a.title === 'include rt;'), JSON.stringify(r.result(2))); }); test('quickfix', 'an owner-name cursor can import a qualified module', () => { const source = 'fn main() { rt.put_n(7); }', start = { line: 0, character: source.indexOf('rt.') }; const r = run(source, [{ needle: 'rt.', method: 'textDocument/codeAction', params: { range: { start, end: start }, context: { diagnostics: [] } } }]); assert.ok(r.result(2)?.some(a => a.title === 'include rt;')); }); test('modulehover', 'source library overview is available without a built-in module entry', () => { const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-module-hover-')); try { fs.mkdirSync(path.join(folder, 'src/lib'), { recursive: true }); fs.writeFileSync(path.join(folder, 'src/lib/widget.zc'), 'namespace widget;\n\n// converts widget coordinates into grid positions.\n\nint position(int x) { return x; }\n'); const r = run('include widget; fn main() { return widget.position(3); }', [{ needle: 'widget;' }], 'file://' + path.join(folder, 'main.zc'), [[path.join(folder, 'src/lib/widget.zc'), fs.readFileSync(path.join(folder, 'src/lib/widget.zc'), 'utf8')]]); assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds)); assert.match(r.result(2)?.contents.value || '', /converts widget coordinates into grid positions/); fs.writeFileSync(path.join(folder, 'src/lib/widget.zc'), 'namespace widget;\n\n/** converts grid positions into widget coordinates. */\n\nint position(int x) { return x; }\n'); const block = run('include widget; fn main() { return widget.position(3); }', [{ needle: 'widget;' }], 'file://' + path.join(folder, 'main.zc'), [[path.join(folder, 'src/lib/widget.zc'), fs.readFileSync(path.join(folder, 'src/lib/widget.zc'), 'utf8')]]); assert.match(block.result(2)?.contents.value || '', /converts grid positions into widget coordinates/); } finally { fs.rmSync(folder, { recursive: true, force: true }); } }); assert.ok(checks, 'no selected protocol contracts: ' + group); process.exitCode = failures ? 1 : 0;