aboutsummaryrefslogtreecommitdiff
path: root/tests/drivers
diff options
context:
space:
mode:
authorGravatar Moon Flower <moonflower@elhenney.local> 2026-10-14 08:29:46 +0330
committerGravatar Moon Flower <moonflower@elhenney.local> 2026-10-14 08:29:46 +0330
commit1c53304d03414890af0da7432d1aea87e754a14a (patch)
tree97190d86cf548c9bc63f68c86a4d2a4f09809713 /tests/drivers
parenta (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')
-rw-r--r--tests/drivers/actor_image_test.py28
-rw-r--r--tests/drivers/actor_test.py40
-rw-r--r--tests/drivers/analysis_cache_test.js45
-rw-r--r--tests/drivers/analyzer_property_test.py28
-rw-r--r--tests/drivers/array_methods_test.py67
-rw-r--r--tests/drivers/cli_help_test.py55
-rw-r--r--tests/drivers/codeactions_client_test.js24
-rw-r--r--tests/drivers/compiler_contract_test.py30
-rw-r--r--tests/drivers/compiler_growth_test.py61
-rw-r--r--tests/drivers/completion_test.js105
-rw-r--r--tests/drivers/core_lsp_test.js8
-rw-r--r--tests/drivers/ext_cache_test.js27
-rw-r--r--tests/drivers/float_backends_test.py22
-rw-r--r--tests/drivers/format_test.js88
-rw-r--r--tests/drivers/frame_growth_test.py16
-rw-r--r--tests/drivers/fuzz_compiler.py87
-rw-r--r--tests/drivers/fuzz_lsp.js89
-rw-r--r--tests/drivers/fuzz_malformed.py63
-rw-r--r--tests/drivers/fuzz_wasm.js38
-rw-r--r--tests/drivers/hints_test.js31
-rw-r--r--tests/drivers/hover_features_test.js86
-rw-r--r--tests/drivers/hover_surface_test.js65
-rw-r--r--tests/drivers/hover_values_test.js68
-rw-r--r--tests/drivers/icon_test.py32
-rw-r--r--tests/drivers/include_test.js94
-rw-r--r--tests/drivers/lsp_longevity_test.js44
-rw-r--r--tests/drivers/lsp_test.js52
-rw-r--r--tests/drivers/lsp_vm.js14
-rw-r--r--tests/drivers/lsp_wasm.js14
-rw-r--r--tests/drivers/nav-library.zc2
-rw-r--r--tests/drivers/nav_host_test.js52
-rw-r--r--tests/drivers/nav_test.js75
-rw-r--r--tests/drivers/parser_growth_test.py7
-rw-r--r--tests/drivers/runtime_image_test.py34
-rw-r--r--tests/drivers/sourceprovider_test.js31
-rw-r--r--tests/drivers/storage_analysis_test.js11
-rw-r--r--tests/drivers/stream_host_test.js48
-rw-r--r--tests/drivers/stream_test.js55
-rw-r--r--tests/drivers/thread_context_test.py37
-rw-r--r--tests/drivers/toolchain_lsp_contract_test.js306
-rw-r--r--tests/drivers/tools_contract_test.py58
-rw-r--r--tests/drivers/vscode_hover_test.js21
-rw-r--r--tests/drivers/wasm_test.js3
43 files changed, 2060 insertions, 101 deletions
diff --git a/tests/drivers/actor_image_test.py b/tests/drivers/actor_image_test.py
new file mode 100644
index 0000000..9df8828
--- /dev/null
+++ b/tests/drivers/actor_image_test.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+# source syntax callbacks execute in the host while actor code is emitted separately.
+from pathlib import Path
+import argparse
+import subprocess
+import tempfile
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--host', default='bin/zy')
+parser.add_argument('--case', default='')
+args = parser.parse_args()
+prefix = b''.join(Path(f).read_bytes() + b'\n' for f in ('src/runtime/rt.zc', 'src/core/core.zc', 'src/runtime/run.zc'))
+cases = [('declaration', Path('tests/actor/accept/declaration.zc').read_text(), b'5 7 0 0\n'), ('generic', Path('tests/actor/accept/generic_declaration.zc').read_text(), b'5 7 4 0\n')]
+for name, source, expected in cases:
+ if args.case and args.case not in name:
+ continue
+ with tempfile.TemporaryDirectory(prefix='tinyvm-actor-image-') as work:
+ directory = Path(work)
+ compiled = subprocess.run([args.host], input=Path('src/tools/build.zc').read_bytes() + b'`' + prefix + source.encode(), capture_output=True, timeout=30)
+ assert compiled.returncode == 0 and len(compiled.stdout) >= 56 and not compiled.stdout.startswith(b'zc:'), (name, compiled.stdout[:2000], compiled.stderr)
+ header = subprocess.run(['bin/mkgenome'], input=compiled.stdout, capture_output=True, check=True)
+ (directory / 'genome.h').write_bytes(header.stdout)
+ for f in ('src/core/memmap.h', 'src/core/fops.h', 'src/core/bops.h', 'src/core/stack.h', 'src/micro/micro.c', 'src/micro/micro_port.h', 'src/micro/port_linux.c'):
+ (directory / Path(f).name).write_bytes(Path(f).read_bytes())
+ subprocess.run(['cc', '-O2', '-w', '-fwrapv', '-pthread', '-DZY_THREADS', str(directory / 'micro.c'), str(directory / 'port_linux.c'), '-I' + work, '-o', str(directory / 'image')], check=True, timeout=30)
+ result = subprocess.run([str(directory / 'image')], capture_output=True, timeout=12)
+ assert result.returncode == 0 and result.stdout == expected, (name, result.returncode, result.stdout, result.stderr)
+ print('ok actor compiler-free image: ' + name)
diff --git a/tests/drivers/actor_test.py b/tests/drivers/actor_test.py
new file mode 100644
index 0000000..43a110f
--- /dev/null
+++ b/tests/drivers/actor_test.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+# exercise syntax injection, isolated workers, typed graph copying and regressions.
+from pathlib import Path
+import argparse
+import subprocess
+import time
+
+parser = argparse.ArgumentParser()
+parser.add_argument('vm', nargs='?', default='bin/zy')
+parser.add_argument('--case', default='')
+parser.add_argument('--library', default='')
+args = parser.parse_args()
+root = Path(__file__).resolve().parents[2]
+expected = {'parallel': '0 0\n11 21\n', 'state': '0 5 7\n4\n', 'graphs': '25 a 5\n', 'results': 'ok\n', 'plain': '3\n', 'module_helpers': '10\n', 'safepoint': '8\n', 'values': '1\n', 'functions': '12\n', 'pointer_scalar': '1\n', 'declaration': '5 7 0 0\n', 'lifecycle': '-1 3\n', 'generic_extensions': '3\n', 'failed_worker': '-1 -1 3\n', 'generic_declaration': '5 7 4 0\n', 'generic_typed_method': '7 2\n'}
+count = 0
+for case in sorted((root / 'tests/actor').glob('*/*.zc')):
+ if args.case and args.case not in str(case):
+ continue
+ start = time.monotonic()
+ source = case.read_text()
+ if args.library:
+ source = source.replace('include rt, actor;', f'include rt; include \"{args.library}\"; include actor;')
+ proc = subprocess.run([str(root / args.vm)], input=source, text=True, capture_output=True, cwd=root, timeout=15)
+ elapsed = time.monotonic() - start
+ if case.parent.name == 'reject':
+ required = {'no_include': "actor: isn't a type", 'input_type': 'type error:', 'output_type': 'type error:', 'explicit_this': 'this is implicit', 'nested_missing_import': 'module access requires `include rt;`', 'generic_input': 'type error:', 'generic_method_return': 'type error:'}[case.stem]
+ assert required in proc.stdout, f'{case}: expected {required!r}, got {proc.stdout!r}'
+ else:
+ actual = proc.stdout
+ if case.stem == 'parallel':
+ lines = actual.splitlines(keepends=True)
+ assert len(lines) == 3, f'invalid parallel output: {actual!r}'
+ runtime_ms = int(lines[-1])
+ assert 280 <= runtime_ms < 580, f'workers did not overlap: {runtime_ms}ms'
+ actual = ''.join(lines[:-1])
+ assert proc.returncode == 0 and actual == expected[case.stem], f'{case}: stdout={proc.stdout!r}, stderr={proc.stderr!r}'
+ print(f'ok {case.relative_to(root)} ({elapsed:.3f}s)')
+ count += 1
+assert count, 'no actor fixtures selected'
+print(f'{count} actor fixtures passed')
diff --git a/tests/drivers/analysis_cache_test.js b/tests/drivers/analysis_cache_test.js
new file mode 100644
index 0000000..3ffde33
--- /dev/null
+++ b/tests/drivers/analysis_cache_test.js
@@ -0,0 +1,45 @@
+// Repeated replies use cached results; edits, include updates and reopen invalidate them.
+const assert = require('assert'), cp = require('child_process'), path = require('path');
+const uri = 'file:///cache/main.zc', text = 'include "dep.zc";\nfn main() { int n = 7; return n + value(); }';
+const msgs = []; let id = 1;
+const request = (method, params = {}) => { const rid = id++; msgs.push({ id: rid, method, params }); return rid; };
+const note = (method, params) => msgs.push({ method, params });
+const stats = () => request('zc/cacheStats');
+const hover = () => request('textDocument/hover', { textDocument: { uri }, position: { line: 1, character: 30 } });
+request('initialize'); note('zc/file', { path: '/cache/dep.zc', text: 'int value() { return 1; }' });
+note('textDocument/didOpen', { textDocument: { uri, version: 1, text } });
+const a = hover(), before = stats(), b = hover(), after = stats();
+const methods = ['textDocument/semanticTokens/full', 'textDocument/definition', 'textDocument/references', 'textDocument/completion'];
+const pairs = methods.map(method => [request(method, { textDocument: { uri }, position: { line: 1, character: 30 } }), request(method, { textDocument: { uri }, position: { line: 1, character: 30 } })]);
+const all = stats();
+note('textDocument/didChange', { textDocument: { uri, version: 2 }, contentChanges: [{ text: text.replace('= 7', '= 9') }] });
+const changed = hover(), editStats = stats();
+note('zc/file', { path: '/cache/dep.zc', text: 'int value() { return 2; }' });
+hover(); const includeStats = stats();
+note('textDocument/didClose', { textDocument: { uri } });
+note('textDocument/didOpen', { textDocument: { uri, version: 1, text } });
+const reopened = hover(), reopenStats = stats(); note('exit', {});
+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]); };
+let out;
+if (process.argv[2] === '--wasm' || process.argv[2] === '--vm') {
+ const wasm = process.argv[2] === '--wasm', host = require(path.resolve('editors/vscode/client', wasm ? 'wasmproc.js' : 'vmproc.js'));
+ const p = (wasm ? host.wasmProcess : host.vmProcess)(path.resolve(process.argv[3]), { log: () => {} });
+ const chunks = []; p.stdout.on('data', d => chunks.push(d)); msgs.forEach(m => p.stdin.write(frame(m))); out = Buffer.concat(chunks);
+} else {
+ const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 60000, maxBuffer: 16 << 20 });
+ assert.strictEqual(r.status, 0, String(r.stderr)); out = r.stdout;
+}
+const replies = new Map();
+while (out.length) { const e = out.indexOf('\r\n\r\n'), n = Number(/Content-Length:\s*(\d+)/i.exec(out.subarray(0, e).toString())[1]); const m = JSON.parse(out.subarray(e + 4, e + 4 + n)); replies.set(m.id, m.result); out = out.subarray(e + 4 + n); }
+assert.deepStrictEqual(replies.get(a), replies.get(b));
+assert.ok(replies.get(a)?.contents.value.includes('7'), 'first hover has value');
+assert.strictEqual(replies.get(after).hits, replies.get(before).hits + 1);
+assert.strictEqual(replies.get(after).checks, replies.get(before).checks);
+pairs.forEach(([x, y]) => assert.deepStrictEqual(replies.get(x), replies.get(y)));
+assert.strictEqual(replies.get(all).hits, replies.get(after).hits + methods.length);
+assert.ok(replies.get(changed)?.contents.value.includes('9'), 'edit invalidates value');
+assert.strictEqual(replies.get(editStats).misses, replies.get(all).misses + 1);
+assert.strictEqual(replies.get(includeStats).misses, replies.get(editStats).misses + 1);
+assert.ok(replies.get(reopened)?.contents.value.includes('7'), 'reopen invalidates value');
+assert.strictEqual(replies.get(reopenStats).misses, replies.get(includeStats).misses + 1);
+console.log('ok analysis cache reuses all request types and invalidates on edits, includes and reopen');
diff --git a/tests/drivers/analyzer_property_test.py b/tests/drivers/analyzer_property_test.py
new file mode 100644
index 0000000..d816f47
--- /dev/null
+++ b/tests/drivers/analyzer_property_test.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+"""deterministic safe/invalid neighbors for ownership and projection analysis."""
+import argparse
+import random
+import subprocess
+
+parser = argparse.ArgumentParser()
+parser.add_argument('host')
+parser.add_argument('--cases', type=int, default=32)
+args = parser.parse_args()
+rng = random.Random(0xB0770)
+for i in range(args.cases):
+ n = rng.randrange(1, 12); at = rng.randrange(n)
+ prefix = f'include rt; int* saved; fn main() {{ let p = rt.new(int, {n}); p[{at}] = {i}; '
+ variants = [
+ (f'return p[{at}]; }}', True),
+ (f'saved = p; return saved[{at}]; }}', True),
+ (f'saved = p; return p[{at}]; }}', False),
+ (f'rt.free(p); return p[{at}]; }}', False),
+ (f'int* q = p; rt.free(p); return q[{at}]; }}', False),
+ ]
+ for tail, accepted in variants:
+ src = prefix + tail
+ r = subprocess.run([args.host], input=src.encode(), capture_output=True, timeout=5)
+ text = (r.stdout + r.stderr).decode(errors='replace')
+ error = 'zc:' in text or r.returncode != 0
+ assert r.returncode >= 0 and (not error) == accepted, (i, accepted, src, text)
+print(f'ok {args.cases * 5} generated ownership accept/reject neighbors (seed 0xB0770)')
diff --git a/tests/drivers/array_methods_test.py b/tests/drivers/array_methods_test.py
new file mode 100644
index 0000000..82fef67
--- /dev/null
+++ b/tests/drivers/array_methods_test.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+"""array receiver calls: execution, type checks and mutation contracts."""
+import argparse
+import re
+import subprocess
+
+parser = argparse.ArgumentParser()
+parser.add_argument('host', nargs='?', default='bin/zy')
+parser.add_argument('--case', default='')
+args = parser.parse_args()
+
+cases = [
+ ('growable', True, 'include rt; fn main() { int[?] a = {2,3}; a.push(7); rt.put_n(a.len()); rt.put_c(32); rt.put_n(a.cap() >= a.len()); rt.put_c(32); rt.put_n(a.pop()); a.resize(4); rt.put_c(32); rt.put_n(a[3]); rt.put_c(32); rt.put_n(a.len()); }', '3 1 7 0 4'),
+ ('fixed length', True, 'include rt; fn main() { int[3] a = {2,3,4}; rt.put_n(a.len()); rt.put_c(32); rt.put_n(a.cap()); fixed int[?] b = rt.new(fixed int[?], 5); rt.put_c(32); rt.put_n(b.len()); }', '3 3 5'),
+ ('packed', True, 'include rt; fn main() { u8[?] a = {1}; a.push(300); rt.put_n(a.pop()); rt.put_c(32); i16[?] b = {-5}; rt.put_n(b.pop()); rt.put_c(32); f32[?] c = {2.5}; c.push(1.5); rt.put_n(c.pop() == 1.5); }', '44 -5 1'),
+ ('optimized', True, 'include rt; o3 int change(int[?] a) { a.push(9); int n = a.pop(); a.resize(3); return n + a.len(); } fn main() { int[?] a = {1}; rt.put_n(change(a)); rt.put_c(32); rt.put_n(a.len()); }', '12 3'),
+ ('field receiver', True, 'include rt; struct box { int[?] items; }; fn main() { box b; b.items = rt.new(int[?]); b.items.push(8); rt.put_n(b.items.pop()); }', '8'),
+ ('nested arguments', True, 'include rt; fn main() { int[?] a = {1}, b = {2,3}; a.push(b.pop() + b.len()); a.resize(b.len() + 2); rt.put_n(a[1]); rt.put_c(32); rt.put_n(a.len()); }', '4 3'),
+ ('fixed push', False, 'fn main() { int[2] a; a.push(1); }', None),
+ ('fixed pop', False, 'fn main() { int[2] a; return a.pop(); }', None),
+ ('fixed resize', False, 'fn main() { int[2] a; a.resize(3); }', None),
+ ('fixed handle push', False, 'include rt; fn main() { fixed int[?] a = rt.new(fixed int[?], 2); a.push(1); }', None),
+ ('element mismatch', False, 'include rt; fn main() { int[?] a = {}; a.push("wrong"); }', None),
+ ('extra argument', False, 'include rt; fn main() { int[?] a = {}; a.len(1); }', None),
+ ('invalidated borrow', False, 'include rt; fn main() { int[?] a = {1}; int* p = &a[0]; a.resize(10); return *p; }', None),
+ ('free fixed push', False, 'fn main() { int[2] a; push(a,1); }', None),
+ ('free fixed pop', False, 'fn main() { int[2] a; return pop(a); }', None),
+ ('registered generic', True, 'include rt, run; T first<T>(fixed T[?] a) { return a[0]; } immediate fn register() { run.next(); run.add_method("first", "first"); } register fn main() { int[?] a = {9}; rt.put_n(a.first()); int[2] b = {7,8}; rt.put_c(32); rt.put_n(b.first()); }', '9 7'),
+ ('registered callback', True, 'include rt, run; U apply<T,U>(fixed T[?] a, U(T) f) { return f(a[0]); } immediate fn register() { run.next(); run.add_method("apply", "apply"); } register fn main() { int[?] a = {4}; rt.put_n(a.apply(fn (e) { return e + 2; })); rt.put_c(32); float result = a.apply(fn:float(int e) { return e * 0.5; }); rt.put_n(result == 2.0); }', '6 1'),
+ ('registered callback mismatch', False, 'include rt, run; U apply<T,U>(fixed T[?] a, U(T) f) { return f(a[0]); } immediate fn register() { run.next(); run.add_method("apply", "apply"); } register fn main() { int[?] a = {4}; return a.apply(fn:int(char* e) { return e[0]; }); }', None),
+ ('registration requires receiver', False, 'include run; int empty() { return 0; } immediate fn register() { run.next(); run.add_method("empty", "empty"); } register fn main() { return 0; }', None),
+ ('declarative registration', True, 'include rt, run; T first<T>(fixed T[?] a) { return a[0]; } run.method("first", first); fn main() { int[3] a = {6,7,8}; rt.put_n(a.first()); }', '6'),
+ ('map inferred', True, 'include rt, std; fn main() { int[?] a = {1,2,3}; int[?] b = a.map(fn(e) { return e + 2; }); rt.put_n(b[0]); rt.put_c(32); rt.put_n(b[2]); rt.put_c(32); float[?] c = a.map(fn(e) { return e * 0.5; }); rt.put_n(c[2] == 1.5); }', '3 5 1'),
+ ('filter fixed', True, 'include rt, std; fn main() { int[5] a = {1,2,3,4,5}; let b = a.filter(fn(e) { return e % 2; }); rt.put_n(b.len()); rt.put_c(32); rt.put_n(b[2]); }', '3 5'),
+ ('search methods', True, 'include rt, std; fn main() { int[?] a = {2,4,6}; rt.put_n(a.find(4)); rt.put_c(32); rt.put_n(a.find(5)); rt.put_c(32); rt.put_n(a.contains(6)); rt.put_c(32); rt.put_n(a.find_if(fn(e) { return e > 4; })); }', '1 -1 1 2'),
+ ('reduce and predicates', True, 'include rt, std; fn main() { int[3] a = {2,4,6}; rt.put_n(a.reduce(1, fn(acc,e) { return acc + e; })); rt.put_c(32); rt.put_n(a.all(fn(e) { return e % 2 == 0; })); rt.put_c(32); rt.put_n(a.any(fn(e) { return e > 5; })); }', '13 1 1'),
+ ('empty algorithms', True, 'include rt, std; fn main() { int[?] a = {}; int calls = 0; let b = a.map(fn(e) { calls++; return e; }); let c = a.filter(fn(e) { calls++; return e; }); rt.put_n(b.len() + c.len() + calls); rt.put_c(32); rt.put_n(a.all(fn(e) { return e; })); rt.put_c(32); rt.put_n(a.any(fn(e) { return e; })); rt.put_c(32); rt.put_n(a.reduce(9, fn(acc,e) { return acc + e; })); }', '0 1 0 9'),
+ ('shared filter references', True, 'include rt, std; fn main() { char*[?] a = rt.new(char*[?]); char* s = rt.new(char, 2); s[0] = 65; a.push(s); let b = a.filter(fn(e) { return e[0] == 65; }); rt.free(a); rt.gc(); rt.put_c(b[0][0]); }', 'A'),
+ ('map needs import', False, 'include rt; fn main() { int[?] a = {1}; let b = a.map(fn(e) { return e; }); }', None),
+ ('pop transfers ownership', True, 'include rt; fn main() { int before = rt.live_objects(); { int*[?] a = rt.new(int*[?]); int* p = rt.new(int); *p = 7; a.push(p); let saved = a.pop(); rt.free(a); rt.put_n(rt.isobj(saved)); rt.put_c(32); rt.put_n(*saved); } rt.put_c(32); rt.put_n(rt.live_objects() - before); }', '1 7 0'),
+ ('returned pop survives array', True, 'include rt; int*.u take() { int*[?] a = rt.new(int*[?]); int* p = rt.new(int); *p = 8; a.push(p); return a.pop(); } fn main() { let p = take(); rt.put_n(rt.isobj(p)); rt.put_c(32); rt.put_n(*p); }', '1 8'),
+ ('single field struct push', False, 'include rt; struct item { int value; }; fn main() { item[?] a = rt.new(item[?]); item x; x.value = 7; a.push(x); }', None),
+ ('single field struct pop', False, 'include rt; struct item { int value; }; fn main() { item[?] a = rt.new(item[?], 1); return a.pop().value; }', None),
+ ('resize drops children', True, 'include rt; fn main() { int before = rt.live_objects(); int*[?] a = rt.new(int*[?]); a.push(rt.new(int)); a.resize(0); rt.put_n(rt.live_objects() - before); }', '2'),
+ ('callback borrows references', False, 'include rt, std; fn main() { int*[?] a = rt.new(int*[?]); a.push(rt.new(int)); let b = a.filter(fn(e) { rt.free(e); return 1; }); }', None),
+ ('packed map and filter', True, 'include rt, std; fn main() { i16[?] a = {-5,7}; i16[?] b = a.filter(fn(e) { return e < 0; }); rt.put_n(b[0]); rt.put_c(32); f32[2] c = {1.5,2.5}; f32[?] d = c.map(fn(e) { return e; }); rt.put_n(d[1] == 2.5); }', '-5 1'),
+ ('numeric coercion', True, 'include rt; fn main() { float[?] a = rt.new(float[?]); a.push(2); rt.put_n(a[0] == 2.0); rt.put_c(32); int[?] b = {}; b.push(2.5); b.resize(3.0); rt.put_n(b[0]); rt.put_c(32); rt.put_n(b.len()); }', '1 2 3'),
+ ('invalid resize length', False, 'include rt; fn main() { int[?] a = {}; a.resize("bad"); }', None),
+ ('free pop ownership', True, 'include rt; fn main() { int*[?] a = rt.new(int*[?]); int* p = rt.new(int); *p = 9; push(a,p); let saved = pop(a); rt.free(a); rt.put_n(rt.isobj(saved)); rt.put_c(32); rt.put_n(*saved); }', '1 9'),
+ ('packed string values', True, 'include rt; struct box { bstr text; }; fn main() { bstr[?] a = {b"hi"}; let s = a.pop(); rt.put_n(s[1]); rt.put_c(32); box b; b.text = b"ok"; rt.put_n(b.text[1]); }', '105 107'),
+ ('field array borrow', False, 'include rt; struct box { int[?] items; }; fn main() { box* b = rt.new(box); b.items = rt.new(int[?], 1); let borrowed = b.items; rt.free(b); return borrowed[0]; }', None),
+]
+
+count = 0
+for name, accept, source, expected in cases:
+ if args.case and args.case not in name:
+ continue
+ source = 'typecheck on;\n' + source
+ check = subprocess.run([args.host], input=source, text=True, capture_output=True, timeout=10)
+ diagnostics = check.stdout + check.stderr
+ failed = check.returncode != 0 or bool(re.search(r'(zc:|zc0:|error:)', diagnostics))
+ assert failed != accept, f'{name}: expected {"accept" if accept else "reject"}\n{diagnostics}'
+ if accept:
+ assert check.returncode == 0 and check.stdout == expected, f'{name}: {check.stdout!r}, {check.stderr!r}; expected {expected!r}'
+ print('ok array methods: ' + name)
+ count += 1
+assert count, 'no cases selected'
diff --git a/tests/drivers/cli_help_test.py b/tests/drivers/cli_help_test.py
new file mode 100644
index 0000000..1b32fa4
--- /dev/null
+++ b/tests/drivers/cli_help_test.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+"""bounded command-line help and option rejection contracts."""
+import argparse
+import os
+import subprocess
+from pathlib import Path
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--group', choices=['binaries', 'scripts', 'js', 'all'], default='all')
+parser.add_argument('--bin-dir', default='bin')
+args = parser.parse_args()
+commands = []
+if args.group in ('binaries', 'all'):
+ commands += [[str(Path(args.bin_dir) / name)] for name in ['zy', 'zym', 'zclsp', 'zc0', 'mkgenome', 'genmap']]
+if args.group in ('scripts', 'all'):
+ commands += [['bash', name] for name in ['tinyvm.sh', 'src/tools/zcc', 'src/tools/automap.sh', 'src/tools/embed.sh', 'src/tools/mklsp.sh', 'src/tools/mklspwasm.sh', 'src/tools/mkvsix.sh', 'src/web/build.sh', 'src/baremetal/run.sh']]
+if args.group in ('js', 'all'):
+ commands += [['node', name] for name in ['src/js/tinyvm.js', 'src/js/extract_genome.js', 'src/js/extract_lsp.js']]
+
+def invoke(command, option):
+ # keep stdin open: help and rejected options must finish without reading it.
+ env = dict(os.environ, TINYVM_MEMORY_WORDS='invalid', TINYVM_MAX_MEMORY_WORDS='invalid')
+ child = subprocess.Popen([*command, option], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
+ try:
+ child.wait(timeout=3)
+ return child.returncode, child.stdout.read(), child.stderr.read()
+ finally:
+ if child.poll() is None:
+ child.kill()
+ child.wait()
+ child.stdin.close()
+ child.stdout.close()
+ child.stderr.close()
+
+failures = 0
+for command in commands:
+ try:
+ for option in ['--help', '-h']:
+ status, out, err = invoke(command, option)
+ assert status == 0 and b'usage:' in out.lower() and not err, (option, status, out[:200], err[:200])
+ status, out, err = invoke(command, '--unknown-option')
+ assert status > 0 and err and not out, (status, out[:200], err[:200])
+ print('ok help and option rejection: ' + ' '.join(command))
+ except (AssertionError, subprocess.TimeoutExpired) as error:
+ failures += 1
+ print('FAIL ' + ' '.join(command) + ': ' + str(error))
+if args.group in ('binaries', 'all'):
+ source = b'int identity(int n) { return n; }\n'
+ for option in ['--format', '--annotate', '--types']:
+ result = subprocess.run([str(Path(args.bin_dir) / 'zclsp'), option], input=source, capture_output=True, timeout=10)
+ assert result.returncode == 0 and not result.stderr, (option, result.stdout, result.stderr)
+ if option != '--types':
+ assert b'identity' in result.stdout, (option, result.stdout)
+ print('ok lsp source options remain available alongside help')
+raise SystemExit(bool(failures))
diff --git a/tests/drivers/codeactions_client_test.js b/tests/drivers/codeactions_client_test.js
new file mode 100644
index 0000000..f1936e7
--- /dev/null
+++ b/tests/drivers/codeactions_client_test.js
@@ -0,0 +1,24 @@
+const assert = require('assert');
+const { convertCodeActions } = require('../../editors/vscode/client/codeactions');
+class Range { constructor(...coordinates) { this.coordinates = coordinates; } }
+class WorkspaceEdit {
+ constructor() { this.edits = []; }
+ replace(uri, range, text) { this.edits.push({ uri, range, text }); }
+}
+class CodeAction { constructor(title, kind) { this.title = title; this.kind = kind; } }
+const vscode = { Range, WorkspaceEdit, CodeAction, CodeActionKind: { QuickFix: 'quickfix' }, Uri: { parse: s => s } };
+const uri = 'file:///test.zc', doc = { uri: { toString: () => uri }, version: 3 };
+const edit = { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, newText: 'include rt;\n' };
+const source = { title: 'include rt;', kind: 'quickfix', isPreferred: true, edit: { documentChanges: [{ textDocument: { uri, version: 3 }, edits: [edit] }] } };
+const actions = convertCodeActions(vscode, [source], doc);
+assert.strictEqual(actions.length, 1);
+assert.strictEqual(actions[0].kind, 'quickfix');
+assert.strictEqual(actions[0].isPreferred, true);
+assert.deepStrictEqual(actions[0].edit.edits[0], { uri, range: new Range(0, 0, 0, 0), text: 'include rt;\n' });
+assert.deepStrictEqual(convertCodeActions(vscode, [source], { ...doc, version: 4 }), [], 'stale document versions cannot become editor edits');
+assert.strictEqual(convertCodeActions(vscode, [source], { ...doc, version: 1 }, 3).length, 1, 'protocol version epochs are distinct from editor versions');
+assert.strictEqual(convertCodeActions(vscode, [{ ...source, edit: { changes: { [uri]: [edit] } } }], doc).length, 1);
+assert.deepStrictEqual(convertCodeActions(vscode, null, doc), []);
+assert.deepStrictEqual(convertCodeActions(vscode, [{ title: 'no edit' }, { ...source, edit: { changes: {} } }], doc), []);
+assert.deepStrictEqual(source.edit.documentChanges[0].edits, [edit], 'conversion keeps protocol objects intact');
+console.log('ok quick-fix client conversion, stale-version rejection and empty responses');
diff --git a/tests/drivers/compiler_contract_test.py b/tests/drivers/compiler_contract_test.py
new file mode 100644
index 0000000..63e476c
--- /dev/null
+++ b/tests/drivers/compiler_contract_test.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+"""accept/reject contracts, selectable in small groups before implementation."""
+import argparse
+import json
+import re
+import subprocess
+from pathlib import Path
+
+parser = argparse.ArgumentParser()
+parser.add_argument('host')
+parser.add_argument('--group')
+parser.add_argument('--case')
+args = parser.parse_args()
+cases = json.loads(Path('tests/spec/compiler_contracts.json').read_text())
+cases = [c for c in cases if (not args.group or c['group'] == args.group) and (not args.case or args.case in c['name'])]
+assert cases, 'no selected contracts'
+failures = []
+for c in cases:
+ source = ('typecheck on;\n' + c['source']).encode()
+ r = subprocess.run([args.host], input=source, capture_output=True, timeout=10)
+ text = (r.stdout + r.stderr).decode(errors='replace')
+ error = bool(re.search(r'(zc:|zc0:|error:|undefined|not defined)', text)) or r.returncode != 0
+ valid = (not error) == c['accept'] and r.returncode >= 0
+ if valid and 'output' in c:
+ valid = r.returncode == 0 and r.stdout.decode() == c['output']
+ print(('ok ' if valid else 'FAIL ') + c['group'] + ': ' + c['name'])
+ if not valid:
+ failures.append(c['name'])
+ print(text.strip()[:1000])
+raise SystemExit(1 if failures else 0)
diff --git a/tests/drivers/compiler_growth_test.py b/tests/drivers/compiler_growth_test.py
index 982611c..e56bf43 100644
--- a/tests/drivers/compiler_growth_test.py
+++ b/tests/drivers/compiler_growth_test.py
@@ -18,65 +18,68 @@ def run(source, expected):
assert result.stdout == expected, result.stdout[:1000]
-locals_source = 'fn main() {\n'
+locals_source = 'include rt; fn main() {\n'
locals_source += ''.join(f'int local_{i} = {i};\n' for i in range(900))
-locals_source += 'put_n(local_899); put_c(10); return 0; }\n'
+locals_source += 'rt.put_n(local_899); rt.put_c(10); return 0; }\n'
run(locals_source, '899\n')
-types_source = ''.join(f'type array_{i} = [{i + 1}]int;\n' for i in range(1500))
-types_source += 'fn main() { put_n(ntl > 1500); put_c(10); gc(); put_n(tsz(tarr(tint, 1500))); put_c(10); return 0; }\n'
+types_source = ''.join(f'type array_{i} = int[{i + 1}];\n' for i in range(1500))
+types_source = 'include rt, run;\n' + types_source
+types_source += 'fn main() { rt.put_n(run.ntl > 1500); rt.put_c(10); rt.gc(); rt.put_n(run.tsz(run.tarr(run.tint, 1500))); rt.put_c(10); return 0; }\n'
run(types_source, '1\n1500\n')
-names_source = ''.join(f'const int repeated_long_identifier_for_pool_and_hash_growth_{i} = {i};\n' for i in range(options.names))
+names_source = 'include rt, run;\n' + ''.join(f'const int repeated_long_identifier_for_pool_and_hash_growth_{i} = {i};\n' for i in range(options.names))
names_source += f'''fn main() {{
- word saved = intern("a name that must survive growth");
- reserve_names(hashcap * 4); reserve_symbols(symcap * 2); reserve_locals(loccap * 2);
- reserve_fixes(fixcap * 2); reserve_R_SRC(4096); reserve_R_CTX(4096);
- reserve_R_LOOP(4096); reserve_R_SNAP(32768); reserve_R_QBUF(32768); reserve_R_EXIT(8192);
- gc();
- put_n(saved == intern("a name that must survive growth")); put_c(10);
- put_n(repeated_long_identifier_for_pool_and_hash_growth_{options.names - 1}); put_c(10);
- put_n(poolblocks != 0 && symcap > NS && hashcap > IHN); put_c(10);
+ word saved = run.intern("a name that must survive growth");
+ run.reserve_names(run.hashcap * 4); run.reserve_symbols(run.symcap * 2); run.reserve_locals(run.loccap * 2);
+ run.reserve_fixes(run.fixcap * 2); run.reserve_R_SRC(4096); run.reserve_R_CTX(4096);
+ run.reserve_R_LOOP(4096); run.reserve_R_SNAP(32768); run.reserve_R_QBUF(32768); run.reserve_R_EXIT(8192);
+ rt.gc();
+ rt.put_n(saved == run.intern("a name that must survive growth")); rt.put_c(10);
+ rt.put_n(repeated_long_identifier_for_pool_and_hash_growth_{options.names - 1}); rt.put_c(10);
+ rt.put_n(run.poolblocks != 0 && run.symcap > run.NS && run.hashcap > run.IHN); rt.put_c(10);
return 0;
}}
'''
run(names_source, f'1\n{options.names - 1}\n1\n')
long_name = 'identifier_' + 'x' * 1500
-run(f'const int {long_name} = 7; fn main() {{ put_n({long_name}); put_c(10); return 0; }}\n', '7\n')
+run(f'include rt; const int {long_name} = 7; fn main() {{ rt.put_n({long_name}); rt.put_c(10); return 0; }}\n', '7\n')
-mark_source = '''let roots[60000];
+mark_source = '''include rt; let roots[60000];
unchecked fn main() {
- int i = 0;
- while (i < 60000) { roots[i] = new(1); i++; }
- gc(); put_n(markcap > MARKN); put_c(32); put_n(live_objects() == 60000); put_c(32);
- fill_w(roots, 0, 60000); scrub(); gc();
- // conservative roots can retain incidental addresses in code or untyped words.
- put_n(live_objects() < 1000); put_c(10); return 0;
+ int i = 0, before = rt.live_objects();
+ while (i < 60000) { roots[i] = rt.new(1); i++; }
+ rt.gc(); rt.put_n(rt.markcap > rt.MARKN); rt.put_c(32); rt.put_n(rt.live_objects() - before == 60000); rt.put_c(32);
+ // explicitly release survivors: conservative instruction roots may resemble heap addresses.
+ i = 0; while (i < 60000) { rt.free(roots[i]); i++; }
+ rt.fill_w(roots, 0, 60000); rt.scrub(); rt.gc();
+ rt.put_n(rt.live_objects() == before); rt.put_c(10); return 0;
}
'''
run(mark_source, '1 1 1\n')
generics_source = ''.join(f'int generic_{i}<T>() {{ return sizeof(T); }}\n' for i in range(90))
generics_source += 'int size_of<T>() { return sizeof(T); }\n'
+generics_source = 'include rt;\n' + generics_source
generics_source += 'fn main() { int total = 0;\n'
generics_source += ''.join(f'total += generic_{i}<int>();\n' for i in range(90))
-generics_source += ''.join(f'total += size_of<[{i + 1}]int>();\n' for i in range(150))
-generics_source += 'gc(); put_n(total); put_c(10); return 0; }\n'
+generics_source += ''.join(f'total += size_of<int[{i + 1}]>();\n' for i in range(150))
+generics_source += 'rt.gc(); rt.put_n(total); rt.put_c(10); return 0; }\n'
run(generics_source, '11415\n')
-captures_source = 'fn main() {\n' + ''.join(f'int captured_{i} = {i};\n' for i in range(70))
+captures_source = 'include rt; fn main() {\n' + ''.join(f'int captured_{i} = {i};\n' for i in range(70))
captures_source += 'let f = fn() { return ' + ' + '.join(f'captured_{i}' for i in range(70)) + '; };\n'
-captures_source += 'gc(); put_n(f()); put_c(10); return 0; }\n'
+captures_source += 'rt.gc(); rt.put_n(f()); rt.put_c(10); return 0; }\n'
run(captures_source, '2415\n')
-nested_source = 'fn main() { int captured = 7;\n'
+nested_source = 'include rt; fn main() { int captured = 7;\n'
nested_source += ''.join(f'let nested_{i} = fn() {{\n' for i in range(12))
nested_source += 'return captured;\n'
nested_source += ''.join(f'}}; return nested_{i}();\n' for i in reversed(range(1, 12)))
-nested_source += '}; gc(); put_n(nested_0()); put_c(10); return 0; }\n'
+nested_source += '}; rt.gc(); rt.put_n(nested_0()); rt.put_c(10); return 0; }\n'
run(nested_source, '7\n')
-assembly_source = 'fn main() { int value = asm { ' + 'future _ ' * 300
-assembly_source += ''.join(f':label_{i} ' for i in range(200)) + ':future 7 }; gc(); put_n(value); put_c(10); return 0; }\n'
+assembly_source = 'include rt; fn main() { int value = asm { ' + 'future _ ' * 300
+assembly_source += ''.join(f':label_{i} ' for i in range(200)) + ':future 7 }; rt.gc(); rt.put_n(value); rt.put_c(10); return 0; }\n'
run(assembly_source, '7\n')
print('ok compiler tables, long identifiers, and mark work grow without losing references')
diff --git a/tests/drivers/completion_test.js b/tests/drivers/completion_test.js
new file mode 100644
index 0000000..22c5fa5
--- /dev/null
+++ b/tests/drivers/completion_test.js
@@ -0,0 +1,105 @@
+// compiler-backed completion, including incomplete source and position-aware lifetimes.
+// usage: node tests/drivers/completion_test.js path/to/zclsp [server arguments]
+const assert = require('assert');
+const cp = require('child_process');
+const path = require('path');
+const uri = 'file://' + path.resolve('tests/drivers/completion-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 complete(source, extra = []) {
+ const at = source.indexOf('|'); assert.ok(at >= 0);
+ const before = source.slice(0, at), text = source.replace('|', '');
+ const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 };
+ const msgs = [
+ { id: 1, method: 'initialize', params: {} },
+ { method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } },
+ { id: 2, method: 'textDocument/completion', params: { textDocument: { uri }, position } },
+ ...extra.map(m => ({ ...m, params: { textDocument: { uri }, position, ...m.params } })),
+ { method: 'exit' }
+ ].map(m => ({ jsonrpc: '2.0', ...m }));
+ let output;
+ if (process.argv[2] === '--wasm' || process.argv[2] === '--vm') {
+ const wasm = process.argv[2] === '--wasm';
+ const host = require(path.resolve('editors/vscode/client', wasm ? 'wasmproc.js' : 'vmproc.js'));
+ const proc = (wasm ? host.wasmProcess : host.vmProcess)(path.resolve(process.argv[3]), { log: () => {} });
+ const chunks = []; proc.stdout.on('data', b => chunks.push(b));
+ for (const m of msgs) proc.stdin.write(frame(m));
+ output = Buffer.concat(chunks);
+ } else {
+ const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 30000, maxBuffer: 32 << 20 });
+ assert.strictEqual(r.status, 0, String(r.stderr)); output = r.stdout;
+ }
+ let b = output; const replies = [];
+ while (b.length) {
+ const e = b.indexOf('\r\n\r\n'); assert.ok(e >= 0, b.toString());
+ const n = Number(/Content-Length:\s*(\d+)/i.exec(b.subarray(0, e).toString())[1]);
+ replies.push(JSON.parse(b.subarray(e + 4, e + 4 + n))); b = b.subarray(e + 4 + n);
+ }
+ assert.ok(replies.find(m => m.id === 1).result.capabilities.completionProvider);
+ const reply = replies.find(m => m.id === 2); assert.ok(reply && reply.result, JSON.stringify(reply));
+ return { items: reply.result.items, replies };
+}
+const get = (items, label) => { const item = items.find(c => c.label === label); assert.ok(item, `${label} missing: ${items.map(c => c.label)}`); return item; };
+let r = complete('/* computes a total */\nint total(int count) { return count + 1; }\nfn main() {\n int tally = 3;\n ta|\n}\n');
+assert.match(get(r.items, 'tally').detail, /int tally/);
+assert.ok(!r.items.some(c => c.label === 'count'), 'other function parameters are out of scope');
+r = complete('/* computes a total */\nint total(int count) { return count + 1; }\nfn main() { to| }');
+assert.match(get(r.items, 'total').detail, /int total\(int count\)/);
+assert.match(get(r.items, 'total').documentation.value, /computes a total/);
+r = complete('fn main() {\n int count = 1;\n { int hidden = 3; }\n |\n}\n');
+get(r.items, 'count'); assert.ok(!r.items.some(c => c.label === 'hidden'));
+r = complete('include rt; fn main(int input) {\n let p = rt.new(int, 3);\n rt.free(p);\n |\n}\n');
+const p = get(r.items, 'p'); assert.deepStrictEqual(p.tags, [1]);
+assert.match(p.documentation.value, /lifetime ended/);
+get(r.items, 'input');
+assert.ok(r.items.findIndex(c => c.label === 'p') > r.items.findIndex(c => c.label === 'input'));
+assert.ok(r.items.slice(r.items.findIndex(c => c.tags)).every(c => c.tags), 'all consumed variables sort last');
+r = complete('include rt; fn main() {\n let p = rt.new(int, 3);\n p|;\n rt.free(p);\n return 0;\n}');
+assert.ok(!get(r.items, 'p').tags, 'later consumption does not expire earlier completion');
+r = complete('fn main() {\n int old = 1;\n int bold = 2;\n int other_local_data = 3;\n old|\n}');
+assert.strictEqual(r.items[0].label, 'old', 'exact match comes first'); get(r.items, 'other_local_data');
+r = complete('include std;\nfn main() { std.Text.at| }');
+assert.match(get(r.items, 'atoi').detail, /atoi\(/);
+assert.ok(r.items.every(c => !c.label.includes('.')), 'member labels omit receiver');
+r = complete('struct point { int x; int y; };\nfn main() { point p; p.| }');
+get(r.items, 'x'); get(r.items, 'y'); assert.ok(!r.items.some(c => c.label === 'main'));
+r = complete('fn main() { Te| }'); assert.ok(!r.items.some(c => c.label === 'Text'), 'std is not imported automatically');
+r = complete('int shared;\nfn main() { char shared = 1; sh| }');
+assert.match(get(r.items, 'shared').detail, /char shared/);
+assert.strictEqual(r.items.filter(c => c.label === 'shared').length, 1);
+r = complete('fn main() { let hello = "hi"; hel| }');
+assert.match(get(r.items, 'hello').detail, /char\* hello/);
+r = complete('fn main() { let bytes = b"hi"; byt| }');
+assert.match(get(r.items, 'bytes').detail, /bstr bytes/);
+r = complete('fn main() { int count = 1; cou|nt; return count; }', [{ id: 3, method: 'textDocument/hover' }]);
+assert.ok(r.replies.find(m => m.id === 3).result, 'hover still works after a partial completion check');
+assert.strictEqual(get(r.items, 'count').textEdit.newText, 'count');
+assert.strictEqual(get(r.items, 'count').textEdit.range.end.character - get(r.items, 'count').textEdit.range.start.character, 5);
+r = complete('include rt; fn main() {\n let p = rt.new(int, 3);\n let alias = p;\n rt.free(p);\n |\n}');
+assert.deepStrictEqual(get(r.items, 'alias').tags, [1], 'freed aliases are also unavailable');
+r = complete('include rt; fn take(.d p) { rt.free(p); }\nfn main() {\n let p = rt.new(int, 3);\n take(d*p);\n |\n}');
+assert.deepStrictEqual(get(r.items, 'p').tags, [1], 'ownership transfers are unavailable');
+r = complete('fn main() {\n int outer = 1;\n let f = fn(int inner) {\n ou|\n };\n}');
+get(r.items, 'outer');
+r = complete('fn first() { int wrong = 1; return wrong; }\n|\nfn second() { int other = 2; return other; }');
+assert.ok(!r.items.some(c => c.label === 'wrong' || c.label === 'other'), 'top-level completion excludes function locals');
+r = complete('fn main() {\n /* the latest count */\n int latest = 1;\n la|');
+assert.match(get(r.items, 'latest').documentation.value, /the latest count/);
+r = complete('include rt; fn main() { int[?] items = {}; items.| }');
+for (const name of ['len', 'cap', 'push', 'pop', 'resize']) get(r.items, name);
+assert.match(get(r.items, 'push').detail, /push\(int value\)/);
+assert.match(get(r.items, 'pop').detail, /int pop\(\)/);
+assert.match(get(r.items, 'resize').documentation.value, /length/);
+assert.ok(!r.items.some(c => c.label === 'main'), 'array member completion excludes globals');
+r = complete('fn main() { int[3] items = {1,2,3}; items.| }');
+get(r.items, 'len'); get(r.items, 'cap');
+assert.ok(!r.items.some(c => ['push', 'pop', 'resize'].includes(c.label)), 'fixed arrays omit mutation methods');
+r = complete('include rt; fn main() { fixed int[?] items = rt.new(fixed int[?], 3); items.| }');
+get(r.items, 'len'); get(r.items, 'cap');
+assert.ok(!r.items.some(c => ['push', 'pop', 'resize'].includes(c.label)), 'fixed handles omit mutation methods');
+r = complete('include std; fn main() { int[3] items = {1,2,3}; items.| }');
+for (const name of ['find', 'contains', 'find_if', 'any', 'all', 'count', 'map', 'filter', 'reduce']) get(r.items, name);
+assert.match(get(r.items, 'map').documentation.value, /transform.*each element/);
+assert.ok(!r.items.some(c => ['push', 'pop', 'resize'].includes(c.label)), 'library algorithms do not make a fixed array growable');
+r = complete('include rt; fn main() { int[?] items = {}; items.| }');
+assert.ok(!r.items.some(c => ['map', 'filter'].includes(c.label)), 'library methods require an explicit import');
+console.log('ok completion: signatures, docs, fuzzy ranking, scope, array members, shadowing and consumed lifetimes');
diff --git a/tests/drivers/core_lsp_test.js b/tests/drivers/core_lsp_test.js
index 41d0095..983f9aa 100644
--- a/tests/drivers/core_lsp_test.js
+++ b/tests/drivers/core_lsp_test.js
@@ -4,7 +4,7 @@ const fs = require('fs');
const cp = require('child_process');
const path = require('path');
const target = process.argv[2];
-const files = ['src/runtime/rt.zc', 'src/compiler/compiler.zc', 'src/runtime/run.zc', 'src/lib/std.zc', 'src/lsp/server.zc', 'src/tools/build.zc'];
+const files = ['src/core/core.zc', 'src/runtime/rt.zc', 'src/compiler/compiler.zc', 'src/runtime/run.zc', 'src/lib/std.zc', 'src/lib/icon.zc', 'src/lsp/server.zc', 'src/tools/build.zc'];
const frame = message => {
const body = Buffer.from(JSON.stringify(message));
return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]);
@@ -18,6 +18,7 @@ for (const file of files) {
{ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } },
{ jsonrpc: '2.0', id: 2, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } },
{ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version: 2 }, contentChanges: [{ text: text + '\n' }] } },
+ { jsonrpc: '2.0', id: 4, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } },
{ jsonrpc: '2.0', method: 'textDocument/didClose', params: { textDocument: { uri } } },
{ jsonrpc: '2.0', id: 3, method: 'shutdown', params: null },
{ jsonrpc: '2.0', method: 'exit' },
@@ -34,10 +35,11 @@ for (const file of files) {
data = data.subarray(end + 4 + length);
}
const diagnostics = replies.filter(m => m.method === 'textDocument/publishDiagnostics');
- assert.strictEqual(diagnostics.length, 3, file + ': open, change, and close publish diagnostics');
+ assert.ok(diagnostics.length >= 3, file + ': open and change are checked before close, with streamed publications allowed');
const errors = diagnostics.flatMap(m => m.params.diagnostics).filter(d => d.severity === 1);
assert.deepStrictEqual(errors, [], file + ': ' + errors.map(d => (d.range.start.line + 1) + ': ' + d.message).join('\n'));
assert.ok(replies.find(m => m.id === 2).result.data.length, file + ': semantic tokens are available');
- assert.deepStrictEqual(diagnostics[2].params.diagnostics, [], 'close clears diagnostics');
+ assert.ok(replies.find(m => m.id === 4).result.data.length, file + ': the changed source also has semantic tokens');
+ assert.deepStrictEqual(diagnostics.at(-1).params.diagnostics, [], 'close clears diagnostics');
console.log('ok editor open, highlight, change, and close: ' + file);
}
diff --git a/tests/drivers/ext_cache_test.js b/tests/drivers/ext_cache_test.js
index 11bf50b..f389fcc 100644
--- a/tests/drivers/ext_cache_test.js
+++ b/tests/drivers/ext_cache_test.js
@@ -29,8 +29,15 @@ const stub = {
appendMarkdown(v) { this.value += v; return this; }
appendCodeblock(v, language) { this.value += '\n```' + language + '\n' + v + '\n```\n'; return this; }
},
+ CompletionItem: class { constructor(label, kind) { Object.assign(this, { label, kind }); } },
+ CompletionList: class { constructor(items, isIncomplete) { Object.assign(this, { items, isIncomplete }); } },
+ CompletionItemKind: { Variable: 5 },
+ CompletionItemTag: { Deprecated: 1 },
+ CodeActionKind: { QuickFix: 'quickfix' },
+ CodeAction: class { constructor(title, kind) { Object.assign(this, { title, kind }); } },
+ WorkspaceEdit: class { constructor() { this.edits = []; } replace(uri, range, newText) { this.edits.push({ uri, range, newText }); } },
Hover: class { constructor(c) { this.contents = c; } },
- Uri: { parse: s => ({ toString: () => s, fsPath: decodeURIComponent(s.replace(/^file:\/\//, '')) }) },
+ Uri: { file: p => ({ toString: () => 'file://' + p, fsPath: p }), parse: s => ({ toString: () => s, fsPath: decodeURIComponent(s.replace(/^file:\/\//, '')) }) },
workspace: {
workspaceFolders: undefined,
textDocuments: world.textDocuments,
@@ -41,6 +48,7 @@ const stub = {
for (const k of ['onDidChange', 'onDidCreate', 'onDidDelete']) w[k] = f => { w.handlers[k] = f; return { dispose() {} }; };
return w;
},
+ registerTextDocumentContentProvider: () => ({ dispose() {} }),
...['onDidChangeConfiguration', 'onDidOpenTextDocument', 'onDidChangeTextDocument', 'onDidSaveTextDocument', 'onDidCloseTextDocument']
.reduce((o, k) => { o[k] = f => { world.listeners[k] = f; return { dispose() {} }; }; return o; }, {}),
},
@@ -54,7 +62,12 @@ const stub = {
delete() {}, dispose() {},
}),
registerDocumentSemanticTokensProvider: () => ({ dispose() {} }),
+ registerCompletionItemProvider: (sel, p) => { world.completion = p; return { dispose() {} }; },
+ registerCodeActionsProvider: (sel, p) => { world.actions = p; return { dispose() {} }; },
registerHoverProvider: (sel, p) => { world.hover = p; return { dispose() {} }; },
+ registerDocumentFormattingEditProvider: () => ({ dispose() {} }),
+ registerDefinitionProvider: (sel, p) => { world.definition = p; return { dispose() {} }; },
+ registerReferenceProvider: (sel, p) => { world.references = p; return { dispose() {} }; },
},
commands: { registerCommand: () => ({ dispose() {} }) },
};
@@ -116,6 +129,9 @@ const mainUri = 'file://' + main;
const hv = world.hover && await world.hover.provideHover(mainDoc, { line: 1, character: 4 });
const hc = hv && hv.contents[0];
check('the hover is markdown with a zc code block, parsed and not escaped', hc && !hc.escaped && /^```zc\n/.test(hc.value) && /\n```/.test(hc.value), JSON.stringify(hc));
+ const suggestions = await world.completion.provideCompletionItems(mainDoc, { line: 1, character: 4 }, { isCancellationRequested: false });
+ check('completion provider returns signatures and replacement ranges', suggestions instanceof stub.CompletionList && suggestions.items.some(c => c.detail && c.range && c.insertText), JSON.stringify(suggestions && suggestions.items.slice(0, 2)));
+ check('cancelled completion requests are ignored', await world.completion.provideCompletionItems(mainDoc, { line: 1, character: 4 }, { isCancellationRequested: true }) === null);
check('session one: the answer is saved under a key', savedKeys.length === 1 && /^[0-9a-f]{64}$/.test(savedKeys[0]), JSON.stringify(savedKeys));
ext.deactivate();
await sleep(700);
@@ -146,6 +162,15 @@ const mainUri = 'file://' + main;
await sleep(200);
check('the old tree is answered from the cache, without asking the server', world.lines.some(l => /from the cache/.test(l)) && !world.lines.some(l => /-> textDocument\/didChange/.test(l)), JSON.stringify(world.lines.slice(-4)));
+ const expiredDoc = doc(path.join(root, 'expired.zc'), 'include rt; fn main() {\n let used = rt.new(int, 3);\n rt.free(used);\n \n}');
+ const expiredList = await world.completion.provideCompletionItems(expiredDoc, { line: 3, character: 1 }, { isCancellationRequested: false });
+ const expiredItem = expiredList && expiredList.items.find(c => c.label === 'used');
+ check('completion maps consumed lifetimes to the editor strikethrough tag', expiredItem && expiredItem.tags?.[0] === stub.CompletionItemTag.Deprecated && /lifetime ended/.test(expiredItem.documentation?.value));
+ const actionDoc = doc(path.join(root, 'imports.zc'), 'fn main() { rt.put_n(7); }');
+ const actions = await world.actions.provideCodeActions(actionDoc, new stub.Range(0, 15, 0, 15), {}, { isCancellationRequested: false });
+ check('quick fixes reach the editor as workspace edits', actions.some(a => a.kind === 'quickfix' && a.edit.edits.some(e => e.newText === 'include rt;\n')));
+ const cancelled = await world.actions.provideCodeActions(actionDoc, new stub.Range(0, 15, 0, 15), {}, { isCancellationRequested: true });
+ check('cancelled quick-fix requests produce no editor edits', cancelled.length === 0);
ext.deactivate();
await sleep(700);
fs.rmSync(root, { recursive: true, force: true });
diff --git a/tests/drivers/float_backends_test.py b/tests/drivers/float_backends_test.py
index 2cb9473..cdcf48b 100644
--- a/tests/drivers/float_backends_test.py
+++ b/tests/drivers/float_backends_test.py
@@ -5,19 +5,19 @@ import subprocess
root = pathlib.Path(__file__).resolve().parents[2]
commands = [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]
-sources = [(root / 'tests/programs/float.zc').read_text(), '''fn render(bits, precision, mode) { asm { bits @ precision @ mode @ #1236 } return 0; }
+sources = [(root / 'tests/programs/float.zc').read_text(), '''include rt; fn render(bits, precision, mode) { asm { bits @ precision @ mode @ #1236 } return 0; }
fn convert(bits) { return asm { bits @ #1218 }; }
fn main() {
- [4]f32 values;
+ f32[4] values;
values[0] = 1.5; values[1] = -2.25; values[2] = 1e-45; values[3] = 1e40;
- put_n(values[0] == 1.5); put_c(32); put_n(values[1] == -2.25); put_c(32);
- put_n(values[2] > 0.0); put_c(32); put_n(values[3] == 1.0 / 0.0); put_c(10);
- render(0.125, 2, 1); put_c(32); render(-0.125, 2, 1); put_c(32);
- render(1e30, 6, 1); put_c(32); render(1.7976931348623157e308, 6, 1); put_c(10);
- render(0.1, 0, 0); put_c(32); render(1e-5, 0, 0); put_c(32); render(1e16, 0, 0); put_c(10);
- render(9.999, 3, 2); put_c(32); render(5e-324, 3, 2); put_c(32); render(0, 3, 2); put_c(10);
- let negative_zero = 1 << 63; render(negative_zero, 6, 1); put_c(32); render(negative_zero, 0, 0); put_c(10);
- put_n(convert(0.0 / 0.0)); put_c(32); put_n(convert(1.0 / 0.0)); put_c(32); put_n(convert(-1.0 / 0.0)); put_c(10);
+ rt.put_n(values[0] == 1.5); rt.put_c(32); rt.put_n(values[1] == -2.25); rt.put_c(32);
+ rt.put_n(values[2] > 0.0); rt.put_c(32); rt.put_n(values[3] == 1.0 / 0.0); rt.put_c(10);
+ render(0.125, 2, 1); rt.put_c(32); render(-0.125, 2, 1); rt.put_c(32);
+ render(1e30, 6, 1); rt.put_c(32); render(1.7976931348623157e308, 6, 1); rt.put_c(10);
+ render(0.1, 0, 0); rt.put_c(32); render(1e-5, 0, 0); rt.put_c(32); render(1e16, 0, 0); rt.put_c(10);
+ render(9.999, 3, 2); rt.put_c(32); render(5e-324, 3, 2); rt.put_c(32); render(0, 3, 2); rt.put_c(10);
+ let negative_zero = 1 << 63; render(negative_zero, 6, 1); rt.put_c(32); render(negative_zero, 0, 0); rt.put_c(10);
+ rt.put_n(convert(0.0 / 0.0)); rt.put_c(32); rt.put_n(convert(1.0 / 0.0)); rt.put_c(32); rt.put_n(convert(-1.0 / 0.0)); rt.put_c(10);
return 0;
}
''']
@@ -40,7 +40,7 @@ for i, source in enumerate(sources):
# an exponent after a long fractional spelling must reach the parser intact.
literal = '1.' + '0' * 300 + 'e1'
-source = f'fn main() {{ put_n(({literal} as word) == 0x4024000000000000); put_c(10); return 0; }}\n'
+source = f'include rt; fn main() {{ rt.put_n(({literal} as word) == 0x4024000000000000); rt.put_c(10); return 0; }}\n'
for command in commands:
result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=15)
assert result.returncode == 0 and result.stdout == '1\n', (command, result.stdout, result.stderr)
diff --git a/tests/drivers/format_test.js b/tests/drivers/format_test.js
new file mode 100644
index 0000000..94890ae
--- /dev/null
+++ b/tests/drivers/format_test.js
@@ -0,0 +1,88 @@
+// the pretty-printer: it never changes a program's tokens, it is stable (formatting twice is the same as once),
+// it follows .zcformat and the editor's tab settings, and it leaves asm blocks, strings and comments alone.
+// usage: node tests/drivers/format_test.js path/to/zclsp (run from the repository root)
+const assert = require('assert');
+const cp = require('child_process');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const server = process.argv[2];
+const root = path.resolve(__dirname, '..', '..');
+const format = (text, args = []) => cp.spawnSync(server, ['--format', ...args], { input: text, cwd: root, maxBuffer: 1 << 28 }).stdout.toString();
+// tokens of a source, white space dropped; the lines of a block comment are compared without their indent
+function tokens(src) {
+ const out = [];
+ let i = 0;
+ const n = src.length;
+ while (i < n) {
+ const c = src[i];
+ if (/\s/.test(c)) { i++; continue; }
+ if (c === '`') { out.push(src.slice(i)); break; }
+ if (c === '/' && src[i + 1] === '/') { let j = src.indexOf('\n', i); if (j < 0) j = n; out.push(src.slice(i, j).trimEnd()); i = j; continue; }
+ if (c === '/' && src[i + 1] === '*') { let j = src.indexOf('*/', i + 2); j = j < 0 ? n : j + 2; out.push(src.slice(i, j).split('\n').map(l => l.trim()).join('\n')); i = j; continue; }
+ if (c === '"' || c === "'") { let j = i + 1; while (j < n && src[j] !== c && src[j] !== '\n') { if (src[j] === '\\') j++; j++; } out.push(src.slice(i, j + 1)); i = j + 1; continue; }
+ if (/[A-Za-z0-9_\u0080-ï¿¿]/.test(c)) { let j = i; while (j < n && /[A-Za-z0-9_\u0080-ï¿¿]/.test(src[j])) j++; out.push(src.slice(i, j)); i = j; continue; }
+ out.push(c); i++;
+ }
+ // adjacent symbol characters are compared one by one, so merged or split operators still match
+ return out;
+}
+const files = [];
+for (const dir of ['examples', 'examples/learn', 'tests/programs', 'tests/errors', 'tests/typeerrors', 'tests/langerrors', 'src/lib', 'src/compiler', 'src/runtime', 'src/lsp', 'src/tools'])
+ for (const f of fs.readdirSync(path.join(root, dir))) if (f.endsWith('.zc')) files.push(path.join(dir, f));
+assert.ok(files.length > 100, 'the corpus: ' + files.length);
+let checked = 0;
+for (const f of files) {
+ const src = fs.readFileSync(path.join(root, f), 'utf8');
+ const once = format(src);
+ assert.deepStrictEqual(tokens(once), tokens(src), f + ': formatting changed the tokens');
+ assert.strictEqual(format(once), once, f + ': formatting is not stable');
+ assert.ok(once === '' || once.endsWith('\n'), f + ': ends with a line break');
+ if (!src.includes('`')) assert.ok(!/[ \t]+$/m.test(once), f + ': trailing white space');
+ checked++;
+}
+// the behaviour
+const sample = 'fn main(){\nint x=1+2 ;\n if(x>1){\n put_n( x );\n}\n\n\n\nreturn 0;}\n';
+assert.strictEqual(format(sample), 'fn main() {\n\tint x = 1+2;\n\tif (x>1) {\n\t\tput_n(x);\n\t}\n\n\treturn 0; }\n');
+assert.strictEqual(format('fn f(a,b){return a*b;}\n'), 'fn f(a, b) { return a*b; }\n');
+assert.strictEqual(format('fn main() {\n\tswitch (k) {\n\tcase 1: return 1;\n\tdefault:\n\t\treturn 2;\n\t}\n}\n'), 'fn main() {\n\tswitch (k) {\n\t\tcase 1: return 1;\n\t\tdefault:\n\t\t\treturn 2;\n\t}\n}\n');
+assert.strictEqual(format('fn main() {\nlet a = 1 +\n2;\nif (a)\nreturn 1;\nreturn 0;\n}\n'), 'fn main() {\n\tlet a = 1 +\n\t\t2;\n\tif (a)\n\t\treturn 1;\n\treturn 0;\n}\n');
+assert.strictEqual(format('fn put_f(x) { asm { x @ #1220 } return 0; }\n'), 'fn put_f(x) { asm { x @ #1220 } return 0; }\n');
+assert.strictEqual(format('fn k() { asm {\n 0 , ?\n} return 1; }\n'), 'fn k() { asm {\n\t\t0 , ?\n\t} return 1; }\n');
+assert.strictEqual(format('/* a\n * b\n */\nint g = 1; // note\nint h;\n'), '/* a\n * b\n */\nint g = 1; // note\nint h;\n');
+assert.strictEqual(format('fn main() {\n\tif (x)\n\t{\n\t\ty();\n\t}\n\telse\n\t{\n\t\tz();\n\t}\n}'), 'fn main() {\n\tif (x)\n\t{\n\t\ty();\n\t}\n\telse\n\t{\n\t\tz();\n\t}\n}\n');
+assert.strictEqual(format('fn main() { return 0; }\n`data stays\n as it is\n'), 'fn main() { return 0; }\n`data stays\n as it is\n');
+// .zcformat next to a document: found from the document's directory upward; the editor's tab settings apply without one
+const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'zcfmt-'));
+fs.mkdirSync(path.join(dir, 'a', 'b'), { recursive: true });
+fs.writeFileSync(path.join(dir, '.zcformat'), '# comment\nUseTab: false\nIndentWidth: 2\nMaxEmptyLines: 0\nNormalizeSpacing: false\n');
+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 lsp(uri, text, options) {
+ const input = Buffer.concat([
+ { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
+ { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } },
+ { jsonrpc: '2.0', id: 2, method: 'textDocument/formatting', params: { textDocument: { uri }, options } },
+ { jsonrpc: '2.0', method: 'exit' }].map(frame));
+ const r = cp.spawnSync(server, process.argv.slice(3), { input, cwd: root, maxBuffer: 1 << 26 });
+ let d = r.stdout, caps, res;
+ while (d.length) {
+ const e = d.indexOf('\r\n\r\n'), n = Number(/Content-Length:\s*(\d+)/i.exec(d.subarray(0, e).toString())[1]);
+ const m = JSON.parse(d.subarray(e + 4, e + 4 + n)); d = d.subarray(e + 4 + n);
+ if (m.id === 1) caps = m.result.capabilities;
+ if (m.id === 2) res = m.result;
+ }
+ return { caps, res };
+}
+const messy = 'fn main(){\nif(1){\nput_n( 1 );\n\n\nput_c(10);}\nreturn 0;}\n';
+let r = lsp('file://' + path.join(dir, 'a', 'b', 'x.zc'), messy, { tabSize: 8, insertSpaces: false });
+assert.ok(r.caps.documentFormattingProvider, 'formatting is advertised');
+assert.strictEqual(r.res.length, 1);
+assert.strictEqual(r.res[0].newText, 'fn main(){\n if(1){\n put_n( 1 );\n put_c(10);}\n return 0;}\n', '.zcformat from two directories up wins over the editor');
+assert.deepStrictEqual(r.res[0].range.start, { line: 0, character: 0 });
+fs.rmSync(path.join(dir, '.zcformat'));
+r = lsp('file://' + path.join(dir, 'a', 'y.zc'), messy, { tabSize: 3, insertSpaces: true });
+assert.strictEqual(r.res[0].newText, 'fn main() {\n if (1) {\n put_n(1);\n\n put_c(10); }\n return 0; }\n', 'the editor settings without a .zcformat');
+r = lsp('file://' + path.join(dir, 'a', 'y.zc'), 'fn main() {\n\treturn 0;\n}\n', { tabSize: 4, insertSpaces: false });
+assert.deepStrictEqual(r.res, [], 'a formatted document gets no edit');
+fs.rmSync(dir, { recursive: true });
+console.log('ok formatter: ' + checked + ' files keep their tokens and format stably; options and edits');
diff --git a/tests/drivers/frame_growth_test.py b/tests/drivers/frame_growth_test.py
index 74156e3..6e03cbc 100644
--- a/tests/drivers/frame_growth_test.py
+++ b/tests/drivers/frame_growth_test.py
@@ -4,22 +4,22 @@ import subprocess
import sys
command = sys.argv[1:]
-source = '''unchecked int descend(int depth, int* root) {
- [2000]int padding;
+source = '''include rt; unchecked int descend(int depth, int* root) {
+ int[2000] padding;
padding[0] = depth;
- if (depth == 0) { gc(); return *root; }
+ if (depth == 0) { rt.gc(); return *root; }
return descend(depth - 1, root) + padding[0];
}
unchecked int oversized(int* root) {
- [300000]int values;
+ int[300000] values;
values[0] = *root; values[299999] = 9;
- gc(); return values[0] + values[299999];
+ rt.gc(); return values[0] + values[299999];
}
unchecked fn main() {
int value = 7;
- put_n(descend(300, &value)); put_c(32);
- put_n(oversized(&value)); put_c(32);
- gc(); put_n(frame_node == 0); put_c(10);
+ rt.put_n(descend(300, &value)); rt.put_c(32);
+ rt.put_n(oversized(&value)); rt.put_c(32);
+ rt.gc(); rt.put_n(rt.frame_node == 0); rt.put_c(10);
return 0;
}
'''
diff --git a/tests/drivers/fuzz_compiler.py b/tests/drivers/fuzz_compiler.py
new file mode 100644
index 0000000..512ec26
--- /dev/null
+++ b/tests/drivers/fuzz_compiler.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+"""Deterministic grammar fuzzing for the parser, runtime, and ownership paths.
+
+Usage: python3 tests/drivers/fuzz_compiler.py [path/to/zy] [path/to/zym] [--js]
+"""
+import random
+import subprocess
+import sys
+
+
+args = [arg for arg in sys.argv[1:] if arg != "--js"]
+js = "--js" in sys.argv[1:]
+zy = args[0] if len(args) > 0 else "bin/zy"
+zym = args[1] if len(args) > 1 else "bin/zym"
+rng = random.Random(0x5A17)
+values = []
+library_values = []
+
+
+def expr(depth):
+ if depth == 0 or rng.randrange(4) == 0:
+ value = rng.randrange(10)
+ return str(value), value
+ left, a = expr(depth - 1)
+ right, b = expr(depth - 1)
+ op = rng.choice(("+", "-", "*"))
+ if op == "+":
+ value = a + b
+ elif op == "-":
+ value = a - b
+ else:
+ value = a * b
+ return f"({left} {op} {right})", value
+
+
+lines = ["include std;", "fn churn(n) {", " int i = 0;", " while (i < n) {", " { int* p = new(int); *p = i; }", " i++;", " }", " return 0;", "}", "fn stable() { return 73; }", "fn main() {", " let base = live_objects();"]
+for _ in range(128):
+ source, value = expr(4)
+ lines.append(f" put_n({source}); put_c(32);")
+ values.append(value)
+for _ in range(32):
+ a = "".join(rng.choice("abc") for _ in range(rng.randrange(11)))
+ b = "".join(rng.choice("abc") for _ in range(rng.randrange(6)))
+ lines.append(f' put_n(Text.len("{a}")); put_c(32); put_n(Text.eq("{a}", "{b}")); put_c(32);')
+ lines.append(f' put_n(Text.starts("{a}", "{b}")); put_c(32); put_n(Text.ends("{a}", "{b}")); put_c(32); put_n(Text.find("{a}", "{b}")); put_c(32);')
+ lines.append(f' put_n(Bytes.len(b"{a}")); put_c(32); put_n(Bytes.eq(b"{a}", b"{b}")); put_c(32);')
+ lines.append(f' put_n(Bytes.starts(b"{a}", b"{b}")); put_c(32); put_n(Bytes.ends(b"{a}", b"{b}")); put_c(32); put_n(Bytes.find(b"{a}", b"{b}")); put_c(32);')
+ library_values += [len(a), int(a == b), int(a.startswith(b)), int(a.endswith(b)), a.find(b), len(a.encode()), int(a == b), int(a.startswith(b)), int(a.endswith(b)), a.find(b)]
+lines += [
+ " churn(300); gc(); put_n(live_objects() - base); put_c(32);",
+ ' char* source = "fn f() { return 7; }"; let parsed = tokens(source);',
+ ' put_n(parsed[0] > 0); put_c(32); put_n(eq_w(source, "fn f() { return 7; }")); put_c(32);',
+ ' char* eval_source = "return 42;"; int e = 0;',
+ ' while (e < 16) { let q = tokens(eval_source); put_n(eval(q)); put_c(32); e++; }',
+ ' put_n(stable()); put_c(32); put_n(eq_w(eval_source, "return 42;")); put_c(10);',
+ " return 0;",
+ "}",
+]
+program = "\n".join(lines) + "\n"
+expected = " ".join(str(x) for x in values + library_values + [0, 1, 1, *([42] * 16), 73, 1]) + "\n"
+
+
+def run(binary):
+ result = subprocess.run(binary, input=program, text=True, capture_output=True, timeout=30)
+ if result.returncode or result.stdout != expected:
+ actual_words = result.stdout.split()
+ expected_words = expected.split()
+ mismatch = next((i for i, pair in enumerate(zip(actual_words, expected_words)) if pair[0] != pair[1]), min(len(actual_words), len(expected_words)))
+ raise SystemExit(
+ f"{' '.join(binary)} failed deterministic fuzz seed 0x5a17\n"
+ f"exit={result.returncode}, token mismatch at {mismatch}\n"
+ f"expected={expected_words[max(0, mismatch - 3):mismatch + 4]!r}\n"
+ f"stdout={actual_words[max(0, mismatch - 3):mismatch + 4]!r}\n"
+ f"stderr={result.stderr!r}"
+ )
+ return result.stdout
+
+
+hosts = [(zy, [zy]), (zym, [zym])]
+if js:
+ hosts.append(("javascript", ["node", "src/js/tinyvm.js"]))
+outputs = [run(command) for _, command in hosts]
+if any(output != outputs[0] for output in outputs[1:]):
+ raise SystemExit("compiler hosts disagreed on deterministic fuzz output")
+print("ok compiler/runtime/stdlib fuzz: 128 generated expressions, 300 scoped allocations and GC, "
+ "16 eval recompiles, unchanged source strings, and 32 randomized Text/Bytes cases on "
+ + ", ".join(name for name, _ in hosts))
diff --git a/tests/drivers/fuzz_lsp.js b/tests/drivers/fuzz_lsp.js
new file mode 100644
index 0000000..e22a44b
--- /dev/null
+++ b/tests/drivers/fuzz_lsp.js
@@ -0,0 +1,89 @@
+// Deterministic source/protocol fuzzing for diagnostics, caching, and recovery.
+// Usage: node tests/drivers/fuzz_lsp.js path/to/zclsp
+const { spawnSync } = require('child_process');
+const server = process.argv[2] || 'bin/zclsp';
+let state = 0x5a17;
+function next() { state = (Math.imul(state, 1664525) + 1013904223) >>> 0; return state; }
+function frame(m) {
+ const body = Buffer.from(JSON.stringify(m), 'utf8');
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]);
+}
+function parse(buf) {
+ const out = []; let at = 0;
+ while (at < buf.length) {
+ const end = buf.indexOf('\r\n\r\n', at);
+ if (end < 0) throw new Error('truncated LSP header');
+ const match = /Content-Length: (\d+)/.exec(buf.slice(at, end).toString());
+ if (!match) throw new Error('missing Content-Length');
+ const size = Number(match[1]), start = end + 4;
+ if (start + size > buf.length) throw new Error('truncated LSP body');
+ out.push(JSON.parse(buf.slice(start, start + size).toString('utf8')));
+ at = start + size;
+ }
+ return out;
+}
+const msgs = [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }];
+const docs = [];
+for (let i = 0; i < 32; i++) {
+ const uri = `file:///fuzz-${i}.zc`, n = next() % 1000;
+ const broken = i % 3 === 0;
+ const initial = `fn calc_${i}(x) { return (x + ${n}) * 2; }\nfn main() { let y = calc_${i}(${n})${broken ? '' : ';'} return y; }\n`;
+ const fixed = `fn calc_${i}(x) { return (x + ${n}) * 2; }\nfn main() { let y = calc_${i}(${n}); return y; }\n`;
+ docs.push({ uri, broken });
+ msgs.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text: initial } } });
+ msgs.push({ jsonrpc: '2.0', id: 100 + i, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } });
+ msgs.push({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version: 2 }, contentChanges: [{ text: fixed }] } });
+}
+const stdUri = 'file:///fuzz-std.zc';
+msgs.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri: stdUri, languageId: 'zc', version: 1, text: 'include std;\nfn main() { return Text.starts("alpha", "al"); }\n' } } });
+
+// Keep one server alive while documents are repeatedly opened, made malformed,
+// repaired, closed, and reopened at a new version. This exercises cache
+// invalidation and stale-diagnostic cleanup across a long-lived editor session.
+const churn = new Map();
+for (let i = 0; i < 96; i++) {
+ const slot = i % 4, uri = `file:///churn-${slot}.zc`, n = next() % 10000;
+ const malformed = `fn churn_${slot}(x) { let y = (x + ${n}); return y; }\nfn main() { return churn_${slot}(${n}) }\n`;
+ const valid = `fn churn_${slot}(x) { let y = (x + ${n}); return y; }\nfn main() { return churn_${slot}(${n}); }\n`;
+ const version = Math.floor(i / 4) * 3 + 1;
+ msgs.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version, text: malformed } } });
+ msgs.push({ jsonrpc: '2.0', id: 1000 + i, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } });
+ msgs.push({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version: version + 1 }, contentChanges: [{ text: valid }] } });
+ msgs.push({ jsonrpc: '2.0', method: 'textDocument/didClose', params: { textDocument: { uri } } });
+ churn.set(uri, valid);
+}
+let churnVersion = 100;
+for (const [uri, text] of churn) {
+ msgs.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: churnVersion++, text } } });
+}
+msgs.push({ jsonrpc: '2.0', method: 'exit' });
+const run = spawnSync(server, [], { input: Buffer.concat(msgs.map(frame)), timeout: 60000, maxBuffer: 1 << 28 });
+if (run.error || run.status !== 0) throw run.error || new Error(`server exit ${run.status}: ${run.stderr}`);
+const out = parse(run.stdout);
+const diagnostics = new Map();
+for (const m of out) {
+ if (m.method === 'textDocument/publishDiagnostics') {
+ const items = diagnostics.get(m.params.uri) || [];
+ items.push(m.params.diagnostics);
+ diagnostics.set(m.params.uri, items);
+ }
+}
+for (let i = 0; i < docs.length; i++) {
+ const { uri, broken } = docs[i], batches = diagnostics.get(uri) || [];
+ if (batches.length < 2) throw new Error(`${uri}: expected diagnostics for open and change`);
+ if (broken && !batches[0].some(d => d.severity === 1)) throw new Error(`${uri}: malformed source was not diagnosed`);
+ if (batches[batches.length - 1].some(d => d.severity === 1)) throw new Error(`${uri}: valid edit retained an error`);
+ const response = out.find(m => m.id === 100 + i);
+ if (!response || response.error || !response.result || response.result.data.length % 5) throw new Error(`${uri}: semantic tokens response was malformed`);
+}
+for (let i = 0; i < 96; i++) {
+ const response = out.find(m => m.id === 1000 + i);
+ if (!response || response.error || !response.result || response.result.data.length % 5) throw new Error(`churn request ${i}: malformed semantic tokens response`);
+}
+const stdDiag = diagnostics.get(stdUri) || [];
+if (!stdDiag.length || stdDiag[stdDiag.length - 1].some(d => d.severity === 1)) throw new Error('stdlib include failed LSP source analysis');
+for (const uri of churn.keys()) {
+ const batches = diagnostics.get(uri) || [];
+ if (!batches.length || batches[batches.length - 1].some(d => d.severity === 1)) throw new Error(`${uri}: reopened document retained stale diagnostics`);
+}
+console.log(`ok lsp fuzz: ${docs.length} deterministic open/change sequences, 96 long-session cache churns, semantic tokens, and include std`);
diff --git a/tests/drivers/fuzz_malformed.py b/tests/drivers/fuzz_malformed.py
new file mode 100644
index 0000000..5bf97cf
--- /dev/null
+++ b/tests/drivers/fuzz_malformed.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+"""Deterministic malformed-source differential fuzzing for compiler hosts.
+
+Usage: python3 tests/drivers/fuzz_malformed.py [native] [microcore]
+"""
+import random
+import subprocess
+import sys
+
+
+bins = sys.argv[1:] or ["bin/zy", "bin/zym"]
+if len(bins) == 1:
+ bins.append(bins[0])
+rng = random.Random(0xC0DE)
+tokens = [
+ b"fn", b"let", b"int", b"char*", b"word", b"return", b"if", b"else",
+ b"while", b"for", b"include", b"std", b";", b"{", b"}", b"(", b")",
+ b"[", b"]", b"=", b",", b".", b":", b"?", b"+", b"-", b"*", b"&",
+ b"|", b"!", b"~", b'"text"', b"'x'", b"0", b"1", b"_", b"NIF",
+ b"asm", b"unchecked", b"\x00", b"\xff", "λ".encode(),
+]
+cases = []
+for i in range(256):
+ words = [rng.choice(tokens) for _ in range(rng.randrange(1, 100))]
+ cases.append(b" ".join(words) + b"\n")
+
+# Mutate a small valid program around parser and analyzer boundaries. Keeping
+# each mutation bounded makes a failing seed easy to reproduce and minimize.
+base = b"include std;\nfn main() { let x = 1; return x; }\n"
+for _ in range(64):
+ data = bytearray(base)
+ for __ in range(rng.randrange(1, 5)):
+ at = rng.randrange(len(data))
+ if rng.randrange(2):
+ data[at:at + 1] = rng.choice(tokens)
+ else:
+ del data[at:at + 1]
+ cases.append(bytes(data))
+
+
+def run(binary, source):
+ return subprocess.run(
+ [binary], input=b"typecheck on;\n" + source, capture_output=True, timeout=3
+ )
+
+
+for i, source in enumerate(cases):
+ results = []
+ for binary in bins:
+ try:
+ result = run(binary, source)
+ except subprocess.TimeoutExpired:
+ raise SystemExit(f"{binary} timed out on malformed seed 0xC0DE case {i}: {source!r}")
+ if result.returncode < 0:
+ raise SystemExit(
+ f"{binary} crashed on malformed seed 0xC0DE case {i}: "
+ f"signal {-result.returncode}, source={source!r}, stderr={result.stderr!r}"
+ )
+ results.append((result.returncode, result.stdout, result.stderr))
+ if any(result != results[0] for result in results[1:]):
+ raise SystemExit(f"compiler hosts disagreed on malformed seed 0xC0DE case {i}: {source!r}")
+
+print(f"ok malformed-source fuzz: {len(cases)} bounded token and byte mutations agree on {len(bins)} compiler hosts")
diff --git a/tests/drivers/fuzz_wasm.js b/tests/drivers/fuzz_wasm.js
new file mode 100644
index 0000000..b0982d4
--- /dev/null
+++ b/tests/drivers/fuzz_wasm.js
@@ -0,0 +1,38 @@
+// Deterministic compiler/runtime/stdlib fuzzing through the WebAssembly host.
+// Usage: node tests/drivers/fuzz_wasm.js path/to/zc.wasm
+const fs = require('fs');
+const ZC = require('../../src/web/zc.js');
+
+let state = 0x5a17;
+function next() { state = (Math.imul(state, 1664525) + 1013904223) >>> 0; return state; }
+function expr(depth) {
+ if (!depth || next() % 4 === 0) { const n = next() % 10; return [String(n), n]; }
+ const [left, a] = expr(depth - 1), [right, b] = expr(depth - 1), op = ['+', '-', '*'][next() % 3];
+ return [`(${left} ${op} ${right})`, op === '+' ? a + b : op === '-' ? a - b : a * b];
+}
+function quote(s) { return '"' + s + '"'; }
+
+(async () => {
+ const lines = ['include std;', 'fn churn(n) { int i = 0; while (i < n) { { int* p = new(int); *p = i; } i++; } return 0; }', 'fn stable() { return 73; }', 'fn main() {', ' let base = live_objects();'];
+ const expected = [];
+ for (let i = 0; i < 128; i++) {
+ const [source, value] = expr(4);
+ lines.push(` put_n(${source}); put_c(32);`); expected.push(value);
+ }
+ for (let i = 0; i < 32; i++) {
+ const a = Array.from({ length: next() % 11 }, () => 'abc'[next() % 3]).join('');
+ const b = Array.from({ length: next() % 6 }, () => 'abc'[next() % 3]).join('');
+ lines.push(` put_n(Text.len(${quote(a)})); put_c(32); put_n(Text.eq(${quote(a)}, ${quote(b)})); put_c(32);`);
+ lines.push(` put_n(Text.starts(${quote(a)}, ${quote(b)})); put_c(32); put_n(Text.ends(${quote(a)}, ${quote(b)})); put_c(32); put_n(Text.find(${quote(a)}, ${quote(b)})); put_c(32);`);
+ lines.push(` put_n(Bytes.len(b${quote(a)})); put_c(32); put_n(Bytes.eq(b${quote(a)}, b${quote(b)})); put_c(32);`);
+ lines.push(` put_n(Bytes.starts(b${quote(a)}, b${quote(b)})); put_c(32); put_n(Bytes.ends(b${quote(a)}, b${quote(b)})); put_c(32); put_n(Bytes.find(b${quote(a)}, b${quote(b)})); put_c(32);`);
+ expected.push(a.length, Number(a === b), Number(a.startsWith(b)), Number(a.endsWith(b)), a.indexOf(b), a.length, Number(a === b), Number(a.startsWith(b)), Number(a.endsWith(b)), a.indexOf(b));
+ }
+ lines.push(' churn(300); gc(); put_n(live_objects() - base); put_c(32);', ' put_n(stable()); put_c(10);', ' return 0;', '}');
+ expected.push(0, 73);
+ const source = lines.join('\n') + '\n', want = expected.join(' ') + '\n';
+ const zc = await ZC.load(fs.readFileSync(process.argv[2] || 'src/web/zc.wasm'));
+ const result = await zc.run(source);
+ if (result.output !== want) throw new Error(`wasm fuzz mismatch\nexpected: ${want.slice(0, 180)}\nactual: ${result.output.slice(0, 180)}\nmessages: ${result.messages}`);
+ console.log('ok wasm compiler/runtime/stdlib fuzz: 128 expressions, 300 scoped allocations and GC, and 32 Text/Bytes cases');
+})().catch(error => { console.error(error); process.exitCode = 1; });
diff --git a/tests/drivers/hints_test.js b/tests/drivers/hints_test.js
new file mode 100644
index 0000000..8bb1934
--- /dev/null
+++ b/tests/drivers/hints_test.js
@@ -0,0 +1,31 @@
+// hints are informational diagnostics (severity 3) and follow positional hints on / small / off directives.
+// usage: node tests/drivers/hints_test.js path/to/zclsp
+const assert = require('assert');
+const cp = require('child_process');
+const server = process.argv[2];
+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 diagnostics(text) {
+ const uri = 'file:///hints.zc';
+ const input = Buffer.concat([
+ { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
+ { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } },
+ { jsonrpc: '2.0', method: 'exit' }].map(frame));
+ const r = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000, maxBuffer: 4 << 20 });
+ assert.strictEqual(r.status, 0, String(r.stderr));
+ let d = r.stdout, out = [];
+ while (d.length) {
+ const e = d.indexOf('\r\n\r\n'), n = Number(/Content-Length:\s*(\d+)/i.exec(d.subarray(0, e).toString())[1]);
+ const m = JSON.parse(d.subarray(e + 4, e + 4 + n)); d = d.subarray(e + 4 + n);
+ if (m.method === 'textDocument/publishDiagnostics') out = m.params.diagnostics;
+ }
+ return out;
+}
+const names = ds => ds.map(d => d.message.split(' ')[0]).sort().join(',');
+const flow = 'fn main() {\n let a = 1;\n hints small;\n let b = 2;\n int c = 5 * 8;\n hints off;\n let d = 3;\n int k = 3 * 4;\n hints on;\n let e = 4;\n return 0;\n}\n';
+const ds = diagnostics(flow);
+assert.ok(ds.length > 0 && ds.every(d => d.severity === (d.range.start.line === 3 || d.range.start.line === 4 ? 4 : 3)), 'small uses compact severity 4; on uses informational severity 3: ' + JSON.stringify(ds));
+assert.strictEqual(names(ds), 'a,b,c,e,multiplying', 'off suppresses hints; small retains them with compact markers');
+const style = diagnostics('fn main() {\n int n = 1;\n n = n * 8;\n hints small;\n n = n * 16;\n return n;\n}\n');
+assert.strictEqual(style.filter(d => /is a shift/.test(d.message)).length, 2, 'small retains style hints with compact markers');
+assert.strictEqual(diagnostics('fn main() { int x = ; return 0; }').filter(d => d.severity === 1).length > 0, true);
+console.log('ok hints are blue information diagnostics with positional on, small and off');
diff --git a/tests/drivers/hover_features_test.js b/tests/drivers/hover_features_test.js
index 895e923..d6e1226 100644
--- a/tests/drivers/hover_features_test.js
+++ b/tests/drivers/hover_features_test.js
@@ -38,15 +38,15 @@ for (const type of ['word', 'int', 'char', 'bool', 'float', 'f64', 'f32', 'byte'
assert.ok(value && value.startsWith('```zc\n') && value.includes('\n\n'), type + ' has formatted built-in documentation');
}
assert.match(hover('void noop() {}\nfn main() { return 0; }\n', 'void'), /no meaningful result/);
-const array = 'fn main() {\n [4]u8 values;\n return values[0];\n}\n';
+const array = 'fn main() {\n u8[4] values;\n return values[0];\n}\n';
assert.match(hover(array, '[4]'), /fixed length of 4/);
-assert.match(hover(array, 'values', 1), /type \[4\]u8/);
+assert.match(hover(array, 'values', 1), /\(u8\[4\]\) let values/);
const struct = 'struct pair { int left; float right; };\nfn main() { pair value; return value.left; }\n';
assert.match(hover(struct, 'pair', 1), /int left;[\s\S]*float right;/);
-assert.match(hover(struct, 'value', 1), /type pair/);
+assert.match(hover(struct, 'value', 1), /\(pair\) let value/);
const fields = 'struct pair { int /* left field */ left, /* right field */ right; };\nfn main() { pair p; return p.left + p.right; }\n';
-assert.match(hover(fields, 'left +'), /field; type int[\s\S]*left field/);
-assert.match(hover(fields, 'right;'), /field; type int[\s\S]*right field/);
+assert.match(hover(fields, 'left +'), /\(int\) let left \/\/ field;[\s\S]*left field/);
+assert.match(hover(fields, 'right;'), /\(int\) let right \/\/ field;[\s\S]*right field/);
const empty = '/**/ int helper() { return 1; }\nfn main() { return helper(); }\n';
assert.match(hover(empty, 'helper();'), /int helper\(\)/);
const variableDocs = 'fn main() {\n int /* first variable */ first = 1, /* second variable */ second = 2;\n return first + second;\n}\n';
@@ -63,23 +63,69 @@ const functionDocs = '/**\n * adds **one** to its argument.\n */\nint add(int x)
assert.match(hover(functionDocs, 'add(2)'), /adds \*\*one\*\*/);
const inferred = 'fn twice(x) { return x * 2; }\nfn main() { let value = twice(3); return value; }\n';
assert.match(hover(inferred, 'twice(3)'), /int twice\(int x\)/);
-assert.match(hover(inferred, 'x *'), /type int/);
-assert.match(hover(inferred, 'value;'), /type int/);
+assert.match(hover(inferred, 'x *'), /\(int\) let x \/\/ parameter;/);
+assert.match(hover(inferred, 'value;'), /\(int\) let value/);
const pointer = 'int read(int* p) { return p[0]; }\nfn main() { int value = 3; return read(&value); }\n';
-assert.match(hover(pointer, 'p[0]'), /type int\*[\s\S]*pointer to `int`/);
-const alias = 'type row = [3]int;\nfn main() { row values; return values[0]; }\n';
-assert.match(hover(alias, 'row values'), /\[3\]int[\s\S]*fixed length of 3/);
+assert.match(hover(pointer, 'p[0]'), /\(int\*\) let p \/\/ parameter;[\s\S]*pointer to `int`/);
+const alias = 'type row = int[3];\nfn main() { row values; return values[0]; }\n';
+assert.match(hover(alias, 'row values'), /int\[3\][\s\S]*fixed length of 3/);
const enumeration = 'enum color { red, green = 5, blue };\nfn main() { color shade = green; return shade; }\n';
-assert.match(hover(enumeration, 'shade;'), /type color/);
-const dynamic = 'fn main() { []int items; push(items, 3); return len(items); }\n';
-assert.match(hover(dynamic, 'items);'), /type \[\]int[\s\S]*growable array handle/);
+assert.match(hover(enumeration, 'shade;'), /\(color\) let shade/);
+const dynamic = 'fn main() { int[?] items; push(items, 3); return len(items); }\n';
+assert.match(hover(dynamic, 'items);'), /\(int\[\?\]\) let items[\s\S]*growable array handle/);
const callable = 'fn main() { let apply = fn(int x) { return x + 1; }; return apply(2); }\n';
-assert.match(hover(callable, 'apply(2)'), /type .*\([\s\S]*callable value/);
-assert.match(hover(callable, 'apply(2)'), /type int\(int\)/);
+assert.match(hover(callable, 'apply(2)'), /\(.*\([\s\S]*callable value/);
+assert.match(hover(callable, 'apply(2)'), /\(int\(int\)\) let.m apply/);
const floatClosure = 'fn main() { let f = fn(float x) { return x + 1.5; }; let result = f(2.0); return result; }\n';
-assert.match(hover(floatClosure, 'result;'), /type float/);
-const inferredPointer = 'struct node { int value; };\nfn make_node() { return new(node); }\nfn main() { let p = make_node(); return p.value; }\n';
-assert.match(hover(inferredPointer, 'p.value'), /type node\*/);
-const aggregateAlias = 'fn main() { [3]int a; a[1] = 7; let alias = a; return alias[1]; }\n';
-assert.match(hover(aggregateAlias, 'alias[1]'), /type int\*/);
+assert.match(hover(floatClosure, 'result;'), /\(float\) let result/);
+const inferredPointer = 'include rt;\nstruct node { int value; };\nfn make_node() { return new(node); }\nfn main() { let p = make_node(); return p.value; }\n';
+assert.match(hover(inferredPointer, 'p.value'), /\(node\*\) let.m p/);
+const aggregateAlias = 'fn main() { int[3] a; a[1] = 7; let alias = a; return alias[1]; }\n';
+assert.match(hover(aggregateAlias, 'alias[1]'), /\(int\*\) let alias/);
+const sections = '// --- values ---\n// these declarations form a group.\nconst int limit = 7;\nfn main() { return limit; }';
+assert.ok(!hover(sections, 'limit;').includes('these declarations form a group'));
+assert.ok(!hover(sections, 'limit;').includes('--- values ---'));
+const blockSection = '/* ---- values ---- */\nint limit = 7;\nfn main() { return limit; }';
+assert.ok(!hover(blockSection, 'limit;').includes('---- values ----'));
+const trailing = 'fn main() {\n int n = 7; // the current count\n int next = 2;\n return n + next;\n}';
+assert.match(hover(trailing, 'n +'), /the current count/);
+assert.ok(!hover(trailing, 'next;').includes('the current count'));
+const trailingBlock = 'fn main() { int a = 1, b = 2; /* final count */\n return a + b; }';
+assert.ok(!hover(trailingBlock, 'a +').includes('final count'));
+assert.match(hover(trailingBlock, 'b;'), /final count/);
+const adjacent = 'fn main() { int a /* first count */ = 1, b /* second count */ = 2; return a + b; }';
+assert.match(hover(adjacent, 'a +'), /first count/);
+assert.ok(!hover(adjacent, 'a +').includes('second count'));
+assert.match(hover(adjacent, 'b;'), /second count/);
+// section headings end a doc block; trailing comments belong to their own line's declaration
+const sectionDocs = '// ---- one ----\n\n// adds.\nint add(int a, int b) { return a + b; } // after add\n// overview\n// ---- two ----\nint sub(int a, int b) { return a - b; }\n// ---- three ----\n// own doc\nint mul(int a, int b) { return a * b; }\nint g = 3; /* after g */\nfn main() { return add(1, 2) + sub(3, 1) + mul(2, 2) + g; }\n';
+const addHover = hover(sectionDocs, 'add(1');
+assert.ok(addHover.includes('adds.') && addHover.includes('after add') && !addHover.includes('overview') && !addHover.includes('after g'), addHover);
+assert.strictEqual(hover(sectionDocs, 'sub(3'), '```zc\nint sub(int a, int b)\n```');
+const mulHover = hover(sectionDocs, 'mul(2');
+assert.ok(!mulHover.includes('own doc') && !mulHover.includes('three') && !mulHover.includes('after'), mulHover);
+assert.ok(hover(sectionDocs, 'g; }').includes('after g'));
+// ownership and transfers explained in hovers
+const lifeDocs = 'include rt; struct node { int v; node* next; };\nint*.u make(int n) { int* q = new(int); *q = n; return u*q; }\nfn take(.d p) { return p[0]; }\nfn main() {\n let a = make(1);\n node* b = new(node), c = new(node);\n b.next = c;\n node* r = b.next;\n let d = new(3);\n take(d*d);\n return a[0] + r.v;\n}\n';
+assert.match(hover(lifeDocs, 'q = new'), /let.m q[\s\S]*passed up: returned on line 2/);
+assert.match(hover(lifeDocs, 'a ='), /let.m a[\s\S]*freed where its block ends on line 12/);
+assert.match(hover(lifeDocs, 'r.v'), /borrows from `b`, declared on line 6/);
+assert.match(hover(lifeDocs, 'c = new'), /passed down: stored in a container `b` on line 7/);
+assert.match(hover(lifeDocs, 'd = new'), /passed down: given to a d parameter on line 10/);
+const declaredResult = 'fn main() { let f = fn:float(int x) { return x; }; return f(2); }\n';
+assert.match(hover(declaredResult, 'f(2)'), /\(float\(int\)\) let.m f/);
+const nonDocs = '//: scratch region overview\nint scratch = 1;\n/* public count */ int count = 2; //: implementation note\nfn main() { return scratch + count; }';
+assert.ok(!hover(nonDocs, 'scratch +').includes('scratch region overview'));
+assert.match(hover(nonDocs, 'count;'), /public count/);
+assert.ok(!hover(nonDocs, 'count;').includes('implementation note'));
+const globals = 'let hello = "hello", prog[20000];\nint count = 3;\nfn main() { return hello[0] + prog[0] + count; }\n';
+for (const [name, type, use] of [['hello', 'char*', 'hello[0]'], ['prog', 'word[20000]', 'prog[0]'], ['count', 'int', 'count;']]) {
+ for (const needle of [name, use]) {
+ const value = hover(globals, needle);
+ assert.ok(value.startsWith('```zc\n(' + type + ') let ' + name + ' // global;\n```'), value);
+ assert.ok(!value.includes('; type '), value);
+ }
+}
+assert.match(hover('include run; fn main() { return run.sval[0]; }', 'sval'), /symbol[\s\S]*value/i);
+assert.match(hover('include rt; fn main() { return rt.len_s(b"bytes"); }', 'len_s'), /number of bytes in a packed byte string/i);
console.log('ok built-in and composite types, inferred signatures, and separate block docs');
diff --git a/tests/drivers/hover_surface_test.js b/tests/drivers/hover_surface_test.js
new file mode 100644
index 0000000..13a9a3f
--- /dev/null
+++ b/tests/drivers/hover_surface_test.js
@@ -0,0 +1,65 @@
+// hover declarations and instructions in the source-only documents opened by f12.
+const assert = require('assert');
+const fs = require('fs');
+const cp = require('child_process');
+const path = require('path');
+const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); };
+const messages = [{ id: 1, method: 'initialize', params: {} }], checks = [];
+let id = 2;
+function doc(uri, text) {
+ messages.push({ method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } });
+ return (needle, offset = 0) => {
+ const at = text.indexOf(needle) + offset; assert.ok(at >= 0, needle);
+ const before = text.slice(0, at), rid = id++;
+ messages.push({ id: rid, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } } });
+ return rid;
+ };
+}
+const compiler = fs.readFileSync('src/compiler/compiler.zc', 'utf8');
+const ask = doc('tinyvm:/compiler.zc', compiler);
+checks.push([ask('ifrcap = NIF', 9), /const int NIF = 431/, /431/]);
+checks.push([ask('capture_width = NCAP', 16), /const int NCAP = 32/, /initial capture capacity/, /32/]);
+checks.push([ask('closure_cap = NCLO', 14), /const int NCLO = 8/, /initial closure nesting capacity/]);
+checks.push([ask('clo_n = clo_n_initial', 8), /word\[NCLO\] clo_n_initial/]);
+checks.push([ask('int nclo, closure_cap', 10), /\(int\) let closure_cap/, /value at this position:[\s\S]*\n8\n/]);
+const rt = doc('tinyvm:/rt.zc', fs.readFileSync('src/runtime/rt.zc', 'utf8'));
+checks.push([rt('SYS_CLOSE = 2'), /const int SYS_CLOSE = 2/]);
+const fixture = doc('tinyvm:/fixture.zc', 'const int FIRST = 1, SECOND = 2;\nword[4] backing; word active = backing;\nint x = SECOND;\n');
+checks.push([fixture('x = SECOND', 4), /const int SECOND = 2/]);
+checks.push([fixture('active = backing', 9), /word\[4\] backing/]);
+const mnemonic = 'q@!$_~cjzr,?.+-*&|^><%';
+for (const op of mnemonic) {
+ const get = doc('tinyvm:/asm-' + op.charCodeAt(0) + '.zc', `fn f() { asm { ${op} } return 0; }`);
+ checks.push([get(`{ ${op}`, 2), /opcode \d+/, /(stack:|return stack|data stack)/]);
+}
+const special = doc('tinyvm:/special.zc', 'fn f() { asm { #1224 } }\ninclude std;\n');
+checks.push([special('#1224'), /load byte/, /byte address/]);
+checks.push([special('1224'), /load byte/]);
+checks.push([special('std;'), /include std;/, /embedded standard library/, /nothing imports it automatically/]);
+const normal = doc('file:///ordinary.zc', 'fn main() { int std = 7; int c = 2; return std + c; }');
+checks.push([normal('std +'), /\(int\) let std/, /value at this position/]);
+checks.push([normal('c;'), /\(int\) let c/]);
+messages.push({ method: 'exit' });
+let out;
+if (process.argv[2] === '--wasm' || process.argv[2] === '--vm') {
+ const wasm = process.argv[2] === '--wasm', host = require(path.resolve('editors/vscode/client', wasm ? 'wasmproc.js' : 'vmproc.js'));
+ const p = (wasm ? host.wasmProcess : host.vmProcess)(path.resolve(process.argv[3]), { log: () => {} });
+ const chunks = []; p.stdout.on('data', d => chunks.push(d));
+ for (const m of messages) p.stdin.write(frame({ jsonrpc: '2.0', ...m }));
+ out = Buffer.concat(chunks);
+} else {
+ const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(messages.map(m => frame({ jsonrpc: '2.0', ...m }))), timeout: 60000, maxBuffer: 32 << 20 });
+ assert.strictEqual(r.status, 0, String(r.stderr)); out = r.stdout;
+}
+const replies = new Map();
+while (out.length) {
+ const e = out.indexOf('\r\n\r\n'), n = Number(/Content-Length:\s*(\d+)/i.exec(out.subarray(0, e).toString())[1]);
+ const m = JSON.parse(out.subarray(e + 4, e + 4 + n)); replies.set(m.id, m); out = out.subarray(e + 4 + n);
+}
+for (const [rid, ...patterns] of checks) {
+ const value = replies.get(rid)?.result?.contents?.value || '';
+ for (const pattern of patterns) assert.match(value, pattern, `hover #${rid}: ${value}`);
+}
+assert.ok(!replies.get(checks[2][0]).result.contents.value.includes('closures being compiled'), 'the section overview is not NCLO documentation');
+assert.ok(!replies.get(checks[checks.length - 1][0]).result.contents.value.includes('opcode'), 'ordinary c is a variable');
+console.log('ok embedded declaration hovers, constant values, asm instructions and explicit std');
diff --git a/tests/drivers/hover_values_test.js b/tests/drivers/hover_values_test.js
new file mode 100644
index 0000000..badc2ed
--- /dev/null
+++ b/tests/drivers/hover_values_test.js
@@ -0,0 +1,68 @@
+// value previews follow the compiler's bindings and the position of each use.
+const assert = require('assert');
+const cp = require('child_process');
+const server = process.argv[2];
+function hover(text, needle, nth = 0) {
+ let at = -1;
+ for (let n = 0; n <= nth; n++) at = text.indexOf(needle, at + 1);
+ assert.ok(at >= 0, needle);
+ const before = text.slice(0, at), uri = 'file:///hover-values.zc';
+ const messages = [
+ { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
+ { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } },
+ { 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 } } },
+ { jsonrpc: '2.0', method: 'exit' }
+ ];
+ const input = Buffer.concat(messages.map(m => {
+ const body = Buffer.from(JSON.stringify(m));
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]);
+ }));
+ const result = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000, maxBuffer: 8 << 20 });
+ assert.strictEqual(result.status, 0, String(result.stderr));
+ let data = result.stdout;
+ while (data.length) {
+ const end = data.indexOf('\r\n\r\n');
+ const n = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
+ const reply = JSON.parse(data.subarray(end + 4, end + 4 + n));
+ data = data.subarray(end + 4 + n);
+ if (reply.id === 2) return reply.result?.contents?.value || '';
+ }
+ assert.fail('missing hover response');
+}
+function value(text, needle, expected, nth = 0) {
+ const md = hover(text, needle, nth);
+ if (expected === null) assert.ok(!md.includes('value at this position:'), md);
+ else assert.ok(md.includes('value at this position:\n```zc\n' + expected + '\n```'), md);
+}
+const flow = 'fn unknown() { return 9; }\nfn main() { let n = 42; put_n(n); n = 7; put_n(n); n = unknown(); return n; }';
+value(flow, 'n = 42', '42');
+value(flow, 'n);', '42');
+value(flow, 'n);', '7', 1);
+value(flow, 'n;', null);
+value('const answer = 6 * 7; fn main() { return answer; }', 'answer;', '42');
+value('const answer = 42; fn main() { return answer; }', 'answer =', '42');
+value('enum color { red, green = 5 }; fn main() { color shade = green; return shade; }', 'shade;', '5');
+value('fn main() { let n = -9223372036854775808; return n; }', 'n;', '-9223372036854775808');
+value('fn main() { u8 n = 300; return n; }', 'n;', '44');
+value('fn main() { i8 n = 255; return n; }', 'n;', '-1');
+value('fn main() { let n = 3.25; return n; }', 'n;', '3.25');
+value('fn main() { float n = 2; return n; }', 'n;', '2');
+value('fn main() { int n = 3.75; return n; }', 'n;', '3');
+value('fn main() { f32 n = 0.1; return n; }', 'n;', '0.10000000149011612');
+value('fn main() { let n = -0.0; return n; }', 'n;', '0');
+value('fn main() { let text = "hello\\nworld"; return text; }', 'text;', '"hello\\nworld"');
+value('fn main() { let text = b"hello"; return text; }', 'text;', 'b"hello"');
+value('fn main() { let n = 7; let copy = n; n = 8; return copy; }', 'copy;', '7');
+value('fn main() { let n = 1; { let n = 2; put_n(n); } return n; }', 'n);', '2');
+value('fn main() { let n = 1; { let n = 2; put_n(n); } return n; }', 'n;', '1');
+value('fn change(int* p) { p[0] = 9; } fn main() { int n = 7; let p = &n; n = 8; change(p); return n; }', 'n;', null, 1);
+value('fn main() { int n = 7; let p = &n; n = 8; p[0] = 9; return n; }', 'n;', null, 1);
+value('fn main() { let text = "hey"; text[0] = 120; return text; }', 'text;', null);
+value('fn main() { let n = 1; if (1) n = 2; return n; }', 'n;', null);
+value('fn main() { let n = 1; while (0) n = 2; return n; }', 'n;', null);
+value('fn main() { let n = 1; let ignored = 0 && (n = 2); return n; }', 'n;', null);
+value('fn main() { let p = new(int, 3); return p; }', 'p;', null);
+value('fn main() { let n = clz(0); return n; }', 'n;', null);
+value('let global = 7; fn main() { return global; }', 'global;', null);
+value('let global = 7; fn main() { global = 8; return global; }', 'global;', '8');
+console.log('ok position-aware integer, float, string, constant, and alias values; unknown effects invalidate previews');
diff --git a/tests/drivers/icon_test.py b/tests/drivers/icon_test.py
new file mode 100644
index 0000000..560c622
--- /dev/null
+++ b/tests/drivers/icon_test.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# execute isolated icon acceptance, rejection and regression fixtures.
+from pathlib import Path
+import argparse
+import subprocess
+
+parser = argparse.ArgumentParser()
+parser.add_argument('vm', nargs='?', default='bin/zy')
+parser.add_argument('--case', default='')
+parser.add_argument('--js', action='store_true')
+parser.add_argument('--source', action='store_true', help='test the working library source instead of the embedded module')
+args = parser.parse_args()
+root = Path(__file__).resolve().parents[2]
+cases = sorted((root / 'tests/icon').glob('*/*.zc'))
+count = 0
+for case in cases:
+ if args.case and args.case not in str(case):
+ continue
+ expected = case.with_suffix('.expected').read_text()
+ command = ['node', str(root / 'src/js/tinyvm.js')] if args.js else [str(root / args.vm)]
+ program = case.read_text()
+ if args.source:
+ # keep module imports while suppressing its embedded source loader.
+ library = (root / 'src/lib/icon.zc').read_text()
+ setup = '\nnamespace;\ninclude run;\nimmediate fn icon_working_source() { run.icon_done = 1; run.next(); return 0; }\nicon_working_source\n'
+ program = library + setup + program
+ proc = subprocess.run(command, input=program, text=True, capture_output=True, cwd=root, timeout=30)
+ assert proc.returncode == 0 and proc.stdout == expected, f'{case.relative_to(root)}\nstdout: {proc.stdout}\nstderr: {proc.stderr}\nexpected: {expected}'
+ print(f'ok {case.relative_to(root)}')
+ count += 1
+assert count, 'no icon fixtures selected'
+print(f'{count} icon fixtures passed')
diff --git a/tests/drivers/include_test.js b/tests/drivers/include_test.js
new file mode 100644
index 0000000..6ba4e5f
--- /dev/null
+++ b/tests/drivers/include_test.js
@@ -0,0 +1,94 @@
+// include: a path is found next to the file that has it, then in the workspace folder, then as written; the
+// error for one that cannot be read is on its string; include std; needs no file; a server with no files of its own
+// is sent the text of each (zc/file). usage: node tests/drivers/include_test.js path/to/zclsp [path/to/lsp.wasm]
+const assert = require('assert');
+const cp = require('child_process');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const server = path.resolve(process.argv[2]);
+const wasm = process.argv[3];
+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 parse(out) {
+ const res = [];
+ let d = out;
+ while (d.length) {
+ const e = d.indexOf('\r\n\r\n'), n = Number(/Content-Length:\s*(\d+)/i.exec(d.subarray(0, e).toString())[1]);
+ res.push(JSON.parse(d.subarray(e + 4, e + 4 + n))); d = d.subarray(e + 4 + n);
+ }
+ return res;
+}
+// runs one session on the native server (cwd is where includes are read as written) or the wasm host
+function check(uri, text, { files = [], root = null, cwd = os.tmpdir(), host = 'native', navigation = false } = {}) {
+ const msgs = [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: { rootUri: root } },
+ ...files.map(([p, t]) => ({ jsonrpc: '2.0', method: 'zc/file', params: { path: p, text: t } })),
+ { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } },
+ ...(navigation ? [2, 3].map(id => ({ jsonrpc: '2.0', id, method: 'textDocument/definition', params: { textDocument: { uri }, position: { line: 2, character: 20 } } })) : []),
+ { jsonrpc: '2.0', method: 'exit' }];
+ let out;
+ if (host === 'native') out = cp.spawnSync(server, [], { input: Buffer.concat(msgs.map(frame)), cwd, maxBuffer: 1 << 26 }).stdout;
+ else {
+ const { wasmProcess } = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'wasmproc.js'));
+ const p = wasmProcess(path.resolve(wasm), { log: () => {} });
+ const chunks = [];
+ p.stdout.on('data', d => chunks.push(d));
+ for (const m of msgs) p.stdin.write(frame(m));
+ out = Buffer.concat(chunks);
+ }
+ const replies = parse(out);
+ if (navigation) {
+ for (const id of [2, 3]) assert.deepStrictEqual(replies.find(m => m.id === id).result, [{ uri: 'file:///nowhere/proj/lib/virt.zc', range: { start: { line: 0, character: 4 }, end: { line: 0, character: 10 } } }], host + ': navigation uses client-provided includes repeatedly');
+ }
+ const note = replies.filter(m => m.method === 'textDocument/publishDiagnostics').pop();
+ return note ? note.params.diagnostics : null;
+}
+const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'zcinc-'));
+const w = (f, t) => { fs.mkdirSync(path.dirname(path.join(dir, f)), { recursive: true }); fs.writeFileSync(path.join(dir, f), t); };
+w('src/lib/util.zc', 'int twice(int v) { return v * 2; }\n');
+w('src/lib/deep.zc', 'include "util.zc";\nint quad(int v) { return twice(twice(v)); }\n');
+w('src/tools/main.zc', 'include "../lib/deep.zc";\nfn main() { return quad(1); }\n');
+w('top.zc', 'int fromtop() { return 7; }\n');
+const uri = f => 'file://' + path.join(dir, f);
+const read = f => fs.readFileSync(path.join(dir, f), 'utf8');
+const errors = d => d.filter(x => x.severity === 1);
+// next to the file, and nested includes next to the file that has them (the cwd is not the document's directory)
+assert.deepStrictEqual(errors(check(uri('src/tools/main.zc'), read('src/tools/main.zc'))), [], 'a relative include, nested');
+// the original build.zc reproduction, checked from an unrelated working directory
+const buildFile = path.resolve(__dirname, '../../src/tools/build.zc');
+const buildText = fs.readFileSync(buildFile, 'utf8').replace('include std;', 'include "../lib/std.zc";');
+assert.deepStrictEqual(errors(check('file://' + buildFile, buildText)), [], 'build.zc finds ../lib/std.zc');
+// the workspace folder
+assert.deepStrictEqual(errors(check(uri('src/tools/a.zc'), 'include "top.zc";\nfn main() { return fromtop(); }\n', { root: 'file://' + dir })), [], 'found in the workspace folder');
+// as written, from the working directory
+assert.deepStrictEqual(errors(check(uri('src/tools/b.zc'), 'include "top.zc";\nfn main() { return fromtop(); }\n', { cwd: dir })), [], 'found from the working directory');
+// an include that is not there: the error is on its string
+let d = check(uri('src/tools/c.zc'), 'include "../lib/nothere.zc";\nfn main() { return 0; }\n');
+assert.strictEqual(errors(d).length, 1);
+assert.deepStrictEqual(errors(d)[0].range, { start: { line: 0, character: 8 }, end: { line: 0, character: 27 } }, 'the error is on the string: ' + JSON.stringify(errors(d)[0].range));
+assert.match(errors(d)[0].message, /can't read that include/);
+// include std; and the same twice
+assert.deepStrictEqual(errors(check(uri('src/tools/d.zc'), 'include std;\ninclude std;\nfn main() { return std.Text.atoi("4"); }\n')), [], 'include std;');
+d = check(uri('src/tools/e.zc'), 'include std\nfn main() { return 0; }\n');
+assert.ok(errors(d).length >= 1, 'include std without a semicolon is an error');
+d = check(uri('src/tools/f.zc'), 'include other;\nfn main() { return 0; }\n');
+assert.match(errors(d)[0].message, /can't read that module include/);
+assert.deepStrictEqual(errors(d)[0].range.start, { line: 0, character: 8 }, 'an unknown library is reported at its name');
+// findings inside an included file: hints are left out, an error is shown on the include with where it is
+w('src/lib/loud.zc', 'int a() { int unused = 1; return 2 * 2; }\nint b() { return 1 }\n');
+d = check(uri('src/tools/g.zc'), 'include "../lib/loud.zc";\nfn main() { return a(); }\n');
+assert.strictEqual(d.filter(x => x.severity !== 1).length, 0, 'no hints from a library: ' + JSON.stringify(d));
+assert.strictEqual(errors(d).length, 1);
+assert.match(errors(d)[0].message, /^in .*loud\.zc, line 2: .*expected ';'/);
+assert.deepStrictEqual(errors(d)[0].range.start, { line: 0, character: 8 });
+// a server that cannot read files is sent them
+const virtual = '/nowhere/proj/lib/virt.zc';
+const vdoc = 'file:///nowhere/proj/app/main.zc';
+const vtext = 'include "../lib/virt.zc";\ninclude "../lib/gone.zc";\nfn main() { return thrice(2); }\n';
+const files = [[virtual, 'int thrice(int v) { return v * 3; }\n']];
+for (const host of wasm ? ['native', 'wasm'] : ['native']) {
+ d = check(vdoc, vtext, { files, host, navigation: true });
+ assert.strictEqual(errors(d).length, 1, host + ': ' + JSON.stringify(d));
+ assert.deepStrictEqual(errors(d)[0].range.start, { line: 1, character: 8 }, host + ': only the missing file is an error');
+}
+fs.rmSync(dir, { recursive: true });
+console.log('ok includes: found next to the file, in the workspace and as written; errors on the string; include std; files sent by the client');
diff --git a/tests/drivers/lsp_longevity_test.js b/tests/drivers/lsp_longevity_test.js
new file mode 100644
index 0000000..5d450ad
--- /dev/null
+++ b/tests/drivers/lsp_longevity_test.js
@@ -0,0 +1,44 @@
+// repeated document changes must not accumulate unbounded live analysis storage.
+const assert = require('assert');
+const cp = require('child_process');
+const host = cp.spawn(process.argv[2], process.argv.slice(3), { stdio: ['pipe', 'pipe', 'pipe'] });
+const cycles = Number(process.env.LSP_CYCLES || 256), uri = 'file:///longevity.zc';
+let buffer = Buffer.alloc(0), errors = '', next = 1;
+const pending = new Map();
+const timeout = setTimeout(() => { host.kill(); console.error('FAIL lsp longevity timed out'); process.exitCode = 1; }, 60000);
+function send(m) { const b = Buffer.from(JSON.stringify({ jsonrpc: '2.0', ...m })); host.stdin.write(Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b])); }
+function request(method, params) { const id = next++; return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); send({ id, method, params }); }); }
+host.stderr.on('data', b => { errors += b; });
+host.stdout.on('data', b => {
+ buffer = Buffer.concat([buffer, b]);
+ while (true) {
+ const e = buffer.indexOf('\r\n\r\n'); if (e < 0) break;
+ const n = Number(/Content-Length:\s*(\d+)/i.exec(buffer.subarray(0, e).toString())[1]); if (buffer.length < e + 4 + n) break;
+ const m = JSON.parse(buffer.subarray(e + 4, e + 4 + n)); buffer = buffer.subarray(e + 4 + n);
+ if (pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
+ if (m.method === 'textDocument/publishDiagnostics') assert.ok(!m.params.diagnostics.some(d => d.severity === 1), JSON.stringify(m.params));
+ }
+});
+host.on('error', e => { for (const p of pending.values()) p.reject(e); });
+function rss() { const r = cp.spawnSync('ps', ['-o', 'rss=', '-p', String(host.pid)], { encoding: 'utf8' }); assert.strictEqual(r.status, 0); return Number(r.stdout.trim()); }
+(async () => {
+ try {
+ await request('initialize', {});
+ const text = i => `fn square(int n) { return n * n; } fn main() { let answer = square(${i % 16}); return answer; }`;
+ send({ method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text: text(0) } } });
+ let baseline;
+ for (let i = 0; i < cycles; i++) {
+ if (i) send({ method: 'textDocument/didChange', params: { textDocument: { uri, version: i + 1 }, contentChanges: [{ text: text(i) }] } });
+ const tokens = await request('textDocument/semanticTokens/full', { textDocument: { uri } });
+ assert.ok(tokens.data.length);
+ if (i === 31 || (cycles <= 32 && i === 0)) baseline = rss();
+ if (i % 32 === 0) {
+ const r = await request('textDocument/definition', { textDocument: { uri }, position: { line: 0, character: text(i).lastIndexOf('answer') } }); assert.ok(r?.length);
+ }
+ }
+ const end = rss(); assert.ok(end - baseline < 96 * 1024, `rss grew by ${end - baseline} KiB`);
+ send({ method: 'textDocument/didClose', params: { textDocument: { uri } } });
+ await request('shutdown', null); send({ method: 'exit' }); host.stdin.end();
+ console.log(`ok ${cycles} editor changes with tokens/navigation; post-warmup rss delta ${end - baseline} KiB`);
+ } finally { clearTimeout(timeout); host.kill(); }
+})().catch(e => { console.error(e); console.error(errors); process.exitCode = 1; });
diff --git a/tests/drivers/lsp_test.js b/tests/drivers/lsp_test.js
index a783e1e..ab43c41 100644
--- a/tests/drivers/lsp_test.js
+++ b/tests/drivers/lsp_test.js
@@ -51,7 +51,7 @@ check(nf && nf.error && nf.error.code === -32601, 'unknown request gets MethodNo
check(r.msgs.length === 2, 'an unknown notification is ignored');
// a clean program has no diagnostics
-const good = 'fn main() {\n\tput_n(1 + 2);\n\treturn 0;\n}\n';
+const good = 'fn main() {\n\treturn 1 + 2;\n}\n';
r = run([init(1), open(A, good)]);
let d = diags(r.msgs, A);
check(d.length === 1 && d[0].params.diagnostics.length === 0, 'a clean program publishes an empty list', d);
@@ -64,7 +64,7 @@ const e1 = d.length && d[0].params.diagnostics[0];
check(e1 && e1.severity === 1 && e1.range.start.line === 1 && /expected ';', found 'return'/.test(e1.message), 'a missing semicolon is reported on its line', e1);
// utf-16 columns: an emoji (two code units) and an accented letter before the error
-const uni = '// café 😀\nfn main() { put_s("é😀"); return undefined_name; }\n';
+const uni = '// café 😀\nfn main() { let text = "é😀"; return text[0] + undefined_name; }\n';
r = run([init(1), open(A, uni)]);
d = diags(r.msgs, A);
const e2 = d.length && d[0].params.diagnostics[0];
@@ -82,7 +82,7 @@ const da = diags(r.msgs, A), db = diags(r.msgs, B2);
check(da.length === 2 && JSON.stringify(da[0].params.diagnostics) === JSON.stringify(da[1].params.diagnostics) && db[0].params.diagnostics.length === 0, 'documents are checked in isolation');
// syntax modifications made by the program itself apply to the check, like the compiler
-const pipeDef = 'immediate fn pipe_op() { next(); add_binop("|>", 1, "c"); return 0; }\npipe_op\n';
+const pipeDef = 'include run; immediate fn pipe_op() { run.next(); run.add_binop("|>", 1, "c"); return 0; }\npipe_op\n';
const pipeUse = 'fn double(x) { return x * 2; }\nfn main() { return 4 |> double; }\n';
r = run([init(1), open(A, pipeDef + pipeUse), open(B2, pipeUse)]);
check(diags(r.msgs, A)[0].params.diagnostics.filter(x => x.severity === 1).length === 0, 'a parse word that adds an operator is understood');
@@ -108,11 +108,11 @@ function tokens(text) {
}
return out;
}
-const meth = 'struct point {\n\tlet x, y;\n\tfn sum() { return this.x + this.y; }\n}\nfn main() {\n\tpoint* p = new(point);\n\treturn p.sum();\n}\n';
+const meth = 'struct point {\n\tlet x, y;\n\tfn sum() { return this.x + this.y; }\n}\nfn main() {\n\tpoint p;\n\treturn p.sum();\n}\n';
const tk = tokens(meth);
check(tk.filter(t => t === 'sum:function').length === 2, 'a method is a function where it is declared and where it is called', tk);
check(tk.includes('this:parameter') && tk.includes('x:property'), 'this is a parameter and a field is a property', tk);
-check(tk.filter(t => t === 'point:type').length >= 3, 'a user type is a type, also before a postfix star', tk);
+check(tk.filter(t => t === 'point:type').length >= 2, 'a user type is a type, also before a postfix star', tk);
// a large document goes through the streaming reader
let big = '';
@@ -120,7 +120,8 @@ for (let i = 0; i < 2500; i++) big += 'fn f' + i + '(a, b) { return a * b + ' +
big += 'fn main() { return f0(1, 2); }\n';
r = run([init(1), open(A, big)]);
d = diags(r.msgs, A);
-check(d.length === 1 && d[0].params.diagnostics.filter(x => x.severity === 1).length === 0, 'a document of ' + big.length + ' bytes is checked', d.length);
+// a large check streams what it has found, so there may be several lists; the last is the whole answer
+check(d.length >= 1 && d[d.length - 1].params.diagnostics.filter(x => x.severity === 1).length === 0, 'a document of ' + big.length + ' bytes is checked', d.length);
// local tables grow past their initial reservation, and the next document still checks
let many = 'fn main() {\n';
@@ -141,54 +142,58 @@ const hsrc = '// adds two numbers\nint add(int a, int b) { return a + b; }\nint
let hv = hover(hsrc, 4, at(hsrc, 4, 'add'));
check(hv && hv.includes('int add(int a, int b)') && hv.includes('adds two numbers'), 'hover on a call shows the signature and the doc', hv);
check(hv && /^```zc\n/.test(hv) && /\n```\n\nadds two numbers/.test(hv), 'hover separates the code fence from Markdown documentation', hv);
-const unicodeHover = 'fn main() { put_s("é😀"); put_c(10); return 0; }\n';
-hv = hover(unicodeHover, 0, at(unicodeHover, 0, 'put_c'));
-check(hv && hv.includes('put_c('), 'hover converts UTF-16 columns after BMP and astral characters', hv);
+const unicodeHover = 'fn main() { let text = "é😀"; int value = 1; return value; }\n';
+hv = hover(unicodeHover, 0, at(unicodeHover, 0, 'value'));
+check(hv && hv.includes('(int) let value'), 'hover converts UTF-16 columns after BMP and astral characters', hv);
hv = hover(hsrc, 0, hsrc.split('\n')[0].length + 10);
check(hv === null, 'an out-of-range hover column cannot select a name on the following line', hv);
hv = hover(hsrc, 6, at(hsrc, 6, 'twice'));
check(hv && hv.includes('int twice(int a)') && !hv.includes('adds two numbers'), 'a function with no doc still shows its signature', hv);
hv = hover(hsrc, 1, at(hsrc, 1, 'a', 2));
-check(hv && hv.includes('parameter') && hv.includes('int a'), 'hover on a parameter shows its declaration', hv);
+check(hv && hv.includes('(int) let a // parameter;'), 'hover on a parameter shows its declaration', hv);
hv = hover(hsrc, 5, at(hsrc, 5, 'i'));
check(hv && hv.includes('let i') && hv.includes('local'), 'hover on a local shows its declaration', hv);
hv = hover(hsrc, 6, at(hsrc, 6, 'i', 1));
check(hv && hv.includes('let i') && hv.includes('local'), 'hover on a use of a local shows its declaration', hv);
-const tsrc = 'fn main() {\n\tlet i = 3;\n\tlet p = new(int);\n\treturn i;\n}\n';
+const tsrc = 'fn main() {\n\tlet i = 3;\n\tint* p;\n\treturn i;\n}\n';
// an untyped parameter and an untyped function show the types annotate gives them
const isrc = 'fn twice(x) { return x * 2; }\nfn main() {\n\tlet n = twice(4);\n\treturn n;\n}\n';
let hi = hover(isrc, 0, 9);
-check(hi && hi.includes('x // parameter; type int'), 'an untyped parameter shows its inferred type', hi);
+check(hi && hi.includes('(int) let x // parameter;'), 'an untyped parameter shows its inferred type', hi);
hi = hover(isrc, 0, 3);
check(hi && hi.includes('int twice(int x)'), 'an untyped function shows its inferred parameter and return types', hi);
hv = hover(tsrc, 1, 5);
-check(hv && hv.includes('type int'), 'an untyped number local shows the type annotate gives it', hv);
-hv = hover(tsrc, 2, 5);
-check(hv && hv.includes('type int*'), 'a pointer local shows its pointer type on its declaration line', hv);
+check(hv && hv.includes('(int) let i'), 'an untyped number local shows the type annotate gives it', hv);
+hv = hover(tsrc, 2, 6);
+check(hv && hv.includes('(int*) let p; // local'), 'a pointer local shows its pointer type on its declaration line', hv);
hv = hover('fn main() {\n\tlet i = 3;\n\treturn i;\n}\n', 2, 8);
check(hv && hv.includes('let i'), 'a return of a local is a use, not a declaration', hv);
hv = hover(hsrc, 6, at(hsrc, 6, 'return'));
check(hv && hv.includes('leaves the function with a value'), 'a keyword shows what it does', hv);
-hv = hover('fn main() {\n\tput_c(32);\n\treturn 0;\n}\n', 1, 1);
+const builtinSource = 'include rt; fn main() {\n\trt.put_c(32);\n\treturn 0;\n}\n';
+hv = hover(builtinSource, 1, at(builtinSource, 1, 'put_c'));
check(hv && hv.includes('put_c('), 'a built-in function shows its signature', hv);
-const kwsrc = 'fn main() {\n\tint* p = new(int);\n\tfree(p);\n\treturn 0;\n}\n';
+const kwsrc = 'include rt; fn main() {\n\tint* p = new(int);\n\trt.free(p);\n\treturn 0;\n}\n';
hv = hover(kwsrc, 1, at(kwsrc, 1, 'new'));
-check(hv && hv.includes('new(T)') && hv.includes('fresh object'), 'hover on new shows its form and what it does', hv);
+check(hv && hv.includes('new(T)') && hv.includes('fresh heap object') && hv.includes('owns it'), 'hover on new shows its form and ownership behavior', hv);
hv = hover(kwsrc, 2, at(kwsrc, 2, 'free'));
-check(hv && hv.includes('frees a heap object'), 'hover on free shows its doc', hv);
+check(hv && hv.includes('releases a heap object') && hv.includes('consumes an owned object automatically'), 'hover on free shows inferred consumption', hv);
const kdoc = '// the number of lines\nfn main() {\n\treturn 0;\n}\n';
hv = hover(kdoc, 2, 1);
check(hv === null || !hv.includes('number of lines'), 'a doc comment is not attached to a keyword in a body', hv);
// a method of a struct is indented in the body, and its hover is still its line and doc
-const msrc = 'struct point {\n\tlet x;\n\t// the sum of the parts\n\tfn sum() { return this.x; }\n\tstatic fn zero() { return 0; }\n}\nfn main() {\n\tpoint* p = new(point);\n\treturn p.sum() + point.zero();\n}\n';
+const msrc = 'struct point {\n\tlet x;\n\t// the sum of the parts\n\tfn sum() { return this.x; }\n\tstatic fn zero() { return 0; }\n}\nfn main() {\n\tpoint p;\n\treturn p.sum() + point.zero();\n}\n';
hv = hover(msrc, 8, at(msrc, 8, 'sum'));
check(hv && hv.includes('fn sum()') && hv.includes('the sum of the parts'), 'a method called through a pointer shows its signature and doc', hv);
hv = hover(msrc, 8, at(msrc, 8, 'zero'));
check(hv && hv.includes('static fn zero()'), 'a static function of a struct shows its signature', hv);
+hv = hover(msrc, 4, at(msrc, 4, 'static'));
+check(hv && hv.includes('static fn name') && hv.includes('hidden this'), 'static keyword hover explains the declaration form', hv);
+check(tokens(msrc).includes('static:keyword'), 'static is highlighted as a keyword', tokens(msrc));
// a typed method shows its return type and parameters
-const tmsrc = 'struct point {\n\tlet x;\n\t// the sum of the parts\n\tint sum(int k) { return this.x + k; }\n}\nfn main() {\n\tpoint* p = new(point);\n\treturn p.sum(1);\n}\n';
+const tmsrc = 'struct point {\n\tlet x;\n\t// the sum of the parts\n\tint sum(int k) { return this.x + k; }\n}\nfn main() {\n\tpoint p;\n\treturn p.sum(1);\n}\n';
hv = hover(tmsrc, 7, at(tmsrc, 7, 'sum'));
check(hv && hv.includes('int sum(int k)') && hv.includes('the sum of the parts'), 'a typed method shows its return type and parameters', hv);
@@ -219,7 +224,8 @@ check(dirty.length === 0, 'every example and test program checks clean (' + clea
let missed = [], n = 0;
for (const dir of ['errors', 'typeerrors', 'langerrors']) {
for (const f of fs.readdirSync('tests/' + dir).filter(x => x.endsWith('.zc'))) {
- const want = fs.readFileSync('tests/' + dir + '/' + f, 'utf8').split(/\r?\n/)[0].replace('// expect: ', '');
+ const wantLine = fs.readFileSync('tests/' + dir + '/' + f, 'utf8').split(/\r?\n/).find(line => line.startsWith('// expect: '));
+ const want = wantLine ? wantLine.slice('// expect: '.length) : '';
const out = cli('tests/' + dir + '/' + f);
n++;
if (!out.includes(want)) missed.push(dir + '/' + f + ' wanted [' + want + '] got [' + out.split('\n')[0] + ']');
@@ -236,7 +242,7 @@ const sct = tokens(scp);
check(sct.includes('cap:parameter') && sct.includes('cap:variable') && !sct.includes('cap:keyword') && !sct.includes('cap:function'), 'a name like a built-in is a variable or parameter where the scope says', sct);
check(sct.includes('len:keyword'), 'a built-in that is a keyword in the compiler is still a keyword', sct);
let hcv = hover(scp, 4, at(scp, 4, 'cap'));
-check(hcv && hcv.includes('int cap') && hcv.includes('local'), 'hover on a use of a local named like a built-in shows the local', hcv);
+check(hcv && hcv.includes('(int) let cap') && hcv.includes('local'), 'hover on a use of a local named like a built-in shows the local', hcv);
hcv = hover(scp, 1, at(scp, 1, 'cap', 1));
check(hcv && hcv.includes('parameter'), 'hover on a use of a parameter named like a built-in shows the parameter', hcv);
hcv = hover(scp, 4, at(scp, 4, 'len'));
diff --git a/tests/drivers/lsp_vm.js b/tests/drivers/lsp_vm.js
index 24c1a5b..e1f5d7a 100644
--- a/tests/drivers/lsp_vm.js
+++ b/tests/drivers/lsp_vm.js
@@ -79,6 +79,20 @@ const diagsOf = (s, uri) => s.notes.filter(m => m.method === 'textDocument/publi
const h2 = s2.replies[6];
check(h2 && h2.result && /packed documentation/.test(h2.result.contents.value), 'hover shows the docs of a compiler name', h2);
}
+// definition navigation returns the embedded runtime source through the same vm host.
+{
+ const src = 'include rt; fn main() { return rt.s_to_n(b"42"); }\n';
+ const s = session([['file:///nav.zc', src]]);
+ const at = src.indexOf('s_to_n'), before = src.slice(0, at);
+ s.send({ jsonrpc: '2.0', id: 7, method: 'textDocument/definition', params: { textDocument: { uri: 'file:///nav.zc' }, position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } } });
+ s.send({ jsonrpc: '2.0', id: 8, method: 'zc/source', params: { path: '/rt.zc' } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ const loc = s.replies[7] && s.replies[7].result && s.replies[7].result[0];
+ check(loc && loc.uri === 'tinyvm:/rt.zc', 's_to_n opens the embedded runtime source', loc);
+ const source = s.replies[8] && s.replies[8].result;
+ const line = source && loc && source.split('\n')[loc.range.start.line];
+ check(line && line.slice(loc.range.start.character, loc.range.end.character) === 's_to_n' && source === fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'runtime', 'rt.zc'), 'utf8'), 'the virtual runtime source matches its declaration');
+}
// a document that is bigger than one feed: the input is split and comes out whole
{
let big = 'int main() {\n';
diff --git a/tests/drivers/lsp_wasm.js b/tests/drivers/lsp_wasm.js
index 4ff2d16..e49bcc5 100644
--- a/tests/drivers/lsp_wasm.js
+++ b/tests/drivers/lsp_wasm.js
@@ -79,6 +79,20 @@ const diagsOf = (s, uri) => s.notes.filter(m => m.method === 'textDocument/publi
const h2 = s2.replies[6];
check(h2 && h2.result && /packed documentation/.test(h2.result.contents.value), 'hover shows the docs of a compiler name', h2);
}
+// definition navigation returns the embedded runtime source through the wasm host.
+{
+ const src = 'include rt; fn main() { return rt.s_to_n(b"42"); }\n';
+ const s = session([['file:///nav.zc', src]]);
+ const at = src.indexOf('s_to_n'), before = src.slice(0, at);
+ s.send({ jsonrpc: '2.0', id: 7, method: 'textDocument/definition', params: { textDocument: { uri: 'file:///nav.zc' }, position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } } });
+ s.send({ jsonrpc: '2.0', id: 8, method: 'zc/source', params: { path: '/rt.zc' } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ const loc = s.replies[7] && s.replies[7].result && s.replies[7].result[0];
+ check(loc && loc.uri === 'tinyvm:/rt.zc', 's_to_n opens the embedded runtime source', loc);
+ const source = s.replies[8] && s.replies[8].result;
+ const line = source && loc && source.split('\n')[loc.range.start.line];
+ check(line && line.slice(loc.range.start.character, loc.range.end.character) === 's_to_n' && source === fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'runtime', 'rt.zc'), 'utf8'), 'the virtual runtime source matches its declaration');
+}
// a document that is bigger than one feed: the input is split and comes out whole
{
let big = 'int main() {\n';
diff --git a/tests/drivers/nav-library.zc b/tests/drivers/nav-library.zc
new file mode 100644
index 0000000..55943d0
--- /dev/null
+++ b/tests/drivers/nav-library.zc
@@ -0,0 +1,2 @@
+struct NavValue { int value; };
+int nav_helper(int x) { return x + 1; }
diff --git a/tests/drivers/nav_host_test.js b/tests/drivers/nav_host_test.js
new file mode 100644
index 0000000..3ffb563
--- /dev/null
+++ b/tests/drivers/nav_host_test.js
@@ -0,0 +1,52 @@
+// verify embedded built-in navigation through the extension's vm and wasm hosts.
+// usage: node tests/drivers/nav_host_test.js vm|wasm path/to/server
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const mode = process.argv[2];
+const server = path.resolve(process.argv[3]);
+const client = path.resolve(__dirname, '..', '..', 'editors', 'vscode', 'client');
+const processHost = mode === 'vm'
+ ? require(path.join(client, 'vmproc.js')).vmProcess
+ : mode === 'wasm'
+ ? require(path.join(client, 'wasmproc.js')).wasmProcess
+ : null;
+assert.ok(processHost, 'host must be vm or wasm');
+const proc = processHost(server, { log: () => {} });
+const replies = new Map();
+let inbox = Buffer.alloc(0);
+proc.stdout.on('data', chunk => {
+ inbox = Buffer.concat([inbox, chunk]);
+ while (true) {
+ const end = inbox.indexOf('\r\n\r\n');
+ if (end < 0) return;
+ const size = Number(/Content-Length:\s*(\d+)/i.exec(inbox.subarray(0, end).toString())[1]);
+ if (inbox.length < end + 4 + size) return;
+ const message = JSON.parse(inbox.subarray(end + 4, end + 4 + size).toString());
+ inbox = inbox.subarray(end + 4 + size);
+ if (message.id !== undefined) replies.set(message.id, message);
+ }
+});
+function send(message) {
+ const body = Buffer.from(JSON.stringify({ jsonrpc: '2.0', ...message }));
+ proc.stdin.write(Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body]));
+}
+const source = 'include rt; fn main() { return rt.s_to_n(b"42"); }\n';
+const needle = 's_to_n';
+const offset = source.indexOf(needle);
+const before = source.slice(0, offset);
+const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 };
+send({ id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } });
+send({ method: 'initialized', params: {} });
+send({ method: 'textDocument/didOpen', params: { textDocument: { uri: 'file:///nav.zc', languageId: 'zc', version: 1, text: source } } });
+send({ id: 2, method: 'textDocument/definition', params: { textDocument: { uri: 'file:///nav.zc' }, position } });
+send({ id: 3, method: 'zc/source', params: { path: '/rt.zc' } });
+send({ method: 'exit' });
+const location = replies.get(2)?.result?.[0];
+assert.ok(location, 's_to_n has a definition');
+assert.strictEqual(location.uri, 'tinyvm:/rt.zc');
+const runtime = replies.get(3)?.result;
+assert.strictEqual(runtime, fs.readFileSync(path.resolve(__dirname, '..', '..', 'src', 'runtime', 'rt.zc'), 'utf8'));
+const line = runtime.split('\n')[location.range.start.line];
+assert.strictEqual(line.slice(location.range.start.character, location.range.end.character), needle);
+console.log(`ok ${mode} host opens the embedded runtime source for s_to_n`);
diff --git a/tests/drivers/nav_test.js b/tests/drivers/nav_test.js
new file mode 100644
index 0000000..23afbf8
--- /dev/null
+++ b/tests/drivers/nav_test.js
@@ -0,0 +1,75 @@
+// go to definition and find references, in the document and across includes.
+// usage: node tests/drivers/nav_test.js path/to/zclsp (run from the repository root)
+const assert = require('assert');
+const cp = require('child_process');
+const fs = require('fs');
+const path = require('path');
+const server = process.argv[2];
+const root = path.resolve(__dirname, '..', '..');
+const uri = 'file://' + path.join(root, 'tests', 'drivers', 'nav-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 request(text, needle, method, extra = {}) {
+ const at = text.indexOf(needle);
+ assert.ok(at >= 0, needle);
+ const before = text.slice(0, at);
+ const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 };
+ const input = Buffer.concat([
+ { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
+ { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } },
+ { jsonrpc: '2.0', id: 2, method, params: { textDocument: { uri }, position, ...extra } },
+ { jsonrpc: '2.0', method: 'exit' }].map(frame));
+ const r = cp.spawnSync(server, process.argv.slice(3), { input, cwd: root, timeout: 30000, maxBuffer: 8 << 20 });
+ assert.strictEqual(r.status, 0, String(r.stderr));
+ let d = r.stdout, init;
+ while (d.length) {
+ const e = d.indexOf('\r\n\r\n'), n = Number(/Content-Length:\s*(\d+)/i.exec(d.subarray(0, e).toString())[1]);
+ const m = JSON.parse(d.subarray(e + 4, e + 4 + n)); d = d.subarray(e + 4 + n);
+ if (m.id === 1) init = m.result.capabilities;
+ if (m.id === 2) return { result: m.result, init };
+ }
+ assert.fail('no reply');
+}
+const lines = r => (r || []).map(l => l.range.start.line + ':' + l.range.start.character).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
+const src = 'include "tests/drivers/nav-library.zc";\nstruct pt { int x; int y; };\nint twice(int v) { return v * 2; }\nfn main() {\n\tpt p;\n\tp.x = twice(2);\n\tlet n = p.x + twice(3);\n\tNavValue value;\n\treturn n + p.y + nav_helper(value.value);\n}\n';
+const def = (needle, nth) => request(src, needle, 'textDocument/definition').result;
+assert.ok(request(src, 'twice(2', 'textDocument/definition').init.definitionProvider && request(src, 'twice(2', 'textDocument/definition').init.referencesProvider);
+assert.deepStrictEqual(lines(def('twice(2')), ['2:4'], 'a function goes to its definition');
+assert.deepStrictEqual(lines(def('p.y')), ['4:4'], 'a local goes to its declaration');
+assert.deepStrictEqual(lines(def('pt p')), ['1:7'], 'a struct goes to its declaration');
+const inc = def('NavValue value');
+assert.ok(inc && inc.length === 1 && inc[0].uri === 'file://' + path.join(root, 'tests', 'drivers', 'nav-library.zc'), 'a type from an include goes to the included file: ' + JSON.stringify(inc));
+const includeText = fs.readFileSync(path.join(root, 'tests', 'drivers', 'nav-library.zc'), 'utf8').split('\n');
+assert.ok(/NavValue/.test(includeText[inc[0].range.start.line].slice(inc[0].range.start.character, inc[0].range.end.character)), 'the range covers the name');
+assert.strictEqual(request(src, 'return', 'textDocument/definition').result, null, 'a keyword has no definition');
+const refs = (needle, includeDeclaration) => request(src, needle, 'textDocument/references', { context: { includeDeclaration } }).result;
+assert.deepStrictEqual(lines(refs('twice(2', true)), ['2:4', '5:7', '6:15'], 'references with the declaration');
+assert.deepStrictEqual(lines(refs('twice(2', false)), ['5:7', '6:15'], 'references without the declaration');
+assert.deepStrictEqual(lines(refs('n + p.y', true)), ['6:5', '8:8'], 'a local: declaration and uses');
+const shared = refs('nav_helper(value', true);
+assert.ok(shared.some(loc => loc.uri === 'file://' + path.join(root, 'tests', 'drivers', 'nav-library.zc')), 'references include the declaration in another file');
+assert.deepStrictEqual(lines(refs('y; };', true)).length >= 1, true);
+// built-ins: the image carries its sources. the runtime, the compiler and (for include std;) the standard library
+const std = 'include std, rt;\nfn main() {\n\trt.put_s("hi");\n\tlet n = std.Text.atoi("4");\n\tlet m = rt.s_to_n(b"1");\n\treturn n + m + std.Host.now();\n}\n';
+const bdef = needle => request(std, needle, 'textDocument/definition').result;
+const source = name => request(std, 'main', 'zc/source', { path: name }).result;
+const slice = (text, loc) => { const l = text.split('\n')[loc.range.start.line]; return l.slice(loc.range.start.character, loc.range.end.character); };
+let loc = bdef('put_s');
+assert.ok(loc && loc.length === 1 && loc[0].uri === 'tinyvm:/rt.zc', 'a runtime function goes to the runtime source: ' + JSON.stringify(loc));
+assert.strictEqual(slice(source('/rt.zc'), loc[0]), 'put_s');
+assert.strictEqual(source('/rt.zc'), fs.readFileSync(path.join(root, 'src', 'runtime', 'rt.zc'), 'utf8'), 'the embedded runtime is the file');
+loc = bdef('Text.atoi');
+assert.ok(loc && loc[0].uri === 'tinyvm:/std.zc', 'a std type goes to the std source');
+assert.strictEqual(slice(source('std.zc'), loc[0]), 'Text');
+loc = bdef('atoi(');
+assert.strictEqual(loc[0].uri, 'tinyvm:/std.zc');
+assert.strictEqual(slice(source('/std.zc'), loc[0]), 'atoi', 'a method goes to its own line');
+loc = bdef('now(');
+assert.strictEqual(slice(source('/std.zc'), loc[0]), 'now');
+assert.strictEqual(source('/std.zc'), fs.readFileSync(path.join(root, 'src', 'lib', 'std.zc'), 'utf8'), 'the embedded std is the file');
+assert.strictEqual(source('/compiler.zc'), fs.readFileSync(path.join(root, 'src', 'compiler', 'compiler.zc'), 'utf8'), 'the embedded compiler is the file');
+assert.strictEqual(source('/server.zc'), fs.readFileSync(path.join(root, 'src', 'lsp', 'server.zc'), 'utf8'), 'the embedded server is the file');
+loc = bdef('s_to_n(');
+assert.ok(loc && loc.length === 1 && loc[0].uri === 'tinyvm:/rt.zc', 's_to_n opens the runtime source: ' + JSON.stringify(loc));
+assert.strictEqual(slice(source('/rt.zc'), loc[0]), 's_to_n');
+assert.strictEqual(source('/nope.zc'), null);
+console.log('ok definition and references across the document and includes');
diff --git a/tests/drivers/parser_growth_test.py b/tests/drivers/parser_growth_test.py
index 4cbfbb2..9aee57e 100644
--- a/tests/drivers/parser_growth_test.py
+++ b/tests/drivers/parser_growth_test.py
@@ -22,6 +22,13 @@ source = 'struct methods { int value;\n'
source += ''.join(f'int get_{i}() {{ return this.value + {i}; }}\n' for i in range(100))
source += '}\nfn main() { methods item; item.value = 7; gc(); put_n(item.get_99()); put_c(10); return 0; }\n'
run(source, '106\n')
+# grabfn owns a single token buffer while repeated growth moves replacements
+# into its released slot; the replayed method must survive compilation and gc.
+source = 'struct long_method { int seed; int count() { int value = 0;\n'
+source += 'value += 1;\n' * 400
+source += 'return value + this.seed; } };\n'
+source += 'fn main() { long_method item; item.seed = 7; gc(); put_n(item.count()); put_c(10); return 0; }\n'
+run(source, '407\n')
source = 'enum expanded {\n'
source += ',\n'.join(f'member_with_long_generated_name_{i} {{ ' + ''.join(f'int field_with_long_generated_name_{j}; ' for j in range(8)) + '}' for i in range(40))
source += '\n};\nfn main() { gc(); put_n(ebcap > 8192); put_c(10); return 0; }\n'
diff --git a/tests/drivers/runtime_image_test.py b/tests/drivers/runtime_image_test.py
new file mode 100644
index 0000000..c1f4b9c
--- /dev/null
+++ b/tests/drivers/runtime_image_test.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+"""self-hosted run-only images retain enhancements and generic method bodies."""
+import argparse
+import subprocess
+import tempfile
+from pathlib import Path
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--case', default='')
+parser.add_argument('--host', default='bin/zy')
+parser.add_argument('--mkgenome', default='bin/mkgenome')
+args = parser.parse_args()
+cases = [
+ ('enhancements', 'include rt; fn main() { rt.put_n(17 % 5); rt.put_c(32); rt.put_n(17 / 5); }', b'2 3'),
+ ('array algorithms', Path('examples/learn/31-array-algorithms.zc').read_text(), Path('examples/learn/31-array-algorithms.out').read_bytes()),
+ ('owned pop', 'include rt; int*.u take() { int*[?] a = rt.new(int*[?]); int* p = rt.new(int); *p = 8; a.push(p); return a.pop(); } fn main() { let p = take(); rt.put_n(rt.isobj(p)); rt.put_c(32); rt.put_n(*p); }', b'1 8'),
+]
+prefix = b''.join(Path(f).read_bytes() + b'\n' for f in ['src/runtime/rt.zc', 'src/core/core.zc', 'src/runtime/run.zc'])
+for name, source, expected in cases:
+ if args.case and args.case not in name:
+ continue
+ with tempfile.TemporaryDirectory(prefix='tinyvm-image-') as work:
+ directory = Path(work)
+ r = subprocess.run([args.host], input=Path('src/tools/build.zc').read_bytes() + b'`' + prefix + source.encode(), capture_output=True, timeout=30)
+ assert r.returncode == 0 and len(r.stdout) >= 56 and not r.stdout.startswith(b'zc:'), (name, r.stdout[:1000], r.stderr)
+ header = subprocess.run([args.mkgenome], input=r.stdout, capture_output=True, timeout=5)
+ assert header.returncode == 0, (name, header.stderr)
+ (directory / 'genome.h').write_bytes(header.stdout)
+ for f in ['src/core/memmap.h', 'src/core/fops.h', 'src/core/bops.h', 'src/core/stack.h', 'src/micro/micro.c', 'src/micro/micro_port.h', 'src/micro/port_linux.c']:
+ (directory / Path(f).name).write_bytes(Path(f).read_bytes())
+ subprocess.run(['cc', '-O2', '-w', '-fwrapv', '-pthread', '-DZY_THREADS', str(directory / 'micro.c'), str(directory / 'port_linux.c'), '-I' + work, '-o', str(directory / 'image')], check=True, timeout=30)
+ r = subprocess.run([str(directory / 'image')], capture_output=True, timeout=10)
+ assert r.returncode == 0 and r.stdout == expected, (name, r.returncode, r.stdout, r.stderr, expected)
+ print('ok self-hosted run-only image: ' + name)
diff --git a/tests/drivers/sourceprovider_test.js b/tests/drivers/sourceprovider_test.js
new file mode 100644
index 0000000..f9551da
--- /dev/null
+++ b/tests/drivers/sourceprovider_test.js
@@ -0,0 +1,31 @@
+// verify the extension's virtual source provider and its language-server request.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { createSourceProvider } = require('../../editors/vscode/client/sourceprovider.js');
+
+async function main() {
+ const calls = [];
+ const runtime = fs.readFileSync(path.resolve(__dirname, '..', '..', 'src', 'runtime', 'rt.zc'), 'utf8');
+ const provider = createSourceProvider(async (method, params) => {
+ calls.push({ method, params });
+ return runtime;
+ }, () => true);
+ assert.strictEqual(await provider.provideTextDocumentContent({ path: '/rt.zc' }), runtime);
+ assert.deepStrictEqual(calls, [{ method: 'zc/source', params: { path: '/rt.zc' } }]);
+
+ const missing = createSourceProvider(async () => null, () => true);
+ assert.strictEqual(await missing.provideTextDocumentContent({ path: '/missing.zc' }), '// no such built-in source\n');
+ const failed = createSourceProvider(async () => { throw new Error('closed'); }, () => true);
+ assert.strictEqual(await failed.provideTextDocumentContent({ path: '/rt.zc' }), '// the language server could not give this source\n');
+
+ let readyChecks = 0, delays = 0;
+ const starting = createSourceProvider(async () => runtime, () => ++readyChecks > 2, async () => { delays++; });
+ assert.strictEqual(await starting.provideTextDocumentContent({ path: '/rt.zc' }), runtime);
+ assert.strictEqual(delays, 2);
+ const stopped = createSourceProvider(async () => runtime, () => false, async () => {});
+ assert.strictEqual(await stopped.provideTextDocumentContent({ path: '/rt.zc' }), '// the language server is not running\n');
+ console.log('ok virtual source provider waits for the server and returns embedded source text');
+}
+
+main().catch(error => { console.error(error); process.exitCode = 1; });
diff --git a/tests/drivers/storage_analysis_test.js b/tests/drivers/storage_analysis_test.js
index da967f5..774fa89 100644
--- a/tests/drivers/storage_analysis_test.js
+++ b/tests/drivers/storage_analysis_test.js
@@ -26,7 +26,7 @@ function check(text, needle) {
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]]) {
+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/);
@@ -56,16 +56,21 @@ const safe = [
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]; }',
- 'fn unknown(int* p) {}\n' + prefix + ' let p = new(int, 3); unknown(p); return p[9]; }',
+ '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]; }',
+ 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');
diff --git a/tests/drivers/stream_host_test.js b/tests/drivers/stream_host_test.js
new file mode 100644
index 0000000..99213e2
--- /dev/null
+++ b/tests/drivers/stream_host_test.js
@@ -0,0 +1,48 @@
+// the extension's two in-process servers (wasm and the javascript vm) stream partial diagnostics and drop a
+// check when the next message is already written. a step of either host is synchronous, so the server
+// suspends at its poll and the host lets the event loop deliver input before it goes on.
+// usage: node tests/drivers/stream_host_test.js path/to/lsp.wasm path/to/vmserver-dir
+const assert = require('assert');
+const path = require('path');
+const client = path.join(__dirname, '..', '..', 'editors', 'vscode', 'client');
+const { wasmProcess } = require(path.join(client, 'wasmproc.js'));
+const { vmProcess } = require(path.join(client, 'vmproc.js'));
+const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b]); };
+const doc = (n, tail) => { let t = ''; for (let i = 0; i < n; i++) t += `int f${i}(int a) { return a + ${i}; }\n`; return t + tail; };
+function run(make, steps) {
+ return new Promise(resolve => {
+ const p = make();
+ const got = [];
+ let inbox = Buffer.alloc(0);
+ p.stdout.on('data', d => {
+ inbox = Buffer.concat([inbox, d]);
+ for (;;) {
+ const e = inbox.indexOf('\r\n\r\n'); if (e < 0) return;
+ const n = parseInt(/Content-Length: (\d+)/.exec(inbox.slice(0, e).toString())[1], 10);
+ if (inbox.length < e + 4 + n) return;
+ got.push(JSON.parse(inbox.slice(e + 4, e + 4 + n).toString('utf8')));
+ inbox = inbox.slice(e + 4 + n);
+ }
+ });
+ for (const m of steps) p.stdin.write(frame(m));
+ setTimeout(() => { p.stdin.write(frame({ jsonrpc: '2.0', method: 'exit' })); setTimeout(() => resolve(got), 200); }, 4000);
+ });
+}
+const init = { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} };
+const open = (uri, version, text) => ({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version, languageId: 'zc', text } } });
+const change = (uri, version, text) => ({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version }, contentChanges: [{ text }] } });
+const diags = (got, uri) => got.filter(m => m.method === 'textDocument/publishDiagnostics' && m.params.uri === uri);
+(async () => {
+ const A = 'file:///host-stream.zc';
+ const ok = doc(300, 'fn main() { return f0(1); }\n');
+ const hosts = [['wasm', () => wasmProcess(path.resolve(process.argv[2]), { log: () => {} })], ['vm', () => vmProcess(path.resolve(process.argv[3]), { log: () => {} })]];
+ for (const [name, make] of hosts) {
+ let got = await run(make, [init, open(A, 1, 'int broken( { }\n' + ok)]);
+ let d = diags(got, A);
+ assert.ok(d.length >= 2 && d[0].params.diagnostics.some(x => x.severity === 1), name + ': a large check streams: ' + d.length);
+ got = await run(make, [init, open(A, 1, ok), change(A, 2, 'fn main() { return 0; }\n')]);
+ d = diags(got, A);
+ assert.ok(d.length >= 1 && d.every(m => m.params.version === 2), name + ': the stale check is dropped: ' + JSON.stringify(d.map(m => m.params.version)));
+ console.log('ok ' + name + ' server streams and drops stale checks');
+ }
+})();
diff --git a/tests/drivers/stream_test.js b/tests/drivers/stream_test.js
new file mode 100644
index 0000000..e783862
--- /dev/null
+++ b/tests/drivers/stream_test.js
@@ -0,0 +1,55 @@
+// large checks stream partial diagnostics, and a check is dropped when newer input is already waiting.
+// usage: node tests/drivers/stream_test.js path/to/zclsp
+const assert = require('assert');
+const cp = require('child_process');
+const server = process.argv[2];
+const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b]); };
+const doc = (n, tail) => { let t = ''; for (let i = 0; i < n; i++) t += `int f${i}(int a) { return a + ${i}; }\n`; return t + tail; };
+function messages(out) {
+ const res = [];
+ let d = out;
+ while (d.length) {
+ const e = d.indexOf('\r\n\r\n'), n = Number(/Content-Length:\s*(\d+)/i.exec(d.subarray(0, e).toString())[1]);
+ res.push(JSON.parse(d.subarray(e + 4, e + 4 + n))); d = d.subarray(e + 4 + n);
+ }
+ return res;
+}
+const published = (msgs, uri) => msgs.filter(m => m.method === 'textDocument/publishDiagnostics' && m.params.uri === uri);
+// spawn the server and write each step after the previous one has had time to run (delay in ms)
+function session(steps) {
+ return new Promise(resolve => {
+ const p = cp.spawn(server, process.argv.slice(3));
+ let out = Buffer.alloc(0);
+ p.stdout.on('data', c => { out = Buffer.concat([out, c]); });
+ p.on('exit', () => resolve(messages(out)));
+ let at = 0;
+ const next = () => {
+ if (at >= steps.length) { p.stdin.write(frame({ jsonrpc: '2.0', method: 'exit' })); return; }
+ const [delay, msgs] = steps[at++];
+ p.stdin.write(Buffer.concat(msgs.map(frame)));
+ setTimeout(next, delay);
+ };
+ next();
+ });
+}
+const init = { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} };
+const open = (uri, version, text) => ({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version, languageId: 'zc', text } } });
+const change = (uri, version, text) => ({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version }, contentChanges: [{ text }] } });
+(async () => {
+ const A = 'file:///stream.zc';
+ // a large document with an error at the top: partial lists arrive before the end, the last holds everything
+ const bad = 'int broken( { }\n' + doc(1500, 'fn main() { return f0(1); }\n');
+ let msgs = published(await session([[3000, [init, open(A, 1, bad)]]]), A);
+ assert.ok(msgs.length >= 2, 'a large check streams several lists: ' + msgs.length);
+ assert.ok(msgs[0].params.diagnostics.some(d => d.severity === 1), 'the first list already has the error');
+ assert.ok(msgs[msgs.length - 1].params.diagnostics.length >= msgs[0].params.diagnostics.length, 'later lists grow');
+ // a change that is already waiting drops the stale check: only the newer version is published
+ const ok = doc(1500, 'fn main() { return f0(1); }\n');
+ msgs = published(await session([[3000, [init, open(A, 1, ok), change(A, 2, 'fn main() { return 0; }\n')]]]), A);
+ assert.ok(msgs.length >= 1 && msgs.every(m => m.params.version === 2), 'the stale version 1 is never published: ' + JSON.stringify(msgs.map(m => m.params.version)));
+ // an interrupted open of another document is still answered once nothing newer waits
+ const B = 'file:///other.zc';
+ msgs = await session([[3000, [init, open(A, 1, ok), open(B, 1, 'fn main() { return 0; }\n')]]]);
+ assert.ok(published(msgs, A).length >= 1 && published(msgs, B).length >= 1, 'both documents are published');
+ console.log('ok large checks stream partial diagnostics and drop stale checks');
+})();
diff --git a/tests/drivers/thread_context_test.py b/tests/drivers/thread_context_test.py
new file mode 100644
index 0000000..fa3f144
--- /dev/null
+++ b/tests/drivers/thread_context_test.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# verify actual threads, independent vm state, parallelism, cancellation and join.
+from pathlib import Path
+import argparse
+import subprocess
+
+parser = argparse.ArgumentParser()
+parser.add_argument('vm', nargs='?', default='bin/zy')
+parser.add_argument('--case', default='')
+parser.add_argument('--library', default='')
+parser.add_argument('--backend', choices=('native', 'micro'), default='native')
+args = parser.parse_args()
+root = Path(__file__).resolve().parents[2]
+expected = {'isolation': '21 16 7 16 7 4 1 1\n', 'parallel': '11 21\n', 'cancel': '0\n', 'cancel_o3': '0\n', 'invalid': '-1 -1\n', 'recycle': '66\n', 'channels': '-1 0\n', 'failed_worker': '5 7\n', 'ffi_o3': '1 5050 0\n', 'actor_identity': '1\n', 'resources': '1 24 0 0 0\n', 'close_order': '0 0 -1\n', 'resources_counts': '24 0 0 0\n', 'nested': '1 0 0 0\n'}
+count = 0
+for case in sorted((root / 'tests/threads').glob('*/*.zc')):
+ if args.case and args.case not in str(case):
+ continue
+ if args.backend == 'micro' and case.stem in ('ffi_o3', 'resources'):
+ print(f'skip {case.relative_to(root)} (native ffi fixture)')
+ continue
+ source = case.read_text()
+ if args.library:
+ source = source.replace('include rt, actor;', f'include rt; include \"{args.library}\"; include actor;')
+ proc = subprocess.run([str(root / args.vm)], input=source, text=True, capture_output=True, cwd=root, timeout=12)
+ actual = proc.stdout
+ if case.stem in ('parallel', 'cancel', 'cancel_o3'):
+ lines = actual.splitlines(keepends=True)
+ assert len(lines) == 2, f'{case}: invalid output {actual!r}, stderr={proc.stderr!r}'
+ milliseconds = int(lines[-1])
+ assert 0 <= milliseconds < (580 if case.stem == 'parallel' else 500), f'{case}: elapsed {milliseconds}ms'
+ actual = lines[0]
+ assert proc.returncode == 0 and actual == expected[case.stem], f'{case}: exit={proc.returncode}, stdout={proc.stdout!r}, stderr={proc.stderr!r}'
+ print(f'ok {case.relative_to(root)}')
+ count += 1
+assert count
+print(f'{count} thread context fixtures passed')
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;
diff --git a/tests/drivers/tools_contract_test.py b/tests/drivers/tools_contract_test.py
new file mode 100644
index 0000000..df02b2b
--- /dev/null
+++ b/tests/drivers/tools_contract_test.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+"""bounded accept/reject tests for standalone build tools and generated images."""
+import argparse
+import re
+import struct
+import subprocess
+import tempfile
+from pathlib import Path
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--group', choices=['image', 'map', 'runners'], default='image')
+args = parser.parse_args()
+
+def command(argv, data=None):
+ r = subprocess.run(argv, input=data, capture_output=True, timeout=15)
+ assert r.returncode >= 0, (argv, r.returncode, r.stderr)
+ return r
+
+if args.group == 'image':
+ source = Path('src/core/genome.h').read_text()
+ body = source.split('{', 1)[1].split('}', 1)[0]
+ words = [int(x) for x in re.findall(r'-?\d+', body)]
+ pack = lambda xs: b''.join(struct.pack('<q', x) for x in xs)
+ good = command(['bin/mkgenome'], pack(words))
+ assert good.returncode == 0 and f'#define GLEN {len(words)}'.encode() in good.stdout
+ for data in [b'', b'x', pack(words[:6]), pack(words) + b'x']:
+ r = command(['bin/mkgenome'], data)
+ assert r.returncode != 0 and b'incomplete' in r.stderr
+ for index in range(3):
+ changed = words[:]; changed[index] ^= 2
+ r = command(['bin/mkgenome'], pack(changed))
+ assert r.returncode != 0 and b'not a genome' in r.stderr
+ print('ok mkgenome roundtrip, truncation, alignment and map-header mutation contracts')
+elif args.group == 'map':
+ for preset in ['default', 'small', 'run', 'lsp', 'fixed']:
+ with tempfile.TemporaryDirectory(prefix='tinyvm preset ') as directory:
+ r = command(['bin/genmap', '--preset', preset, '-o', directory])
+ assert r.returncode == 0, (preset, r.stderr)
+ assert (Path(directory) / 'memmap.h').is_file()
+ assert command(['bin/genmap', '--preset', 'default', '--check']).returncode == 0
+ for options in [['--words', '-1'], ['--heap', '0'], ['--data', '0'], ['--symbols', '-1'], ['--total', '999999999999999999999'], ['--preset', 'imaginary']]:
+ r = command(['bin/genmap', *options, '--check'])
+ assert r.returncode != 0, options
+ with tempfile.TemporaryDirectory(prefix='tinyvm map ') as directory:
+ r = command(['bin/genmap', '--preset', 'small', '--locals', '32', '--symbols', '64', '-o', directory])
+ assert r.returncode == 0, r.stderr
+ for name in ['memmap.h', 'rt.zc', 'compiler.zc', 'map.txt']:
+ assert (Path(directory) / name).is_file(), name
+ print('ok genmap presets, invalid capacities and generated paths containing spaces')
+else:
+ r = command(['bash', '-n', 'tinyvm.sh'])
+ assert r.returncode == 0, r.stderr
+ for script in ['src/tools/automap.sh', 'src/tools/mklsp.sh', 'src/tools/mklspwasm.sh', 'src/tools/mkvsix.sh', 'src/tools/embed.sh']:
+ assert command(['bash', '-n', script]).returncode == 0
+ assert command(['bash', 'tinyvm.sh', 'imaginary-command']).returncode != 0
+ assert Path('tinyvm.bat').is_file()
+ assert not list(Path('.').glob('run*.bat')), 'windows run files must be consolidated'
+ print('ok runner and build script syntax; unsupported commands fail explicitly')
diff --git a/tests/drivers/vscode_hover_test.js b/tests/drivers/vscode_hover_test.js
index 378dda2..8239afd 100644
--- a/tests/drivers/vscode_hover_test.js
+++ b/tests/drivers/vscode_hover_test.js
@@ -27,7 +27,7 @@ exports.run = async () => {
assert.ok(md.value.includes('**two**') && md.value.includes('`integer`'));
assert.strictEqual(md.isTrusted, false);
const storageFile = path.join(root, 'build', 'review', 'hover-storage.zc');
- fs.writeFileSync(storageFile, 'unchecked fn main() {\n let p = new(int, 3);\n let /* borrowed array */ q = p;\n free(p);\n return q[0];\n}\n');
+ fs.writeFileSync(storageFile, 'include rt; unchecked fn main() {\n let p = rt.new(int, 3);\n let /* borrowed array */ q = p;\n rt.free(p);\n return q[0];\n}\n');
const storageDoc = await vscode.workspace.openTextDocument(storageFile);
await vscode.window.showTextDocument(storageDoc);
let storageHover;
@@ -42,7 +42,24 @@ exports.run = async () => {
assert.ok(storageMd instanceof vscode.MarkdownString);
assert.match(storageMd.value, /borrowed array/);
assert.match(storageMd.value, /lifetime: ended on line 4/);
- const result = 'ok real vscode markdown, code fences, block docs, inferred pointer types, and lifetime metadata\n';
+ const valueFile = path.join(root, 'build', 'review', 'hover-values.zc');
+ fs.writeFileSync(valueFile, 'include rt; fn unknown() { return 9; }\nfn main() {\n int count = 42;\n rt.put_n(count);\n count = unknown();\n return count;\n}\n');
+ const valueDoc = await vscode.workspace.openTextDocument(valueFile);
+ await vscode.window.showTextDocument(valueDoc);
+ let valueHover;
+ for (let attempt = 0; attempt < 100; attempt++) {
+ const replies = await vscode.commands.executeCommand('vscode.executeHoverProvider', valueDoc.uri, new vscode.Position(3, 11));
+ valueHover = replies && replies.find(reply => reply.contents.some(md => md.value.includes('value at this position:')));
+ if (valueHover) break;
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ assert.ok(valueHover, 'real editor returns a known value before the unknown assignment');
+ assert.ok(valueHover.contents[0] instanceof vscode.MarkdownString);
+ assert.match(valueHover.contents[0].value, /value at this position:\n```zc\n42\n```/);
+ const unknownHover = await vscode.commands.executeCommand('vscode.executeHoverProvider', valueDoc.uri, new vscode.Position(5, 8));
+ assert.ok(unknownHover.length, 'the later variable still has a type hover');
+ assert.ok(!unknownHover.some(reply => reply.contents.some(md => md.value.includes('value at this position:'))));
+ const result = 'ok real vscode markdown, code fences, block docs, inferred types, lifetime metadata, and position-aware values\n';
fs.writeFileSync(path.join(root, 'build', 'review', 'vscode-hover.log'), result);
console.log(result.trim());
};
diff --git a/tests/drivers/wasm_test.js b/tests/drivers/wasm_test.js
index ddab3be..e4f7ecb 100644
--- a/tests/drivers/wasm_test.js
+++ b/tests/drivers/wasm_test.js
@@ -13,6 +13,7 @@ const ZC = require('../../src/web/zc.js');
['examples/struct_closure_opt.zc', '299\n'],
['examples/opt_test.zc', '1303\n'],
['tests/programs/basics.zc', '285 610 42 512\nhey 65 -3 -2 1024 -8\n12456\n757\n9 100-101\n5\nok\n'],
+ ['tests/programs/replace_owner.zc', '98 7 9 0\n'],
];
let ok = 0, bad = 0;
for (const [file, want] of cases) {
@@ -24,5 +25,5 @@ const ZC = require('../../src/web/zc.js');
const r = await zc.run('fn main() { return undefined_name; }');
if (/undefined_name/.test(r.output)) { ok++; console.log('ok wasm compiler errors'); } else { bad++; console.log('FAIL wasm compiler errors: ' + JSON.stringify(r.output)); }
console.log(ok + ' wasm checks passed, ' + bad + ' failed');
- process.exit(bad ? 1 : 0);
+ process.exitCode = bad ? 1 : 0;
})();