// ============================================================================ // 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 { } 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; }