aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--editors/vscode/syntaxes/zc.tmLanguage.json2
-rw-r--r--src/lsp/server.zc10
-rw-r--r--tests/drivers/hold_lsp_test.js54
-rwxr-xr-xtests/test.sh3
4 files changed, 67 insertions, 2 deletions
diff --git a/editors/vscode/syntaxes/zc.tmLanguage.json b/editors/vscode/syntaxes/zc.tmLanguage.json
index 3869418..f8743d3 100644
--- a/editors/vscode/syntaxes/zc.tmLanguage.json
+++ b/editors/vscode/syntaxes/zc.tmLanguage.json
@@ -91,7 +91,7 @@
},
{
"name": "keyword.other.zc",
- "match": "\\b(as|unchecked|unbounded|asm|quote|sizeof|typeof|len|cap|push|pop|resize|o3|include|embed|memdebug|fixed|typecheck)\\b"
+ "match": "\\b(as|unchecked|unbounded|hold|shared|asm|quote|sizeof|typeof|len|cap|push|pop|resize|o3|include|embed|memdebug|fixed|typecheck)\\b"
},
{
"name": "keyword.operator.memory.zc",
diff --git a/src/lsp/server.zc b/src/lsp/server.zc
index 65881a9..d776b1d 100644
--- a/src/lsp/server.zc
+++ b/src/lsp/server.zc
@@ -412,6 +412,7 @@ int gap_out(int from, int to) {
return 0;
}
// is c an operator character, which the highlighter marks as an operator.
+int hov_shared; // the hold block of the global being hovered, when it is shared
int isopc(int c) {
char* s = "+-*/%=<>!&|^~?@#$";
int i = 0;
@@ -1725,6 +1726,8 @@ char* kwtext() {
if (hname_is("switch")) return "switch (e) { case 1, 2: ... default: ... }~a branch on constant labels. the cases don't fall through, and break leaves early.";
if (hname_is("break")) return "break;~leaves the innermost loop or switch.";
if (hname_is("continue")) return "continue;~goes on to the next pass of the innermost loop.";
+ if (hname_is("hold")) return "hold { shared let c; fn a() { } fn b() { } }~a monitor at the top level. its shared variables are one copy for every actor, and only the functions written in the block can use them. each call to one of those functions holds the block's lock for the whole call; a call from one to another doesn't lock again. blocks are entered in the order they are declared.";
+ if (hname_is("shared")) return "shared let c; / shared int[4] counts;~inside a hold block: a variable every actor sees, used only by the block's functions under its lock. it holds numbers or arrays of numbers, since each actor has its own heap.";
if (hname_is("defer")) return "defer e; or defer { }~runs the statement when the enclosing block ends, on every way out of it (falling off the end, return, break, continue). several run last registered first, and what they name is read when they run. return, break and continue are not allowed inside. defer rt.free(p); is the transfer of p for the ownership analysis.";
if (hname_is("return")) return "return e; or return;~leaves the function with a value, or leaves a void function. returning an owned local transfers it to the caller automatically.";
if (hname_is("enum")) return "enum name { A, B = 5 };~global constants. under typecheck on they are a type of their own, and a member can carry fields to make a constructor.";
@@ -1843,6 +1846,7 @@ int hover_named(int i, int o, int kind) {
else if (st == run.S_U) hov_text(".u"); else if (st == run.S_D) hov_text(".d"); else if (st == run.S_DEAD) hov_text(".expired");
hov_put(' '); k = o; while (k < end) { hov_put(dc(base, k)); k++; }
if (kind == 1) hov_text(" // parameter;");
+ else if (kind == 3 && hov_shared > 0) { hov_text(" // shared in hold block "); hov_number(hov_shared); hov_text(": the block's functions use it under its lock;"); }
else if (kind == 3) hov_text(" // global;");
else if (kind == 4) hov_text(" // field;");
else hov_text("; // local");
@@ -2004,7 +2008,9 @@ int do_hover(int i) {
else if ((v & 15) == run.K_GVAR || (v & 15) == run.K_GARR) {
s = run.rsrec[r * run.RS_W + 3]; q = 0; o = 0 - 1;
while (q < run.rsn) { if (run.rsrec[q * run.RS_W + 3] == s && lt_find(run.rsrec[q * run.RS_W]) >= 0) { o = run.rsrec[q * run.RS_W]; break; } q++; }
+ hov_shared = 0; if (s >= 0 && (run.sflag[s] & run.F_SHARED)) hov_shared = run.sflag[s] / run.F_SHARED_RANK;
if (o >= 0) hover_named(i, o, 3); else if (v & 16) hover_global(i, 2); else hover_global(i, 1);
+ hov_shared = 0;
} else if (v & 16) hover_global(i, 2); else hover_global(i, 1);
}
else hover_global(i, 0);
@@ -3123,7 +3129,7 @@ int cp_symbol(int s, char* label) {
bstr sig = cp_signature(run.sname[s]), doc; int k = 0, kind = 6;
hn = 0;
if (sig) { while (k < rt.len_s(sig)) { hov_put(rt.at_s(sig, k)); k++; } }
- else { if (run.stype[s]) { hov_type(run.stype[s]); hov_put(' '); } hov_text(label); }
+ else { if ((run.skind[s] == run.K_GVAR || run.skind[s] == run.K_GARR) && (run.sflag[s] & run.F_SHARED)) hov_text("shared "); if (run.stype[s]) { hov_type(run.stype[s]); hov_put(' '); } hov_text(label); }
if (run.skind[s] == run.K_FN || run.skind[s] == run.K_ENH) kind = 3;
else if (run.skind[s] == run.K_TYPE) kind = 22;
else if (run.skind[s] == run.K_CONST) kind = 21;
@@ -3264,6 +3270,8 @@ int do_completion(int i) {
if (run.skind[s] != run.K_DOC && run.lookup(run.sname[s]) == s && (run.skind[s] != run.K_FN || (run.sflag[s] & (run.F_DEF | run.F_DECL)))) cp_symbol(s, run.sname[s]);
s++;
}
+ if (cp_score("hold") >= 0) { hn = 0; hov_text("hold { shared let c; fn f() { } }"); if (cp_add("hold", 14, 0, 1)) cp_docs[cp_n - 1] = cp_copy("a monitor: shared variables for every actor, used under the block's lock by the functions written in it"); }
+ if (cp_score("shared") >= 0) { hn = 0; hov_text("shared let name;"); if (cp_add("shared", 14, 0, 1)) cp_docs[cp_n - 1] = cp_copy("inside a hold block: a variable every actor sees, used only by the block's functions"); }
run.probe_off = cp_cursor; old = chk_stream; chk_stream = 0; check_doc(i); chk_stream = old; run.probe_off = -1;
member = 0;
if (run.probe_has == 2 && run.probe_mem && run.probe_mem[0] == run.TY_STRUCT) member = run.probe_mem[2];
diff --git a/tests/drivers/hold_lsp_test.js b/tests/drivers/hold_lsp_test.js
new file mode 100644
index 0000000..65d8154
--- /dev/null
+++ b/tests/drivers/hold_lsp_test.js
@@ -0,0 +1,54 @@
+// hold blocks in the language server: hover on hold, shared and a shared variable,
+// completion offers the keywords and marks shared variables, and a use outside the
+// block is a diagnostic. usage: node tests/drivers/hold_lsp_test.js path/to/zclsp
+const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1;
+const assert = require('assert');
+const cp = require('child_process');
+const server = process.argv[2];
+const uri = 'file:///hold.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 session(text, request) {
+ const messages = [
+ { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
+ { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } },
+ ];
+ if (request) messages.push(Object.assign({ jsonrpc: '2.0', id: 2 }, request));
+ messages.push({ jsonrpc: '2.0', method: 'exit' });
+ const result = cp.spawnSync(server, [], { input: Buffer.concat(messages.map(frame)), timeout: 15000 * TIMEOUT_SCALE, maxBuffer: 8 << 20 });
+ assert.strictEqual(result.status, 0, String(result.stderr));
+ const out = []; 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]);
+ out.push(JSON.parse(data.subarray(end + 4, end + 4 + count).toString()));
+ data = data.subarray(end + 4 + count);
+ }
+ return out;
+}
+function at(text, needle, nth = 0, delta = 0) {
+ let o = -1; for (let i = 0; i <= nth; i++) o = text.indexOf(needle, o + 1);
+ assert.ok(o >= 0, needle); o += delta;
+ const before = text.slice(0, o);
+ return { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 };
+}
+const hover = (text, needle, nth) => { const r = session(text, { method: 'textDocument/hover', params: { textDocument: { uri }, position: at(text, needle, nth) } }).find(m => m.id === 2); return r && r.result && r.result.contents.value || ''; };
+const good = 'namespace;\ninclude rt;\nhold {\n shared let count = 0;\n fn inc() { count = count + 1; return count; }\n}\nfn main() { return inc(); }\n';
+let h = hover(good, 'hold');
+assert.ok(h.includes('monitor') && h.includes('lock'), 'hover on hold explains the block: ' + h);
+h = hover(good, 'shared');
+assert.ok(h.includes('every actor'), 'hover on shared: ' + h);
+h = hover(good, 'count', 1);
+assert.ok(h.includes('shared in hold block 1'), 'hover on a shared variable names its block: ' + h);
+const bad = 'namespace;\ninclude rt;\nhold { shared let count; fn inc() { count++; return count; } }\nfn main() { count = 2; return inc(); }\n';
+const diags = session(bad).filter(m => m.method === 'textDocument/publishDiagnostics').flatMap(m => m.params.diagnostics);
+assert.ok(diags.some(d => d.message.includes('count: is shared in a hold block') && d.range.start.line === 3), 'use outside the block is a diagnostic on its line: ' + JSON.stringify(diags));
+const partial = 'namespace;\ninclude rt;\nhold { shared let count; fn inc() { count++; return count; } }\nho\nfn main() { return inc(); }\n';
+let r = session(partial, { method: 'textDocument/completion', params: { textDocument: { uri }, position: at(partial, '\nho', 0, 3) } }).find(m => m.id === 2);
+let items = (r.result.items || r.result);
+assert.ok(items.some(i => i.label === 'hold'), 'completion offers hold');
+const partial2 = 'namespace;\ninclude rt;\nhold { shared let count; fn inc() { cou; return count; } }\nfn main() { return inc(); }\n';
+r = session(partial2, { method: 'textDocument/completion', params: { textDocument: { uri }, position: at(partial2, 'cou;', 0, 3) } }).find(m => m.id === 2);
+items = (r.result.items || r.result);
+const it = items.find(i => i.label === 'count');
+assert.ok(it && String(it.detail).startsWith('shared'), 'completion marks a shared variable: ' + JSON.stringify(it));
+console.log('hold lsp: 6 checks passed');
diff --git a/tests/test.sh b/tests/test.sh
index 3b505fc..90599ed 100755
--- a/tests/test.sh
+++ b/tests/test.sh
@@ -646,6 +646,9 @@ s_lsp() {
node tests/drivers/codeactions_client_test.js > $T/codeactions-client.out 2>&1; r=$?
grep -v '^ok' $T/codeactions-client.out
chk $r 0 "lsp: versioned import quick-fix edits"
+ node tests/drivers/hold_lsp_test.js $lsp > $T/holdlsp.out 2>&1; r=$?
+ cat $T/holdlsp.out | grep -v passed
+ chk $r 0 "lsp hold blocks: hover, completion and diagnostics"
node tests/drivers/completion_test.js $lsp > $T/completion.out 2>&1; r=$?
grep -v '^ok' $T/completion.out
chk $r 0 "lsp: fuzzy completions, signatures, scope and expired lifetimes"