diff options
| author | 2026-10-12 08:46:23 +0330 | |
|---|---|---|
| committer | 2026-10-12 08:46:23 +0330 | |
| commit | ecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch) | |
| tree | 70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /src/lsp | |
a
Diffstat (limited to 'src/lsp')
| -rw-r--r-- | src/lsp/server.zc | 1929 |
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; +} |
