diff options
| author | 2026-10-14 08:29:46 +0330 | |
|---|---|---|
| committer | 2026-10-14 08:29:46 +0330 | |
| commit | 1c53304d03414890af0da7432d1aea87e754a14a (patch) | |
| tree | 97190d86cf548c9bc63f68c86a4d2a4f09809713 /editors/vscode/client | |
| parent | a (diff) | |
feat: grow memory limits, add icon/actor libraries, overhaul LSP diagnostics
Compiler and VM
- make every memory map limit dynamic and adjustable at both build time and
runtime, including compiler tables, stacks, and every execution region
- reserve and grow tables, operand and return stacks, frame segments, and
image-building output without relocating live code or globals
- rename i/o and string helpers consistently (puts -> put_s, putn -> put_n,
scat -> cat_s) across compiler, runtime, examples, tests, and docs
- recast and consume values assigned to globals from inside functions
- allow fixed and const together, add typed closure returns (fn:t(...)),
multi-include syntax (include rt, run, core;), and recurse type inference
- never infer string literals as word, anywhere, as argument, variable, or
return type
- draft docs/BORROW.md: a borrow checker less intrusive than Rust's but more
intelligent
LSP and editor integration
- require explicit includes and give every toolchain area its own namespace
(rt, lsp, run, core)
- hover for built-in types, inferred types, values known at hover time, array
and pointer lifetime/size, library names in include statements, and inferred
lifetimes with explanations
- honor /* */ comments as documentation for variables and arguments, and stop
misattributing section headers or trailing comments as variable hints
- stream diagnostics and cancel stale requests on change
- jump to definition and references, including cross-file
- quick fixes with automatic library import, and namespace completion
Hints and diagnostics
- render hints as blue squigglies by default, with hints on/small/off per file
- suppress magic-number hints via a preceding /* */ comment, and say so
- warn on unused arguments; allow multiple _ discards in parameters, pattern
matching, and tagged enums, warning if a _ is actually used
- rewrite hint and comment prose in professional compiler-doc register
Libraries
- add icon.zc: run-time self-modification, find/replace syntax, type and block
edits, and step-through inspection of locals and globals
- add actor.zc: async actors with Actor<I,O>, implicit this, spawn/recv/send,
injected into the compiler machinery at include time rather than built in
Tooling and docs
- reorganize sources, tests, and designs into subdirectories
- unify windows entry points in tinyvm.bat and add a matching tinyvm.sh
- add --help across the toolchain
- polish the learning guide, stdlib docs, and design notes
Tests
- add accept, reject, and regression coverage across the compiler, LSP,
- runtime, icon, actors, threads, formatting, and the toolchain contracts,
plus fuzzers and property tests
Diffstat (limited to 'editors/vscode/client')
| -rw-r--r-- | editors/vscode/client/codeactions.js | 27 | ||||
| -rw-r--r-- | editors/vscode/client/diagcache.js | 12 | ||||
| -rw-r--r-- | editors/vscode/client/extension.js | 108 | ||||
| -rw-r--r-- | editors/vscode/client/sourceprovider.js | 17 | ||||
| -rw-r--r-- | editors/vscode/client/vmproc.js | 3 | ||||
| -rw-r--r-- | editors/vscode/client/wasmproc.js | 3 |
6 files changed, 167 insertions, 3 deletions
diff --git a/editors/vscode/client/codeactions.js b/editors/vscode/client/codeactions.js new file mode 100644 index 0000000..dd79b3f --- /dev/null +++ b/editors/vscode/client/codeactions.js @@ -0,0 +1,27 @@ +// convert protocol edits into editor actions while rejecting stale versions. +function convertCodeActions(vscode, results, document, expectedVersion = document.version) { + return (Array.isArray(results) ? results : []).flatMap(result => { + if (!result.edit) return []; + const changes = result.edit.documentChanges || Object.entries(result.edit.changes || {}).map(([uri, edits]) => ({ textDocument: { uri }, edits })); + if (changes.some(change => change.textDocument?.uri === document.uri.toString() && change.textDocument.version != null && change.textDocument.version !== expectedVersion)) return []; + const edit = new vscode.WorkspaceEdit(); + let count = 0; + for (const change of changes) { + if (!change.textDocument || !Array.isArray(change.edits)) continue; + const uri = vscode.Uri.parse(change.textDocument.uri); + for (const replacement of change.edits) { + const range = replacement.range; + if (!range || typeof replacement.newText !== 'string') continue; + edit.replace(uri, new vscode.Range(range.start.line, range.start.character, range.end.line, range.end.character), replacement.newText); + count++; + } + } + if (!count) return []; + const action = new vscode.CodeAction(result.title, vscode.CodeActionKind.QuickFix); + action.edit = edit; + action.isPreferred = !!result.isPreferred; + return [action]; + }); +} + +module.exports = { convertCodeActions }; diff --git a/editors/vscode/client/diagcache.js b/editors/vscode/client/diagcache.js index 6e77207..2e1758a 100644 --- a/editors/vscode/client/diagcache.js +++ b/editors/vscode/client/diagcache.js @@ -42,6 +42,7 @@ function includeNames(text) { while ((m = tokens.exec(text)) !== null) { const token = m[0]; if (token.startsWith('//') || token.startsWith('/*')) continue; + // include std; names the library that is part of the compiler: there is no file to read if (include && token.startsWith('"')) { names.push(token.slice(1, -1).replace(/\\([\\"nrt0])/g, (_, c) => ({ n: '\n', r: '\r', t: '\t', 0: '\0' })[c] || c)); } @@ -80,13 +81,20 @@ function sourceTree(text, docPath, workspace) { // the key for a document and the files it reads. key is null when the tree is // too large to cover, and then nothing is cached. deps lists every file the // tree names (existing or not), so a change to any of them can be noticed. +// texts maps each file that exists to its text, which a server with no files +// of its own is sent before it checks the document. function diagKey(stamp, text, docPath, workspace) { const tree = sourceTree(text, docPath, workspace); const deps = [...tree.files.keys()].sort(); - if (!tree.complete) return { key: null, deps }; + const texts = new Map(); + for (const [p, hash] of tree.files) { + if (hash === 'missing') continue; + try { texts.set(p, fs.readFileSync(p, 'utf8')); } catch (e) { /* gone since it was hashed */ } + } + if (!tree.complete) return { key: null, deps, texts }; const parts = [stamp, docPath, sha256(text)]; for (const p of deps) parts.push(p + '=' + tree.files.get(p)); - return { key: sha256(parts.join('\n')), deps }; + return { key: sha256(parts.join('\n')), deps, texts }; } // the cache itself: key -> diagnostics, least recently used first. saved is diff --git a/editors/vscode/client/extension.js b/editors/vscode/client/extension.js index c51b12e..3846eca 100644 --- a/editors/vscode/client/extension.js +++ b/editors/vscode/client/extension.js @@ -13,10 +13,12 @@ const vscode = require('vscode'); const cp = require('child_process'); const path = require('path'); const fs = require('fs'); +const { convertCodeActions } = require('./codeactions'); const { vmProcess } = require('./vmproc'); const { wasmProcess } = require('./wasmproc'); const { DiagCache, diagKey, fileStamp, sha256 } = require('./diagcache'); const { hoverContents } = require('./hover'); +const { createSourceProvider } = require('./sourceprovider'); const CHANNEL = 'ZC Language Server'; const SLOW_MS = 2000; // a request that takes longer than this is logged as slow @@ -40,6 +42,8 @@ let store = null; // the extension's globalState, where the cache is saved let cache = new DiagCache(null); let serverStamp = 'none'; // identifies the server build; it is part of every key const docKeys = new Map(); // uri -> key of the document as it is now (null: not cacheable) +const docFiles = new Map(); // uri -> the texts of the files the document includes, sent to the server before it checks the document +const sentFiles = new Map(); // path -> the text the server was last sent for it const docDeps = new Map(); // uri -> the files the document reads, for checking it again when one changes const serverKeys = new Map(); // uri -> key of the text the server holds const sentKeys = new Map(); // uri -> keys sent to the server whose diagnostics have not come back @@ -190,6 +194,7 @@ function keyOf(doc) { const r = diagKey(serverStamp, doc.getText(), doc.uri.fsPath, workspaceDir()); docKeys.set(uri, r.key); docDeps.set(uri, r.deps); + docFiles.set(uri, r.texts); return r.key; } @@ -238,6 +243,17 @@ function showCached(doc) { return true; } +// the server of this extension has no files of its own: it is sent the text of every file a document +// includes, keyed by its full path, and looks an include up there. only what changed is sent again. +function sendIncludes(uri) { + const texts = docFiles.get(uri); + if (!texts) return; + for (const [p, text] of texts) { + if (sentFiles.get(p) === text) continue; + sentFiles.set(p, text); + send({ jsonrpc: '2.0', method: 'zc/file', params: { path: p.split(path.sep).join('/'), text } }); + } +} // sends doc's text to the server: didOpen the first time, didChange after that function sendDoc(doc, key) { if (!proc || !proc.stdin.writable || !ready) return false; @@ -248,6 +264,7 @@ function sendDoc(doc, key) { q.push({ key, version }); sentKeys.set(uri, q); serverKeys.set(uri, key); + sendIncludes(uri); if (!opened.has(uri)) { opened.add(uri); send({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version, text: doc.getText() } } }); @@ -346,6 +363,7 @@ function startServer() { opened.clear(); serverKeys.clear(); sentKeys.clear(); + sentFiles.clear(); docVersions.clear(); serverStamp = serverStampFor(bin); const t0 = Date.now(); @@ -398,7 +416,7 @@ function startServer() { } }); watchInit(gen, !!bin); - request('initialize', { processId: process.pid, capabilities: {} }).then(res => { + request('initialize', { processId: process.pid, rootUri: workspaceDir() ? vscode.Uri.file(workspaceDir()).toString() : null, capabilities: {} }).then(res => { if (gen !== generation || stopping || !proc) return; if (initTimer) { clearTimeout(initTimer); initTimer = null; } const caps = res && res.capabilities ? Object.keys(res.capabilities) : []; @@ -527,6 +545,94 @@ function activate(context) { } }; context.subscriptions.push(vscode.languages.registerHoverProvider({ language: 'zc' }, hover)); + + // the sources the server carries (the runtime, compiler, standard library and icon module): shown read only at tinyvm:/NAME.zc + // when go to definition lands on a built-in. the text comes from the server, so it is the code the editor's checks run on + context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('tinyvm', createSourceProvider(request, () => ready))); + + // completions refresh as the prefix changes; deprecated lifetime entries stay visible at the end. + context.subscriptions.push(vscode.languages.registerCompletionItemProvider({ language: 'zc' }, { + async provideCompletionItems(doc, position, token) { + if (!isZc(doc) || !ready || token.isCancellationRequested) return null; + ensureServer(doc); + const version = doc.version; + try { + const res = await request('textDocument/completion', { textDocument: { uri: doc.uri.toString() }, position: { line: position.line, character: position.character } }); + if (!res || token.isCancellationRequested || doc.version !== version) return null; + const items = (res.items || []).map(c => { + const item = new vscode.CompletionItem(c.label, c.kind ? c.kind - 1 : vscode.CompletionItemKind.Variable); + item.detail = c.detail; + item.sortText = c.sortText; + item.filterText = c.filterText; + if (c.tags && c.tags.includes(1)) item.tags = [vscode.CompletionItemTag.Deprecated]; + if (c.documentation) { + item.documentation = new vscode.MarkdownString(c.documentation.value || c.documentation); + item.documentation.isTrusted = false; + } + if (c.textEdit) { + const r = c.textEdit.range; + item.range = new vscode.Range(r.start.line, r.start.character, r.end.line, r.end.character); + item.insertText = c.textEdit.newText; + } + return item; + }); + return new vscode.CompletionList(items, !!res.isIncomplete); + } catch (err) { return null; } + } + }, '.', '>')); + + // compiler-backed import fixes use the same document version as their diagnostics. + if (vscode.languages.registerCodeActionsProvider) context.subscriptions.push(vscode.languages.registerCodeActionsProvider({ language: 'zc' }, { + async provideCodeActions(doc, range, actionContext, token) { + if (!isZc(doc) || !ready || token.isCancellationRequested) return []; + ensureServer(doc); + const version = doc.version; + const serverVersion = docVersions.get(doc.uri.toString()); + try { + const results = await request('textDocument/codeAction', { + textDocument: { uri: doc.uri.toString() }, + range: { start: { line: range.start.line, character: range.start.character }, end: { line: range.end.line, character: range.end.character } }, + context: { diagnostics: [] } + }); + if (token.isCancellationRequested || doc.version !== version || docVersions.get(doc.uri.toString()) !== serverVersion) return []; + return convertCodeActions(vscode, results, doc, serverVersion); + } catch (err) { return []; } + } + }, { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] })); + + // go to definition and find references: the compiler's own records, including names from included files + const locations = (res) => (Array.isArray(res) ? res : []).map(l => new vscode.Location(vscode.Uri.parse(l.uri), + new vscode.Range(l.range.start.line, l.range.start.character, l.range.end.line, l.range.end.character))); + context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'zc' }, { + async provideDefinition(doc, position) { + if (!isZc(doc) || !ready) return null; + ensureServer(doc); + try { + return locations(await request('textDocument/definition', { textDocument: { uri: doc.uri.toString() }, position: { line: position.line, character: position.character } })); + } catch (err) { return null; } + } + })); + // formatting: the server's pretty-printer, with the editor's tab settings (a .zcformat file overrides them) + context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider({ language: 'zc' }, { + async provideDocumentFormattingEdits(doc, options) { + if (!isZc(doc) || !ready) return []; + ensureServer(doc); + try { + const res = await request('textDocument/formatting', { textDocument: { uri: doc.uri.toString() }, options: { tabSize: options.tabSize, insertSpaces: options.insertSpaces } }); + return (Array.isArray(res) ? res : []).map(e => vscode.TextEdit.replace( + new vscode.Range(e.range.start.line, e.range.start.character, e.range.end.line, e.range.end.character), e.newText)); + } catch (err) { return []; } + } + })); + context.subscriptions.push(vscode.languages.registerReferenceProvider({ language: 'zc' }, { + async provideReferences(doc, position, refContext) { + if (!isZc(doc) || !ready) return null; + ensureServer(doc); + try { + return locations(await request('textDocument/references', { textDocument: { uri: doc.uri.toString() }, position: { line: position.line, character: position.character }, context: { includeDeclaration: !!refContext.includeDeclaration } })); + } catch (err) { return null; } + } + })); } function deactivate() { diff --git a/editors/vscode/client/sourceprovider.js b/editors/vscode/client/sourceprovider.js new file mode 100644 index 0000000..d03d746 --- /dev/null +++ b/editors/vscode/client/sourceprovider.js @@ -0,0 +1,17 @@ +// serves source documents carried by the language server at tinyvm:/NAME.zc. +function createSourceProvider(request, isReady, delay = ms => new Promise(resolve => setTimeout(resolve, ms))) { + return { + async provideTextDocumentContent(uri) { + for (let waited = 0; !isReady() && waited < 10000; waited += 100) await delay(100); + if (!isReady()) return '// the language server is not running\n'; + try { + const result = await request('zc/source', { path: uri.path }); + return typeof result === 'string' ? result : '// no such built-in source\n'; + } catch (error) { + return '// the language server could not give this source\n'; + } + } + }; +} + +module.exports = { createSourceProvider }; diff --git a/editors/vscode/client/vmproc.js b/editors/vscode/client/vmproc.js index d4806e4..8ce8c52 100644 --- a/editors/vscode/client/vmproc.js +++ b/editors/vscode/client/vmproc.js @@ -51,6 +51,9 @@ function vmProcess(dir, opts = {}) { dead = true; log(status === 0 ? 'message' : 'problem', 'the vm halted with status ' + status + ' after ' + steps + ' steps, ' + bytesIn + ' bytes in, ' + bytesOut + ' out'); proc.emit('exit', status); + } else if (status === 3 && vm.polling) { + // the server asked whether input is waiting: let the event loop deliver it, then go on + setImmediate(() => { if (!dead && vm.polling) after(vm.resume()); }); } }; diff --git a/editors/vscode/client/wasmproc.js b/editors/vscode/client/wasmproc.js index 0aa674a..e8250bc 100644 --- a/editors/vscode/client/wasmproc.js +++ b/editors/vscode/client/wasmproc.js @@ -58,6 +58,9 @@ function wasmProcess(file, opts = {}) { dead = true; log(status === 0 ? 'message' : 'problem', 'the server halted with status ' + status + ' after ' + steps + ' steps, ' + bytesIn + ' bytes in, ' + bytesOut + ' out'); proc.emit('exit', status); + } else if (status === 3 && x.wasm_polling && x.wasm_polling()) { + // the server asked whether input is waiting: let the event loop deliver it, then go on + setImmediate(() => { if (!dead && x.wasm_polling && x.wasm_polling()) after(x.wasm_resume()); }); } }; |
