aboutsummaryrefslogtreecommitdiff
path: root/tests/drivers
diff options
context:
space:
mode:
authorGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
committerGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
commitecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch)
tree70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /tests/drivers
a
Diffstat (limited to 'tests/drivers')
-rw-r--r--tests/drivers/code_address_growth_test.py23
-rw-r--r--tests/drivers/code_emission_growth_test.py40
-rw-r--r--tests/drivers/compiler_growth_test.py82
-rw-r--r--tests/drivers/core_lsp_test.js43
-rw-r--r--tests/drivers/diagcache_test.js140
-rw-r--r--tests/drivers/diagnostic_revision_test.js47
-rw-r--r--tests/drivers/enhancement_backends_test.py45
-rw-r--r--tests/drivers/ext_cache_test.js154
-rw-r--r--tests/drivers/float_backends_test.py47
-rw-r--r--tests/drivers/frame_growth_test.py32
-rw-r--r--tests/drivers/genome_blob_test.py32
-rw-r--r--tests/drivers/hover_features_test.js85
-rw-r--r--tests/drivers/hover_test.js23
-rw-r--r--tests/drivers/image_growth_test.py25
-rw-r--r--tests/drivers/image_run.js14
-rw-r--r--tests/drivers/inference_growth_test.py34
-rw-r--r--tests/drivers/layout_config_test.py26
-rw-r--r--tests/drivers/learn.py64
-rw-r--r--tests/drivers/local_index_test.py23
-rw-r--r--tests/drivers/lsp_growth_test.js53
-rw-r--r--tests/drivers/lsp_live.js23
-rw-r--r--tests/drivers/lsp_sandbox.js22
-rw-r--r--tests/drivers/lsp_stream.js11
-rw-r--r--tests/drivers/lsp_test.js246
-rw-r--r--tests/drivers/lsp_vm.js112
-rw-r--r--tests/drivers/lsp_wasm.js112
-rw-r--r--tests/drivers/memory_limits_test.py23
-rw-r--r--tests/drivers/parser_growth_test.py48
-rw-r--r--tests/drivers/protocol_growth_test.js34
-rw-r--r--tests/drivers/seed_growth_test.py25
-rw-r--r--tests/drivers/stack_growth_test.py58
-rw-r--r--tests/drivers/storage_analysis_test.js71
-rw-r--r--tests/drivers/vscode_hover_test.js48
-rw-r--r--tests/drivers/wasm_test.js28
-rw-r--r--tests/drivers/word_literals_test.py19
35 files changed, 1912 insertions, 0 deletions
diff --git a/tests/drivers/code_address_growth_test.py b/tests/drivers/code_address_growth_test.py
new file mode 100644
index 0000000..a454406
--- /dev/null
+++ b/tests/drivers/code_address_growth_test.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# native address metadata grows without losing warm translations or rewrite tracking.
+import subprocess
+import sys
+
+source = '''o3 int warmed(int value) { return value + 1; }
+fn main() {
+ put_n(warmed(6)); put_c(32);
+ let code = new(6);
+ code[0] = code + 3; code[3] = 8; code[4] = 15; code[5] = 18;
+ put_n(syscall(9, 9, code + 6, 0) >= code + 6); put_c(32);
+ put_n(code()); put_c(32);
+ fill_w(code + 4, 19, 1); put_n(code()); put_c(32);
+ let replacement = new(1); replacement[0] = 23;
+ copy_w(code + 4, replacement, 1); put_n(code()); put_c(32);
+ gc(); put_n(warmed(8)); put_c(32);
+ put_n(syscall(9, 9, syscall(9, 0, 0, 0) + 1, 0) == -1); put_c(10);
+ return 0;
+}
+'''
+result = subprocess.run(sys.argv[1:], input=source, text=True, capture_output=True, timeout=20)
+assert result.returncode == 0 and result.stdout == '7 1 7 9 11 9 1\n', (result.returncode, result.stdout, result.stderr)
+print('ok new code addresses, warm calls, bulk rewrites, and denied metadata growth')
diff --git a/tests/drivers/code_emission_growth_test.py b/tests/drivers/code_emission_growth_test.py
new file mode 100644
index 0000000..f13c141
--- /dev/null
+++ b/tests/drivers/code_emission_growth_test.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+# emitted code and global data keep their addresses when the program area fills.
+import argparse
+import subprocess
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--length', type=int, default=600000)
+parser.add_argument('command', nargs=argparse.REMAINDER)
+options = parser.parse_args()
+source = 'int earlier() { return 7; }\n'
+source += f'let storage[{options.length}];\n'
+source += 'let text = "' + 's' * 3500 + '";\n'
+source += 'int later(int n) { return earlier() + n; }\n'
+source += '''fn main() {
+ storage[len(storage) - 1] = 9;
+ gc(); put_n(later(3)); put_c(32); put_n(storage[len(storage) - 1]); put_c(32);
+ put_n(len_w(text)); put_c(32); put_n(code_blocks != 0); put_c(32);
+ let quoted = quote { return 5; };
+ put_n(eval(quoted)); put_c(32);
+ let f = fn(float x) { return x + 1.5; }; put_n((f(2.0) as word) == 0x400c000000000000); put_c(10);
+ return 0;
+}
+'''
+result = subprocess.run(options.command, input=source, text=True, capture_output=True, timeout=40)
+assert result.returncode == 0 and result.stdout == '10 9 3500 1 5 1\n', (result.returncode, result.stdout, result.stderr)
+print('ok linked code segments preserve functions, globals, strings, quotes, and float payloads')
+
+# force the next float opcode and its payload to need a new segment.
+source = 'immediate fn force_edge() { next(); hlimit = here + 3; return 0; }\n'
+source += 'fn main() { force_edge let value = 1.5; gc(); put_n((value as word) == 0x3ff8000000000000); put_c(10); return 0; }\n'
+result = subprocess.run(options.command, input=source, text=True, capture_output=True, timeout=20)
+assert result.returncode == 0 and result.stdout == '1\n', (result.returncode, result.stdout, result.stderr)
+print('ok a float opcode and its payload remain adjacent at a segment boundary')
+source = 'immediate fn force_edge() { next(); hlimit = here + 3; return 0; }\n'
+source += 'force_edge\nenhance cube(x) vm { 0 $ 0 $ * * };\n'
+source += 'enhance sum(a, b) micro "a = add a b";\n'
+source += 'fn main() { gc(); put_n(cube(7)); put_c(32); put_n(sum(3, 4)); put_c(10); return 0; }\n'
+result = subprocess.run(options.command, input=source, text=True, capture_output=True, timeout=20)
+assert result.returncode == 0 and result.stdout == '343 7\n', (result.returncode, result.stdout, result.stderr)
+print('ok vm and micro recipes remain contiguous after a code reservation')
diff --git a/tests/drivers/compiler_growth_test.py b/tests/drivers/compiler_growth_test.py
new file mode 100644
index 0000000..982611c
--- /dev/null
+++ b/tests/drivers/compiler_growth_test.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+# focused growth checks for compiler columns, stable names, types, and scratch stacks.
+import argparse
+import subprocess
+import sys
+
+parser = argparse.ArgumentParser()
+parser.add_argument('server')
+parser.add_argument('--names', type=int, default=18000)
+options = parser.parse_args()
+server = options.server
+assert options.names > 0
+
+
+def run(source, expected):
+ result = subprocess.run([server], input=source, text=True, capture_output=True, timeout=30)
+ assert result.returncode == 0, result.stderr
+ assert result.stdout == expected, result.stdout[:1000]
+
+
+locals_source = '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'
+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'
+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 += 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);
+ 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')
+
+mark_source = '''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;
+}
+'''
+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 += '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'
+run(generics_source, '11415\n')
+
+captures_source = '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'
+run(captures_source, '2415\n')
+
+nested_source = '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'
+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'
+run(assembly_source, '7\n')
+print('ok compiler tables, long identifiers, and mark work grow without losing references')
diff --git a/tests/drivers/core_lsp_test.js b/tests/drivers/core_lsp_test.js
new file mode 100644
index 0000000..41d0095
--- /dev/null
+++ b/tests/drivers/core_lsp_test.js
@@ -0,0 +1,43 @@
+// check maintained zc sources through the editor document protocol, one file at a time.
+const assert = require('assert');
+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 frame = message => {
+ const body = Buffer.from(JSON.stringify(message));
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]);
+};
+for (const file of files) {
+ const uri = 'file://' + path.resolve(file);
+ const text = fs.readFileSync(file, 'utf8');
+ const messages = [
+ { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
+ { jsonrpc: '2.0', method: 'initialized', params: {} },
+ { 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', method: 'textDocument/didClose', params: { textDocument: { uri } } },
+ { jsonrpc: '2.0', id: 3, method: 'shutdown', params: null },
+ { jsonrpc: '2.0', method: 'exit' },
+ ];
+ const result = cp.spawnSync(target, [], { input: Buffer.concat(messages.map(frame)), timeout: 30000, maxBuffer: 32 << 20 });
+ assert.strictEqual(result.status, 0, file + ': ' + result.stderr);
+ let data = result.stdout;
+ const replies = [];
+ while (data.length) {
+ const end = data.indexOf('\r\n\r\n');
+ assert.ok(end >= 0, 'complete protocol header');
+ const length = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
+ replies.push(JSON.parse(data.subarray(end + 4, end + 4 + length)));
+ 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');
+ 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');
+ console.log('ok editor open, highlight, change, and close: ' + file);
+}
diff --git a/tests/drivers/diagcache_test.js b/tests/drivers/diagcache_test.js
new file mode 100644
index 0000000..43966cf
--- /dev/null
+++ b/tests/drivers/diagcache_test.js
@@ -0,0 +1,140 @@
+// the rules of the diagnostics cache (editors/vscode/client/diagcache.js), with
+// plain node and no vscode: a key covers the server build, the text, and every
+// file the text reads through include, transitively.
+//
+// node tests/drivers/diagcache_test.js
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const assert = require('assert');
+const dc = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'diagcache.js'));
+
+let passed = 0, failed = 0;
+function check(name, fn) {
+ try { fn(); passed++; console.log('ok ' + name); }
+ catch (e) { failed++; console.log('FAIL ' + name + ': ' + e.message); }
+}
+
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-diagcache-'));
+const write = (rel, text) => { const p = path.join(root, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, text); return p; };
+const A = write('a.zc', 'include "b.zc";\nfn main() { return helper(); }\n');
+const B = write('b.zc', 'include "c.zc";\n');
+const C = write('c.zc', 'fn helper() { return 1; }\n');
+const U = write('unrelated.zc', 'fn other() { return 2; }\n');
+const aText = fs.readFileSync(A, 'utf8');
+
+const keyA = (stamp = 'S1', text = aText, p = A) => dc.diagKey(stamp, text, p, root).key;
+
+check('includeNames reads every include, however it is spaced', () => {
+ assert.deepStrictEqual(dc.includeNames('include "a.zc";\ninclude"b/c.zc";\nfn f() {}'), ['a.zc', 'b/c.zc']);
+ assert.deepStrictEqual(dc.includeNames('fn f() { return 1; }'), []);
+});
+
+check('includeNames ignores comments and literals while allowing comments between tokens', () => {
+ assert.deepStrictEqual(dc.includeNames('// include "fake.zc"\n/* include "also-fake.zc" */\nlet s = "include \\"string.zc\\"";\ninclude /* dependency */ "real.zc";'), ['real.zc']);
+});
+
+check('the same tree gives the same key', () => {
+ assert.strictEqual(keyA(), keyA());
+ assert.match(keyA(), /^[0-9a-f]{64}$/);
+});
+
+check('a change to the document text changes the key', () => {
+ assert.notStrictEqual(keyA('S1', aText + ' '), keyA());
+});
+
+check('a change to the server build changes the key', () => {
+ assert.notStrictEqual(keyA('S2'), keyA('S1'));
+});
+
+check('a change to a file the document includes, two steps down, changes the key', () => {
+ const before = keyA();
+ fs.writeFileSync(C, 'fn helper() { return 3; }\n');
+ const after = keyA();
+ assert.notStrictEqual(before, after);
+ fs.writeFileSync(C, 'fn helper() { return 1; }\n');
+ assert.strictEqual(keyA(), before, 'putting it back gives the old key again');
+});
+
+check('a file nothing includes does not change the key', () => {
+ const before = keyA();
+ fs.writeFileSync(U, 'fn other() { return 9; }\n');
+ assert.strictEqual(keyA(), before);
+});
+
+check('creating a missing include changes the key', () => {
+ const M = write('m.zc', 'include "later.zc";\nfn f() { return 0; }\n');
+ const mText = fs.readFileSync(M, 'utf8');
+ const before = dc.diagKey('S1', mText, M, root);
+ assert.ok(before.deps.some(p => p.endsWith('later.zc')), 'the missing name is still a dependency');
+ write('later.zc', 'fn g() { return 1; }\n');
+ assert.notStrictEqual(dc.diagKey('S1', mText, M, root).key, before.key);
+});
+
+check('an include is found from the workspace folder as well as the including file', () => {
+ const X = write('sub/x.zc', 'include "b.zc";\nfn x() { return 0; }\n');
+ const tree = dc.sourceTree(fs.readFileSync(X, 'utf8'), X, root);
+ assert.ok(tree.files.has(path.join(root, 'b.zc')), 'b.zc is read from the workspace root');
+ assert.ok(tree.files.has(path.join(root, 'sub', 'b.zc')), 'and the directory of x.zc is tried too');
+ assert.ok(tree.complete);
+});
+
+check('a tree too large to cover has no key, so nothing is cached for it', () => {
+ let many = '';
+ for (let i = 0; i < dc.MAX_FILES + 5; i++) many += 'include "nope' + i + '.zc";\n';
+ const big = write('big.zc', many);
+ const r = dc.diagKey('S1', many, big, root);
+ assert.strictEqual(r.key, null);
+ const cache = new dc.DiagCache(null);
+ assert.strictEqual(cache.put(r.key, []), false);
+ assert.strictEqual(cache.size(), 0);
+});
+
+check('the cache answers only for the key it was given', () => {
+ const cache = new dc.DiagCache(null);
+ const d = [{ message: 'x' }];
+ assert.ok(cache.put('k1', d));
+ assert.deepStrictEqual(cache.get('k1'), d);
+ assert.strictEqual(cache.get('k2'), null);
+ assert.strictEqual(cache.get(null), null);
+ assert.strictEqual(cache.put(null, []), false, 'a tree without a key is not kept');
+ assert.strictEqual(cache.size(), 1);
+});
+
+check('two states of one document are both kept, so going back is a hit', () => {
+ const cache = new dc.DiagCache(null);
+ cache.put('state-a', []);
+ cache.put('state-b', [{ message: 'b' }]);
+ assert.deepStrictEqual(cache.get('state-a'), []);
+ assert.deepStrictEqual(cache.get('state-b'), [{ message: 'b' }]);
+});
+
+check('the least recently used entries go first past the limit', () => {
+ const cache = new dc.DiagCache(null, 2);
+ cache.put('1', []);
+ cache.put('2', []);
+ cache.get('1');
+ cache.put('3', []);
+ assert.strictEqual(cache.size(), 2);
+ assert.strictEqual(cache.get('2'), null, 'two was the least recently used');
+ assert.deepStrictEqual(cache.get('1'), []);
+ assert.deepStrictEqual(cache.get('3'), []);
+});
+
+check('the cache carries over a save and load as plain JSON', () => {
+ const cache = new dc.DiagCache(null);
+ cache.put('k1', [{ message: 'z', range: { start: { line: 1, character: 2 } } }]);
+ const back = new dc.DiagCache(JSON.parse(JSON.stringify(cache)));
+ assert.deepStrictEqual(back.get('k1'), [{ message: 'z', range: { start: { line: 1, character: 2 } } }]);
+});
+
+check('a saved entry that is not well formed is dropped, not trusted', () => {
+ const back = new dc.DiagCache({ bad: { key: 'bad' }, good: [] });
+ assert.strictEqual(back.get('bad'), null);
+ assert.deepStrictEqual(back.get('good'), []);
+});
+
+fs.rmSync(root, { recursive: true, force: true });
+console.log(passed + ' passed, ' + failed + ' failed');
+process.exit(failed ? 1 : 0);
diff --git a/tests/drivers/diagnostic_revision_test.js b/tests/drivers/diagnostic_revision_test.js
new file mode 100644
index 0000000..725fb87
--- /dev/null
+++ b/tests/drivers/diagnostic_revision_test.js
@@ -0,0 +1,47 @@
+// exercise delayed diagnostics without relying on an editor's timing.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+const root = path.resolve(__dirname, '../..');
+const displayed = [], cached = [];
+const vscode = {
+ EventEmitter: class { fire() {} },
+ SemanticTokensLegend: class {},
+ Diagnostic: class { constructor(range, message) { this.message = message; } },
+ Range: class {},
+ DiagnosticSeverity: { Hint: 4, Information: 3, Warning: 2, Error: 1 },
+ Uri: { parse: value => value },
+};
+const filename = path.join(root, 'editors/vscode/client/extension.js');
+const context = vm.createContext({
+ module: { exports: {} }, __dirname: path.dirname(filename), Buffer, setTimeout, clearTimeout,
+ require(name) {
+ if (name === 'vscode') return vscode;
+ if (name === './vmproc' || name === './wasmproc') return {};
+ if (name.startsWith('./')) return require(path.join(path.dirname(filename), name));
+ return require(name);
+ },
+ displayed, cached,
+});
+vm.runInContext(fs.readFileSync(filename, 'utf8') + `
+diagnostics = { set(uri, list) { displayed.push(list.map(d => d.message)); } };
+cache = { put(key, raw) { cached.push({ key, raw }); return false; } };
+const uri = 'file:///reopened.zc';
+const diag = message => [{ message, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } } }];
+opened.add(uri); docKeys.set(uri, 'current'); docVersions.set(uri, 3);
+sentKeys.set(uri, [{ key: 'current', version: 3 }]);
+onPublish(uri, diag('old close'), 1);
+onPublish(uri, diag('old open'), 2);
+onPublish(uri, diag('current open'), 3);
+onPublish(uri, diag('duplicate'), 3);
+opened.delete(uri);
+onPublish(uri, diag('closed'), 3);
+opened.add(uri); docKeys.set(uri, null); docVersions.set(uri, 5);
+sentKeys.set(uri, [{ key: null, version: 4 }, { key: null, version: 5 }]);
+onPublish(uri, diag('old uncacheable text'), 4);
+onPublish(uri, diag('current uncacheable text'), 5);
+`, context, { filename });
+assert.deepStrictEqual(displayed.map(list => Array.from(list)), [['current open'], ['current uncacheable text']]);
+assert.deepStrictEqual(cached.map(entry => entry.key), ['current']);
+console.log('ok closed, reopened, duplicate, and uncacheable diagnostic revisions');
diff --git a/tests/drivers/enhancement_backends_test.py b/tests/drivers/enhancement_backends_test.py
new file mode 100644
index 0000000..b7d553b
--- /dev/null
+++ b/tests/drivers/enhancement_backends_test.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+# recipes, vm lowering, fallback, and ignored optimizer regions agree across hosts.
+import pathlib
+import subprocess
+
+root = pathlib.Path(__file__).resolve().parents[2]
+source = (root / 'tests/programs/enhance.zc').read_text()
+# keep this focused check short while still exercising signed and unsigned arithmetic.
+source = source.replace('i < 5000', 'i < 200')
+source += '\n'
+reference = None
+for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=25)
+ assert result.returncode == 0 and result.stdout and 'zc:' not in result.stdout, (command, result.returncode, result.stdout, result.stderr)
+ if reference is None:
+ reference = result.stdout
+ else:
+ assert result.stdout == reference, (command, reference, result.stdout)
+ print('ok vm recipes, multiple outputs, arithmetic, and fallback: ' + command[0])
+source = '''o3 int increment(int value) { return value + 1; }
+fn main() { put_n(increment(6)); put_c(32); put_n(17 / 5); put_c(32); put_n(17 % 5); put_c(10); return 0; }
+'''
+for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=15)
+ assert result.returncode == 0 and result.stdout == '7 3 2\n', (command, result.stdout, result.stderr)
+ print('ok optimizer descriptors preserve portable division slots: ' + command[0])
+
+# slot zero can be replaced, and an unsupported replacement preserves it.
+# real install descriptors are exercised without calling a compiler-specific parser.
+source = '''let descriptor[7], names[2];
+fn main() {
+ names[0] = "a"; names[1] = "b";
+ descriptor[0] = 0; descriptor[1] = 0; descriptor[2] = 2; descriptor[3] = 1;
+ descriptor[4] = "a = add a b"; descriptor[6] = names;
+ int installed = install(descriptor) == 0, first = 17 / 5;
+ descriptor[4] = "a = unknown a b";
+ int rejected = install(descriptor) < 0, second = 17 / 5;
+ descriptor[4] = "a = sdiv a b"; install(descriptor);
+ put_n(installed); put_c(32); put_n(first); put_c(32); put_n(rejected); put_c(32); put_n(second); put_c(10); return 0;
+}
+'''
+for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=15)
+ assert result.returncode == 0 and result.stdout == '1 22 1 22\n', (command, result.stdout, result.stderr)
+ print('ok replacing a built-in slot and rejecting a bad recipe: ' + command[0])
diff --git a/tests/drivers/ext_cache_test.js b/tests/drivers/ext_cache_test.js
new file mode 100644
index 0000000..11bf50b
--- /dev/null
+++ b/tests/drivers/ext_cache_test.js
@@ -0,0 +1,154 @@
+// the extension's diagnostics cache, end to end: the real client
+// (editors/vscode/client/extension.js) with a stub for the vscode module, and
+// the built-in wasm server. the checks: a first session asks the server and
+// saves the answer; a second session shows the saved answer before the server
+// starts; a change to an included file is checked again; putting it back is
+// answered from the cache.
+//
+// node tests/drivers/ext_cache_test.js
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const Module = require('module');
+
+const client = path.join(__dirname, '..', '..', 'editors', 'vscode', 'client');
+
+// the vscode module, as far as the client uses it
+const world = { lines: [], sets: [], listeners: {}, watchers: [], textDocuments: [], saved: undefined, root: '' };
+const stub = {
+ EventEmitter: class { constructor() { this.event = () => ({ dispose() {} }); } fire() {} },
+ Diagnostic: class { constructor(range, message, severity) { Object.assign(this, { range, message, severity }); } },
+ Range: class { constructor(a, b, c, d) { Object.assign(this, { start: { line: a, character: b }, end: { line: c, character: d } }); } },
+ DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 },
+ SemanticTokensLegend: class { constructor(t, m) { this.tokenTypes = t; this.tokenModifiers = m; } },
+ SemanticTokensBuilder: class { constructor() {} push() {} build() { return null; } },
+ MarkdownString: class {
+ constructor(v = '') { this.value = v; this.escaped = false; }
+ appendText(v) { this.value += v; this.escaped = true; return this; }
+ appendMarkdown(v) { this.value += v; return this; }
+ appendCodeblock(v, language) { this.value += '\n```' + language + '\n' + v + '\n```\n'; return this; }
+ },
+ Hover: class { constructor(c) { this.contents = c; } },
+ Uri: { parse: s => ({ toString: () => s, fsPath: decodeURIComponent(s.replace(/^file:\/\//, '')) }) },
+ workspace: {
+ workspaceFolders: undefined,
+ textDocuments: world.textDocuments,
+ getConfiguration: () => ({ get: () => undefined }),
+ createFileSystemWatcher: () => {
+ const w = { handlers: {}, dispose() {} };
+ world.watchers.push(w);
+ for (const k of ['onDidChange', 'onDidCreate', 'onDidDelete']) w[k] = f => { w.handlers[k] = f; return { dispose() {} }; };
+ return w;
+ },
+ ...['onDidChangeConfiguration', 'onDidOpenTextDocument', 'onDidChangeTextDocument', 'onDidSaveTextDocument', 'onDidCloseTextDocument']
+ .reduce((o, k) => { o[k] = f => { world.listeners[k] = f; return { dispose() {} }; }; return o; }, {}),
+ },
+ window: {
+ createOutputChannel: () => ({ appendLine: s => world.lines.push(String(s)), show() {}, dispose() {} }),
+ showWarningMessage() {}, showErrorMessage() {},
+ },
+ languages: {
+ createDiagnosticCollection: () => ({
+ set: (uri, list) => world.sets.push({ uri: uri.toString(), list, at: world.lines.length }),
+ delete() {}, dispose() {},
+ }),
+ registerDocumentSemanticTokensProvider: () => ({ dispose() {} }),
+ registerHoverProvider: (sel, p) => { world.hover = p; return { dispose() {} }; },
+ },
+ commands: { registerCommand: () => ({ dispose() {} }) },
+};
+const realResolve = Module._resolveFilename;
+Module._resolveFilename = function (req, ...rest) { return req === 'vscode' ? 'vscode' : realResolve.call(this, req, ...rest); };
+require.cache['vscode'] = { id: 'vscode', filename: 'vscode', loaded: true, exports: stub };
+
+let passed = 0, failed = 0;
+function check(name, ok, detail) {
+ if (ok) { passed++; console.log('ok ' + name); }
+ else { failed++; console.log('FAIL ' + name + (detail ? ': ' + detail : '')); }
+}
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+async function until(pred, ms = 20000) {
+ const end = Date.now() + ms;
+ while (Date.now() < end) { if (pred()) return true; await sleep(50); }
+ return pred();
+}
+
+function freshClient() {
+ for (const f of Object.keys(require.cache)) if (f.startsWith(client)) delete require.cache[f];
+ return require(path.join(client, 'extension.js'));
+}
+
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-extcache-'));
+stub.workspace.workspaceFolders = [{ uri: { fsPath: root } }];
+const helper = path.join(root, 'helper.zc');
+const main = path.join(root, 'main.zc');
+const helperOk = 'fn helper() { return 1; }\n';
+const helperBad = 'fn helper2() { return 1; }\n';
+fs.writeFileSync(helper, helperOk);
+const mainText = 'include "helper.zc";\nfn main() { return helper(); }\n';
+fs.writeFileSync(main, mainText);
+
+function doc(p, text) {
+ const d = { uri: { toString: () => 'file://' + p, fsPath: p }, languageId: 'zc', version: 1, text, getText() { return this.text; } };
+ return d;
+}
+
+const ctx = () => ({
+ subscriptions: [],
+ globalState: { get: () => world.saved, update: (k, v) => { world.saved = JSON.parse(JSON.stringify(v)); return Promise.resolve(); } },
+});
+const mainDoc = doc(main, mainText);
+const mainUri = 'file://' + main;
+
+(async () => {
+ // session one: nothing cached, so the server is asked
+ world.textDocuments.push(mainDoc);
+ let ext = freshClient();
+ let c1 = ctx();
+ ext.activate(c1);
+ check('session one: the server starts and checks the document', await until(() => world.lines.some(l => /server ready/.test(l))) && await until(() => world.sets.some(s => s.uri === mainUri)));
+ const first = world.sets.filter(s => s.uri === mainUri).pop();
+ check('session one: the server answered with a list', first && Array.isArray(first.list), JSON.stringify(first && first.list));
+ await sleep(800);
+ const savedKeys = Object.keys(world.saved || {});
+ // the hover is the server's markdown, parsed: a zc code block, not escaped text
+ 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));
+ 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);
+
+ // session two: the saved answer is shown before the server has started
+ world.lines.length = 0; world.sets.length = 0;
+ ext = freshClient();
+ ext.activate(ctx());
+ check('session two: the saved diagnostics are shown at once', world.sets.some(s => s.uri === mainUri) && world.lines.some(l => /from the cache/.test(l)));
+ check('session two: the server has not started yet', !world.lines.some(l => /server ready/.test(l)));
+ await until(() => world.lines.some(l => /server ready/.test(l)));
+ await sleep(300);
+ check('session two: the server was not asked about the document', !world.lines.some(l => /-> textDocument\/didOpen|-> textDocument\/didChange/.test(l)), JSON.stringify(world.lines.slice(0, 6)));
+ check('session two: the cache holds the same key as before', JSON.stringify(Object.keys(world.saved || {})) === JSON.stringify(savedKeys));
+
+ // a change to the included file, saved: the document is checked again and the error shows
+ world.lines.length = 0; world.sets.length = 0;
+ fs.writeFileSync(helper, helperBad);
+ for (const h of world.watchers) if (h.handlers.onDidChange) h.handlers.onDidChange({ fsPath: helper });
+ check('an included file changed: the document is checked again', await until(() => world.lines.some(l => /-> textDocument\/didOpen|-> textDocument\/didChange/.test(l)) && world.sets.some(s => s.uri === mainUri)), JSON.stringify(world.lines.slice(-4)));
+ await sleep(900);
+ check('an included file changed: the new state is saved under its own key', Object.keys(world.saved || {}).length === 2, JSON.stringify(Object.keys(world.saved || {})));
+
+ // the file goes back: its tree is the one the cache has, so the old answer is shown from the cache
+ world.lines.length = 0; world.sets.length = 0;
+ fs.writeFileSync(helper, helperOk);
+ for (const h of world.watchers) if (h.handlers.onDidChange) h.handlers.onDidChange({ fsPath: helper });
+ 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)));
+
+ ext.deactivate();
+ await sleep(700);
+ fs.rmSync(root, { recursive: true, force: true });
+ console.log(passed + ' passed, ' + failed + ' failed');
+ process.exit(failed ? 1 : 0);
+})().catch(e => { console.log('FAIL harness: ' + (e.stack || e)); process.exit(1); });
diff --git a/tests/drivers/float_backends_test.py b/tests/drivers/float_backends_test.py
new file mode 100644
index 0000000..2cb9473
--- /dev/null
+++ b/tests/drivers/float_backends_test.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+# compare float instructions, packed binary32 conversion, and decimal output.
+import pathlib
+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; }
+fn convert(bits) { return asm { bits @ #1218 }; }
+fn main() {
+ [4]f32 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);
+ return 0;
+}
+''']
+expected_arithmetic = '4.000000 4.500000 3.000000 2.500000 2.500000 0.001500 0.100000 -0.125000 inf -inf nan 1 1 1 0 0 3 -3 3.000000 -2.000000 2\n'
+for i, source in enumerate(sources):
+ reference = None
+ for command in commands:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=30)
+ assert result.returncode == 0 and result.stdout and 'zc:' not in result.stdout, (command, result.returncode, result.stdout, result.stderr)
+ if reference is None:
+ if i == 0:
+ assert result.stdout == expected_arithmetic, result.stdout
+ else:
+ assert result.stdout.startswith('1 1 1 1\n0.13 -0.13 '), result.stdout
+ assert result.stdout.endswith('0 9223372036854775807 -9223372036854775808\n'), result.stdout
+ reference = result.stdout
+ else:
+ assert result.stdout == reference, (command, i, reference, result.stdout)
+ print('ok float arithmetic and output fixture ' + str(i + 1) + ': ' + command[0])
+
+# 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'
+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)
+ print('ok float parsing reads the exponent beyond 127 characters: ' + command[0])
diff --git a/tests/drivers/frame_growth_test.py b/tests/drivers/frame_growth_test.py
new file mode 100644
index 0000000..74156e3
--- /dev/null
+++ b/tests/drivers/frame_growth_test.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# growing frames keep pointers into older segments alive through collection.
+import subprocess
+import sys
+
+command = sys.argv[1:]
+source = '''unchecked int descend(int depth, int* root) {
+ [2000]int padding;
+ padding[0] = depth;
+ if (depth == 0) { gc(); return *root; }
+ return descend(depth - 1, root) + padding[0];
+}
+unchecked int oversized(int* root) {
+ [300000]int values;
+ values[0] = *root; values[299999] = 9;
+ 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);
+ return 0;
+}
+'''
+result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=40)
+assert result.returncode == 0 and result.stdout == '45157 16 1\n', (command, result.returncode, result.stdout, result.stderr)
+print('ok deep and oversized frames preserve borrowed locals through collection: ' + command[0])
+optimized = source.replace('unchecked int descend', 'unchecked o3 int descend').replace('unchecked int oversized', 'unchecked o3 int oversized')
+result = subprocess.run(command, input=optimized, text=True, capture_output=True, timeout=40)
+assert result.returncode == 0 and result.stdout == '45157 16 1\n', (command, result.returncode, result.stdout, result.stderr)
+print('ok optimized recursive calls wait for the matching return depth: ' + command[0])
diff --git a/tests/drivers/genome_blob_test.py b/tests/drivers/genome_blob_test.py
new file mode 100644
index 0000000..81533a7
--- /dev/null
+++ b/tests/drivers/genome_blob_test.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# documentation precedes the symbol table; the boot header must skip those bytes.
+import pathlib
+import re
+
+source = pathlib.Path('src/core/genome.h').read_text()
+org = int(re.search(r'#define GORG (\d+)', source)[1])
+length = int(re.search(r'#define GLEN (\d+)', source)[1])
+words = [int(word.strip()) for word in re.search(r'genome\[\]\s*=\s*\{([\s\S]*?)\};', source)[1].split(',') if word.strip()]
+assert len(words) == length and words[6] == org + length
+cursor = words[5] - org
+assert 7 <= cursor < length
+records = {}
+while words[cursor]:
+ kind, value, signature = words[cursor:cursor + 3]
+ assert kind in (1, 2, 3, 4, 6, 8), (cursor, kind)
+ cursor += 3
+ name = ''
+ while words[cursor]:
+ assert 0 < words[cursor] < 128, (cursor, words[cursor])
+ name += chr(words[cursor])
+ cursor += 1
+ cursor += 1
+ records[name] = (kind, value, signature)
+assert cursor == length - 1
+assert len(records) > 1000 and records['start'][0] == 1
+assert records['reserve_code'][0] == 1 and records['build_image_grow'][0] == 1
+assert any(name.startswith('doc ') for name in records), 'the fixture contains documentation bytes'
+for name, (kind, value, _) in records.items():
+ if kind == 8:
+ assert org <= value < org + length and words[value - org] >= 0, name
+print('ok documented genome starts its symbol table at the header address')
diff --git a/tests/drivers/hover_features_test.js b/tests/drivers/hover_features_test.js
new file mode 100644
index 0000000..895e923
--- /dev/null
+++ b/tests/drivers/hover_features_test.js
@@ -0,0 +1,85 @@
+// focused type, inference, and block-documentation hover regressions.
+// usage: node tests/drivers/hover_features_test.js path/to/zclsp
+const assert = require('assert');
+const cp = require('child_process');
+const server = process.argv[2];
+const uri = 'file:///hover-features.zc';
+const frame = m => {
+ const body = Buffer.from(JSON.stringify(m));
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]);
+};
+function hover(text, needle, nth = 0) {
+ let offset = -1;
+ for (let i = 0; i <= nth; i++) offset = text.indexOf(needle, offset + 1);
+ assert.ok(offset >= 0, 'fixture contains ' + needle);
+ const before = text.slice(0, offset);
+ const line = before.split('\n').length - 1;
+ const character = before.length - before.lastIndexOf('\n') - 1;
+ const messages = [
+ { 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/hover', params: { textDocument: { uri }, position: { line, character } } },
+ { jsonrpc: '2.0', method: 'exit' },
+ ];
+ const result = cp.spawnSync(server, process.argv.slice(3), { input: Buffer.concat(messages.map(frame)), timeout: 10000, maxBuffer: 4 << 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 count = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
+ const msg = JSON.parse(data.subarray(end + 4, end + 4 + count).toString());
+ data = data.subarray(end + 4 + count);
+ if (msg.id === 2) return msg.result && msg.result.contents.value;
+ }
+ return null;
+}
+for (const type of ['word', 'int', 'char', 'bool', 'float', 'f64', 'f32', 'byte', 'u8', 'i8', 'u16', 'i16', 'u32', 'i32']) {
+ const value = hover(type + ' global;\nfn main() { return 0; }\n', type);
+ 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';
+assert.match(hover(array, '[4]'), /fixed length of 4/);
+assert.match(hover(array, 'values', 1), /type \[4\]u8/);
+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/);
+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/);
+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';
+const first = hover(variableDocs, 'first +');
+const second = hover(variableDocs, 'second;');
+assert.ok(first.includes('first variable') && !first.includes('second variable'), first);
+assert.ok(second.includes('second variable') && !second.includes('first variable'), second);
+const paramDocs = 'int add(int /* left argument */ a, int /* right argument */ b) { return a + b; }\nfn main() { return add(1, 2); }\n';
+const a = hover(paramDocs, 'a +');
+const b = hover(paramDocs, 'b;');
+assert.ok(a.includes('left argument') && !a.includes('right argument'), a);
+assert.ok(b.includes('right argument') && !b.includes('left argument'), b);
+const functionDocs = '/**\n * adds **one** to its argument.\n */\nint add(int x) { return x + 1; }\nfn main() { return add(2); }\n';
+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/);
+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/);
+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/);
+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\)/);
+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\*/);
+console.log('ok built-in and composite types, inferred signatures, and separate block docs');
diff --git a/tests/drivers/hover_test.js b/tests/drivers/hover_test.js
new file mode 100644
index 0000000..10af643
--- /dev/null
+++ b/tests/drivers/hover_test.js
@@ -0,0 +1,23 @@
+// protocol conversion checks. run with node tests/drivers/hover_test.js.
+const assert = require('assert');
+const { hoverContents } = require('../../editors/vscode/client/hover');
+class MarkdownString {
+ constructor() { this.value = ''; this.calls = []; }
+ appendMarkdown(value) { this.calls.push('markdown'); this.value += value; return this; }
+ appendText(value) { this.calls.push('text'); this.value += value; return this; }
+ appendCodeblock(value, language) { this.calls.push('code'); this.value += '\n```' + language + '\n' + value + '\n```\n'; return this; }
+}
+const api = { MarkdownString };
+const markdown = '```zc\nint add(int a, int b)\n```\n\nAdds **two** numbers.';
+const md = hoverContents(api, { kind: 'markdown', value: markdown })[0];
+assert.strictEqual(md.value, markdown);
+assert.deepStrictEqual(md.calls, ['markdown']);
+assert.strictEqual(md.isTrusted, false);
+assert.strictEqual(md.supportHtml, false);
+assert.deepStrictEqual(hoverContents(api, { kind: 'plaintext', value: '*literal*' })[0].calls, ['text']);
+const legacy = hoverContents(api, { language: 'zc', value: 'int x;' })[0];
+assert.match(legacy.value, /```zc\nint x;\n```/);
+assert.deepStrictEqual(legacy.calls, ['code']);
+assert.strictEqual(hoverContents(api, ['**Doc**', { language: 'zc', value: 'fn main()' }]).length, 2);
+for (const contents of [null, undefined, '', [], {}, { value: 123 }]) assert.deepStrictEqual(hoverContents(api, contents), []);
+console.log('ok hover conversion preserves Markdown, plaintext, code, and arrays');
diff --git a/tests/drivers/image_growth_test.py b/tests/drivers/image_growth_test.py
new file mode 100644
index 0000000..064eae7
--- /dev/null
+++ b/tests/drivers/image_growth_test.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# image output can grow past its first buffer and the building host's genome slot.
+import pathlib
+import subprocess
+import sys
+
+root = pathlib.Path(__file__).resolve().parents[2]
+source = '''fn main() {
+ int retained = 7;
+ int n = build_image_grow(GORG, 8);
+ gc();
+ put_n(n > FBOT - GORG); put_c(32);
+ put_n(image_size == n && image_words[6] == GORG + n); put_c(32);
+ put_n(image_words[5] > GORG && image_words[5] < image_words[6]); put_c(32);
+ put_n(retained); put_c(10);
+ return 0;
+}
+`'''
+source += (root / 'src/runtime/rt.zc').read_text()
+source += (root / 'src/runtime/run.zc').read_text()
+source += 'let padding[350000]; fn main() { return padding[349999]; }\n'
+result = subprocess.run(sys.argv[1:], input=source, capture_output=True, text=True, timeout=40)
+assert result.returncode == 0, (result.stdout, result.stderr)
+assert result.stdout == '1 1 1 7\n', result.stdout[:1000]
+print('ok growing image output keeps logical addresses and older frames intact')
diff --git a/tests/drivers/image_run.js b/tests/drivers/image_run.js
new file mode 100644
index 0000000..6d0b949
--- /dev/null
+++ b/tests/drivers/image_run.js
@@ -0,0 +1,14 @@
+// run a generated image directly, avoiding compiler time in vm execution tests.
+const fs = require('fs');
+const { TinyVM } = require('../../src/js/tinyvm.js');
+const { mapValues } = require('../../src/js/mapvalues.js');
+const header = fs.readFileSync(process.argv[2], 'utf8');
+const org = Number(header.match(/#define\s+GORG\s+(\d+)/)[1]);
+const words = header.match(/genome\[\]\s*=\s*\{([\s\S]*?)\};/)[1].split(',').map(s => s.trim()).filter(Boolean);
+const layout = mapValues(process.argv[3], ['GORG', 'DS0', 'DSLIM', 'RS0', 'RSLIM', 'SYSGATE', 'ZFP', 'HEAP_LO', 'HEAP_HI']);
+const machine = new TinyVM({ layout, onMessage: message => process.stderr.write(message) });
+machine.loadGenome(words, org);
+if (process.argv[4] === '--stdin') machine.setInput(fs.readFileSync(0));
+const status = machine.run();
+process.stdout.write(machine.getOutput());
+process.exit(status);
diff --git a/tests/drivers/inference_growth_test.py b/tests/drivers/inference_growth_test.py
new file mode 100644
index 0000000..d747f70
--- /dev/null
+++ b/tests/drivers/inference_growth_test.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+# infer past the original function-record and edit-count limits.
+import re
+import subprocess
+import sys
+
+source = ''.join(f'fn function_{i}(x) {{ return x + 1; }}\n' for i in range(600))
+source += 'fn main() {\n'
+source += ''.join(f'let value_{i} = {i};\n' for i in range(5000))
+source += ''.join(f'function_{i}(1);\n' for i in range(600))
+source += 'return value_4999; }\n'
+result = subprocess.run([sys.argv[1], '--annotate'], input=source, text=True, capture_output=True, timeout=45)
+assert result.returncode == 0, result.stderr
+assert re.search(r'int function_599\(int x\)', result.stdout), result.stdout[-1000:]
+assert 'int value_4999 = 4999;' in result.stdout, result.stdout[-1000:]
+assert 'let value_' not in result.stdout, 'later inference edits were dropped'
+print('ok inference handles 600 functions and 5000 local declarations')
+
+# later parameters and nested functions retain the enclosing inference context.
+params = ', '.join(f'arg_{i}' for i in range(20))
+source = f'fn wide({params}) {{ let nested = fn(int y) {{ return y + 1; }}; return arg_19 + nested(1); }}\n'
+source += 'fn main() { return wide(' + ', '.join('1' for _ in range(20)) + '); }\n'
+result = subprocess.run([sys.argv[1], '--annotate'], input=source, text=True, capture_output=True, timeout=15)
+assert result.returncode == 0, result.stderr
+assert 'int arg_19' in result.stdout, result.stdout
+assert 'int wide(' in result.stdout, result.stdout
+print('ok later arguments and nested closures preserve inferred signatures')
+
+source = 'struct point { int x, y; };\nfn main() { let a[3]; a[1] = 7; let alias = a; point p; p.x = 9; let ref = p; return alias[1] + ref.x; }\n'
+result = subprocess.run([sys.argv[1], '--annotate'], input=source, text=True, capture_output=True, timeout=15)
+assert result.returncode == 0 and 'word* alias' in result.stdout and 'point* ref' in result.stdout, result.stdout
+again = subprocess.run([sys.argv[1], '--annotate'], input=result.stdout, text=True, capture_output=True, timeout=15)
+assert again.returncode == 0 and again.stdout == result.stdout, again.stdout
+print('ok array and struct aliases get compatible pointer annotations')
diff --git a/tests/drivers/layout_config_test.py b/tests/drivers/layout_config_test.py
new file mode 100644
index 0000000..469e7dd
--- /dev/null
+++ b/tests/drivers/layout_config_test.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# generated layouts expose initial reservations and reject arithmetic overflow.
+import pathlib
+import subprocess
+import tempfile
+
+root = pathlib.Path(__file__).resolve().parents[2]
+tool = root / 'bin/genmap'
+result = subprocess.run([tool, '--help'], capture_output=True, text=True)
+assert result.returncode == 0 and '--generated-text' in result.stderr
+invalid = [
+ ['--symbols', '2147483647'], ['--code', '2147483647'],
+ ['--frame', '2147483647'], ['--return', '2147483647'],
+ ['--asm-labels', '2147483647'], ['--enum-members', '2147483647'],
+ ['--regions', '2147483647'], ['--heap', '7'], ['--hash', '3'],
+]
+for options in invalid:
+ result = subprocess.run([tool, '--preset', 'default', *options], capture_output=True, text=True)
+ assert result.returncode != 0 and result.stderr, options
+with tempfile.TemporaryDirectory(dir=root / 'build') as directory:
+ options = ['--asm-labels', '4', '--asm-refs', '8', '--exit', '32', '--enum-members', '8', '--tagged-enums', '2', '--methods', '2', '--generated-text', '64', '--facts', '2', '--paths', '2', '--regions', '2']
+ subprocess.run([tool, '--preset', 'default', *options, '-o', directory], check=True, capture_output=True)
+ source = (pathlib.Path(directory) / 'compiler.zc').read_text()
+ for name, value in [('ASM_LABELS', '4'), ('ASM_REFS', '8'), ('EXIT_WORDS', '20'), ('ENUM_MEMBERS', '8'), ('TAGGED_ENUMS', '2'), ('STRUCT_METHODS', '2'), ('GENERATED_WORDS', '40'), ('RANGE_FACTS', '2'), ('JOIN_PATHS', '2'), ('O3_REGIONS', '2')]:
+ assert f'const {name} = 0x{value};' in source, name
+print('ok parser layout controls and invalid configuration checks')
diff --git a/tests/drivers/learn.py b/tests/drivers/learn.py
new file mode 100644
index 0000000..42f729f
--- /dev/null
+++ b/tests/drivers/learn.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+# checks docs/LEARN.md against the examples it shows. every examples/learn/*.zc runs
+# from the repo root with its stdout compared to the matching .out file, and
+# every ```zc block in docs/LEARN.md must be exactly one of those files, so the
+# guide can't drift from the code it teaches. usage: learn.py path/to/zy
+import glob, os, platform, re, subprocess, sys
+
+root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+os.chdir(root)
+zy = os.path.abspath(sys.argv[1])
+x86_only = {"22-raw-x86"}
+bad = 0
+
+def ok(name):
+ print(f"ok {name}")
+
+def fail(name, why):
+ global bad
+ bad += 1
+ print(f"FAIL {name}: {why}")
+
+files = sorted(glob.glob("examples/learn/*.zc"))
+for path in files:
+ name = os.path.splitext(os.path.basename(path))[0]
+ if name in x86_only and platform.machine() not in ("x86_64", "AMD64"):
+ print(f"skip {name} (x86-64 only)")
+ continue
+ with open(path, "rb") as f:
+ src = f.read()
+ try:
+ r = subprocess.run([zy], stdin=open(path, "rb"), capture_output=True, timeout=30)
+ except subprocess.TimeoutExpired:
+ fail(f"learn {name}", "timed out")
+ continue
+ got = r.stdout.decode("utf-8", "replace")
+ want_path = os.path.splitext(path)[0] + ".out"
+ if not os.path.exists(want_path):
+ fail(f"learn {name}", f"no {want_path}")
+ elif got == open(want_path, encoding="utf-8").read():
+ ok(f"learn {name} prints what it says")
+ else:
+ fail(f"learn {name}", f"got [{got}]")
+
+# every zc block in docs/LEARN.md is one of the example files
+with open("docs/LEARN.md", encoding="utf-8") as f:
+ doc = f.read()
+blocks = re.findall(r"^```zc\n(.*?)^```", doc, re.S | re.M)
+srcs = {}
+for path in files:
+ with open(path, encoding="utf-8") as f:
+ srcs[os.path.basename(path)] = f.read()
+used = set()
+for b in blocks:
+ hit = [n for n, s in srcs.items() if s == b]
+ if hit:
+ used.add(hit[0])
+ else:
+ fail("learn block", "a ```zc block in docs/LEARN.md is not an examples/learn file: " + b.splitlines()[0])
+for n in srcs:
+ if n not in used:
+ fail(f"learn {n}", "not shown in docs/LEARN.md")
+if not bad:
+ ok(f"docs/LEARN.md: {len(blocks)} blocks, all match examples/learn")
+sys.exit(1 if bad else 0)
diff --git a/tests/drivers/local_index_test.py b/tests/drivers/local_index_test.py
new file mode 100644
index 0000000..95bfd78
--- /dev/null
+++ b/tests/drivers/local_index_test.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# local name heads survive shadowing, scope reuse, closures, and context switches.
+import subprocess
+import sys
+source = '''
+fn choose(int x) {
+ let total = x;
+ { let x = 7; total += x; }
+ { let x = 9; total += x; }
+ let f = fn(int x) { return x + 1; };
+ let g = fn() { return total; };
+ return total + x + f(3) + g();
+}
+o3 int fast(int x) { int result = x; { int x = 8; result += x; } return result + x; }
+fn main() { put_n(choose(2)); put_c(32); put_n(fast(3)); put_c(10); return 0; }
+'''
+result = subprocess.run(sys.argv[1:], input=source, text=True, capture_output=True, timeout=15)
+assert result.returncode == 0 and result.stdout == '42 14\n', (result.returncode, result.stdout, result.stderr)
+print('ok local shadow chains, reused scopes, closure parameters, captures, and o3 contexts')
+source = 'struct point { int x, y; };\nfn main() { let a[3]; a[1] = 7; let alias = a; point p; p.x = 9; let ref = p; put_n(alias[1]); put_c(32); put_n(ref.x); put_c(10); return 0; }\n'
+result = subprocess.run(sys.argv[1:], input=source, text=True, capture_output=True, timeout=15)
+assert result.returncode == 0 and result.stdout == '7 9\n', (result.returncode, result.stdout, result.stderr)
+print('ok inferred aliases load array and struct addresses rather than the alias slot')
diff --git a/tests/drivers/lsp_growth_test.js b/tests/drivers/lsp_growth_test.js
new file mode 100644
index 0000000..b06230a
--- /dev/null
+++ b/tests/drivers/lsp_growth_test.js
@@ -0,0 +1,53 @@
+// focused regressions for growing document, uri, hover, and name-set storage.
+// usage: node tests/drivers/lsp_growth_test.js path/to/zclsp-or-lsp.wasm
+const assert = require('assert');
+const cp = require('child_process');
+const { wasmProcess } = require('../../editors/vscode/client/wasmproc');
+const target = process.argv[2];
+const frame = m => {
+ const body = Buffer.from(JSON.stringify(m));
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]);
+};
+function parse(input) {
+ const messages = [];
+ for (let offset = 0; offset < input.length;) {
+ const end = input.indexOf('\r\n\r\n', offset);
+ assert.ok(end >= 0, 'complete response header');
+ const length = Number(/Content-Length:\s*(\d+)/i.exec(input.subarray(offset, end).toString())[1]);
+ const start = end + 4;
+ assert.ok(start + length <= input.length, 'complete response body');
+ messages.push(JSON.parse(input.subarray(start, start + length).toString()));
+ offset = start + length;
+ }
+ return messages;
+}
+const uri = 'file:///' + 'long-directory/'.repeat(30) + 'main.zc';
+const docs = '// **formatted** documentation with `inline code`.\n' + ('// a long paragraph of documentation.\n').repeat(150) + '// final documentation line\n';
+const source = docs + 'int helper() { return 1; }\nfn main() { return helper(); }\n';
+const messages = [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }];
+for (let i = 0; i < 32; i++) messages.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri: i === 31 ? uri : 'file:///doc' + i + '.zc', languageId: 'zc', version: 1, text: source } } });
+messages.push({ jsonrpc: '2.0', id: 2, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: source.split('\n').length - 2, character: 19 } } });
+messages.push({ jsonrpc: '2.0', method: 'exit' });
+let output;
+if (target.endsWith('.wasm')) {
+ const chunks = [];
+ const server = wasmProcess(target);
+ server.stdout.on('data', data => chunks.push(data));
+ for (const message of messages) assert.ok(server.stdin.write(frame(message)));
+ output = Buffer.concat(chunks);
+} else {
+ const result = cp.spawnSync(target, [], { input: Buffer.concat(messages.map(frame)), timeout: 30000, maxBuffer: 8 << 20 });
+ assert.strictEqual(result.status, 0, String(result.stderr));
+ output = result.stdout;
+}
+const responses = parse(output);
+const published = responses.filter(m => m.method === 'textDocument/publishDiagnostics');
+assert.strictEqual(published.length, 32, 'every open document is published');
+assert.strictEqual(published[31].params.uri, uri, 'a long uri is echoed without truncation');
+assert.ok(published.every(m => !m.params.diagnostics.some(d => d.severity === 1)), 'all documents check cleanly');
+const hover = responses.find(m => m.id === 2).result.contents;
+assert.strictEqual(hover.kind, 'markdown');
+assert.match(hover.value, /^```zc\nint helper\(\)\n```\n\n/);
+assert.ok(hover.value.includes('**formatted**'));
+assert.ok(hover.value.includes('final documentation line'), 'hover documentation grows past its initial buffer');
+console.log('ok lsp grows beyond 16 documents, 160 uri bytes, and 2300 hover characters');
diff --git a/tests/drivers/lsp_live.js b/tests/drivers/lsp_live.js
new file mode 100644
index 0000000..b5fe805
--- /dev/null
+++ b/tests/drivers/lsp_live.js
@@ -0,0 +1,23 @@
+// the native server must answer while the client keeps the pipe open and waits, as an editor
+// does: one message in, the answer out, before anything else is sent. an output buffer that
+// is only flushed at exit, or an end-of-input peek that waits for the next message, fails this.
+// usage: node tests/drivers/lsp_live.js path/to/zclsp
+const cp = require('child_process');
+const p = cp.spawn(process.argv[2], []);
+let inbox = Buffer.alloc(0), step = 0, done = false;
+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 finish = (ok, what) => { if (done) return; done = true; p.kill(); console.log((ok ? 'ok ' : 'FAIL ') + what); process.exit(ok ? 0 : 1); };
+const timer = setTimeout(() => finish(false, 'lsp live: no answer in 5 s while the pipe stayed open (step ' + step + ')'), 5000);
+p.stdout.on('data', d => {
+ inbox = Buffer.concat([inbox, d]);
+ 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;
+ const m = JSON.parse(inbox.slice(e + 4, e + 4 + n).toString());
+ inbox = inbox.slice(e + 4 + n);
+ if (step === 0 && m.id === 1) {
+ step = 1; // the answer came with the pipe still open: ask for something else
+ p.stdin.write(frame({ jsonrpc: '2.0', id: 2, method: 'shutdown', params: null }));
+ } else if (step === 1 && m.id === 2) { clearTimeout(timer); finish(true, 'lsp live: initialize and shutdown are answered while the pipe stays open'); }
+});
+p.stdin.write(frame({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } }));
diff --git a/tests/drivers/lsp_sandbox.js b/tests/drivers/lsp_sandbox.js
new file mode 100644
index 0000000..14bc1e3
--- /dev/null
+++ b/tests/drivers/lsp_sandbox.js
@@ -0,0 +1,22 @@
+// opening a file in the editor checks it, and a check runs the file's compile-time code
+// (immediate functions). the server must not let that code write files, so a file that
+// tries to leaves nothing behind and the server still answers.
+// usage: node tests/drivers/lsp_sandbox.js path/to/zclsp
+const cp = require('child_process'), fs = require('fs'), os = require('os'), path = require('path');
+const target = path.join(os.tmpdir(), 'zc-sandbox-' + process.pid);
+try { fs.unlinkSync(target); } catch (e) {}
+const src = 'immediate fn evil() { let fd = syscall(1, "' + target + '", 1, 0); syscall(4, fd, 65, 1); syscall(2, fd, 0, 0); return 0; }\nevil\nfn main() { return 0; }\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]); };
+const input = Buffer.concat([
+ frame({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } }),
+ frame({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri: 'file:///evil.zc', languageId: 'zc', version: 1, text: src } } }),
+ frame({ jsonrpc: '2.0', id: 3, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri: 'file:///evil.zc' } } }),
+ frame({ jsonrpc: '2.0', method: 'exit' }),
+]);
+const r = cp.spawnSync(process.argv[2], [], { input, timeout: 20000 });
+const wrote = fs.existsSync(target);
+if (wrote) fs.unlinkSync(target);
+const answered = r.stdout && r.stdout.includes('"id":3');
+const ok = !wrote && answered;
+console.log((ok ? 'ok ' : 'FAIL ') + 'lsp sandbox: compile-time code cannot write files' + (ok ? '' : ' (wrote: ' + wrote + ', answered: ' + !!answered + ')'));
+process.exit(ok ? 0 : 1);
diff --git a/tests/drivers/lsp_stream.js b/tests/drivers/lsp_stream.js
new file mode 100644
index 0000000..0dd0553
--- /dev/null
+++ b/tests/drivers/lsp_stream.js
@@ -0,0 +1,11 @@
+// expose either extension server host as a stdin/stdout process for focused tests.
+const fs = require('fs');
+const path = require('path');
+const target = path.resolve(process.argv[2]);
+const { wasmProcess } = require('../../editors/vscode/client/wasmproc');
+const { vmProcess } = require('../../editors/vscode/client/vmproc');
+const server = target.endsWith('.wasm') ? wasmProcess(target) : vmProcess(target);
+server.stdout.on('data', bytes => process.stdout.write(bytes));
+server.stderr.on('data', bytes => process.stderr.write(bytes));
+server.on('exit', status => { if (status !== 0) process.exitCode = 1; });
+server.stdin.write(fs.readFileSync(0));
diff --git a/tests/drivers/lsp_test.js b/tests/drivers/lsp_test.js
new file mode 100644
index 0000000..a783e1e
--- /dev/null
+++ b/tests/drivers/lsp_test.js
@@ -0,0 +1,246 @@
+// protocol tests for the language server (node, no packages).
+// usage: node tests/drivers/lsp_test.js path/to/zclsp
+const { spawnSync } = require('child_process');
+const fs = require('fs');
+const server = process.argv[2];
+let ok = 0, bad = 0;
+function check(cond, label, extra) {
+ if (cond) { ok++; console.log('ok lsp ' + label); }
+ else { bad++; console.log('FAIL lsp ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra))); }
+}
+function frame(m) {
+ const b = Buffer.from(JSON.stringify(m), 'utf8');
+ return Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b]);
+}
+function parse(buf) {
+ const out = []; let i = 0;
+ while (i < buf.length) {
+ const j = buf.indexOf('\r\n\r\n', i);
+ if (j < 0) break;
+ const n = parseInt(/Content-Length: (\d+)/.exec(buf.slice(i, j).toString())[1], 10);
+ out.push(JSON.parse(buf.slice(j + 4, j + 4 + n).toString('utf8')));
+ i = j + 4 + n;
+ }
+ return out;
+}
+function run(msgs) {
+ const r = spawnSync(server, [], { input: Buffer.concat(msgs.map(frame)), timeout: 30000, maxBuffer: 1 << 28 });
+ return { code: r.status, msgs: parse(r.stdout) };
+}
+const init = id => ({ jsonrpc: '2.0', id, method: 'initialize', params: {} });
+const open = (uri, text) => ({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } });
+const change = (uri, text) => ({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version: 2 }, contentChanges: [{ text }] } });
+const sem = (id, uri) => ({ jsonrpc: '2.0', id, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } });
+const diags = (msgs, uri) => msgs.filter(m => m.method === 'textDocument/publishDiagnostics' && (!uri || m.params.uri === uri));
+const A = 'file:///a.zc', B2 = 'file:///b.zc';
+
+// initialize: capabilities and the 12 type legend
+let r = run([init(1), { jsonrpc: '2.0', method: 'exit' }]);
+let res = r.msgs.find(m => m.id === 1);
+check(res && res.result.capabilities.semanticTokensProvider.legend.tokenTypes.length === 12, 'initialize advertises the legend');
+check(res && res.result.capabilities.textDocumentSync.change === 1, 'initialize asks for full-text sync');
+
+// a request before initialize is refused, and the id is echoed
+r = run([{ jsonrpc: '2.0', id: 7, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri: A } } }]);
+check(r.msgs.length === 1 && r.msgs[0].error && r.msgs[0].error.code === -32002 && r.msgs[0].id === 7, 'a request before initialize gets -32002');
+
+// unknown methods: MethodNotFound with numeric and string ids, silence for notifications
+r = run([init(1), { jsonrpc: '2.0', id: 'abc', method: 'workspace/nothing' }, { jsonrpc: '2.0', method: 'workspace/nothing' }]);
+const nf = r.msgs.find(m => m.id === 'abc');
+check(nf && nf.error && nf.error.code === -32601, 'unknown request gets MethodNotFound, string id echoed', nf);
+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';
+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);
+
+// an error lands on the right line and column
+const bad1 = 'fn main() {\n\tlet x = 5\n\treturn x;\n}\n';
+r = run([init(1), open(A, bad1)]);
+d = diags(r.msgs, A);
+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';
+r = run([init(1), open(A, uni)]);
+d = diags(r.msgs, A);
+const e2 = d.length && d[0].params.diagnostics[0];
+const col = uni.split('\n')[1].indexOf('undefined_name');
+check(e2 && e2.range.start.line === 1 && e2.range.start.character === col, 'columns count utf-16 units', e2 && e2.range);
+
+// fixing the text through didChange clears the error
+r = run([init(1), open(A, bad1), change(A, good)]);
+d = diags(r.msgs, A);
+check(d.length === 2 && d[0].params.diagnostics.length > 0 && d[1].params.diagnostics.length === 0, 'didChange re-checks the document');
+
+// two documents do not leak into each other, and checks repeat the same way
+r = run([init(1), open(A, bad1), open(B2, good), change(A, bad1)]);
+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 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');
+check(diags(r.msgs, B2)[0].params.diagnostics.filter(x => x.severity === 1).length > 0, 'the same program without the parse word is an error');
+
+// semantic tokens: groups of five, keywords and functions classified from the compiler's own tables
+r = run([init(1), open(A, good), sem(2, A)]);
+res = r.msgs.find(m => m.id === 2);
+const data = res && res.result && res.result.data;
+check(data && data.length > 0 && data.length % 5 === 0, 'semantic tokens come in groups of five');
+check(data && data[3] === 0 && data[0] === 0 && data[1] === 0, 'fn is a keyword at 0:0', data && data.slice(0, 5));
+check(data && data[8] === 2, 'a function name is a function', data && data.slice(5, 10));
+
+// decode the semantic tokens of a document into text:type pairs
+function tokens(text) {
+ const rr = run([init(1), open(A, text), sem(2, A)]);
+ const dd = rr.msgs.find(m => m.id === 2).result.data;
+ const kinds = ['keyword', 'type', 'function', 'variable', 'string', 'number', 'operator', 'comment', 'parameter', 'property', 'macro', 'enumMember'];
+ const lines = text.split('\n'); let l = 0, c = 0; const out = [];
+ for (let i = 0; i < dd.length; i += 5) {
+ l += dd[i]; c = dd[i] ? dd[i + 1] : c + dd[i + 1];
+ out.push(lines[l].substr(c, dd[i + 2]) + ':' + kinds[dd[i + 3]]);
+ }
+ 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 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);
+
+// a large document goes through the streaming reader
+let big = '';
+for (let i = 0; i < 2500; i++) big += 'fn f' + i + '(a, b) { return a * b + ' + i + '; }\n';
+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);
+
+// local tables grow past their initial reservation, and the next document still checks
+let many = 'fn main() {\n';
+for (let i = 0; i < 600; i++) many += '\tlet v' + i + ' = ' + i + ';\n';
+many += '\treturn 0;\n}\n';
+r = run([init(1), open(A, many), open(B2, good)]);
+d = diags(r.msgs, A);
+check(d.length === 1 && !d[0].params.diagnostics.some(x => x.severity === 1) && diags(r.msgs, B2).length === 1, 'locals grow past 512 and the server carries on', d.length);
+
+// hover: the text at a position, zero-based line and character, or null
+function hover(text, line, ch) {
+ const rr = run([init(1), open(A, text), { jsonrpc: '2.0', id: 3, method: 'textDocument/hover', params: { textDocument: { uri: A }, position: { line, character: ch } } }]);
+ const x = rr.msgs.find(m => m.id === 3);
+ return x && x.result ? x.result.contents.value : null;
+}
+const at = (text, line, word, nth) => { const s = text.split('\n')[line]; let i = -1; for (let k = 0; k <= (nth || 0); k++) i = s.indexOf(word, i + 1); return i; };
+const hsrc = '// adds two numbers\nint add(int a, int b) { return a + b; }\nint twice(int a) { return a * 2; }\nint main() {\n\tint n = add(1, 2);\n\tlet i = 3;\n\treturn twice(n) + i;\n}\n';
+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);
+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);
+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';
+// 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);
+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);
+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);
+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';
+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);
+hv = hover(kwsrc, 2, at(kwsrc, 2, 'free'));
+check(hv && hv.includes('frees a heap object'), 'hover on free shows its doc', 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';
+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);
+
+// 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';
+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);
+
+// externs are declarations, so they draw no "never reassigned" hint
+const ext = 'extern double sqrt(double);\nextern double pow(double x, double y);\nfn main() {\n\treturn 0;\n}\n';
+r = run([init(1), open(A, ext)]);
+d = diags(r.msgs, A);
+check(d.length === 1 && !d[0].params.diagnostics.some(x => /never reassigned/.test(x.message)), 'an extern draws no hint about its stub', d.length && d[0].params.diagnostics);
+
+// shutdown and exit end the process cleanly
+r = run([init(1), { jsonrpc: '2.0', id: 9, method: 'shutdown' }, { jsonrpc: '2.0', method: 'exit' }]);
+check(r.code === 0 && r.msgs.some(m => m.id === 9 && m.result === null), 'shutdown then exit ends with status 0', r.code);
+
+// the corpus: every valid program checks clean, every bad one is rejected with its message
+function cli(file) {
+ const r = spawnSync(server, [], { input: fs.readFileSync(file), timeout: 30000 });
+ return r.stdout.toString();
+}
+let clean = 0, dirty = [];
+for (const dir of ['examples', 'tests/programs']) {
+ for (const f of fs.readdirSync(dir).filter(n => n.endsWith('.zc'))) {
+ if (f === 'manyerrors.zc' || f === 'undefined_call.zc' || f === 'zc0typed.zc') continue; // manyerrors and undefined_call are bad on purpose; zc0typed.zc is stage 0 syntax
+ const out = cli(dir + '/' + f);
+ if (/: error: /.test(out)) dirty.push(dir + '/' + f + ' ' + out.split('\n')[0]); else clean++;
+ }
+}
+check(dirty.length === 0, 'every example and test program checks clean (' + clean + ' files)', dirty);
+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 out = cli('tests/' + dir + '/' + f);
+ n++;
+ if (!out.includes(want)) missed.push(dir + '/' + f + ' wanted [' + want + '] got [' + out.split('\n')[0] + ']');
+ }
+}
+check(missed.length === 0, 'every bad program is rejected with the compiler\'s message (' + n + ' files)', missed);
+const manyOut = cli('tests/programs/manyerrors.zc').split('\n').filter(l => /: error: /.test(l));
+check(manyOut.length === 4, 'several errors in one run are all reported', manyOut);
+
+// names resolve the way the compiler resolves them: a local or a parameter named
+// like a built-in is that variable, and the built-in is only what the scope leaves
+const scp = 'int cap = 2;\nfn grow(int cap) { return cap + 1; }\nfn main() {\n\tint cap = 3;\n\treturn len(grow(cap));\n}\n';
+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);
+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'));
+check(hcv && hcv.includes('the number of items'), 'hover on the built-in still shows its text where no local shadows it', hcv);
+
+console.log(ok + ' lsp checks passed, ' + bad + ' failed');
+process.exit(bad ? 1 : 0);
diff --git a/tests/drivers/lsp_vm.js b/tests/drivers/lsp_vm.js
new file mode 100644
index 0000000..24c1a5b
--- /dev/null
+++ b/tests/drivers/lsp_vm.js
@@ -0,0 +1,112 @@
+// tests for the language server run by the javascript vm (src/js/tinyvm.js), through
+// the extension's own host (editors/vscode/client/vmproc.js): the same checks as
+// tests/drivers/lsp_wasm.js. the vm starts at the image's heap and grows as the server asks.
+// usage: node tests/drivers/lsp_vm.js path/to/server/dir (tinyvm.js, lsp.json, layout.js)
+const path = require('path');
+const { vmProcess } = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'vmproc.js'));
+const fs = require('fs');
+const dir = path.resolve(process.argv[2]);
+let ok = 0, bad = 0;
+function check(cond, label, extra) {
+ if (cond) { ok++; console.log('ok lsp vm ' + label); }
+ else { bad++; console.log('FAIL lsp vm ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra).slice(0, 300))); }
+}
+// runs one session: the messages are sent, and the answers collected by id
+function session(docs) {
+ const replies = {}, notes = [];
+ const p = vmProcess(dir, { log: () => {} });
+ const exited = { code: undefined };
+ p.on('exit', c => { exited.code = c; });
+ 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;
+ const m = JSON.parse(inbox.slice(e + 4, e + 4 + n).toString('utf8'));
+ inbox = inbox.slice(e + 4 + n);
+ if (m.id !== undefined) replies[m.id] = m; else notes.push(m);
+ }
+ });
+ const send = m => { const b = Buffer.from(JSON.stringify(m), 'utf8'); p.stdin.write(Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b])); };
+ send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } });
+ send({ jsonrpc: '2.0', method: 'initialized', params: {} });
+ for (const [uri, text] of docs) send({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } });
+ return { p, replies, notes, send, exited };
+}
+const clean = 'int main() {\n\tint x = 1;\n\treturn x;\n}\n';
+const bad_src = 'int main() {\n\tint x = 1\n\treturn x;\n}\n';
+const tokOf = (s, uri) => {
+ s.send({ jsonrpc: '2.0', id: 2, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } });
+ s.send({ jsonrpc: '2.0', id: 3, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: 1, character: 9 } } });
+ s.send({ jsonrpc: '2.0', id: 4, method: 'shutdown', params: null });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+};
+const diagsOf = (s, uri) => s.notes.filter(m => m.method === 'textDocument/publishDiagnostics' && m.params.uri === uri);
+
+// a clean program: initialize, no diagnostics, tokens, and it exits on its own
+{
+ const s = session([['file:///a.zc', clean]]);
+ tokOf(s, 'file:///a.zc');
+ const init = s.replies[1];
+ check(init && init.result.capabilities.semanticTokensProvider && init.result.capabilities.hoverProvider, 'initialize advertises tokens and hover', init);
+ check(diagsOf(s, 'file:///a.zc').length === 1 && diagsOf(s, 'file:///a.zc')[0].params.diagnostics.every(x => x.severity !== 1), 'a clean program has no errors (hints are fine)');
+ const t = s.replies[2];
+ check(t && t.result && t.result.data.length > 0 && t.result.data.length % 5 === 0, 'semantic tokens come back in groups of five', t && t.result && t.result.data.length);
+ check(s.replies[4] && s.replies[4].result === null, 'shutdown is answered');
+ check(s.p.stdin.write(Buffer.alloc(0)) === false, 'the server has exited after exit');
+}
+// a broken program: the compiler's own message is the diagnostic
+{
+ const s = session([['file:///b.zc', bad_src]]);
+ const d = diagsOf(s, 'file:///b.zc');
+ check(d.length === 1 && d[0].params.diagnostics.length > 0, 'a missing semicolon gives a diagnostic');
+ check(d.length === 1 && d[0].params.diagnostics.some(x => /expected/.test(x.message)), 'the message is the compiler\'s', d);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+}
+// hover: a name the document declares, and a compiler name with docs in the image
+{
+ const src = '// adds one to its argument\nint inc(int x) { return x + 1; }\nint main() {\n\treturn inc(2);\n}\n';
+ const s = session([['file:///c.zc', src]]);
+ s.send({ jsonrpc: '2.0', id: 5, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///c.zc' }, position: { line: 3, character: 9 } } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ const h = s.replies[5];
+ check(h && h.result && /adds one/.test(h.result.contents.value), 'hover shows the comment above a declaration', h);
+ const s2 = session([['file:///d.zc', 'int main() {\n\tint z = docofs("x");\n\treturn 0;\n}\n']]);
+ s2.send({ jsonrpc: '2.0', id: 6, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///d.zc' }, position: { line: 1, character: 10 } } });
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ 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);
+}
+// a document that is bigger than one feed: the input is split and comes out whole
+{
+ let big = 'int main() {\n';
+ for (let i = 0; i < 4000; i++) big += '\tint v' + i + ' = ' + i + ';\n';
+ big += '\treturn 0;\n}\n';
+ const s = session([['file:///e.zc', big]]);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///e.zc').length === 1, 'a document bigger than one feed is read whole (' + big.length + ' bytes)');
+}
+// the regression: the server halted without an answer on any document that
+// needed more memory than its first heap (examples/bf.zc, and a document twice
+// the size of the earlier one). each must be checked and published, and the
+// server must still be running until it is told to exit
+{
+ const bfPath = path.join(__dirname, '..', '..', 'examples', 'bf.zc');
+ const bf = fs.readFileSync(bfPath, 'utf8');
+ const s = session([['file:///bf.zc', bf]]);
+ s.send({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri: 'file:///bf.zc', version: 2 }, contentChanges: [{ text: bf }] } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///bf.zc').length >= 1, 'examples/bf.zc is checked and published (' + bf.length + ' bytes)');
+ check(s.exited.code === 0, 'the server runs until exit, then leaves with status 0', s.exited);
+ let huge = 'int main() {\n';
+ for (let i = 0; i < 12000; i++) huge += '\tint h' + i + ' = ' + i + ' + ' + (i % 7) + ';\n';
+ huge += '\treturn 0;\n}\n';
+ const s2 = session([['file:///huge.zc', huge]]);
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s2, 'file:///huge.zc').length === 1, 'a document past the first heap is checked and published (' + huge.length + ' bytes)', s2.exited);
+ check(s2.exited.code === 0, 'the big document leaves the server with status 0 after exit', s2.exited);
+}
+console.log(ok + ' lsp vm checks passed, ' + bad + ' failed');
+process.exit(bad ? 1 : 0);
diff --git a/tests/drivers/lsp_wasm.js b/tests/drivers/lsp_wasm.js
new file mode 100644
index 0000000..4ff2d16
--- /dev/null
+++ b/tests/drivers/lsp_wasm.js
@@ -0,0 +1,112 @@
+// tests for the language server as WebAssembly, through the extension's own
+// host (editors/vscode/client/wasmproc.js). the same protocol checks as the
+// native server, plus the hover docs of a compiler name.
+// usage: node tests/drivers/lsp_wasm.js path/to/lsp.wasm
+const path = require('path');
+const { wasmProcess } = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'wasmproc.js'));
+const fs = require('fs');
+const wasm = process.argv[2];
+let ok = 0, bad = 0;
+function check(cond, label, extra) {
+ if (cond) { ok++; console.log('ok lsp wasm ' + label); }
+ else { bad++; console.log('FAIL lsp wasm ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra).slice(0, 300))); }
+}
+// runs one session: the messages are sent, and the answers collected by id
+function session(docs) {
+ const replies = {}, notes = [];
+ const p = wasmProcess(wasm, { log: () => {} });
+ const exited = { code: undefined };
+ p.on('exit', c => { exited.code = c; });
+ 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;
+ const m = JSON.parse(inbox.slice(e + 4, e + 4 + n).toString('utf8'));
+ inbox = inbox.slice(e + 4 + n);
+ if (m.id !== undefined) replies[m.id] = m; else notes.push(m);
+ }
+ });
+ const send = m => { const b = Buffer.from(JSON.stringify(m), 'utf8'); p.stdin.write(Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b])); };
+ send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } });
+ send({ jsonrpc: '2.0', method: 'initialized', params: {} });
+ for (const [uri, text] of docs) send({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } });
+ return { p, replies, notes, send, exited };
+}
+const clean = 'int main() {\n\tint x = 1;\n\treturn x;\n}\n';
+const bad_src = 'int main() {\n\tint x = 1\n\treturn x;\n}\n';
+const tokOf = (s, uri) => {
+ s.send({ jsonrpc: '2.0', id: 2, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } });
+ s.send({ jsonrpc: '2.0', id: 3, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: 1, character: 9 } } });
+ s.send({ jsonrpc: '2.0', id: 4, method: 'shutdown', params: null });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+};
+const diagsOf = (s, uri) => s.notes.filter(m => m.method === 'textDocument/publishDiagnostics' && m.params.uri === uri);
+
+// a clean program: initialize, no diagnostics, tokens, and it exits on its own
+{
+ const s = session([['file:///a.zc', clean]]);
+ tokOf(s, 'file:///a.zc');
+ const init = s.replies[1];
+ check(init && init.result.capabilities.semanticTokensProvider && init.result.capabilities.hoverProvider, 'initialize advertises tokens and hover', init);
+ check(diagsOf(s, 'file:///a.zc').length === 1 && diagsOf(s, 'file:///a.zc')[0].params.diagnostics.every(x => x.severity !== 1), 'a clean program has no errors (hints are fine)');
+ const t = s.replies[2];
+ check(t && t.result && t.result.data.length > 0 && t.result.data.length % 5 === 0, 'semantic tokens come back in groups of five', t && t.result && t.result.data.length);
+ check(s.replies[4] && s.replies[4].result === null, 'shutdown is answered');
+ check(s.p.stdin.write(Buffer.alloc(0)) === false, 'the server has exited after exit');
+}
+// a broken program: the compiler's own message is the diagnostic
+{
+ const s = session([['file:///b.zc', bad_src]]);
+ const d = diagsOf(s, 'file:///b.zc');
+ check(d.length === 1 && d[0].params.diagnostics.length > 0, 'a missing semicolon gives a diagnostic');
+ check(d.length === 1 && d[0].params.diagnostics.some(x => /expected/.test(x.message)), 'the message is the compiler\'s', d);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+}
+// hover: a name the document declares, and a compiler name with docs in the image
+{
+ const src = '// adds one to its argument\nint inc(int x) { return x + 1; }\nint main() {\n\treturn inc(2);\n}\n';
+ const s = session([['file:///c.zc', src]]);
+ s.send({ jsonrpc: '2.0', id: 5, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///c.zc' }, position: { line: 3, character: 9 } } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ const h = s.replies[5];
+ check(h && h.result && /adds one/.test(h.result.contents.value), 'hover shows the comment above a declaration', h);
+ const s2 = session([['file:///d.zc', 'int main() {\n\tint z = docofs("x");\n\treturn 0;\n}\n']]);
+ s2.send({ jsonrpc: '2.0', id: 6, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///d.zc' }, position: { line: 1, character: 10 } } });
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ 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);
+}
+// a document that is bigger than one feed: the input is split and comes out whole
+{
+ let big = 'int main() {\n';
+ for (let i = 0; i < 4000; i++) big += '\tint v' + i + ' = ' + i + ';\n';
+ big += '\treturn 0;\n}\n';
+ const s = session([['file:///e.zc', big]]);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///e.zc').length === 1, 'a document bigger than one feed is read whole (' + big.length + ' bytes)');
+}
+// the regression: the server halted without an answer on any document that
+// needed more memory than its first heap (examples/bf.zc, and a document twice
+// the size of the earlier one). each must be checked and published, and the
+// server must still be running until it is told to exit
+{
+ const bfPath = path.join(__dirname, '..', '..', 'examples', 'bf.zc');
+ const bf = fs.readFileSync(bfPath, 'utf8');
+ const s = session([['file:///bf.zc', bf]]);
+ s.send({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri: 'file:///bf.zc', version: 2 }, contentChanges: [{ text: bf }] } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///bf.zc').length >= 1, 'examples/bf.zc is checked and published (' + bf.length + ' bytes)');
+ check(s.exited.code === 0, 'the server runs until exit, then leaves with status 0', s.exited);
+ let huge = 'int main() {\n';
+ for (let i = 0; i < 12000; i++) huge += '\tint h' + i + ' = ' + i + ' + ' + (i % 7) + ';\n';
+ huge += '\treturn 0;\n}\n';
+ const s2 = session([['file:///huge.zc', huge]]);
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s2, 'file:///huge.zc').length === 1, 'a document past the first heap is checked and published (' + huge.length + ' bytes)', s2.exited);
+ check(s2.exited.code === 0, 'the big document leaves the server with status 0 after exit', s2.exited);
+}
+console.log(ok + ' lsp wasm checks passed, ' + bad + ' failed');
+process.exit(bad ? 1 : 0);
diff --git a/tests/drivers/memory_limits_test.py b/tests/drivers/memory_limits_test.py
new file mode 100644
index 0000000..a973480
--- /dev/null
+++ b/tests/drivers/memory_limits_test.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# focused host memory configuration and growth checks.
+import os
+import subprocess
+
+source = '''fn main() {
+ int initial = syscall(9, 0, 0, 0);
+ put_n(syscall(9, 5, initial, 0) == initial); put_c(32);
+ put_n(syscall(9, 4, initial + 4096, 0) == -1); put_c(32);
+ put_n(syscall(9, 5, initial + 8192, 0) == initial + 8192); put_c(32);
+ put_n(syscall(9, 4, initial + 4096, 0) == initial + 4096); put_c(32);
+ put_n(syscall(9, 5, initial - 1, 0) == -1); put_c(10);
+ return 0;
+}
+'''
+commands = [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]
+environment = dict(os.environ, TINYVM_MAX_MEMORY_WORDS='16777216')
+for command in commands:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, env=environment, timeout=15)
+ assert result.returncode == 0 and result.stdout == '1 1 1 1 1\n', (command, result.stdout, result.stderr)
+ invalid = subprocess.run(command, input=source, text=True, capture_output=True, env=dict(environment, TINYVM_MEMORY_WORDS='1'), timeout=15)
+ assert invalid.returncode != 0 and invalid.stderr, (command, invalid.stdout)
+ print('ok runtime memory limits, denied growth, successful growth, and invalid startup: ' + command[0])
diff --git a/tests/drivers/parser_growth_test.py b/tests/drivers/parser_growth_test.py
new file mode 100644
index 0000000..4cbfbb2
--- /dev/null
+++ b/tests/drivers/parser_growth_test.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# parser storage grows across enum members, tagged enums, methods, and generated text.
+import subprocess
+import sys
+
+command = sys.argv[1:]
+
+
+def run(source, expected):
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=30)
+ assert result.returncode == 0, (result.stdout, result.stderr)
+ assert result.stdout == expected, result.stdout[:1000]
+
+
+source = 'enum values { ' + ', '.join(f'member_{i}' for i in range(300)) + ' };\n'
+source += 'fn main() { gc(); put_n(member_299); put_c(10); return 0; }\n'
+run(source, '299\n')
+source = ''.join(f'enum tagged_{i} {{ item_{i} {{ int value; }}, empty_{i} }};\n' for i in range(25))
+source += 'fn main() { tagged_24*.m item = tagged_24.item_24(7); gc(); put_n(item.item_24_value); put_c(10); return 0; }\n'
+run(source, '7\n')
+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')
+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'
+run(source, '1\n')
+source = 'fn main() { int result = 0; switch (127) {\n'
+source += ''.join(f'case {i}: result = {i};\n' for i in range(128))
+source += '} gc(); put_n(result); put_c(32); put_n(jpscap > 64); put_c(10); return 0; }\n'
+run(source, '127 1\n')
+source = 'fn main() { int result = 0;\n'
+source += ''.join('o3 { result += 1; }\n' for _ in range(40))
+source += 'gc(); put_n(result); put_c(32); put_n(o3pendcap > 10); put_c(10); return 0; }\n'
+run(source, '40 1\n')
+source = 'fn main() { int i = 0; while (i < 120) { putfact(i, i, 1); i++; } gc(); put_n(factcap > 32); put_c(32); put_n(getlo(119)); put_c(10); return 0; }\n'
+run(source, '1 119\n')
+long_type = 'structure_' + 't' * 180
+long_method = 'method_' + 'm' * 180
+source = f'struct {long_type} {{ int {long_method}() {{ return 7; }} }};\n'
+source += f'fn main() {{ {long_type} value; put_n(value.{long_method}()); put_c(10); return 0; }}\n'
+run(source, '7\n')
+name = 'generic_' + 'g' * 180
+source = f'int {name}_one<T>() {{ return sizeof(T); }}\nint {name}_two<T>() {{ return sizeof(T) + 1; }}\n'
+source += f'fn main() {{ put_n({name}_one<int>() + {name}_two<int>()); put_c(10); return 0; }}\n'
+run(source, '3\n')
+print('ok parser tables, analyzer paths, queued regions, and generated names grow')
diff --git a/tests/drivers/protocol_growth_test.js b/tests/drivers/protocol_growth_test.js
new file mode 100644
index 0000000..64015d8
--- /dev/null
+++ b/tests/drivers/protocol_growth_test.js
@@ -0,0 +1,34 @@
+// preserve long request identifiers and ignore deeply nested unknown json fields.
+const assert = require('assert');
+const cp = require('child_process');
+const target = process.argv[2];
+const id = 'request_' + 'x'.repeat(500);
+let nested = { ['unknown_' + 'y'.repeat(100)]: 1 };
+for (let i = 0; i < 40; i++) nested = { child: nested };
+const frame = message => {
+ const body = Buffer.from(JSON.stringify(message));
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\nX-Extra: ' + 'z'.repeat(500) + '\r\n\r\n'), body]);
+};
+const messages = [{ jsonrpc: '2.0', id, method: 'initialize', extra: nested, params: {} }];
+for (let i = 0; i < 24; i++) messages.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: {
+ uri: 'file:///enum_' + i + '.zc', version: 1, languageId: 'zc',
+ text: 'enum choice { item { int value; }, empty };\nfn main() { choice*.m p = choice.item(7); return p.item_value; }\n',
+} } });
+messages.push({ jsonrpc: '2.0', id: 'finish', method: 'shutdown', params: null }, { jsonrpc: '2.0', method: 'exit' });
+const result = cp.spawnSync(target, [], { input: Buffer.concat(messages.map(frame)), timeout: 20000, maxBuffer: 8 << 20 });
+assert.strictEqual(result.status, 0, String(result.stderr));
+const responses = [];
+let data = result.stdout;
+while (data.length) {
+ const end = data.indexOf('\r\n\r\n');
+ assert.ok(end >= 0);
+ const length = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
+ responses.push(JSON.parse(data.subarray(end + 4, end + 4 + length).toString()));
+ data = data.subarray(end + 4 + length);
+}
+assert.ok(responses.some(message => message.id === id && message.result.capabilities), 'the full request id is echoed');
+assert.ok(responses.some(message => message.id === 'finish'), 'the next request is parsed');
+const diagnostics = responses.filter(message => message.method === 'textDocument/publishDiagnostics');
+assert.strictEqual(diagnostics.length, 24);
+assert.ok(diagnostics.every(message => !message.params.diagnostics.some(diagnostic => diagnostic.severity === 1)), JSON.stringify(diagnostics));
+console.log('ok long protocol fields, deep json, and tagged enums across document checks');
diff --git a/tests/drivers/seed_growth_test.py b/tests/drivers/seed_growth_test.py
new file mode 100644
index 0000000..426c970
--- /dev/null
+++ b/tests/drivers/seed_growth_test.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# stage 0 accepts long source streams and grows a tiny initial image reservation.
+import pathlib
+import subprocess
+import tempfile
+
+root = pathlib.Path(__file__).resolve().parents[2]
+with tempfile.TemporaryDirectory(prefix='seed-growth-', dir=root / 'build') as directory:
+ folder = pathlib.Path(directory)
+ subprocess.run(['bin/genmap', '--preset', 'run', '--grow', '--genome', '600000', '--heap', '1048576', '-o', directory], check=True, capture_output=True)
+ compiler = folder / 'zc0'
+ subprocess.run(['cc', '-O1', '-w', '-fwrapv', '-DMAXW=16', 'src/tools/zc0.c', '-I' + directory, '-Isrc/core', '-o', str(compiler)], check=True)
+ source = '/*' + 'x' * (5 << 20) + '*/\n'
+ source += (folder / 'rt.zc').read_text() + (root / 'src/runtime/run.zc').read_text()
+ source += 'let data[550000]; fn main() { data[549999] = 7; put_n(data[549999]); put_c(32); put_n(4611686018427387904); put_c(10); return 0; }\n'
+ with (folder / 'genome.h').open('w') as output:
+ subprocess.run([str(compiler)], input=source, text=True, stdout=output, check=True, timeout=30)
+ binary = folder / 'vm'
+ import shutil
+ for path in ['src/micro/micro.c', 'src/micro/micro_port.h', 'src/micro/port_linux.c', 'src/core/fops.h', 'src/core/bops.h', 'src/core/stack.h']:
+ shutil.copy(root / path, folder)
+ subprocess.run(['cc', '-O2', '-w', '-fwrapv', str(folder / 'micro.c'), str(folder / 'port_linux.c'), '-I' + directory, '-o', str(binary)], check=True)
+ result = subprocess.run([str(binary)], text=True, capture_output=True, timeout=20)
+ assert result.returncode == 0 and result.stdout == '7 4611686018427387904\n', (result.returncode, result.stdout, result.stderr)
+print('ok stage 0 reads beyond four megabytes and grows beyond its initial image capacity')
diff --git a/tests/drivers/stack_growth_test.py b/tests/drivers/stack_growth_test.py
new file mode 100644
index 0000000..d4abe81
--- /dev/null
+++ b/tests/drivers/stack_growth_test.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+# operand values and return addresses survive explicit and automatic growth.
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+command = sys.argv[1:]
+
+
+def run(source, expected, compile_image=False):
+ if compile_image and command[0] == 'node':
+ # compile the execution stress cases once with stage 0, so a javascript
+ # compiler's speed does not determine the execution-test timeout.
+ with tempfile.TemporaryDirectory(prefix='stack-image-', dir='build') as directory:
+ target = Path(directory)
+ subprocess.run(['bin/genmap', '--preset', 'run', '--grow', '--heap', '1048576', '--genome', '131072', '--frame', '4096', '--return', '256', '--data', '3072', '-o', directory], check=True, capture_output=True)
+ compiler = target / 'zc0'
+ subprocess.run(['cc', '-O1', '-w', '-fwrapv', 'src/tools/zc0.c', '-I' + directory, '-Isrc/core', '-o', str(compiler)], check=True, capture_output=True)
+ full_source = (target / 'rt.zc').read_text() + Path('src/runtime/run.zc').read_text() + source
+ image = subprocess.run([str(compiler)], input=full_source, text=True, capture_output=True, check=True)
+ header = target / 'genome.h'
+ header.write_text(image.stdout)
+ result = subprocess.run(['node', 'tests/drivers/image_run.js', str(header), str(target / 'memmap.h')], text=True, capture_output=True, timeout=45)
+ else:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=45)
+ assert result.returncode == 0 and result.stdout == expected, (command, result.returncode, result.stdout, result.stderr)
+
+
+run('''fn main() {
+ int old_data = data_top - data_low, old_return = return_top - return_low;
+ put_n(reserve_data(old_data + 1) > old_data); put_c(32);
+ put_n(reserve_returns(old_return + 1) > old_return); put_c(32);
+ gc(); put_n(data_node != 0 && return_node != 0); put_c(10); return 0;
+}
+''', '1 1 1\n')
+
+run('''fn main() {
+ put_n(reserve_data(-1) == -1); put_c(32); put_n(reserve_returns(-1) == -1); put_c(32);
+ put_n(syscall(9, 6, 0, 0) == -1); put_c(32);
+ int current = syscall(9, 0, 0, 0); syscall(9, 5, current, 0);
+ put_n(reserve_data(0x10000000) == -1); put_c(32); gc(); put_n(7); put_c(10); return 0;
+}
+''', '1 1 1 1 7\n')
+
+count = 40000
+source = 'fn main() { int total = asm { ' + ' '.join(str(i + 1) for i in range(count))
+source += ' + ' * (count - 1) + ' }; gc(); put_n(total); put_c(32); '
+source += 'put_n(data_node != 0); put_c(10); return 0; }\n'
+run(source, '800020000 1\n', compile_image=True)
+
+run('''unchecked int descend(int n) {
+ if (!n) { gc(); return 7; }
+ return descend(n - 1) + 1;
+}
+fn main() { put_n(descend(70000)); put_c(32); put_n(return_node != 0); put_c(10); return 0; }
+''', '70007 1\n', compile_image=True)
+print('ok explicit reservation, 40000 operands, and 70000 return addresses: ' + command[0])
diff --git a/tests/drivers/storage_analysis_test.js b/tests/drivers/storage_analysis_test.js
new file mode 100644
index 0000000..da967f5
--- /dev/null
+++ b/tests/drivers/storage_analysis_test.js
@@ -0,0 +1,71 @@
+// definite storage findings use the compiler's parser and the editor protocol.
+const assert = require('assert');
+const cp = require('child_process');
+const server = process.argv[2];
+function check(text, needle) {
+ const uri = 'file:///storage-analysis.zc';
+ const messages = [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
+ { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } }];
+ if (needle) {
+ const at = text.lastIndexOf(needle), before = text.slice(0, at);
+ assert.ok(at >= 0);
+ messages.push({ jsonrpc: '2.0', id: 2, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } } });
+ }
+ messages.push({ jsonrpc: '2.0', method: 'exit' });
+ const input = Buffer.concat(messages.map(m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b]); }));
+ const r = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000, maxBuffer: 8 << 20 });
+ assert.strictEqual(r.status, 0, String(r.stderr));
+ const replies = []; let data = r.stdout;
+ while (data.length) {
+ const end = data.indexOf('\r\n\r\n'); assert.ok(end >= 0);
+ const n = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
+ replies.push(JSON.parse(data.subarray(end + 4, end + 4 + n)));
+ data = data.subarray(end + 4 + n);
+ }
+ const diagnostics = replies.find(m => m.method === 'textDocument/publishDiagnostics').params.diagnostics;
+ return { diagnostics, warnings: diagnostics.filter(d => d.severity === 2), hover: replies.find(m => m.id === 2)?.result?.contents?.value };
+}
+const prefix = 'unchecked fn main() {\n';
+for (const [type, length] of [['int', 3], ['u8', 3], ['fixed []int', 3], ['[]u8', 3]]) {
+ const r = check(prefix + ` let p = new(${type}, ${length});\n let alias = p;\n return alias[${length}];\n}\n`, 'alias[');
+ assert.strictEqual(r.warnings.length, 1, JSON.stringify(r.diagnostics));
+ assert.match(r.warnings[0].message, /out-of-bounds.*index 3.*length 3/);
+ assert.match(r.hover, /size: 3 elements.*lifetime: heap storage; live here/);
+ assert.strictEqual(r.warnings[0].range.start.line, 3);
+}
+const negative = check(prefix + ' let p = new(3); return p[-1];\n}\n');
+assert.match(negative.warnings[0]?.message || '', /index -1.*length 3/);
+const freed = check(prefix + ' let p = new(int, 3);\n let q = p;\n free(p);\n return q[0];\n}\n', 'q[');
+assert.strictEqual(freed.warnings.length, 1, JSON.stringify(freed.diagnostics));
+assert.match(freed.warnings[0].message, /use after free.*line 4/);
+assert.match(freed.hover, /size: 3 elements.*lifetime: ended on line 4/);
+const dereference = check(prefix + ' let p = new(int); free(p); return *p;\n}\n');
+assert.match(dereference.warnings[0]?.message || '', /use after free/);
+const field = check('struct item { int value; };\n' + prefix + ' let p = new(item); free(p); return p.value;\n}\n');
+assert.match(field.warnings[0]?.message || '', /use after free/);
+const scope = check(prefix + ' int* p;\n { let a[3]; p = a; }\n return p[0];\n}\n', 'p[');
+assert.match(scope.warnings[0]?.message || '', /use after free.*line 3/);
+assert.match(scope.hover, /lifetime: ended on line 3/);
+const safe = [
+ prefix + ' let p = new(int, 3); let q = p as u8*; return q[23]; }',
+ prefix + ' let p = new(int, 3); let q = (u8*)p; return q[23]; }',
+ prefix + ' int n = 2; let p = &n; let q = n; return q[2]; }',
+ prefix + ' let p = new(int, 3); let ignored = 0 && free(p); return p[0]; }',
+ prefix + ' let p = new(int, 3); let ignored = 1 || free(p); return p[0]; }',
+ prefix + ' let p = new(int, 50); return p[(u8)300]; }',
+ prefix + ' let p = new(int, 50); return p[300 as u8]; }',
+ prefix + ' let p = new(int, 3); let q = p; q[2] = 7; free(p); return 0; }',
+ prefix + ' let p = new(3); free(p); p = new(3); return p[0]; }',
+ 'fn unknown(int* p) {}\n' + prefix + ' let p = new(int, 3); unknown(p); return p[9]; }',
+ 'fn maybe(int n) { return n; }\n' + prefix + ' let p = new(int, 3); return p[maybe(9)]; }',
+ prefix + ' let p = new(int, 3); if (1) free(p); return p[0]; }',
+ prefix + ' let p = new(int, 3); while (0) free(p); return p[0]; }',
+ prefix + ' let p = new(int, 3); let f = fn() { free(p); return 0; }; return p[0]; }',
+ prefix + ' let p = new([]int, 0); push(p, 9); return p[0]; }',
+];
+for (const source of safe) assert.deepStrictEqual(check(source).warnings, [], source);
+const 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/);
+console.log('ok definite bounds, free and scope lifetimes, alias hovers, and conservative unknown cases');
diff --git a/tests/drivers/vscode_hover_test.js b/tests/drivers/vscode_hover_test.js
new file mode 100644
index 0000000..378dda2
--- /dev/null
+++ b/tests/drivers/vscode_hover_test.js
@@ -0,0 +1,48 @@
+// run inside a vscode extension test host to check the real hover api objects.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vscode = require('vscode');
+exports.run = async () => {
+ const root = path.resolve(__dirname, '..', '..');
+ const fixture = path.join(root, 'build', 'review', 'hover-fixture.zc');
+ fs.mkdirSync(path.dirname(fixture), { recursive: true });
+ fs.writeFileSync(fixture, '// adds **two** numbers with `integer` arithmetic.\nint add(int a, int b) { return a + b; }\nfn main() { return add(1, 2); }\n');
+ const extension = vscode.extensions.getExtension('tinyvm.zc-language-support');
+ assert.ok(extension, 'development extension is loaded');
+ const doc = await vscode.workspace.openTextDocument(fixture);
+ await vscode.window.showTextDocument(doc);
+ await extension.activate();
+ let hover;
+ for (let attempt = 0; attempt < 100; attempt++) {
+ const replies = await vscode.commands.executeCommand('vscode.executeHoverProvider', doc.uri, new vscode.Position(2, 19));
+ hover = replies && replies.find(reply => reply.contents.some(md => md.value.includes('adds **two** numbers')));
+ if (hover) break;
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ assert.ok(hover, 'real vscode provider returns declaration documentation');
+ const md = hover.contents[0];
+ assert.ok(md instanceof vscode.MarkdownString, 'hover contains a real MarkdownString');
+ assert.match(md.value, /^```zc\nint add\(int a, int b\)\n```\n\n/);
+ 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');
+ const storageDoc = await vscode.workspace.openTextDocument(storageFile);
+ await vscode.window.showTextDocument(storageDoc);
+ let storageHover;
+ for (let attempt = 0; attempt < 100; attempt++) {
+ const replies = await vscode.commands.executeCommand('vscode.executeHoverProvider', storageDoc.uri, new vscode.Position(4, 8));
+ storageHover = replies && replies.find(reply => reply.contents.some(md => md.value.includes('size: 3 elements')));
+ if (storageHover) break;
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ assert.ok(storageHover, 'real editor returns storage metadata');
+ const storageMd = storageHover.contents[0];
+ 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';
+ 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
new file mode 100644
index 0000000..ddab3be
--- /dev/null
+++ b/tests/drivers/wasm_test.js
@@ -0,0 +1,28 @@
+// runs programs on the webassembly build (src/web/zc.wasm) and compares with the expected output.
+// usage: node tests/drivers/wasm_test.js [path/to/zc.wasm]
+const fs = require('fs');
+const ZC = require('../../src/web/zc.js');
+(async () => {
+ const zc = await ZC.load(fs.readFileSync(process.argv[2] || __dirname + '/../../src/web/zc.wasm'));
+ const cases = [
+ ['examples/hello.zc', 'hello, world!'],
+ ['examples/fact.zc', '1101110101111100000000\n3628800\n'],
+ ['examples/selfpatch.zc', 'ABC'],
+ ['examples/lists.zc', '[0 1 4 9 16 25]\nlive: 7\nafter the block: 0\n'],
+ ['examples/wiring.zc', '16 1234 25 4/9'],
+ ['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'],
+ ];
+ let ok = 0, bad = 0;
+ for (const [file, want] of cases) {
+ const r = await zc.run(fs.readFileSync(__dirname + '/../../' + file, 'utf8'));
+ if (r.output.replace(/\n+$/, '') === want.replace(/\n+$/, '')) { ok++; console.log('ok wasm ' + file); }
+ else { bad++; console.log('FAIL wasm ' + file + ': got ' + JSON.stringify(r.output) + ' ' + r.messages); }
+ }
+ // an error is reported the way the interpreter reports it
+ 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);
+})();
diff --git a/tests/drivers/word_literals_test.py b/tests/drivers/word_literals_test.py
new file mode 100644
index 0000000..a2d230d
--- /dev/null
+++ b/tests/drivers/word_literals_test.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+# every 64-bit word is representable, even outside a single literal's 63-bit range.
+import subprocess
+
+values = [0, 1, (1 << 62) - 1, 1 << 62, (1 << 63) - 1, -(1 << 62), -(1 << 62) - 1, -(1 << 63), -1]
+source = 'fn main() {\n' + ''.join(f'put_n({value}); put_c(10);\n' for value in values) + 'return 0; }\n'
+expected = ''.join(str(value) + '\n' for value in values)
+for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=20)
+ assert result.returncode == 0 and result.stdout == expected, (command, result.stdout, result.stderr)
+ print('ok full-width positive and negative word literals: ' + command[0])
+source = '''immediate fn add_word_operator() { next(); add_binop("**", 11, "4611686018427387904 | +"); return 0; }
+add_word_operator
+fn main() { put_n(0 ** 0); put_c(10); return 0; }
+'''
+for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=20)
+ assert result.returncode == 0 and result.stdout == '4611686018427387904\n', (command, result.stdout, result.stderr)
+ print('ok full-width word in a stored operator sequence: ' + command[0])