aboutsummaryrefslogtreecommitdiff
path: root/src/lsp/server.zc
diff options
context:
space:
mode:
Diffstat (limited to 'src/lsp/server.zc')
-rw-r--r--src/lsp/server.zc1929
1 files changed, 1929 insertions, 0 deletions
diff --git a/src/lsp/server.zc b/src/lsp/server.zc
new file mode 100644
index 0000000..98e71c3
--- /dev/null
+++ b/src/lsp/server.zc
@@ -0,0 +1,1929 @@
+// ============================================================================
+// server.zc: the language server. it is appended to the genome after the
+// compiler (cat src/runtime/rt.zc src/compiler/compiler.zc src/lsp/server.zc | zc0), so it does not
+// analyze zc with a second implementation of the grammar: it runs the
+// compiler itself over the document, in check mode (see chk_unit in
+// compiler.zc), and reports what the compiler said. a parse word, an
+// operator or a type the document defines changes how the rest of the
+// document is read, for the checker and for the highlighter, because both are
+// the compiler. it speaks json-rpc on stdin and stdout, and when stdin is not
+// a protocol stream but a source file it prints the diagnostics as text.
+// ============================================================================
+
+// the first constant the server declares: the blob cut for a document starts
+// here (blob_load, compiler.zc), so this name must stay the first one of the server.
+const int LSP_SERVER = 1;
+
+// the compiler's own functions, declared here so this file checks on its own
+// in an editor: the build places them in front of this file, so these lines only
+// promise what is already defined there
+int chk_unit(int src, int buf, int cap, int sets, int lim);
+int hl_begin(int src);
+int hl_end();
+
+// ---- memory ----
+// nothing here has a fixed address. a document is a heap block of its text, the
+// diagnostics and the check's name sets are blocks too, and a check collects
+// what the last one left behind (chk_unit). a document compiles into the
+// program region from 0 up to CODE_END, which is the map's own.
+
+const int DGCAP = 12000;
+word dgb, sets; int jsize, tcap;
+int dgbcap = DGCAP;
+
+const int MAXDOC = 16, URIW = 160;
+
+// ---- output ----
+// every message is written twice: once to count its bytes for the header,
+// once for real. wc is the only thing that touches the terminal.
+int wmode, wcnt;
+// writes one byte of output. while a message is being sized the byte is only
+// counted; otherwise it goes to the terminal.
+int wc(int c) {
+ if (wmode) wcnt++; else raw_c(c);
+ return 0;
+}
+// writes a zero-terminated word string, one byte per character.
+int ws(word s) {
+ int i = 0;
+ while (s[i]) { wc(s[i]); i++; }
+ return 0;
+}
+// writes n in decimal, with a minus sign when it is negative.
+int wn(int n) {
+ [24]word b; int i = 0;
+ if (n < 0) { wc('-'); n = 0 - n; }
+ while (1) { b[i] = '0' + n % 10; i++; n = n / 10; if (!n) break; }
+ while (i) { i--; wc(b[i]); }
+ return 0;
+}
+// writes byte c inside a json string: quotes and backslashes are escaped, and
+// the control characters become their escapes (\n, \t, \u00xx) so the text stays one line.
+int wesc(int c) {
+ if (c == 34) { wc(92); wc(34); return 0; }
+ if (c == 92) { wc(92); wc(92); return 0; }
+ if (c == 10) { wc(92); wc('n'); return 0; }
+ if (c == 13) { wc(92); wc('r'); return 0; }
+ if (c == 9) { wc(92); wc('t'); return 0; }
+ if (c < 32) {
+ ws("\\u00");
+ wc("0123456789abcdef"[c >> 4]);
+ wc("0123456789abcdef"[c & 15]);
+ return 0;
+ }
+ wc(c);
+ return 0;
+}
+// a json string from n chars at p, one per word
+int wjs(word p, int n) {
+ int i = 0;
+ wc(34);
+ while (i < n) { wesc(p[i]); i++; }
+ wc(34);
+ return 0;
+}
+// sends one message. gen writes its body: it runs once to count the bytes for the
+// Content-Length header, and once more to write them.
+int send(int gen) {
+ wmode = 1; wcnt = 0; gen();
+ wmode = 0;
+ ws("Content-Length: "); wn(wcnt); ws("\r\n\r\n");
+ gen();
+ return 0;
+}
+
+// ---- documents ----
+// a document is its text, four bytes to a word, in a heap block of its own
+// (doff), with a zero word after the last byte so the lexer sees the end. a
+// replaced text is a new block, and the old one is garbage for the collector.
+
+int nodoc, doccap = MAXDOC;
+word duri, dulen, doff, dsize, dver, dgen, dlive;
+// Grow a heap buffer while retaining its contents. Globals keep these buffers rooted.
+unchecked word lsp_grow(word old, int count, int capacity) {
+ word p = new(capacity);
+ fill_w(p, 0, capacity);
+ if (old && count) copy_w(p, old, count);
+ return p;
+}
+// Document columns grow together; each URI has its own string allocation.
+int docs_grow() {
+ int next = doccap * 2;
+ duri = lsp_grow(duri, doccap, next); dulen = lsp_grow(dulen, doccap, next);
+ doff = lsp_grow(doff, doccap, next); dsize = lsp_grow(dsize, doccap, next);
+ dver = lsp_grow(dver, doccap, next); dgen = lsp_grow(dgen, doccap, next);
+ dlive = lsp_grow(dlive, doccap, next);
+ doccap = next;
+ return 0;
+}
+// the byte at offset i of a text. texts are packed four bytes to a word, low byte first.
+word dc(word base, int i) { return (base[i >> 2] >> ((i & 3) * 8)) & 255; }
+// the document with the uri p (n bytes long), or -1 when the server has no such document.
+int dfind(word p, int n) {
+ int i = 0, k;
+ while (i < doccap) {
+ if (dlive[i] && dulen[i] == n) {
+ k = 0;
+ while (k < n && duri[i][k] == p[k]) k++;
+ if (k == n) return i;
+ }
+ i++;
+ }
+ return 0 - 1;
+}
+// takes a free document slot for the uri p (n bytes long), growing the columns
+// when needed. commit_text sets its text after this.
+int docnew(word p, int n) {
+ int i = 0, k;
+ while (1) {
+ if (i == doccap) docs_grow();
+ if (!dlive[i]) {
+ duri[i] = lsp_grow(0, 0, n + 1);
+ k = 0; while (k < n) { duri[i][k] = p[k]; k++; }
+ dulen[i] = n; dlive[i] = 1; doff[i] = 0; dsize[i] = 0; dver[i] = 0; dgen[i] = 0;
+ return i;
+ }
+ i++;
+ }
+ return 0 - 1;
+}
+// the text, its terminator and a spare zero word
+int dwords(int n) { return (n >> 2) + 2; }
+// ---- the check ----
+int chk_i, chk_gen; // the document the compiler's tables describe, and its generation
+// checks document i with the compiler in check mode. the diagnostics are left in
+// dgb, and the types of the untyped lets are recorded while it runs (lt_on).
+int check_doc(int i) {
+ chk_i = i; chk_gen = dgen[i];
+ outmode = 1;
+ lt_on = 1; lt_n = 0; ltxn = 0; ht_n = 0;
+ chk_unit(doff[i], dgb, dgbcap, sets, CODE_END);
+ dgb = dgbuf; dgbcap = dgcap;
+ lt_on = 0;
+ outmode = 0;
+ return 0;
+}
+// the entry of the untyped local declared at byte offset o, or -1
+int lt_find(int o) {
+ int k = 0;
+ while (k < lt_n) { if (lt_off[k] == o) return k; k++; }
+ return 0 - 1;
+}
+int ensure_checked(int i) {
+ if (chk_i != i || chk_gen != dgen[i]) check_doc(i);
+ return 0;
+}
+
+// ---- positions ----
+// a cursor over the document that turns byte offsets into the line and the
+// utf-16 column the protocol wants. it only moves forward, or starts over.
+int pbase, plen, poff, pline, pcol;
+// starts the position cursor at the top of document i.
+int pc_reset(int i) {
+ pbase = doff[i];
+ plen = dsize[i];
+ poff = 0;
+ pline = 0;
+ pcol = 0;
+ return 0;
+}
+// moves the position cursor forward to byte offset off, counting lines and the
+// utf-16 columns the protocol uses (a character of four bytes takes two columns).
+// a cursor that is past off starts over from the top.
+int pc_to(int off) {
+ int b;
+ if (off < poff) { poff = 0; pline = 0; pcol = 0; }
+ if (off > plen) off = plen;
+ while (poff < off) {
+ b = dc(pbase, poff);
+ if (b == 10) { pline++; pcol = 0; }
+ else if (b < 128 || b >= 192) { if (b >= 240) pcol = pcol + 2; else pcol++; }
+ poff++;
+ }
+ return 0;
+}
+// the byte offset where the 1-based line l starts, or the end of the text when
+// there is no such line.
+int line_off(int l) {
+ int o = 0;
+ while (l > 1 && o < plen) { if (dc(pbase, o) == 10) l--; o++; }
+ return o;
+}
+
+// ---- diagnostics ----
+int gd; // the document being reported
+// writes a json position, {line, character}, for byte offset off.
+int wpos(int off) {
+ pc_to(off);
+ ws("{\"line\":");
+ wn(pline);
+ ws(",\"character\":");
+ wn(pcol);
+ wc('}');
+ return 0;
+}
+// writes the publishDiagnostics message for document gd from dgb. each record
+// becomes a range (the whole line when the compiler only knew the line), a
+// severity and its message.
+int gen_diag() {
+ int at = 0, first = 1, sev, rln, off, rlen, n, o, e;
+ ws("{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/publishDiagnostics\",\"params\":{\"uri\":");
+ wjs(duri[gd], dulen[gd]);
+ ws(",\"version\":"); wn(dver[gd]);
+ ws(",\"diagnostics\":[");
+ pc_reset(gd);
+ while (at < dgpos) {
+ sev = dgb[at]; rln = dgb[at + 1]; off = dgb[at + 2]; rlen = dgb[at + 3]; n = dgb[at + 4];
+ if (off < 0) { // the compiler only knew the line: mark its text
+ o = line_off(rln);
+ while (o < plen && (dc(pbase, o) == 32 || dc(pbase, o) == 9)) o++;
+ e = o;
+ while (e < plen && dc(pbase, e) != 10) e++;
+ if (e > o && dc(pbase, e - 1) == 13) e--;
+ } else {
+ o = off; e = off + rlen;
+ if (o >= plen) { // the end of the file: the last thing before it
+ o = plen;
+ while (o > 0 && (dc(pbase, o - 1) == 32 || dc(pbase, o - 1) == 10 || dc(pbase, o - 1) == 13 || dc(pbase, o - 1) == 9)) {
+ o--;
+ }
+ if (o > 0) o--;
+ e = o + 1;
+ }
+ if (e <= o) e = o + 1;
+ }
+ if (e > plen) e = plen;
+ if (o > e) o = e;
+ if (!first) wc(',');
+ first = 0;
+ ws("{\"range\":{\"start\":"); wpos(o); ws(",\"end\":"); wpos(e);
+ ws("},\"severity\":"); wn(sev); ws(",\"source\":\"zc\",\"message\":");
+ wjs(dgb + at + 5, n);
+ wc('}');
+ at = at + 5 + n;
+ }
+ ws("]}}");
+ return 0;
+}
+// sends the diagnostics of document i.
+int publish(int i) {
+ gd = i;
+ send(gen_diag);
+ return 0;
+}
+
+// ---- semantic tokens ----
+// the legend: keyword type function variable string number operator comment
+// parameter property macro enumMember
+const int TT_KW = 0, TT_TYPE = 1, TT_FN = 2, TT_VAR = 3, TT_STR = 4, TT_NUM = 5, TT_OP = 6, TT_COM = 7, TT_PARAM = 8, TT_PROP = 9, TT_MACRO = 10, TT_ENUM = 11;
+int tfirst, tprevl, tprevc;
+// one token on one line
+int tok_out(int off, int end, int ty) {
+ int l, c, len;
+ pc_to(off); l = pline; c = pcol;
+ pc_to(end); len = pcol - c;
+ if (len <= 0) return 0;
+ if (!tfirst) wc(',');
+ tfirst = 0;
+ if (l == tprevl) { wn(0); wc(','); wn(c - tprevc); } else { wn(l - tprevl); wc(','); wn(c); }
+ wc(','); wn(len); wc(','); wn(ty); ws(",0");
+ tprevl = l; tprevc = c;
+ return 0;
+}
+// a range that may cross lines is cut at the line ends
+int range_out(int off, int end, int ty) {
+ int e;
+ while (off < end) {
+ e = off;
+ while (e < end && dc(pbase, e) != 10) e++;
+ if (e > off && dc(pbase, e - 1) == 13) tok_out(off, e - 1, ty); else tok_out(off, e, ty);
+ off = e + 1;
+ }
+ return 0;
+}
+// between two tokens there is only space and comments
+int gap_out(int from, int to) {
+ int o = from, e, c;
+ while (o < to) {
+ c = dc(pbase, o);
+ if (c == '/' && dc(pbase, o + 1) == '/') {
+ e = o; while (e < to && dc(pbase, e) != 10) e++;
+ range_out(o, e, TT_COM); o = e;
+ } else if (c == '/' && dc(pbase, o + 1) == '*') {
+ e = o + 2;
+ while (e < plen && !(dc(pbase, e) == '*' && dc(pbase, e + 1) == '/')) e++;
+ e = e + 2; if (e > plen) e = plen;
+ range_out(o, e, TT_COM); o = e;
+ } else o++;
+ }
+ return 0;
+}
+// is c an operator character, which the highlighter marks as an operator.
+int isopc(int c) {
+ word s = "+-*/%=<>!&|^~?@#$";
+ int i = 0;
+ while (s[i]) { if (s[i] == c) return 1; i++; }
+ return 0;
+}
+// is the name n one of the words the highlighter shows as keywords even when the
+// compiler has no record for it.
+int iskeyword(int n) {
+ return n == s_fn || n == s_var || n == s_const || n == s_else || n == s_as || n == s_unchecked || n == s_immediate
+ || n == s_case || n == s_default || n == s_do || n == s_while || n == s_enum || n == s_include || n == s_memdebug
+ || n == s_enhance || n == s_micro || n == s_vm || n == s_struct || n == s_type || n == s_typecheck;
+}
+// the record of the name at byte offset o (see rs_note in the compiler), or -1.
+// the records are sorted by offset, so this is a binary search.
+int rs_find(int o) {
+ int lo = 0, hi = rsn - 1, mid, v;
+ while (lo <= hi) {
+ mid = (lo + hi) / 2;
+ v = rsrec[mid * RS_W];
+ if (v == o) return mid;
+ if (v < o) lo = mid + 1; else hi = mid - 1;
+ }
+ return 0 - 1;
+}
+// rs_cur is the cursor of the highlighter's pass over the records, which move
+// forward with the tokens.
+int rs_cur;
+// the class of an identifier at byte offset o: what the compiler resolved it to
+// when it checked the document, so a local named like a built-in is a variable.
+// classify guesses from the name alone, and only serves names with no record.
+int rs_class(int o, int n, int pt, int pv) {
+ int r = -1, k, v, sk;
+ while (rs_cur < rsn && rsrec[rs_cur * RS_W] < o) rs_cur++;
+ if (rs_cur < rsn && rsrec[rs_cur * RS_W] == o) r = rs_cur;
+ if (r < 0) return classify(n, pt, pv);
+ k = rsrec[r * RS_W + 1];
+ if (k == RS_LOCAL) return TT_VAR;
+ if (k == RS_PARAM) return TT_PARAM;
+ if (k == RS_FIELD) return TT_PROP;
+ v = rsrec[r * RS_W + 2];
+ sk = v & 15;
+ if (sk == K_KW) { if (v & 16) return TT_MACRO; return TT_KW; }
+ if (sk == K_TYPE) return TT_TYPE;
+ if (sk == K_FN) { if (v & 32) return TT_FN; return TT_VAR; }
+ if (sk == K_ENH) return TT_FN;
+ if (sk == K_CONST) return TT_ENUM;
+ return TT_VAR;
+}
+// the class of an identifier. pt, pv: the token before it
+int classify(int n, int pt, int pv) {
+ int s, nx = 0;
+ // a name after fn is a declaration; a name after . or -> that is called is a method
+ if (pt == T_ID && pv == s_fn) return TT_FN;
+ if (pt == T_OP && pv == s_dot) {
+ if (n == s_m || n == s_d || n == s_u) return TT_KW;
+ peek();
+ if (pk_tk == T_OP && pk_tv == s_lp) return TT_FN;
+ return TT_PROP;
+ }
+ if (pt == T_OP && pv == s_arrow) {
+ peek();
+ if (pk_tk == T_OP && pk_tv == s_lp) return TT_FN;
+ return TT_PROP;
+ }
+ if (iskeyword(n)) return TT_KW;
+ peek();
+ if (pk_tk == T_OP && pk_tv == s_lp) nx = 1;
+ s = lookup(n);
+ if (s >= 0) {
+ if (skind[s] == K_KW) { if (s >= ubase) return TT_MACRO; return TT_KW; }
+ if (skind[s] == K_TYPE) return TT_TYPE;
+ if (skind[s] == K_FN && !(sflag[s] & F_DEF)) return TT_VAR; // never defined: the check says so
+ if (skind[s] == K_FN || skind[s] == K_ENH) {
+ if (nx) return TT_FN;
+ if (lsget(1, n)) return TT_PARAM;
+ if (lsget(0, n)) return TT_VAR;
+ return TT_FN;
+ }
+ if (skind[s] == K_CONST) return TT_ENUM;
+ if (lsget(1, n)) return TT_PARAM;
+ return TT_VAR;
+ }
+ if (lsget(2, n)) return TT_TYPE;
+ if (lsget(1, n)) return TT_PARAM;
+ if (lsget(0, n)) return TT_VAR;
+ if (lsget(3, n)) return TT_PROP;
+ return TT_VAR;
+}
+// writes the semantic tokens of document gd: every token in order, each with the
+// class the compiler resolved it to (rs_class), and numbers, strings and operators
+// by their kind. comments are left to the editor's own grammar.
+int gen_tokens() {
+ int last = 0, ty, pt = 0, pv = 0, o, e, c;
+ ws("{\"jsonrpc\":\"2.0\",\"id\":"); wid(); ws(",\"result\":{\"data\":[");
+ pc_reset(gd);
+ tfirst = 1; tprevl = 0; tprevc = 0; rs_cur = 0;
+ hl_begin(doff[gd]);
+ while (tk != T_EOF) {
+ o = toff; e = toff + toklen;
+ if (toklen > 0 && o >= last) {
+ gap_out(last, o);
+ ty = 0 - 1;
+ if (tk == T_ID) ty = rs_class(o, tv, pt, pv);
+ else if (tk == T_NUM) {
+ c = dc(pbase, o);
+ if (c == 39) ty = TT_STR; else ty = TT_NUM;
+ } else if (tk == T_FLT) ty = TT_NUM;
+ else if (tk == T_STR || tk == T_BSTR) ty = TT_STR;
+ else if (tk == T_OP) { if (isopc(dc(pbase, o))) ty = TT_OP; }
+ if (ty >= 0) range_out(o, e, ty);
+ last = e;
+ }
+ pt = tk; pv = tv;
+ next();
+ }
+ gap_out(last, plen);
+ hl_end();
+ ws("]}}");
+ return 0;
+}
+
+// ---- json in ----
+// the body of a message is read straight off stdin a char at a time and
+// never held whole: the fields the server cares about are picked out as they
+// go by, and a document's text is decoded into the store as it arrives.
+int jleft, jpk;
+int hline, hchar, hn, hoff, hcursor; word hov; int hovcap = 2300; word htb; int htbc = 80, htn, ann_name_at; word hname; int hnamecap = 80; int hdl, hdle, hcs;
+word ann_text; int ann_n;
+word mbuf, idbuf, jk, kbuf; int mcap = 64, idcap = 64, jkcap = 16, kcap = 40; int mn, idn, idtype; word ubuf; int ucap = URIW; int un, jver, kn;
+word tbase; int tn, thave, tover, tuseen;
+// the next byte of the json body. the body is read from stdin and stops after
+// jleft bytes; -1 means the end. junget returns a byte to the next jgetc.
+int jgetc() {
+ int c;
+ if (jpk != 0 - 2) { c = jpk; jpk = 0 - 2; return c; }
+ if (jleft <= 0) return 0 - 1;
+ jleft--;
+ if (eof()) { jleft = 0; return 0 - 1; }
+ return get_c() & 255;
+}
+// return byte c, so that the next jgetc reads it again.
+int junget(int c) { jpk = c; return 0; }
+// the next byte of the json body that is not white space.
+int jws() {
+ int c = jgetc();
+ while (c == 32 || c == 9 || c == 10 || c == 13) c = jgetc();
+ return c;
+}
+// where the decoded bytes of a string go
+int jput(int sink, int b) {
+ word at;
+ if (sink == 1) {
+ if (mn + 1 >= mcap) { mbuf = lsp_grow(mbuf, mn, mcap * 2); mcap = mcap * 2; }
+ mbuf[mn] = b; mn++;
+ }
+ else if (sink == 2) {
+ if (idn + 1 >= idcap) { idbuf = lsp_grow(idbuf, idn, idcap * 2); idcap = idcap * 2; }
+ idbuf[idn] = b; idn++;
+ }
+ else if (sink == 3) {
+ if (un + 1 >= ucap) { ubuf = lsp_grow(ubuf, un, ucap * 2); ucap = ucap * 2; }
+ ubuf[un] = b; un++;
+ }
+ else if (sink == 4) {
+ if ((tn >> 2) + 2 >= tcap) { tover = 1; return 0; }
+ at = tbase + (tn >> 2);
+ if ((tn & 3) == 0) at[0] = 0;
+ at[0] = at[0] | (b << ((tn & 3) * 8));
+ tn++;
+ } else if (sink == 5) {
+ if (kn + 1 >= kcap) { kbuf = lsp_grow(kbuf, kn, kcap * 2); kcap = kcap * 2; }
+ kbuf[kn] = b; kn++;
+ }
+ return 0;
+}
+// writes the code point cp as utf-8 into sink (one byte at a time through jput).
+int jutf8(int sink, int cp) {
+ if (cp < 128) { jput(sink, cp); return 0; }
+ if (cp < 2048) { jput(sink, 192 | (cp >> 6)); jput(sink, 128 | (cp & 63)); return 0; }
+ if (cp < 65536) {
+ jput(sink, 224 | (cp >> 12));
+ jput(sink, 128 | ((cp >> 6) & 63));
+ jput(sink, 128 | (cp & 63));
+ return 0;
+ }
+ jput(sink, 240 | (cp >> 18));
+ jput(sink, 128 | ((cp >> 12) & 63));
+ jput(sink, 128 | ((cp >> 6) & 63));
+ jput(sink, 128 | (cp & 63));
+ return 0;
+}
+// reads the four hex digits of a \u escape and returns their value. when a byte
+// is not a hex digit, the digits read so far are returned and that byte goes back.
+int jhex4() {
+ int v = 0, i = 0, c, h;
+ while (i < 4) {
+ c = jgetc(); h = hexv(c);
+ if (h < 0) { junget(c); return v; }
+ v = v * 16 + h; i++;
+ }
+ return v;
+}
+// the rest of a string, the opening quote already read
+int jstr(int sink) {
+ int c, e, cp, lo;
+ while (1) {
+ c = jgetc();
+ if (c < 0 || c == 34) return 0;
+ if (c != 92) { jput(sink, c); continue; }
+ e = jgetc();
+ if (e == 'n') jput(sink, 10);
+ else if (e == 't') jput(sink, 9);
+ else if (e == 'r') jput(sink, 13);
+ else if (e == 'b') jput(sink, 8);
+ else if (e == 'f') jput(sink, 12);
+ else if (e == 'u') {
+ cp = jhex4();
+ if (cp >= 55296 && cp < 56320) { // a high surrogate: the low half follows as another \u
+ c = jgetc();
+ if (c == 92) {
+ c = jgetc();
+ if (c == 'u') { lo = jhex4(); cp = 65536 + ((cp - 55296) << 10) + (lo - 56320); }
+ }
+ }
+ jutf8(sink, cp);
+ } else if (e >= 0) jput(sink, e);
+ }
+ return 0;
+}
+// the code of the json key in kbuf, for the keys the server reads (0 for the others).
+int kcode() {
+ if (eq_w(kbuf, "method")) return 1;
+ if (eq_w(kbuf, "id")) return 2;
+ if (eq_w(kbuf, "params")) return 3;
+ if (eq_w(kbuf, "textDocument")) return 4;
+ if (eq_w(kbuf, "uri")) return 5;
+ if (eq_w(kbuf, "text")) return 6;
+ if (eq_w(kbuf, "contentChanges")) return 7;
+ if (eq_w(kbuf, "version")) return 8;
+ if (eq_w(kbuf, "position")) return 9;
+ if (eq_w(kbuf, "line")) return 10;
+ if (eq_w(kbuf, "character")) return 11;
+ return 0;
+}
+// is the key code one of the first dep keys on the json path to the value being read.
+int janc(int dep, int code) {
+ int i = 0;
+ while (i < dep) {
+ if (jk[i] == code) return 1;
+ i++;
+ }
+ return 0;
+}
+// reads one json value at nesting depth dep. the keys the server reads (key) go to
+// their fields, and objects and arrays are walked for the ones inside them.
+int jval(int key, int dep) {
+ int c; word k; int sink, neg;
+ if (dep >= jkcap) { jk = lsp_grow(jk, jkcap, jkcap * 2); jkcap = jkcap * 2; }
+ jk[dep] = key;
+ c = jws();
+ if (c == '{') {
+ while (1) {
+ c = jws();
+ if (c < 0 || c == '}') return 0;
+ if (c == ',') continue;
+ if (c != 34) continue;
+ kn = 0; jstr(5); kbuf[kn] = 0;
+ k = kcode();
+ c = jws(); // the colon
+ jval(k, dep + 1);
+ }
+ }
+ if (c == '[') {
+ while (1) {
+ c = jws();
+ if (c < 0 || c == ']') return 0;
+ // a '}' inside an array is malformed: stop here and leave it for the enclosing object,
+ // since a bare value stops at '}' without taking it, and looping would never end
+ if (c == '}') { junget(c); return 0; }
+ if (c == ',') continue;
+ junget(c);
+ jval(key, dep + 1);
+ }
+ }
+ if (c == 34) {
+ sink = 0;
+ if (key == 1 && dep == 1) { sink = 1; mn = 0; }
+ else if (key == 2 && dep == 1) { sink = 2; idn = 0; idtype = 2; }
+ else if (key == 5 && janc(dep, 4)) { sink = 3; un = 0; tuseen = 1; }
+ else if (key == 6 && (janc(dep, 7) || janc(dep, 4))) { sink = 4; thave = 1; tover = 0; tbase = text_start(); tn = 0; }
+ jstr(sink);
+ if (sink == 1) mbuf[mn] = 0;
+ if (sink == 2) idbuf[idn] = 0;
+ if (sink == 3) ubuf[un] = 0;
+ if (sink == 4) {
+ k = tbase + (tn >> 2); // zero the rest of the last word and the one after it
+ if ((tn & 3) == 0) k[0] = 0;
+ k[1] = 0;
+ }
+ return 0;
+ }
+ if (c < 0) return 0;
+ // a number, true, false or null
+ sink = 0;
+ if (key == 2 && dep == 1) { sink = 2; idn = 0; idtype = 1; }
+ k = 0; neg = 0;
+ while (c >= 0 && c != ',' && c != '}' && c != ']' && c != 32 && c != 10 && c != 13 && c != 9) {
+ if (sink == 2) jput(2, c);
+ if ((key == 8 && janc(dep, 4)) || ((key == 10 || key == 11) && janc(dep, 9))) {
+ if (c == '-') {
+ neg = 1;
+ } else if (isdig(c)) k = k * 10 + c - '0';
+ }
+ c = jgetc();
+ }
+ junget(c);
+ if (sink == 2) {
+ idbuf[idn] = 0;
+ if (eq_w(idbuf, "null")) idtype = 0;
+ }
+ if (key == 8 && janc(dep, 4)) { if (neg) k = 0 - k; jver = k; }
+ if (key == 10 && janc(dep, 9)) hline = k;
+ if (key == 11 && janc(dep, 9)) hchar = k;
+ return 0;
+}
+// the text of a document being read goes into a block that holds the whole
+// message: decoded text is never longer than the message it came in
+word text_start() {
+ tcap = dwords(jsize);
+ tbase = new(tcap);
+ return tbase;
+}
+
+// ---- messages in ----
+word lbuf; int lcap = 160; int rfirst = 0 - 1;
+// one header line, without the line end. returns its length, or -1 at the end of the input
+int rl() {
+ int n = 0, c;
+ while (1) {
+ if (rfirst >= 0) { c = rfirst; rfirst = 0 - 1; }
+ else {
+ if (eof()) { if (!n) return 0 - 1; return n; }
+ c = get_c() & 255;
+ }
+ if (c == 10) return n;
+ if (c != 13) {
+ if (n + 1 >= lcap) { lbuf = lsp_grow(lbuf, n, lcap * 2); lcap = lcap * 2; }
+ lbuf[n] = c; n++;
+ }
+ }
+ return n;
+}
+// the lower case of an ascii letter. other bytes are unchanged.
+int lower(int c) {
+ if (c >= 'A' && c <= 'Z') return c + 32;
+ return c;
+}
+// does the header line in lbuf start with content-length: (in any case).
+int is_clen(int n) {
+ if (n < 15) return 0;
+ word s = "content-length:"; int i = 0;
+ while (s[i]) { if (lower(lbuf[i]) != s[i]) return 0; i++; }
+ return 1;
+}
+int inited, shut, quit;
+// read one message into the field buffers. returns 0 at the end of the input
+int read_message() {
+ int n, size = 0 - 1, i;
+ while (1) {
+ n = rl();
+ if (n < 0) return 0;
+ if (n == 0) { if (size >= 0) break; else continue; }
+ if (is_clen(n)) {
+ i = 15; size = 0;
+ while (i < n && (lbuf[i] == 32 || lbuf[i] == 9)) i++;
+ while (i < n && isdig(lbuf[i])) { size = size * 10 + lbuf[i] - '0'; i++; }
+ }
+ }
+ jleft = size; jpk = 0 - 2; jsize = size;
+ mn = 0; mbuf[0] = 0; idn = 0; idbuf[0] = 0; idtype = 0; un = 0; ubuf[0] = 0; jver = 0;
+ thave = 0; tover = 0; tuseen = 0; tn = 0; tbase = 0; hline = 0; hchar = 0;
+ jval(0, 0);
+ while (jleft > 0) jgetc(); // whatever the parser left behind
+ return 1;
+}
+
+// ---- messages out ----
+int wid() {
+ if (idtype == 2) {
+ wjs(idbuf, idn);
+ } else if (idtype == 1) ws(idbuf); else ws("null");
+ return 0;
+}
+// the reply to initialize: the server's capabilities, which are text sync, hover,
+// and semantic tokens with their legend.
+int gen_init() {
+ ws("{\"jsonrpc\":\"2.0\",\"id\":"); wid();
+ ws(",\"result\":{\"capabilities\":{\"textDocumentSync\":{\"openClose\":true,\"change\":1,\"save\":{\"includeText\":true}},");
+ ws("\"hoverProvider\":true,\"semanticTokensProvider\":{\"legend\":{\"tokenTypes\":[\"keyword\",\"type\",\"function\",\"variable\",\"string\",\"number\",\"operator\",\"comment\",\"parameter\",\"property\",\"macro\",\"enumMember\"],\"tokenModifiers\":[]},\"full\":true}},");
+ ws("\"serverInfo\":{\"name\":\"zc\",\"version\":\"2\"}}}");
+ return 0;
+}
+// a reply with a null result, for the request id in the message.
+int gen_null() {
+ ws("{\"jsonrpc\":\"2.0\",\"id\":");
+ wid();
+ ws(",\"result\":null}");
+ return 0;
+}
+// the error code the next error reply carries.
+int errcode;
+// an error reply with the code errcode and a matching message.
+int gen_error() {
+ ws("{\"jsonrpc\":\"2.0\",\"id\":");
+ wid();
+ ws(",\"error\":{\"code\":");
+ wn(errcode);
+ ws(",\"message\":");
+ if (errcode == 0 - 32601) ws("\"method not found\""); else ws("\"server not initialized\"");
+ ws("}}");
+ return 0;
+}
+// a semantic tokens reply with no tokens, for a document the server does not have.
+int gen_empty() {
+ ws("{\"jsonrpc\":\"2.0\",\"id\":");
+ wid();
+ ws(",\"result\":{\"data\":[]}}");
+ return 0;
+}
+// sends an error reply with the code given.
+int reply_error(int code) {
+ errcode = code;
+ send(gen_error);
+ return 0;
+}
+
+// ---- hover ----
+// the documentation of the name under the cursor: the comment lines directly
+// above its declaration in the document, or, for what the compiler and the
+// standard library define, the docs stored in the image
+int hid(int c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; }
+// the identifier at hline, hchar of document i, into hname. returns its length
+int hover_word(int i) {
+ int base = doff[i], size = dsize[i], o = 0, ln = 0, col = 0, c, step, width, b, e, n = 0;
+ hcursor = 0 - 1; hname[0] = 0;
+ if (hline < 0 || hchar < 0) return 0;
+ while (ln < hline && o < size) { if (dc(base, o) == 10) ln++; o++; }
+ if (ln != hline) return 0;
+ // the protocol counts utf-16 units; the document stores utf-8 bytes.
+ while (col < hchar && o < size) {
+ c = dc(base, o);
+ if (c == 10 || c == 13) return 0;
+ step = 1; width = 1;
+ if (c >= 192 && c < 224) step = 2;
+ else if (c >= 224 && c < 240) step = 3;
+ else if (c >= 240) { step = 4; width = 2; }
+ o = o + step; col = col + width;
+ }
+ if (col != hchar) return 0;
+ hcursor = o;
+ if (o >= size || !hid(dc(base, o))) return 0;
+ b = o;
+ while (b > 0 && hid(dc(base, b - 1))) b--;
+ e = o;
+ while (e < size && hid(dc(base, e))) e++;
+ if (e - b + 1 > hnamecap) { hnamecap = e - b + 1; hname = lsp_grow(0, 0, hnamecap); }
+ hoff = b;
+ while (b < e) { hname[n] = dc(base, b); n++; b++; }
+ hname[n] = 0;
+ return n;
+}
+// is c a delimiter that ends the name in a top-level declaration
+int hdelim(int c) { return c == '(' || c == '=' || c == ';' || c == ',' || c == '[' || c == '<' || c == '{'; }
+// does the line at ls (ending before le) declare hname. only a top-level line
+// counts (hover_scan has already checked it starts in column 0). the name is
+// the word just before the line's first delimiter, so a keyword or type on the
+// line (int, fixed, fn) is never the declared name, and a use of the name
+// further along the line is not a declaration
+word hover_decl(int base, int ls, int le) {
+ int dl = ls, e, b, k = 0;
+ while (dl < le && !hdelim(dc(base, dl))) dl++;
+ if (dl >= le) return 0;
+ e = dl;
+ while (e > ls && (dc(base, e - 1) == 32 || dc(base, e - 1) == 9)) e--;
+ b = e;
+ while (b > ls && hid(dc(base, b - 1))) b--;
+ if (b == e) return 0;
+ while (hname[k] && b + k < e && dc(base, b + k) == hname[k]) k++;
+ return !hname[k] && b + k == e;
+}
+// the top-level declaration of hname, found by its line: hdl and hdle are the
+// line's start and end, and hcs is the start of the comment block directly above
+// it (hcs is -1 when none) which runs to the line at hdl. a declaration needs
+// no comment, so the signature still shows without one.
+int hover_scan(int i) {
+ int base = doff[i], size = dsize[i], ls = 0, le, cs = 0 - 1, c0;
+ while (ls < size) {
+ le = ls;
+ while (le < size && dc(base, le) != 10) le++;
+ c0 = dc(base, ls);
+ if (c0 == '/' && ls + 1 < le && dc(base, ls + 1) == '/') {
+ if (cs < 0) cs = ls;
+ } else {
+ if (le > ls && c0 != 32 && c0 != 9 && hover_decl(base, ls, le)) {
+ hdl = ls; hdle = le; hcs = cs;
+ return 1;
+ }
+ cs = 0 - 1;
+ }
+ ls = le + 1;
+ }
+ return 0;
+}
+// one character of the hover text, up to its cap
+int hov_put(int c) {
+ if (hn == hovcap) { hov = lsp_grow(hov, hn, hovcap * 2); hovcap = hovcap * 2; }
+ hov[hn] = c; hn++;
+ return 0;
+}
+// comment ranges are scanned once per document version, outside strings and chars.
+int hc_i = -1, hc_gen = -1, hc_n, hc_cap = 64;
+word hc_start, hc_end, hc_kind;
+int hover_comments(int i) {
+ int o = 0, size = dsize[i], base = doff[i], c, start, kind, bol = 1, quote;
+ if (hc_i == i && hc_gen == dgen[i]) return 0;
+ hc_i = i; hc_gen = dgen[i]; hc_n = 0;
+ while (o < size) {
+ c = dc(base, o);
+ if (c == 10) { bol = 1; o++; continue; }
+ if (c == 32 || c == 9 || c == 13) { o++; continue; }
+ if (c == 34 || c == 39) {
+ quote = c; o++;
+ while (o < size && dc(base, o) != quote) { if (dc(base, o) == 92) o++; o++; }
+ o++; bol = 0; continue;
+ }
+ kind = 0; start = o;
+ if (c == '/' && dc(base, o + 1) == '/') {
+ kind = 1; if (bol) kind = 2;
+ o = o + 2; while (o < size && dc(base, o) != 10) o++;
+ } else if (c == '/' && dc(base, o + 1) == '*') {
+ kind = 3; o = o + 2;
+ while (o < size && !(dc(base, o) == '*' && dc(base, o + 1) == '/')) { if (dc(base, o) == 10) bol = 1; o++; }
+ if (o < size) o = o + 2;
+ } else { o++; bol = 0; continue; }
+ if (!hc_start || hc_n == hc_cap) {
+ if (hc_start) hc_cap = hc_cap * 2;
+ hc_start = lsp_grow(hc_start, hc_n, hc_cap); hc_end = lsp_grow(hc_end, hc_n, hc_cap); hc_kind = lsp_grow(hc_kind, hc_n, hc_cap);
+ }
+ hc_start[hc_n] = start; hc_end[hc_n] = o; hc_kind[hc_n] = kind; hc_n++;
+ }
+ return 0;
+}
+// whitespace between documentation and its target may cross one line, not a blank line.
+int doc_gap(int base, int from, int to) {
+ int c, lines = 0;
+ while (from < to) {
+ c = dc(base, from); from++;
+ if (c == 10) { lines++; if (lines > 1) return 0; }
+ else if (c != 32 && c != 9 && c != 13) return 0;
+ }
+ return 1;
+}
+// remove comment delimiters and optional line-leading stars; retain markdown.
+int hover_comment_text(int i, int index) {
+ int base = doff[i], o = hc_start[index] + 2, end = hc_end[index], bol = 1, c, before = hn;
+ if (hc_kind[index] == 3) {
+ if (end >= o + 2 && dc(base, end - 2) == '*' && dc(base, end - 1) == '/') end = end - 2;
+ if (o < end && dc(base, o) == '*') o++;
+ }
+ while (o < end && (dc(base, o) == 32 || dc(base, o) == 9 || dc(base, o) == 10 || dc(base, o) == 13)) o++;
+ while (end > o && (dc(base, end - 1) == 32 || dc(base, end - 1) == 9 || dc(base, end - 1) == 10 || dc(base, end - 1) == 13)) end--;
+ if (o == end) return 0;
+ hov_text("\n\n");
+ while (o < end) {
+ if (bol && hc_kind[index] == 3) {
+ while (o < end && (dc(base, o) == 32 || dc(base, o) == 9)) o++;
+ if (o < end && dc(base, o) == '*') { o++; if (dc(base, o) == 32) o++; }
+ bol = 0;
+ }
+ if (o >= end) break;
+ c = dc(base, o); o++;
+ if (c != 13) hov_put(c);
+ if (c == 10) bol = 1;
+ }
+ return hn - before;
+}
+// attach leading docs and adjacent block docs to this exact declaration name.
+int hover_docs(int i, int head, int name, int length) {
+ int k, first = -1, last = -1, pos = head, base = doff[i];
+ hover_comments(i);
+ k = hc_n - 1;
+ while (k >= 0 && hc_end[k] > head) k--;
+ while (k >= 0 && hc_kind[k] != 1 && doc_gap(base, hc_end[k], pos)) {
+ if (last < 0) last = k;
+ first = k; pos = hc_start[k]; k--;
+ }
+ if (first >= 0) { k = first; while (k <= last) { hover_comment_text(i, k); k++; } }
+ k = 0;
+ while (k < hc_n) {
+ if (hc_kind[k] == 3 && (k < first || k > last)) {
+ if (hc_start[k] >= head && hc_end[k] <= name && doc_gap(base, hc_end[k], name)) hover_comment_text(i, k);
+ else if (hc_start[k] >= name + length && doc_gap(base, name + length, hc_start[k])) hover_comment_text(i, k);
+ }
+ k++;
+ }
+ return 0;
+}
+// the markdown for a declaration: its signature as a code block, then the
+// comment block above it, without the slashes, if there is one
+int hover_fill(int i) {
+ int base = doff[i], o = hdl, c, cs, ls, typed = 0, k, start, end;
+ hn = 0;
+ while (o < hdle && dc(base, o) != '{' && dc(base, o) != ';' && dc(base, o) != '=') o++;
+ while (o > hdl && (dc(base, o - 1) == 32 || dc(base, o - 1) == 9)) o--;
+ hov_put('`'); hov_put('`'); hov_put('`'); hov_put('z'); hov_put('c'); hov_put(10);
+ c = hdl;
+ // an untyped fn: the return type the body gives it replaces the fn
+ if (dc(base, hdl) == 'f' && dc(base, hdl + 1) == 'n' && dc(base, hdl + 2) == 32) {
+ ensure_annot(i);
+ k = hdl + 3;
+ while (k < hdle && (dc(base, k) == 32 || dc(base, k) == 9)) k++;
+ if (ann_type(i, k)) {
+ start = ann_name_at;
+ while (start > 0 && dc(ann_text, start - 1) != 10 && dc(ann_text, start - 1) != ';' && dc(ann_text, start - 1) != '}') start--;
+ while (start < ann_name_at && (dc(ann_text, start) == 32 || dc(ann_text, start) == 9)) start++;
+ end = ann_name_at;
+ while (end < ann_n && dc(ann_text, end) != '{' && dc(ann_text, end) != ';') end++;
+ while (end > start && (dc(ann_text, end - 1) == 32 || dc(ann_text, end - 1) == 9 || dc(ann_text, end - 1) == 10 || dc(ann_text, end - 1) == 13)) end--;
+ k = start; while (k < end) { hov_put(dc(ann_text, k)); k++; }
+ hov_text("\n```"); htn = 0; hover_docs(i, hdl, hdl, 0);
+ return hn;
+ }
+ htn = 0;
+ }
+ while (c < o) { hov_put(dc(base, c)); c++; }
+ hov_put(10); hov_put('`'); hov_put('`'); hov_put('`');
+ hover_docs(i, hdl, hdl, 0);
+ return hn;
+}
+// the hover reply: the markdown in hov, or null when there is nothing to show.
+int gen_hover() {
+ ws("{\"jsonrpc\":\"2.0\",\"id\":"); wid();
+ if (!hn) { ws(",\"result\":null}"); return 0; }
+ ws(",\"result\":{\"contents\":{\"kind\":\"markdown\",\"value\":");
+ wjs(hov, hn);
+ ws("}}}");
+ return 0;
+}
+// the doc of a name from the image's own symbol blob, or 0. the checks reset the
+// symbol table (zc_init, chk_mark), so the docs are read from the blob itself,
+// which stays in the image
+word blobdoc(word nm) {
+ word p = GORG[5]; int k;
+ while (*p) {
+ if (p[0] == K_DOC && p[3] == 'd' && p[4] == 'o' && p[5] == 'c' && p[6] == ' ') {
+ k = 0;
+ while (nm[k] && p[7 + k] == nm[k]) k++;
+ if (!nm[k] && !p[7 + k]) return p[1];
+ }
+ p = p + 3 + len_w(p + 3) + 1;
+ }
+ return 0;
+}
+// the signature of a name from the image's own symbol blob, or 0
+word blobsig(word nm) {
+ word p = GORG[5]; int k;
+ while (*p) {
+ if (p[0] == K_DOC && p[3] == 's' && p[4] == 'i' && p[5] == 'g' && p[6] == ' ') {
+ k = 0;
+ while (nm[k] && p[7 + k] == nm[k]) k++;
+ if (!nm[k] && !p[7 + k]) return p[1];
+ }
+ p = p + 3 + len_w(p + 3) + 1;
+ }
+ return 0;
+}
+// the start of the line before the one starting at ls, or -1 at the top
+int hl_prev(int base, int ls) {
+ int q;
+ if (ls <= 0) return 0 - 1;
+ q = ls - 1;
+ while (q > 0 && dc(base, q - 1) != 10) q--;
+ return q;
+}
+// the last declaration of hname above the hovered line, inside the top-level
+// item that holds it: a parameter of its header, or a local (a line of the form
+// type name = ..., so a plain use of the name does not count). the text, marked
+// as a parameter or a local, goes into hov as a code block. returns 1 when found.
+int hover_local(int i) {
+ int base = doff[i], size = dsize[i], hs = 0, ln = 0, o = 0, ts = 0 - 1, q, c, hl, le, e, b, k, a, found = 0, kind = 0, sb = 0, se = 0, da = 0 - 1;
+ while (ln < hline && o < size) { if (dc(base, o) == 10) ln++; o++; }
+ hs = o;
+ // the top-level line: the hovered line if it starts in column 0, else the
+ // nearest such line above it (a line that starts in column 0, not a comment)
+ q = hs;
+ c = dc(base, q);
+ if (c != 10 && c != 32 && c != 9 && !(c == '/' && dc(base, q + 1) == '/')) ts = q;
+ while (ts < 0) {
+ q = hl_prev(base, q);
+ if (q < 0) break;
+ c = dc(base, q);
+ if (c == 10 || c == 32 || c == 9) continue;
+ if (c == '/' && dc(base, q + 1) == '/') continue;
+ ts = q;
+ }
+ if (ts < 0) return 0;
+ // the header line: a parameter named hname is the last word of its segment
+ hl = ts;
+ le = hl;
+ while (le < size && dc(base, le) != 10 && dc(base, le) != '{' && dc(base, le) != ';') le++;
+ o = hl;
+ // the segments of the header, one at a time
+ o = hl;
+ while (o < le && dc(base, o) != '(') o++;
+ o++;
+ while (o < le) {
+ while (o < le && (dc(base, o) == 32 || dc(base, o) == 9)) o++;
+ sb = o;
+ while (o < le && dc(base, o) != ',' && dc(base, o) != ')') o++;
+ se = o;
+ while (se > sb && (dc(base, se - 1) == 32 || dc(base, se - 1) == 9)) se--;
+ e = se;
+ b = e;
+ while (b > sb && hid(dc(base, b - 1))) b--;
+ if (b < e) { // a typed parameter has its type before the name, an untyped one is the name alone
+ k = 0;
+ while (hname[k] && b + k < e && dc(base, b + k) == hname[k]) k++;
+ if (!hname[k] && b + k == e) { found = 1; kind = 1; break; }
+ }
+ if (o < le && dc(base, o) == ')') break;
+ o++;
+ }
+ if (found) { hover_put_sig(base, sb, se, kind, 0 - 1); return 1; }
+ // the body: the last declaration of hname from the item's next line through the
+ // hovered line (a declaration on the hovered line itself counts)
+ o = ts;
+ while (o <= hs) {
+ le = o;
+ while (le < hs && dc(base, le) != 10) le++;
+ if (le == hs) while (le < size && dc(base, le) != 10) le++;
+ // le is the end of this line; skip the blank and comment lines
+ q = o;
+ while (q < le && (dc(base, q) == 32 || dc(base, q) == 9)) q++;
+ if (q < le && !(dc(base, q) == '/' && dc(base, q + 1) == '/') && q > o && !hl_kw(base, q)) {
+ e = hdeclend(base, q, le);
+ if (e < le) {
+ b = e;
+ while (b > q && (dc(base, b - 1) == 32 || dc(base, b - 1) == 9)) b--;
+ a = b;
+ while (a > q && hid(dc(base, a - 1))) a--;
+ if (a < b && a > q) {
+ k = 0;
+ while (hname[k] && a + k < b && dc(base, a + k) == hname[k]) k++;
+ if (!hname[k] && a + k == b && (dc(base, a - 1) == 32 || dc(base, a - 1) == 9)) { found = 1; kind = 2; da = a; sb = q; se = e; while (se > sb && (dc(base, se - 1) == 32 || dc(base, se - 1) == 9)) se--; }
+ }
+ }
+ }
+ if (le >= hs) break;
+ o = le + 1;
+ }
+ if (found) {
+ k = 0 - 1;
+ if (kind == 2) k = lt_find(da);
+ hover_put_sig(base, sb, se, kind, k);
+ return 1;
+ }
+ return 0;
+}
+// does the word from q to e equal w
+word hl_is(int base, int q, int e, word w) {
+ int k = 0;
+ while (w[k] && q + k < e && dc(base, q + k) == w[k]) k++;
+ return !w[k] && q + k == e;
+}
+// does a statement start at q with a keyword that cannot begin a declaration,
+// so that "return i;" is not taken for a declaration of i
+word hl_kw(int base, int q) {
+ int e = q;
+ while (hid(dc(base, e))) e++;
+ return hl_is(base, q, e, "return") || hl_is(base, q, e, "if") || hl_is(base, q, e, "while") || hl_is(base, q, e, "else") || hl_is(base, q, e, "break") || hl_is(base, q, e, "continue");
+}
+// where a declaration's name ends on the line from q: at = ; , { ( or a [ that
+// follows a name (let b[24]); the [ of a type (fixed []int) is passed over
+int hdeclend(int base, int q, int le) {
+ int e = q, c;
+ while (e < le) {
+ c = dc(base, e);
+ if (c == '=' || c == ';' || c == ',' || c == '{' || c == '(' || c == ')') return e;
+ if (c == '[' && e > q && hid(dc(base, e - 1))) return e;
+ e++;
+ }
+ return le;
+}
+const int AROUNDS = 6;
+word abase, aerr; int nbase, aerrcap;
+word afx, aft, amask; int anf, afcap, atcap;
+word awp; int awn;
+word ann_half;
+// the inferred types of the untyped declarations. annotate_doc runs the annotate
+// rounds on a document (in doc slot 0, which is put back afterwards), and the
+// text it ends with is kept in ann_text. ann_type then finds, for a name the
+// document declares untyped, the type annotate gave that name: the text between
+// the token before it in the annotated text and the name, after the last ( , { ; or =.
+int ann_i = -1, ann_gen = -1, ai_end;
+// runs the annotate rounds on document i. the rounds run in doc slot 0, and the
+// slots they use are put back afterwards. the text they end with is kept in
+// ann_text, with its length in ann_n.
+int annotate_doc(int i) {
+ int round = 0, n, k, sv_size, sv_live, sv_ulen, sv_gen, sv_slot1 = 0, sv_live1, sv_ulen1, sv_gen1;
+ word sv_base, sv_base1;
+ sv_base = doff[0]; sv_size = dsize[0]; sv_live = dlive[0]; sv_ulen = dulen[0]; sv_gen = dgen[0];
+ sv_base1 = doff[1]; sv_slot1 = dsize[1]; sv_live1 = dlive[1]; sv_ulen1 = dulen[1]; sv_gen1 = dgen[1];
+ dlive[0] = 1; dulen[0] = 0;
+ doff[0] = doff[i]; dsize[0] = dsize[i]; dgen[0] = sv_gen + 1;
+ ifstrict = 1;
+ check_doc(0);
+ nbase = err_lines(abase);
+ while (round < AROUNDS) {
+ ifon = 1; check_doc(0); ifon = 0;
+ save_facts();
+ if (!anf) break;
+ k = 0; while (k < anf) { amask[k] = 0; k++; }
+ fresh_half();
+ try_range(0, anf);
+ n = apply_masked(ann_half);
+ if (!n) break;
+ doff[0] = ann_half; dsize[0] = awn; dgen[0] = dgen[0] + 1;
+ round++;
+ }
+ ifstrict = 0; ifon = 0;
+ ann_text = doff[0]; ann_n = dsize[0];
+ doff[0] = sv_base; dsize[0] = sv_size; dlive[0] = sv_live; dulen[0] = sv_ulen; dgen[0] = sv_gen;
+ doff[1] = sv_base1; dsize[1] = sv_slot1; dlive[1] = sv_live1; dulen[1] = sv_ulen1; dgen[1] = sv_gen1;
+ chk_i = -1;
+ return 0;
+}
+// annotate for the current state of document i, once per state of it
+int ensure_annot(int i) {
+ if (ann_i == i && ann_gen == dgen[i]) return 0;
+ annotate_doc(i);
+ ann_i = i; ann_gen = dgen[i];
+ return 0;
+}
+// the next identifier of base at or after pos: its start, with its end in ai_end.
+// comments, strings and numbers are passed over. -1 when there is none.
+int ai_next(word base, int size, int pos) {
+ int c, st, q;
+ while (pos < size) {
+ c = dc(base, pos);
+ if (c == '/' && pos + 1 < size && dc(base, pos + 1) == '/') {
+ while (pos < size && dc(base, pos) != 10) pos++;
+ } else if (c == '/' && pos + 1 < size && dc(base, pos + 1) == '*') {
+ pos = pos + 2;
+ while (pos < size && !(dc(base, pos) == '*' && dc(base, pos + 1) == '/')) pos++;
+ if (pos < size) pos = pos + 2;
+ } else if (c == '"' || c == 39) {
+ q = c; pos++;
+ while (pos < size && dc(base, pos) != q && dc(base, pos) != 10) { if (dc(base, pos) == 92) pos++; pos++; }
+ pos++;
+ } else if (c >= '0' && c <= '9') {
+ while (pos < size && hid(dc(base, pos))) pos++;
+ } else if (hid(c)) {
+ st = pos;
+ while (pos < size && hid(dc(base, pos))) pos++;
+ ai_end = pos;
+ return st;
+ } else pos++;
+ }
+ return 0 - 1;
+}
+// do the words a[as..ae) of base a and b[bs..be) of base b read the same
+int ai_same(word a, int as, int ae, word b, int bs, int be) {
+ int k = 0;
+ if (ae - as != be - bs) return 0;
+ while (as + k < ae) { if (dc(a, as + k) != dc(b, bs + k)) return 0; k++; }
+ return 1;
+}
+// is the word at s..e fn or let, which annotate replaces with a type
+int ai_kw(word base, int s, int e) {
+ int k = 0;
+ if (e - s == 2 && dc(base, s) == 'f' && dc(base, s + 1) == 'n') return 1;
+ if (e - s == 3 && dc(base, s) == 'l' && dc(base, s + 1) == 'e' && dc(base, s + 2) == 't') return 1;
+ return k;
+}
+// is the name at offset o of document i declared untyped: a parameter after ( or
+// , and a local after let or a function after fn. only those can gain a type
+int ann_untyped(int i, int o) {
+ word base = doff[i];
+ int p = o - 1;
+ while (p >= 0) {
+ while (p >= 0 && (dc(base, p) == 32 || dc(base, p) == 9 || dc(base, p) == 10 || dc(base, p) == 13)) p--;
+ if (p >= 1 && dc(base, p) == '/' && dc(base, p - 1) == '*') {
+ p = p - 2; while (p >= 1 && !(dc(base, p - 1) == '/' && dc(base, p) == '*')) p--;
+ p = p - 2;
+ } else break;
+ }
+ if (p < 0) return 0;
+ if (dc(base, p) == '(' || dc(base, p) == ',') return 1;
+ if (p >= 2 && dc(base, p) == 't' && dc(base, p - 1) == 'e' && dc(base, p - 2) == 'l') return 1;
+ if (p >= 1 && dc(base, p) == 'n' && dc(base, p - 1) == 'f') return 1;
+ return 0;
+}
+// the type annotate gave the name declared at offset o of document i, in htb
+// (htn bytes), or none. the names of the two texts are walked in step: the
+// annotated text has the same names in the same order, plus the types it put in.
+int ann_type(int i, int o) {
+ word ob = doff[i], ab = ann_text;
+ int osz = dsize[i], asz = ann_n, op = 0, ap = 0, prev = 0, os, oe, as, ae, cut, k, n = 0, c;
+ htn = 0;
+ while (1) {
+ os = ai_next(ob, osz, op);
+ if (os < 0) return 0;
+ oe = ai_end;
+ op = oe;
+ if (ai_kw(ob, os, oe)) continue;
+ while (1) {
+ as = ai_next(ab, asz, ap);
+ if (as < 0) return 0;
+ ae = ai_end;
+ ap = ae;
+ if (ai_same(ob, os, oe, ab, as, ae)) break;
+ }
+ if (os == o) {
+ ann_name_at = as;
+ cut = prev;
+ k = prev;
+ while (k < as) { c = dc(ab, k); if (c == '(' || c == ',' || c == '{' || c == ';' || c == '=' || c == '}' || c == 10) cut = k + 1; k++; }
+ while (cut < as && (dc(ab, cut) == 32 || dc(ab, cut) == 9 || dc(ab, cut) == 10)) cut++;
+ k = cut;
+ while (k < as) {
+ if (dc(ab, k) == '/' && dc(ab, k + 1) == '*') {
+ k = k + 2; while (k < as && !(dc(ab, k) == '*' && dc(ab, k + 1) == '/')) k++;
+ k = k + 2; continue;
+ }
+ if (n == htbc) { htb = lsp_grow(htb, n, htbc * 2); htbc = htbc * 2; }
+ htb[n] = dc(ab, k); n++; k++;
+ }
+ while (n > 0 && (htb[n - 1] == 32 || htb[n - 1] == 9 || htb[n - 1] == 10)) n--;
+ htn = n;
+ if (htn == 2 && htb[0] == 'f' && htb[1] == 'n') htn = 0; // annotate left a fn alone
+ if (htn == 3 && htb[0] == 'l' && htb[1] == 'e' && htb[2] == 't') htn = 0;
+ return htn;
+ }
+ prev = ae;
+ }
+ return 0;
+}
+// the markdown of a local or parameter: its declaration text, then what it is
+int hover_put_sig(int base, int sb, int se, int kind, int lk) {
+ int k = sb;
+ hn = 0;
+ hov_put('`'); hov_put('`'); hov_put('`'); hov_put('z'); hov_put('c'); hov_put(10);
+ while (k < se) { hov_put(dc(base, k)); k++; }
+ if (kind == 1) {
+ hov_put(' '); hov_put(' '); hov_put('/'); hov_put('/'); hov_put(' '); k = 0; while ("parameter"[k]) { hov_put("parameter"[k]); k++; }
+ if (htn > 0) { k = 0; while ("; type "[k]) { hov_put("; type "[k]); k++; } k = 0; while (k < htn) { hov_put(htb[k]); k++; } }
+ }
+ else {
+ hov_put(' '); hov_put(' '); hov_put('/'); hov_put('/'); hov_put(' '); k = 0; while ("local"[k]) { hov_put("local"[k]); k++; }
+ if (lk >= 0) { k = 0; while ("; type "[k]) { hov_put("; type "[k]); k++; } k = lt_at[lk]; while (k < lt_at[lk] + lt_len[lk]) { hov_put(ltxt[k]); k++; } }
+ else if (htn > 0) { k = 0; while ("; type "[k]) { hov_put("; type "[k]); k++; } k = 0; while (k < htn) { hov_put(htb[k]); k++; } }
+ }
+ hov_put(10); hov_put('`'); hov_put('`'); hov_put('`');
+ return hn;
+}
+// a genome name: its signature as a code block, then its doc if the image has one
+int hover_blob(int i) {
+ int ss, ds, k = 0, n = 0;
+ ss = blobsig(hname);
+ ds = blobdoc(hname);
+ hn = 0;
+ if (ss) {
+ hov_put('`'); hov_put('`'); hov_put('`'); hov_put('z'); hov_put('c'); hov_put(10);
+ while (k < len_s(ss)) { hov_put(at_s(ss, k)); k++; }
+ hov_put(10); hov_put('`'); hov_put('`'); hov_put('`');
+ }
+ if (ds) {
+ if (ss) { hov_put(10); hov_put(10); }
+ k = 0;
+ while (k < len_s(ds)) { hov_put(at_s(ds, k)); k++; }
+ }
+ return hn;
+}
+// does the hovered word equal w
+word hname_is(word w) {
+ int k = 0;
+ while (w[k] && hname[k] == w[k]) k++;
+ return !w[k] && !hname[k];
+}
+// append a word string, or a compiler type, to the markdown buffer.
+int hov_text(word s) { int k = 0; while (s[k]) { hov_put(s[k]); k++; } return 0; }
+int hov_type(word t) {
+ int sm = outmode, sb = outbuf, sn = outn, sc = outcap, need;
+ outmode = 3; outn = 0; tprint(t); need = hn + outn;
+ if (need > hovcap) {
+ while (need > hovcap) hovcap = hovcap * 2;
+ hov = lsp_grow(hov, hn, hovcap);
+ }
+ outmode = 2; outbuf = hov; outn = hn; outcap = hovcap; tprint(t); hn = outn;
+ outmode = sm; outbuf = sb; outn = sn; outcap = sc;
+ return 0;
+}
+// explain composite types without relying on their source spelling.
+int hover_type_info(word t) {
+ int kind, k;
+ if (!t) return 0;
+ kind = t[0];
+ if (kind == TY_PTR) {
+ hov_text("\n\na pointer to `"); hov_type(t[4]); hov_text("`. the pointer occupies one word; the pointed-to storage is separate.");
+ } else if (kind == TY_ARR) {
+ hov_text("\n\nan array stored in place, with a fixed length of ");
+ k = t[3]; hov_number(k); hov_text(" elements of type `"); hov_type(t[4]); hov_text("`. storage: "); hov_number(tsz(t)); hov_text(" words.");
+ } else if (kind == TY_DYN) {
+ if (t[3]) hov_text("\n\nan array handle with a fixed runtime length. ");
+ else hov_text("\n\na growable array handle. ");
+ hov_text("element type: `"); hov_type(t[4]); hov_text("`. `len` gives the current length and `cap` gives capacity.");
+ if (t[3]) hov_text(" `push`, `pop`, and `resize` cannot change its length.");
+ } else if (kind == TY_STRUCT) {
+ hov_text("\n\na struct stored in place. storage: "); hov_number(tsz(t)); hov_text(" words.");
+ if (t[3] > 0) {
+ hov_text("\n\nfields:\n\n```zc\n");
+ k = 0;
+ while (k < t[3]) { hov_type(t[4][k * 3 + 1]); hov_put(' '); hov_text(t[4][k * 3]); hov_text(";\n"); k++; }
+ hov_text("```");
+ }
+ } else if (kind == TY_FN) hov_text("\n\na callable value. the type lists its result and parameter types.");
+ return 0;
+}
+// decimal numbers in markdown use the ordinary printer through the capture buffer.
+int hov_number(int n) {
+ int sm = outmode, sb = outbuf, sn = outn, sc = outcap;
+ while (hn + 24 > hovcap) { hov = lsp_grow(hov, hn, hovcap * 2); hovcap = hovcap * 2; }
+ outmode = 2; outbuf = hov; outn = hn; outcap = hovcap; put_n(n); hn = outn;
+ outmode = sm; outbuf = sb; outn = sn; outcap = sc;
+ return 0;
+}
+// a parsed type includes its brackets and stars, so those characters hover too.
+int hover_type_span(int i) {
+ int k = 0, best = 0 - 1, width = 0, end; word t;
+ while (k < ht_n) {
+ end = ht_end[k];
+ while (end > ht_start[k] && (dc(doff[i], end - 1) == 32 || dc(doff[i], end - 1) == 9 || dc(doff[i], end - 1) == 10 || dc(doff[i], end - 1) == 13)) end--;
+ if (hcursor >= ht_start[k] && hcursor < end && end - ht_start[k] > width) { best = k; width = end - ht_start[k]; }
+ k++;
+ }
+ if (best < 0) return 0;
+ t = ht_type[best];
+ hov_text("```zc\n"); hov_type(t); hov_text("\n```"); hover_type_info(t);
+ k = kwtext();
+ if (k && (hname_is("word") || hname_is("int") || hname_is("char") || hname_is("bool") || hname_is("void") || hname_is("float") || hname_is("f32") || hname_is("f64") || hname_is("byte") || hname_is("u8") || hname_is("i8") || hname_is("u16") || hname_is("i16") || hname_is("u32") || hname_is("i32"))) {
+ while (*k && *k != '~') k++;
+ if (*k) { hov_text("\n\n"); hov_text(k + 1); }
+ }
+ return 1;
+}
+// the text of a keyword or a built-in word, from the language guide, or 0. this
+// one table holds every keyword the compiler registers and the built-ins that
+// have their own form. the form comes first, then a ~, then what it does (the ~
+// separates the two). a name the table does not hold gets the image's docs.
+int kwtext() {
+ if (hname_is("word")) return "word~one 64-bit vm word; the default type for untyped integer values and word-sized storage";
+ if (hname_is("int")) return "int~one signed, word-sized integer; arrays of int use one word per element";
+ if (hname_is("char")) return "char~one word holding a character code; arrays of char use one word per element";
+ if (hname_is("bool")) return "bool~one word for a boolean value; comparison and logical operators produce 0 or 1";
+ if (hname_is("void")) return "void~marks a function with no meaningful result; the vm still uses a one-word return convention";
+ if (hname_is("float")) return "float~one-word ieee binary64 value; arrays of float use one word per element";
+ if (hname_is("f64")) return "f64~alias for float, a one-word ieee binary64 value";
+ if (hname_is("f32")) return "f32~binary32 value rounded when stored; arrays and pointers pack two elements per word";
+ if (hname_is("u8")) return "u8~unsigned 8-bit integer, range 0..255; values narrow on store and arrays or pointers pack eight elements per word";
+ if (hname_is("i8")) return "i8~signed 8-bit integer, range -128..127; values narrow on store and arrays or pointers pack eight elements per word";
+ if (hname_is("u16")) return "u16~unsigned 16-bit integer, range 0..65535; values narrow on store and arrays or pointers pack four elements per word";
+ if (hname_is("i16")) return "i16~signed 16-bit integer, range -32768..32767; values narrow on store and arrays or pointers pack four elements per word";
+ if (hname_is("u32")) return "u32~unsigned 32-bit integer, range 0..4294967295; values narrow on store and arrays or pointers pack two elements per word";
+ if (hname_is("i32")) return "i32~signed 32-bit integer, range -2147483648..2147483647; values narrow on store and arrays or pointers pack two elements per word";
+ if (hname_is("byte")) return "byte~alias for u8, an unsigned 8-bit integer packed in arrays and pointers";
+ if (hname_is("fixed")) return "fixed~marks an array whose length cannot change; fixed arrays cannot grow or shrink";
+ if (hname_is("new")) return "new(T) or new(T, n)~returns a fresh object on the heap. bound to a local, the local owns it (tagged m) and the block frees it when it ends, so you don't write free yourself.";
+ if (hname_is("free")) return "free(d*p)~frees a heap object. the block is kept until the next collection finds nothing pointing at it, and a second free does nothing. free takes an object tagged d, so a checked call is free(d*p).";
+ if (hname_is("sizeof")) return "sizeof(e) or sizeof(T)~the size in words, found by compiling the expression dry, so it is not run. a packed array counts (n * bytes + 7) / 8 words.";
+ if (hname_is("typeof")) return "typeof(e) or typeof(T)~the type record of an expression or a type, found by compiling it dry. tkind, tsize, tname and the other t functions read it at runtime.";
+ if (hname_is("syscall")) return "syscall(op, a, b, c)~a call to the host through the gate word. the ops are SYS_OPEN, SYS_CLOSE, SYS_READ, SYS_WRITE, SYS_GETENV, SYS_TIME and the rest of the named ones, and SYS_RAW (40) runs any linux syscall on the host. o3 code does not go through the gate, so call syscall from ordinary code.";
+ if (hname_is("len")) return "len(a)~the number of items in an array, growable or fixed.";
+ if (hname_is("cap")) return "cap(a)~the number of items a growable array can hold before it has to grow.";
+ if (hname_is("push")) return "push(a, x)~adds x to the end of a growable array, growing it when needed. a growable array owns its items.";
+ if (hname_is("pop")) return "pop(a)~removes the last item of a growable array and gives it back.";
+ if (hname_is("resize")) return "resize(a, n)~sets the length of a growable array to n, growing or shrinking it.";
+ if (hname_is("o3")) return "o3~marks code for the optimizing tier. it goes on a function, a block or a single variable, and its variables live in registers.";
+ if (hname_is("fn")) return "fn name(a, b) { }~declares a function whose return is a word. a typed function is written like c, int f(int x) { }.";
+ if (hname_is("let")) return "let name = e~declares a local whose type comes from its value. a number stays a plain word, and a pointer or a struct keeps its type.";
+ if (hname_is("const")) return "const [type] NAME = e~a named constant. its value is worked out while compiling, so it can size an array or label a case.";
+ if (hname_is("else")) return "else~the other branch of an if. after an enhance's body, else names the fallback function that runs where the enhancement can't.";
+ if (hname_is("as")) return "e as T~changes the type of an expression without changing its bits.";
+ if (hname_is("unchecked")) return "unchecked fn f() { } / unchecked { } / unchecked;~turns the lifetime analyzer off for a function, a block, or the rest of the unit. the code is not checked, so the analyzer's rules don't apply to it.";
+ if (hname_is("immediate")) return "immediate fn name() { }~a parse word. it runs during compilation whenever its name shows up as a statement, inside an expression or at the top level.";
+ if (hname_is("case")) return "case v:~a label in a switch. each case ends where the next one begins, so cases don't fall through.";
+ if (hname_is("default")) return "default:~the arm of a switch that runs when no case matches.";
+ if (hname_is("do")) return "do { } while (cond);~a loop that runs its body once before the test.";
+ if (hname_is("while")) return "while (cond) { }~a loop that tests the condition before each pass.";
+ if (hname_is("for")) return "for (init; cond; step) { }~a loop with its start, test and step written on one line.";
+ if (hname_is("if")) return "if (cond) { } else { }~runs one branch or the other, by the condition.";
+ 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("return")) return "return e;~leaves the function with a value. a u object leaves with u*x, and the function's return is then owned.";
+ 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.";
+ if (hname_is("struct")) return "struct name { fields };~a record type. struct name<T> { } makes it generic, one copy for each set of type arguments.";
+ if (hname_is("type")) return "type name = T;~an alias for a type, such as type row = [3]int; for a pointer to an array.";
+ if (hname_is("include")) return "include \"path\";~reads a file and compiles it at that point, its signatures first.";
+ if (hname_is("memdebug")) return "memdebug on; / memdebug off; / memdebug scrub;~header checks, allocation sites and leak reports for the rest of the file. scrub also fills a freed payload with 0xDEAD.";
+ if (hname_is("typecheck")) return "typecheck on; / typecheck off;~turns on type checks for the code that follows, enums included.";
+ if (hname_is("docs")) return "docs on; / docs off;~keeps the doc comments above declarations in the image, so the language server can show them on hover.";
+ if (hname_is("micro")) return "enhance name(args) micro \"recipe\";~the body of an enhancement, written as micro-ops on registers r0 to r15. the inputs arrive in r0 and up, and the outputs leave from r0.";
+ if (hname_is("vm")) return "enhance name(args) vm { ... };~the body of an enhancement, written as stack machine asm. it becomes one opcode when the core lifts it.";
+ if (hname_is("enhance")) return "enhance name(args) micro \"...\" or vm { ... };~installs a custom opcode at compile time, with up to 8 inputs and 8 outputs. an enhancement with one output can be called like a function.";
+ if (hname_is("asm")) return "asm { ... }~a block of stack machine asm, and asm operands can be used inside expressions.";
+ if (hname_is("quote")) return "quote { tokens }~a token list as a fresh object. a parse word can read it and build code from it.";
+ if (hname_is("extern")) return "extern type name(params);~declares a c function on the host and calls it through the native call.";
+ if (hname_is("cfunc")) return "cfunc(f)~a native function pointer that runs the zc function f when c calls it.";
+ return 0;
+}
+// the markdown of a keyword: its form as a code block, then what it does
+int kw_put(word s) {
+ int k = 0;
+ hov_put('`'); hov_put('`'); hov_put('`'); hov_put('z'); hov_put('c'); hov_put(10);
+ while (s[k] && s[k] != '~') { hov_put(s[k]); k++; }
+ hov_put(10); hov_put('`'); hov_put('`'); hov_put('`');
+ if (s[k]) {
+ hov_put(10); hov_put(10); k++;
+ while (s[k]) { hov_put(s[k]); k++; }
+ }
+ return hn;
+}
+// does the line from q declare hname as a method: the name is followed by (, and
+// what comes before it is a type, fn or static, and nothing that makes it a call
+int hm_decl(int base, int q, int le) {
+ int po = q, ns, e, k = 0, c;
+ while (po < le && dc(base, po) != '(') po++;
+ if (po >= le) return 0;
+ e = po;
+ ns = e;
+ while (ns > q && hid(dc(base, ns - 1))) ns--;
+ if (ns == e || ns == q) return 0;
+ while (hname[k] && ns + k < e && dc(base, ns + k) == hname[k]) k++;
+ if (hname[k] || ns + k != e) return 0;
+ if (dc(base, ns - 1) != 32 && dc(base, ns - 1) != 9) return 0;
+ k = q;
+ while (k < ns) {
+ c = dc(base, k);
+ if (c == '=' || c == '.' || c == ';' || c == '(' || c == ',' || c == '{' || c == '}') return 0;
+ k++;
+ }
+ if (hl_kw(base, q)) return 0;
+ return 1;
+}
+// the markdown of a method or a static function of a struct: its line as the
+// signature, then the comment lines right above it. the methods are indented in
+// the struct body, so hover_scan (which reads column 0) does not find them.
+// returns 1 when the hovered word is such a name.
+int hover_method(int i) {
+ int base = doff[i], size = dsize[i], ls = 0, le, q, p, e, k, cs, pv, found = 0, sg, se, n;
+ while (ls < size && !found) {
+ le = ls;
+ while (le < size && dc(base, le) != 10) le++;
+ q = ls;
+ while (q < le && (dc(base, q) == 32 || dc(base, q) == 9)) q++;
+ if (q > ls && hm_decl(base, q, le)) { found = 1; sg = q; se = q; }
+ if (!found) ls = le + 1;
+ }
+ if (!found) return 0;
+ // the signature: up to the body, a ; or the end of the line
+ se = sg;
+ while (se < size && dc(base, se) != 10 && dc(base, se) != '{' && dc(base, se) != ';') se++;
+ while (se > sg && (dc(base, se - 1) == 32 || dc(base, se - 1) == 9)) se--;
+ hn = 0;
+ hov_put('`'); hov_put('`'); hov_put('`'); hov_put('z'); hov_put('c'); hov_put(10);
+ k = sg;
+ while (k < se) { hov_put(dc(base, k)); k++; }
+ hov_put(10); hov_put('`'); hov_put('`'); hov_put('`');
+ hover_docs(i, sg, sg, 0);
+ return 1;
+}
+// the markdown of a local at its declaration: the name's declaration line, with
+// the type an untyped let was given. o is the byte offset of the declared name.
+int hover_named(int i, int o, int kind) {
+ int base = doff[i], end = o, head = o, k, entry, untyped; word t = 0;
+ while (end < dsize[i] && hid(dc(base, end))) end++;
+ if (end == o) return 0;
+ untyped = ann_untyped(i, o); htn = 0;
+ if (untyped) { ensure_annot(i); ann_type(i, o); ensure_checked(i); }
+ entry = lt_find(o);
+ if (entry >= 0) t = lt_type[entry];
+ k = 0;
+ while (k < ht_n) { if (ht_end[k] == o && ht_start[k] < head) head = ht_start[k]; k++; }
+ if (untyped && kind != 1) {
+ k = o - 1;
+ while (k >= 0 && (dc(base, k) == 32 || dc(base, k) == 9)) k--;
+ if (k >= 2 && dc(base, k) == 't' && dc(base, k - 1) == 'e' && dc(base, k - 2) == 'l') head = k - 2;
+ }
+ hn = 0; hov_text("```zc\n");
+ if (untyped && kind != 1) hov_text("let ");
+ else if (!untyped && t) { hov_type(t); hov_put(' '); }
+ k = o; while (k < end) { hov_put(dc(base, k)); k++; }
+ if (kind == 1) hov_text(" // parameter");
+ else if (kind == 3) hov_text(" // global");
+ else if (kind == 4) hov_text(" // field");
+ else hov_text(" // local");
+ if (htn || entry >= 0) {
+ hov_text("; type ");
+ if (htn) { k = 0; while (k < htn) { hov_put(htb[k]); k++; } }
+ else { k = lt_at[entry]; while (k < lt_at[entry] + lt_len[entry]) { hov_put(ltxt[k]); k++; } }
+ }
+ hov_text("\n```"); hover_type_info(t); hover_docs(i, head, o, end - o);
+ if (hoff != o) hover_docs(i, hoff, hoff, len_w(hname));
+ htn = 0;
+ return 1;
+}
+int hover_at_decl(int i, int o) { return hover_named(i, o, 2); }
+// storage facts are copied at the reference's position, before later statements
+// can change them. unknown facts stay unknown in the preview.
+int hover_storage(int r) {
+ word p, t = 0; int entry, kind;
+ if (r < 0 || !hn) return 0;
+ p = rsrec + r * RS_W; kind = p[6];
+ if (!kind) {
+ if (p[1] == RS_LOCAL || p[1] == RS_PARAM || p[1] == RS_FIELD) {
+ entry = lt_find(p[2]); if (entry >= 0) t = lt_type[entry];
+ } else if (p[1] == RS_SYM && p[3] >= 0) t = stype[p[3]];
+ if (!t || (t[0] != TY_PTR && t[0] != TY_ARR && t[0] != TY_DYN)) return 0;
+ hov_text("\n\nsize: ");
+ if (t[0] == TY_ARR) { hov_number(t[3]); hov_text(" elements"); }
+ else hov_text("unknown");
+ hov_text(". lifetime: unknown at this position."); return 0;
+ }
+ hov_text("\n\nsize: ");
+ if (p[4] >= 0) { hov_number(p[4]); hov_text(" elements"); }
+ else hov_text("unknown");
+ hov_text(". lifetime: ");
+ if (p[5] == 2) { hov_text("ended on line "); hov_number(p[8] & 0xFFFFFF); }
+ else if (p[5] == 1) {
+ if (kind == 1) hov_text("stack storage; ends when its declaring scope exits");
+ else hov_text("heap storage; live here, ends when freed or its owner releases it");
+ } else hov_text("unknown after a call or control-flow change");
+ if (p[7]) { hov_text(". storage begins on line "); hov_number(p[7] & 0xFFFFFF); }
+ hov_text("."); return 0;
+}
+// the hover of a name that is not a local. kind is 1 for a built-in (the guide's
+// text, then the image's docs), 2 for a symbol the document declares (its
+// declaration, a method, then the image's docs), and 0 when no record says (the
+// guide, then the text of the document, as before)
+int hover_global(int i, int kind) {
+ int kt;
+ if (kind != 2) { kt = kwtext(); if (kt) return kw_put(kt); }
+ if (kind == 1) { hover_blob(i); return hn; }
+ if (hover_scan(i)) { hover_fill(i); return hn; }
+ if (hover_method(i)) return hn;
+ if (kind == 0 && hover_local(i)) return hn;
+ hover_blob(i);
+ return hn;
+}
+// the hover at the cursor. the compiler's record of the name decides what it
+// is: a local is its declaration, a parameter the header that names it, and a
+// symbol is looked up as that kind of symbol. a name with no record is found by
+// its text, as before.
+int do_hover(int i) {
+ int r, k = 0, v = 0, has_word, s, q, o;
+ hn = 0;
+ ensure_checked(i);
+ has_word = hover_word(i);
+ if (hover_type_span(i)) { send(gen_hover); return 0; }
+ if (has_word) {
+ r = rs_find(hoff);
+ if (r >= 0) { k = rsrec[r * RS_W + 1]; v = rsrec[r * RS_W + 2]; }
+ if (k == RS_LOCAL && v >= 0) hover_at_decl(i, v);
+ else if (k == RS_PARAM && v >= 0) hover_named(i, v, 1);
+ else if (k == RS_FIELD && v >= 0) hover_named(i, v, 4);
+ else if (k == RS_LOCAL) hover_local(i);
+ else if (k == RS_SYM) {
+ if ((v & 15) == K_GVAR || (v & 15) == K_GARR) {
+ s = rsrec[r * RS_W + 3]; q = 0; o = 0 - 1;
+ while (q < rsn) { if (rsrec[q * RS_W + 3] == s && lt_find(rsrec[q * RS_W]) >= 0) { o = rsrec[q * RS_W]; break; } q++; }
+ if (o >= 0) hover_named(i, o, 3); else if (v & 16) hover_global(i, 2); else hover_global(i, 1);
+ } else if (v & 16) hover_global(i, 2); else hover_global(i, 1);
+ }
+ else hover_global(i, 0);
+ r = rs_find(hoff); hover_storage(r);
+ }
+ send(gen_hover);
+ return 0;
+}
+
+// ---- dispatch ----
+// the text just read belongs to the document named by the uri
+int commit_text() {
+ int i;
+ if (!thave || !tuseen) return 0 - 1;
+ i = dfind(ubuf, un);
+ if (i < 0) i = docnew(ubuf, un);
+ if (i < 0) return 0 - 1;
+ doff[i] = tbase; dsize[i] = tn; dver[i] = jver; dgen[i]++;
+ return i;
+}
+// handles one message: the lifecycle (initialize, shutdown, exit), changes to a
+// document (which are checked and published), and the requests for semantic
+// tokens and hovers.
+int handle() {
+ int i;
+ if (eq_w(mbuf, "initialize")) { inited = 1; send(gen_init); return 0; }
+ if (eq_w(mbuf, "exit")) { quit = 1; return 0; }
+ if (eq_w(mbuf, "shutdown")) { shut = 1; send(gen_null); return 0; }
+ if (!inited) { if (idtype) reply_error(0 - 32002); return 0; }
+ if (eq_w(mbuf, "textDocument/didOpen") || eq_w(mbuf, "textDocument/didChange") || eq_w(mbuf, "textDocument/didSave")) {
+ i = commit_text();
+ if (i < 0 && tuseen) i = dfind(ubuf, un);
+ if (i >= 0) { check_doc(i); publish(i); }
+ return 0;
+ }
+ if (eq_w(mbuf, "textDocument/didClose")) {
+ i = dfind(ubuf, un);
+ if (i >= 0) {
+ dgpos = 0; publish(i); // an empty list clears its markers
+ dlive[i] = 0; dsize[i] = 0; doff[i] = 0; duri[i] = 0;
+ chk_i = 0 - 1;
+ hc_i = 0 - 1; ann_i = 0 - 1;
+ }
+ return 0;
+ }
+ if (eq_w(mbuf, "textDocument/semanticTokens/full")) {
+ i = dfind(ubuf, un);
+ if (i < 0) { send(gen_empty); return 0; }
+ ensure_checked(i);
+ gd = i; send(gen_tokens);
+ return 0;
+ }
+ if (eq_w(mbuf, "textDocument/hover")) {
+ i = dfind(ubuf, un);
+ if (i < 0) { send(gen_null); return 0; }
+ do_hover(i);
+ return 0;
+ }
+ if (idtype) reply_error(0 - 32601);
+ return 0;
+}
+
+// ---- command line ----
+// a source file on stdin instead of a protocol stream: print what the
+// compiler found, one line each. with --annotate, print the file with the
+// types the compiler can see written in
+unchecked int load_stdin(int first) {
+ int n = 0, c = first, cap = 4096; word nb; int k;
+ tbase = new(cap);
+ while (c >= 0) {
+ if ((n >> 2) + 4 >= cap) {
+ nb = new(cap * 2); k = 0;
+ while (k < cap) { nb[k] = tbase[k]; k++; }
+ tbase = nb; cap = cap * 2;
+ }
+ if ((n & 3) == 0) tbase[n >> 2] = 0;
+ tbase[n >> 2] = tbase[n >> 2] | (c << ((n & 3) * 8));
+ n++;
+ if (eof()) break;
+ c = get_c() & 255;
+ }
+ if (n & 3) { } else tbase[n >> 2] = 0;
+ tbase[(n >> 2) + 1] = 0;
+ dlive[0] = 1; doff[0] = tbase; dsize[0] = n; dgen[0] = 1; dulen[0] = 0;
+ return n;
+}
+// zclsp given a source file on stdin prints its diagnostics as file:line:column
+// lines, one per error or hint, and says so when there are none.
+int cli(int first) {
+ int at = 0, i, o;
+ load_stdin(first);
+ check_doc(0);
+ pc_reset(0);
+ while (at < dgpos) {
+ o = dgb[at + 2];
+ if (o < 0) { put_s("line "); put_n(dgb[at + 1]); }
+ else { pc_to(o); put_n(pline + 1); put_c(':'); put_n(pcol + 1); }
+ if (dgb[at] == 1) put_s(": error: "); else if (dgb[at] == 2) put_s(": warning: "); else put_s(": hint: ");
+ i = 0; while (i < dgb[at + 4]) { put_c(dgb[at + 5 + i]); i++; }
+ put_c(10);
+ at = at + 5 + dgb[at + 4];
+ }
+ if (!dgpos) put_s("no errors\n");
+ return 0;
+}
+
+// ---- annotate ----
+// zclsp --annotate < file.zc prints the file with the types the compiler can
+// see written in: locals from their initializers, parameters from what every
+// call passes them or from being used as an address, returns from what the
+// function returns (the type facts of compiler.zc). the typechecker is on for
+// it, and every edit has to leave the file with no error it didn't have, so
+// what comes out compiles wherever the input did. when a batch of edits
+// fails, the batch is cut in halves until the bad edits are found and left
+// out. run it again and the types it just wrote let the next round see more.
+int aw_start(int p) { awp = p; awn = 0; return 0; }
+// appends byte c to the output being built at awp (awn bytes so far).
+int aw(int c) {
+ if ((awn & 3) == 0) awp[awn >> 2] = 0;
+ awp[awn >> 2] = awp[awn >> 2] | ((c & 255) << ((awn & 3) * 8));
+ awn++;
+ return 0;
+}
+// ends the output being built: a zero word follows the last byte, so it reads as a text.
+int aw_end() {
+ if ((awn & 3) == 0) awp[awn >> 2] = 0;
+ awp[(awn >> 2) + 1] = 0;
+ return 0;
+}
+// the lines of the errors the last check found
+int err_lines(word dst) {
+ int at = 0, n = 0, need = dgpos / 5 + 1, baseline = dst == abase;
+ if (need > aerrcap) {
+ while (need > aerrcap) { if (!aerrcap) aerrcap = 512; else aerrcap = aerrcap * 2; }
+ abase = lsp_grow(abase, nbase, aerrcap); aerr = lsp_grow(aerr, 0, aerrcap);
+ }
+ if (baseline) dst = abase; else dst = aerr;
+ while (at < dgpos) {
+ if (dgb[at] == 1) { dst[n] = dgb[at + 1]; n++; }
+ at = at + 5 + dgb[at + 4];
+ }
+ return n;
+}
+// is v one of the first n words of set.
+int in_set(int v, word set, int n) {
+ int i = 0;
+ while (i < n) { if (set[i] == v) return 1; i++; }
+ return 0;
+}
+// the facts, in the order of the text
+int sort_facts() {
+ int i = 1, j; word a, b; int t0, t1, t2, t3;
+ while (i < nifx) {
+ j = i;
+ while (j > 0 && ifx[(j - 1) * 4] > ifx[j * 4]) {
+ a = ifx + (j - 1) * 4; b = ifx + j * 4;
+ t0 = a[0]; t1 = a[1]; t2 = a[2]; t3 = a[3];
+ a[0] = b[0]; a[1] = b[1]; a[2] = b[2]; a[3] = b[3];
+ b[0] = t0; b[1] = t1; b[2] = t2; b[3] = t3;
+ j--;
+ }
+ i++;
+ }
+ return 0;
+}
+// the facts the last check collected, kept across the checks that follow
+int save_facts() {
+ int i = 0, nt = 0; word f; int k;
+ sort_facts();
+ if (nifx > afcap) {
+ while (nifx > afcap) { if (!afcap) afcap = 4096; else afcap = afcap * 2; }
+ afx = lsp_grow(afx, 0, afcap * 4); amask = lsp_grow(amask, 0, afcap);
+ }
+ if (nift > atcap) {
+ while (nift > atcap) { if (!atcap) atcap = 24000; else atcap = atcap * 2; }
+ aft = lsp_grow(aft, 0, atcap);
+ }
+ anf = 0;
+ while (i < nifx) {
+ f = ifx + i * 4;
+ i++;
+ afx[anf * 4] = f[0]; afx[anf * 4 + 1] = f[1]; afx[anf * 4 + 2] = nt; afx[anf * 4 + 3] = f[3];
+ k = 0; while (k < f[3]) { aft[nt + k] = ift[f[2] + k]; k++; }
+ nt = nt + f[3];
+ anf++;
+ }
+ return 0;
+}
+// the text of document 0 with the facts in the mask applied, into dst. a fact
+// that overlaps the one before it is left out. returns how many went in
+int apply_masked(int dst) {
+ int i = 0, o = 0; word f; int k, n = 0, size = dsize[0], base = doff[0], last = 0 - 1;
+ aw_start(dst);
+ while (i < anf) {
+ f = afx + i * 4;
+ i++;
+ if (!amask[i - 1] || f[0] < o || f[0] == last || f[0] + f[1] > size) continue;
+ while (o < f[0]) { aw(dc(base, o)); o++; }
+ k = 0; while (k < f[3]) { aw(aft[f[2] + k]); k++; }
+ o = f[0] + f[1]; last = f[0];
+ n++;
+ }
+ while (o < size) { aw(dc(base, o)); o++; }
+ aw_end();
+ return n;
+}
+// the block the masked text is written into: room for the document and every
+// replacement, so it never runs out
+word fresh_half() {
+ int n = dsize[0], i = 0;
+ while (i < anf) { n = n + afx[i * 4 + 3]; i++; }
+ ann_half = new(dwords(n));
+ return ann_half;
+}
+// does the document with the masked facts applied check with no new error?
+int verify() {
+ int ne, i;
+ apply_masked(ann_half);
+ dlive[1] = 1; doff[1] = ann_half; dsize[1] = awn; dgen[1] = dgen[1] + 1; dulen[1] = 0;
+ check_doc(1);
+ ne = err_lines(aerr);
+ i = 0;
+ while (i < ne) { if (!in_set(aerr[i], abase, nbase)) return 0; i++; }
+ return 1;
+}
+// take the facts lo .. hi if they fit together, else halve them and try the halves
+int try_range(int lo, int hi) {
+ int mid, k;
+ if (lo >= hi) return 0;
+ k = lo; while (k < hi) { amask[k] = 1; k++; }
+ if (verify()) return 0;
+ k = lo; while (k < hi) { amask[k] = 0; k++; }
+ if (hi - lo == 1) return 0;
+ mid = (lo + hi) / 2;
+ try_range(lo, mid);
+ try_range(mid, hi);
+ return 0;
+}
+// zclsp --annotate reads a source file on stdin and prints it with the types the
+// compiler can see written in. the rounds run the check with the type facts on,
+// keep the edits that add no error, and run again on the text they made.
+int annotate(int first) {
+ int round = 0, n, i;
+ load_stdin(first);
+ ifstrict = 1; // the typechecker runs, so a type that doesn't fit how a name is used is an error
+ check_doc(0);
+ nbase = err_lines(abase);
+ while (round < AROUNDS) {
+ ifon = 1; check_doc(0); ifon = 0;
+ save_facts();
+ if (!anf) break;
+ i = 0; while (i < anf) { amask[i] = 0; i++; }
+ fresh_half();
+ try_range(0, anf);
+ n = apply_masked(ann_half);
+ if (!n) break;
+ doff[0] = ann_half; dsize[0] = awn; dgen[0] = dgen[0] + 1;
+ round++;
+ }
+ ifstrict = 0;
+ i = 0;
+ while (i < dsize[0]) { put_c(dc(doff[0], i)); i++; }
+ return 0;
+}
+// the first argument: 1 for --annotate, 2 for --types, which lists the edits
+// instead of making them
+unchecked int cli_mode() {
+ int n = syscall(SYS_ARGC, 0, 0, 0), len; word s; int r = 0;
+ if (n < 2) return 0;
+ len = syscall(SYS_ARG, 1, 0, 0);
+ if (len != 10 && len != 7) return 0;
+ s = new(len + 1);
+ syscall(SYS_ARG, 1, s, len + 1);
+ if (s[0] == '-' && s[1] == '-' && s[2] == 'a' && s[3] == 'n' && len == 10) r = 1;
+ if (s[0] == '-' && s[1] == '-' && s[2] == 't' && s[3] == 'y' && len == 7) r = 2;
+ free(s);
+ return r;
+}
+// zclsp --types < file.zc: one line per edit, line:column, how many characters
+// it replaces and the text that goes in
+int list_types(int first) {
+ int i = 0; word f; int k;
+ load_stdin(first);
+ ifon = 1; check_doc(0); ifon = 0;
+ sort_facts();
+ pc_reset(0);
+ while (i < nifx) {
+ f = ifx + i * 4;
+ pc_to(f[0]);
+ put_n(pline + 1); put_c(':'); put_n(pcol + 1); put_c(32); put_n(f[1]); put_c(32);
+ k = 0; while (k < f[3]) { put_c(ift[f[2] + k]); k++; }
+ put_c(10);
+ i++;
+ }
+ return 0;
+}
+
+// the entry point of zclsp. the input is either a source file (checked, and its
+// diagnostics printed), a file to annotate or to list the types of (--annotate,
+// --types), or the json-rpc stream of a language server (when it starts with a
+// Content-Length header).
+int lsp_main() {
+ int c, k;
+ docs_on = 0; // the image was made with docs on; the documents checked here are not docs
+ chk_heap = 1;
+ dgb = new(DGCAP); sets = new(4 * LSW);
+ duri = lsp_grow(0, 0, doccap); dulen = lsp_grow(0, 0, doccap);
+ doff = lsp_grow(0, 0, doccap); dsize = lsp_grow(0, 0, doccap);
+ dver = lsp_grow(0, 0, doccap); dgen = lsp_grow(0, 0, doccap); dlive = lsp_grow(0, 0, doccap);
+ mbuf = lsp_grow(0, 0, mcap); idbuf = lsp_grow(0, 0, idcap); jk = lsp_grow(0, 0, jkcap); kbuf = lsp_grow(0, 0, kcap); lbuf = lsp_grow(0, 0, lcap);
+ ubuf = lsp_grow(0, 0, ucap); hov = lsp_grow(0, 0, hovcap); hname = lsp_grow(0, 0, hnamecap); htb = lsp_grow(0, 0, htbc);
+ chk_i = 0 - 1; jpk = 0 - 2; rfirst = 0 - 1;
+ if (eof()) return 0;
+ c = get_c() & 255;
+ if (c != 'C' && c != 'c') {
+ k = cli_mode();
+ if (k == 1) annotate(c); else if (k == 2) list_types(c); else cli(c);
+ return 0;
+ }
+ rfirst = c;
+ while (!quit) {
+ if (!read_message()) break;
+ handle();
+ }
+ return 0;
+}