diff options
| author | 2026-10-14 08:29:46 +0330 | |
|---|---|---|
| committer | 2026-10-14 08:29:46 +0330 | |
| commit | 1c53304d03414890af0da7432d1aea87e754a14a (patch) | |
| tree | 97190d86cf548c9bc63f68c86a4d2a4f09809713 /tests/drivers/toolchain_lsp_contract_test.js | |
| parent | a (diff) | |
feat: grow memory limits, add icon/actor libraries, overhaul LSP diagnostics
Compiler and VM
- make every memory map limit dynamic and adjustable at both build time and
runtime, including compiler tables, stacks, and every execution region
- reserve and grow tables, operand and return stacks, frame segments, and
image-building output without relocating live code or globals
- rename i/o and string helpers consistently (puts -> put_s, putn -> put_n,
scat -> cat_s) across compiler, runtime, examples, tests, and docs
- recast and consume values assigned to globals from inside functions
- allow fixed and const together, add typed closure returns (fn:t(...)),
multi-include syntax (include rt, run, core;), and recurse type inference
- never infer string literals as word, anywhere, as argument, variable, or
return type
- draft docs/BORROW.md: a borrow checker less intrusive than Rust's but more
intelligent
LSP and editor integration
- require explicit includes and give every toolchain area its own namespace
(rt, lsp, run, core)
- hover for built-in types, inferred types, values known at hover time, array
and pointer lifetime/size, library names in include statements, and inferred
lifetimes with explanations
- honor /* */ comments as documentation for variables and arguments, and stop
misattributing section headers or trailing comments as variable hints
- stream diagnostics and cancel stale requests on change
- jump to definition and references, including cross-file
- quick fixes with automatic library import, and namespace completion
Hints and diagnostics
- render hints as blue squigglies by default, with hints on/small/off per file
- suppress magic-number hints via a preceding /* */ comment, and say so
- warn on unused arguments; allow multiple _ discards in parameters, pattern
matching, and tagged enums, warning if a _ is actually used
- rewrite hint and comment prose in professional compiler-doc register
Libraries
- add icon.zc: run-time self-modification, find/replace syntax, type and block
edits, and step-through inspection of locals and globals
- add actor.zc: async actors with Actor<I,O>, implicit this, spawn/recv/send,
injected into the compiler machinery at include time rather than built in
Tooling and docs
- reorganize sources, tests, and designs into subdirectories
- unify windows entry points in tinyvm.bat and add a matching tinyvm.sh
- add --help across the toolchain
- polish the learning guide, stdlib docs, and design notes
Tests
- add accept, reject, and regression coverage across the compiler, LSP,
- runtime, icon, actors, threads, formatting, and the toolchain contracts,
plus fuzzers and property tests
Diffstat (limited to 'tests/drivers/toolchain_lsp_contract_test.js')
| -rw-r--r-- | tests/drivers/toolchain_lsp_contract_test.js | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/tests/drivers/toolchain_lsp_contract_test.js b/tests/drivers/toolchain_lsp_contract_test.js new file mode 100644 index 0000000..0d1b515 --- /dev/null +++ b/tests/drivers/toolchain_lsp_contract_test.js @@ -0,0 +1,306 @@ +// 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>(T used, T unused) { return used; } fn main() { return first<int>(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(!/(?<!\r)\n/.test(changed)); + if (source.includes('// prints the result')) assert.ok(changed.includes('// prints the result\nfn main()')); + if (source.startsWith('include rt;')) assert.strictEqual((changed.match(/include rt;/g) || []).length, 1); + const checked = run(changed); + assert.ok(!checked.ds.some(d => 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; |
