# lsp.ludic — the Ludic language server, written in Ludic (replaces the C # ludic_lsp.c + ludic_index.h + ludic_workspace.h + ludic_json.h). Speaks LSP # 3.17 over stdio: completion, hover, go-to-definition, references, rename, # document/workspace symbols, semantic tokens, folding, signature help, inlay # hints, formatting, and structural diagnostics — an error-tolerant model of the # workspace that keeps parsing after a syntax error and resolves names across the # whole compilation unit. Ground-truth compilation stays with ludicc; this layer # supplies structure, not judgement. program LudicLsp { import "lsp/imports.ludic" import "lsp/docs.ludic" import "lsp/types.ludic" import "lsp/sighelp.ludic" # ---- token kinds ---- const LT_EOF: int = 0 const LT_NL: int = 1 const LT_COMMENT: int = 2 const LT_ID: int = 3 const LT_KW: int = 4 const LT_TYPE: int = 5 const LT_PHASE: int = 6 const LT_BOOL: int = 7 const LT_INT: int = 8 const LT_FLOAT: int = 9 const LT_STR: int = 10 const LT_CHAR: int = 11 const LT_ANNO: int = 12 const LT_OP: int = 13 const LT_ERR: int = 14 # ---- symbol kinds ---- const LS_UNIT: int = 0 const LS_COMPONENT: int = 1 const LS_FIELD: int = 2 const LS_ARCHETYPE: int = 3 const LS_CONST: int = 4 const LS_VAR: int = 5 const LS_FN: int = 6 const LS_PARAM: int = 7 const LS_EXTERN: int = 8 const LS_SYSTEM: int = 9 const LS_UI: int = 10 const LS_WIDGET: int = 11 const LS_SCENE: int = 12 const LS_LAYER: int = 13 const LS_LOCAL: int = 14 const LS_QUERYVAR: int = 15 const LS_STATE: int = 16 const LS_IMPORT: int = 17 # ---- semantic classes ---- const SC_NONE: int = 0 const SC_KEYWORD: int = 1 const SC_TYPE: int = 2 const SC_COMPONENT: int = 3 const SC_ARCHETYPE: int = 4 const SC_SCENE: int = 5 const SC_LAYER: int = 6 const SC_UI: int = 7 const SC_WIDGET: int = 8 const SC_PROP: int = 9 const SC_FIELD: int = 10 const SC_SYSTEM: int = 11 const SC_FUNCTION: int = 12 const SC_BUILTIN: int = 13 const SC_PARAM: int = 14 const SC_VARIABLE: int = 15 const SC_CONST: int = 16 const SC_MODVAR: int = 17 const SC_PHASE: int = 18 const SC_NUMBER: int = 19 const SC_STRING: int = 20 const SC_COMMENT: int = 21 const SC_OPERATOR: int = 22 const SC_ANNOTATION: int = 23 const SC_UNKNOWN: int = 24 const SC_NAMESPACE: int = 25 # semantic token modifier bits (the legend's order) const SEM_DECLARATION: int = 1 const SEM_READONLY: int = 2 const SEM_DEFAULT_LIBRARY: int = 4 const SEM_VENDOR: int = 8 # ---- tiny helpers ---- function read_file(path: pointer) -> pointer { let f = file_open(path, "rb") if (f == null) { return null } file_seek(f, 0, 2) let n = file_tell(f) file_seek(f, 0, 0) let buf = bytes(n + 1) file_read(f, buf, n) buf[n] = 0 file_close(f) return buf } function cstr_len(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n } function char_is_digit(c: int) -> bool { return c >= '0' and c <= '9' } function char_is_alpha(c: int) -> bool { if c >= 'A' and c <= 'Z' { return true } if c >= 'a' and c <= 'z' { return true } return c == '_' } function char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) } function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F') } function itoa(v: int) -> pointer { if v == 0 { let z = bytes(2); z[0] = '0'; z[1] = 0; return z } var neg = false; var x = v if x < 0 { neg = true; x = -x } let tmp = bytes(16); var n = 0 while x > 0 { tmp[n] = 48 + x % 10; x /= 10; n += 1 } var total = n if neg { total += 1 } let out = bytes(total + 1); var k = 0 if neg { out[0] = '-'; k = 1 } var i = 0 while i < n { out[k + i] = tmp[n - 1 - i]; i += 1 } out[total] = 0 return out } function atoi(s: pointer) -> int { var n = 0; var i = 0; var neg = false while s[i] == ' ' or s[i] == '\t' { i += 1 } if s[i] == '-' { neg = true; i += 1 } while char_is_digit(s[i]) { n = n * 10 + (s[i] - 48); i += 1 } if neg { return -n } return n } # ---- growable byte buffer ---- property Buf { data: pointer = null, len: int = 0, cap: int = 0 } function buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b } function buf_ensure(b: Buf, extra: int) -> void { if b.len + extra + 1 <= b.cap { return } while b.len + extra + 1 > b.cap { b.cap *= 2 } b.data = resize(b.data, b.cap) } function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len += 1 } function buf_puts(b: Buf, s: pointer) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i += 1 } } function buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) } function buf_addrange(b: Buf, s: pointer, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k += 1 } } function buf_str(b: Buf) -> pointer { b.data[b.len] = 0; return b.data } # ---- string helpers ---- function streq(a: pointer, b: pointer) -> bool { return (a == b) } function concat3(a: pointer, b: pointer, c: pointer) -> pointer { return ((a + b) + c) } function ends_with(s: pointer, suf: pointer) -> bool { let n = cstr_len(s); let m = cstr_len(suf) if m > n { return false } return (s[n - m..n] == suf) } function starts_with(s: pointer, pre: pointer) -> bool { let m = cstr_len(pre) if m > cstr_len(s) { return false } return (s[0..m] == pre) } # ============================================================================ # documents # ============================================================================ property Sym { kind: int = 0 name: pointer = null ty: pointer = null detail: pointer = null doc: pointer = null tok: int = 0 start: int = 0 end: int = 0 bstart: int = 0 bend: int = 0 parent: int = 0 exported: int = 0 init: int = 0 # first token of the initializer (let/var/const), or -1 mutable: int = 0 # a `var` local } property Doc { path: pointer = null uri: pointer = null raw: pointer = null text: pointer = null is_md: int = 0 is_unit: int = 0 is_module: int = 0 unit: pointer = null src: pointer = null tk_kind: []int tk_start: []int tk_end: []int tk_line: []int linestart: []int tmatch: []int cls: []int clsmods: []int # extra semantic modifiers per token (declaration, vendor, …) syms: []Sym imports: []pointer had_diags: int = 0 isopen: int = 0 version: int = 0 cc_line: int = 0 # the compiler's last error on this file (1-based line), or 0 cc_msg: pointer = null } var g_docs: []Doc var g_root: pointer = null var g_shutdown: int = 0 # initializationOptions.compilerPath — the ludicc to run on every save when # compilerDiagnostics is on (default: the ludicc beside this binary). null = off. var g_compiler: pointer = null # ---- the lexer, over a Doc ---- function is_op2(c0: int, c1: int) -> bool { if c0 == '-' and c1 == '>' { return true } if c1 == '=' and (c0 == '+' or c0 == '-' or c0 == '*' or c0 == '/' or c0 == '=' or c0 == '!' or c0 == '<' or c0 == '>') { return true } if c0 == '<' and c1 == '<' { return true } # << if c0 == '>' and c1 == '>' { return true } # >> if c0 == '.' and c1 == '.' { return true } if c0 == '=' and c1 == '>' { return true } return false } function is_op1(c: int) -> bool { if c == '+' or c == '-' or c == '*' or c == '/' or c == '%' or c == '<' or c == '>' or c == '=' { return true } # + - * / % < > = if c == '(' or c == ')' or c == '{' or c == '}' or c == '[' or c == ']' { return true } # ( ) { } [ ] if c == ',' or c == ':' or c == '.' or c == '!' or c == '@' or c == ';' { return true } # , : . ! @ ; return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~ } # the vocabulary, written by `ludic-dev syntax` from `ludicc --emit-syntax` # ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit function is_type_word(w: pointer) -> bool { if (w == "int") or (w == "long") or (w == "float") or (w == "double") or (w == "fixed") or (w == "bool") { return true } if (w == "byte") or (w == "string") or (w == "pointer") or (w == "void") or (w == "entity") or (w == "countdown") { return true } if (w == "words") or (w == "floats") or (w == "doubles") or (w == "fixeds") or (w == "pointers") or (w == "Vector") { return true } if (w == "IVec2") or (w == "Rect") { return true } return false } function is_phase_word(w: pointer) -> bool { if (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render") { return true } if (w == "Overlay") { return true } return false } function is_keyword_word(w: pointer) -> bool { if (w == "program") or (w == "module") or (w == "import") or (w == "property") or (w == "state") or (w == "event") { return true } if (w == "action") or (w == "reducer") or (w == "enum") or (w == "model") or (w == "prefab") or (w == "scene") { return true } if (w == "handler") or (w == "ui") or (w == "namespace") or (w == "alias") or (w == "port") or (w == "bind") { return true } if (w == "registry") or (w == "def") or (w == "component") or (w == "view") or (w == "prop") or (w == "const") { return true } if (w == "var") or (w == "function") or (w == "fn") or (w == "extern") or (w == "entry") or (w == "test") { return true } if (w == "export") or (w == "internal") or (w == "friend") or (w == "uses") or (w == "open") or (w == "of") { return true } if (w == "as") or (w == "from") or (w == "numbers") or (w == "unsafe") or (w == "mut") or (w == "phase") { return true } if (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true } if (w == "shows") or (w == "lasts") or (w == "then") or (w == "loads") or (w == "system") or (w == "let") { return true } if (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "where") { return true } if (w == "match") or (w == "machine") or (w == "become") or (w == "return") or (w == "break") or (w == "continue") { return true } if (w == "new") or (w == "spawn") or (w == "despawn") or (w == "enable") or (w == "disable") or (w == "attach") { return true } if (w == "detach") or (w == "emit") or (w == "cancel") or (w == "dispatch") or (w == "try") or (w == "and") { return true } if (w == "or") or (w == "not") { return true } return false } # ludic-dev syntax: end function is_widget_word(w: pointer) -> bool { return (w == "panel") or (w == "col") or (w == "row") or (w == "label") or (w == "button") or (w == "image") or (w == "spacer") } function lex_doc(D: Doc) -> void { let s = D.src D.tk_kind = new []int; D.tk_start = new []int; D.tk_end = new []int; D.tk_line = new []int D.linestart = new []int push(D.linestart, 0) var i = 0; var line = 0 var tmode = 0 # 0 code, 1 template text, 2 inside a template hole var tdepth = 0 # braces opened inside the current hole while s[i] != 0 { let c = s[i] if c == '\n' { push(D.tk_kind, LT_NL); push(D.tk_start, i); push(D.tk_end, i + 1); push(D.tk_line, line); i += 1; line += 1; push(D.linestart, i); continue } if c == ' ' or c == '\t' or c == '\r' { i += 1; continue } var st = i if c == '#' { while s[i] != 0 and s[i] != '\n' { i += 1 }; push(D.tk_kind, LT_COMMENT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } if c == '"' { i += 1; while s[i] != 0 and s[i] != '"' and s[i] != '\n' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } }; if s[i] == '"' { i += 1 }; push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } # `text {hole} text`: the text runs are string tokens (the hole's braces # ride on them), the hole is lexed as code, so its names resolve if c == '`' or (tmode == 2 and c == '}' and tdepth == 0) { if c == '`' and tmode == 2 { i += 1; tmode = 0; push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } i += 1 tmode = 1 var seg_line = line while s[i] != 0 and tmode == 1 { if s[i] == '`' { i += 1; tmode = 0 } else if s[i] == '{' and s[i + 1] == '{' { i += 2 } else if s[i] == '}' and s[i + 1] == '}' { i += 2 } else if s[i] == '{' { i += 1; tmode = 2; tdepth = 0 } else if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else if s[i] == '\n' { # a template may span lines: close this run, keep the line table right if i > st { push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, seg_line) } i += 1; line += 1; push(D.linestart, i) st = i; seg_line = line } else { i += 1 } } if tmode == 1 { tmode = 0 } if i > st { push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, seg_line) } continue } if tmode == 2 and c == '{' { tdepth += 1 } if tmode == 2 and c == '}' { tdepth -= 1 } if c == '\'' { i += 1; if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { if s[i] != 0 and s[i] != '\n' { i += 1 } }; if s[i] == '\'' { i += 1 }; push(D.tk_kind, LT_CHAR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } if char_is_digit(c) { if c == '0' and (s[i + 1] == 'x' or s[i + 1] == 'X') { i += 2; while char_is_hex(s[i]) { i += 1 }; push(D.tk_kind, LT_INT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } while char_is_digit(s[i]) { i += 1 } if s[i] == '.' and char_is_digit(s[i + 1]) { i += 1; while char_is_digit(s[i]) { i += 1 }; push(D.tk_kind, LT_FLOAT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } push(D.tk_kind, LT_INT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } if c == '@' and (char_is_alpha(s[i + 1]) or s[i + 1] == '_') { i += 1; while char_is_alnum(s[i]) { i += 1 }; push(D.tk_kind, LT_ANNO); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } if char_is_alpha(c) { while char_is_alnum(s[i]) { i += 1 } let w = s[st..i] var k = LT_ID if (w == "true") or (w == "false") or (w == "null") { k = LT_BOOL } else { if is_type_word(w) { k = LT_TYPE } else { if is_phase_word(w) { k = LT_PHASE } else { if is_keyword_word(w) { k = LT_KW } } } } push(D.tk_kind, k); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } if is_op2(c, s[i + 1]) { push(D.tk_kind, LT_OP); push(D.tk_start, i); push(D.tk_end, i + 2); push(D.tk_line, line); i += 2; continue } if is_op1(c) { push(D.tk_kind, LT_OP); push(D.tk_start, i); push(D.tk_end, i + 1); push(D.tk_line, line); i += 1; continue } var ln = 1 if c >= 240 { ln = 4 } else { if c >= 224 { ln = 3 } else { if c >= 128 { ln = 2 } } } var kk = 1 while kk < ln { if s[i + kk] == 0 or (s[i + kk] & 192) != 128 { ln = kk }; kk += 1 } push(D.tk_kind, LT_ERR); push(D.tk_start, i); push(D.tk_end, i + ln); push(D.tk_line, line); i += ln } push(D.tk_kind, LT_EOF); push(D.tk_start, i); push(D.tk_end, i); push(D.tk_line, line) demote_contextual_keywords(D) } function ntok(D: Doc) -> int { return len(D.tk_kind) } function tlen(D: Doc, i: int) -> int { return D.tk_end[i] - D.tk_start[i] } function ttext(D: Doc, i: int) -> pointer { return D.src[D.tk_start[i]..D.tk_end[i]] } function tis(D: Doc, i: int, s: pointer) -> bool { if i < 0 or i >= ntok(D) { return false }; return (ttext(D, i) == s) } function next_sig(D: Doc, i: int) -> int { var j = i + 1; while j < ntok(D) { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j += 1 }; return -1 } function prev_sig(D: Doc, i: int) -> int { var j = i - 1; while j >= 0 { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j -= 1 }; return -1 } # ---- positions (UTF-16 for LSP) ---- function utf16_len(s: pointer, a: int, nbytes: int) -> int { var u = 0; var i = 0 while i < nbytes { let c = s[a + i] if c < 128 { i += 1; u += 1 } else { if c < 224 { i += 2; u += 1 } else { if c < 240 { i += 3; u += 1 } else { i += 4; u += 2 } } } } return u } function line_of(D: Doc, off: int) -> int { var lo = 0; var hi = len(D.linestart) - 1 while lo < hi { let mid = (lo + hi + 1) / 2; if D.linestart[mid] <= off { lo = mid } else { hi = mid - 1 } } return lo } function col_of(D: Doc, off: int) -> int { let l = line_of(D, off); return utf16_len(D.text, D.linestart[l], off - D.linestart[l]) } function offset_of(D: Doc, line: int, ch: int) -> int { if line < 0 { return 0 } if line >= len(D.linestart) { return cstr_len(D.text) } var off = D.linestart[line] var endo = cstr_len(D.text) if line + 1 < len(D.linestart) { endo = D.linestart[line + 1] } var u = 0 while off < endo and u < ch { let c = D.text[off] if c < 128 { off += 1; u += 1 } else { if c < 224 { off += 2; u += 1 } else { if c < 240 { off += 3; u += 1 } else { off += 4; u += 2 } } } } return off } function tok_at(D: Doc, off: int) -> int { var touching = -1 var i = 0 let N = ntok(D) while i < N { let k = D.tk_kind[i] if k != LT_NL and k != LT_EOF { if off >= D.tk_start[i] and off < D.tk_end[i] { return i } if off == D.tk_end[i] and touching < 0 { touching = i } if D.tk_start[i] > off { return touching } } i += 1 } return touching } # ---- brace matching ---- function match_braces(D: Doc) -> void { let N = ntok(D) D.tmatch = new []int var i = 0 while i < N { push(D.tmatch, -1); i += 1 } let stack = words(1200) var top = 0 i = 0 while i < N { if D.tk_kind[i] == LT_OP and tlen(D, i) == 1 { let c = D.src[D.tk_start[i]] if c == '{' or c == '(' or c == '[' { if top < 1200 { stack[top] = i; top += 1 } } else { if c == '}' or c == ')' or c == ']' { if top > 0 { top -= 1; let o = stack[top]; D.tmatch[o] = i; D.tmatch[i] = o } } } } i += 1 } } # ---- doc comment: the comment block directly above a declaration ---- function collect_doc(D: Doc, decl_tok: int) -> pointer { var line = D.tk_line[decl_tok] var first = -1 var i = decl_tok - 1 while i >= 0 { let k = D.tk_kind[i] if k == LT_NL { i -= 1; continue } if k != LT_COMMENT { break } if D.tk_line[i] >= line { break } let p = prev_sig(D, i) if p >= 0 and D.tk_line[p] == D.tk_line[i] { break } if D.tk_line[i] < line - 1 and first >= 0 { break } if first >= 0 and D.tk_line[first] != D.tk_line[i] + 1 { break } first = i; line = D.tk_line[i] i -= 1 } if first < 0 { return "" } let out = buf_new() i = first while i < decl_tok { if D.tk_kind[i] == LT_COMMENT { var s = D.tk_start[i] + 1 while s < D.tk_end[i] and (D.text[s] == ' ' or D.text[s] == '\t' or D.text[s] == '#' or D.text[s] == '*') { s += 1 } var rule = true var r = s while r < D.tk_end[i] { let rc = D.text[r]; if not (rc == '=' or rc == '-' or rc == '~' or rc == ' ') { rule = false }; r += 1 } if not rule or s == D.tk_end[i] { buf_addrange(out, D.text, s, D.tk_end[i]) buf_putc(out, '\n') } } i += 1 } return buf_str(out) } # ============================================================================ # the shallow parser — records declarations and bindings with their spans # ============================================================================ var pD: Doc = null var pi: int = 0 function pk() -> int { if pi < ntok(pD) { return pD.tk_kind[pi] }; return LT_EOF } function pis(s: pointer) -> bool { return tis(pD, pi, s) } function pskipnl() -> void { while pi < ntok(pD) and (pk() == LT_NL or pk() == LT_COMMENT) { pi += 1 } } function padv() -> void { if pi < ntok(pD) { pi += 1 }; pskipnl() } function pword() -> pointer { if pi < ntok(pD) { return ttext(pD, pi) }; return "" } function pname() -> int { let k = pk() if k == LT_ID or k == LT_TYPE or k == LT_PHASE or k == LT_KW or k == LT_BOOL { let t = pi; padv(); return t } return -1 } function sym_add(kind: int, nametok: int, parent: int) -> int { let s = new Sym s.kind = kind; s.tok = nametok; s.parent = parent s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""; s.init = -1; s.mutable = 0 if nametok >= 0 { s.start = pD.tk_start[nametok]; s.end = pD.tk_end[nametok]; s.name = ttext(pD, nametok) } s.bstart = 0; s.bend = cstr_len(pD.text) push(pD.syms, s) return len(pD.syms) - 1 } function cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return -1 } function skip_group() -> void { let m = cur_match(); if m < 0 { padv(); return }; pi = m + 1; pskipnl() } # `name: Type` pairs inside ( ) — record params, build the signature into sig function lp_params(owner: int, kind: int, sig: Buf) -> void { if not pis("(") { return } let close = cur_match() buf_putc(sig, '(') # '(' padv() var first = true while pi < ntok(pD) and (close < 0 or pi < close) { if pis(")") { break } if pis(",") { padv(); continue } let nt = pname() if nt < 0 { padv(); continue } var ty = "" var mu = "" if pis(":") { padv() if pis("mut") { padv(); mu = "mut " } # a state it may change let tt = pname(); if tt >= 0 { ty = ttext(pD, tt) } } let sidx = sym_add(kind, nt, owner) pD.syms[sidx].ty = ty pD.syms[sidx].detail = concat3("param ", pD.syms[sidx].name, concat3(": ", mu, ty)) if not first { buf_puts(sig, ", ") } buf_puts(sig, pD.syms[sidx].name); buf_puts(sig, ": ") if ty == "" { buf_puts(sig, "?") } else { buf_puts(sig, ty) } first = false } if close >= 0 { pi = close + 1 } pskipnl() buf_putc(sig, ')') # ')' } # the type written at pi (`int`, `[]Doc`, `?Actor`), consumed; "" when none function lp_type_text() -> pointer { var pre = "" while pis("[") or pis("?") { if pis("[") { let m = cur_match(); if m < 0 { return "" }; pi = m + 1; pre = pre + "[]" } else { pi += 1; pre = pre + "?" } } let tt = pname() if tt < 0 { return "" } return pre + ttext(pD, tt) } # (a, b) variable list -> token indices into out; returns count function lp_varlist(out: []int) -> int { var n = 0 if not pis("(") { return 0 } let close = cur_match() padv() while pi < ntok(pD) and (close < 0 or pi < close) { if pis(")") { break } if pis(",") { padv(); continue } let nt = pname() if nt < 0 { padv(); continue } push(out, nt); n += 1 } if close >= 0 { pi = close + 1 } pskipnl() return n } # [Pos, Vel, {Enemy}] — bind listed components to vars in order (skip {Tag}) function lp_query_terms(owner: int, vars: []int, nvars: int, scope_start: int, scope_end: int) -> void { if not pis("[") { return } let close = cur_match() padv() var bind = 0 while pi < ntok(pD) and (close < 0 or pi < close) { if pis("]") { break } if pis(",") { padv(); continue } if pis("{") { let cb = cur_match(); padv(); let nt = pname(); if cb >= 0 { pi = cb + 1 } else { padv() }; pskipnl(); continue } let nt = pname() if nt < 0 { padv(); continue } if bind < nvars and vars[bind] >= 0 { let sidx = sym_add(LS_QUERYVAR, vars[bind], owner) pD.syms[sidx].ty = ttext(pD, nt) pD.syms[sidx].bstart = scope_start; pD.syms[sidx].bend = scope_end pD.syms[sidx].detail = concat3(pD.syms[sidx].name, ": ", concat3(pD.syms[sidx].ty, " (query binding)", "")) } bind += 1 } if close >= 0 { pi = close + 1 } pskipnl() if pis("where") { padv() } } function lp_block(owner: int, scope_end: int) -> void { if not pis("{") { return } let close = cur_match() var endo = scope_end if close >= 0 { endo = pD.tk_start[close] } padv() while pi < ntok(pD) and pk() != LT_EOF { if close >= 0 and pi >= close { break } if pis("}") { break } if pis("{") { lp_block(owner, endo); continue } let before = pi lp_stmt(owner, endo) if pi == before { padv() } } if close >= 0 { pi = close + 1 } else { padv() } pskipnl() } function lp_stmt(owner: int, scope_end: int) -> void { if pis("let") or pis("var") { let is_var = pis("var") padv(); let nt = pname(); if nt < 0 { return } var ty = "" if pis(":") { padv(); ty = lp_type_text() } let sidx = sym_add(LS_LOCAL, nt, owner) pD.syms[sidx].ty = ty if is_var { pD.syms[sidx].mutable = 1 } if pis("=") { padv(); pD.syms[sidx].init = pi } pD.syms[sidx].bstart = pD.tk_start[nt]; pD.syms[sidx].bend = scope_end return } if pis("for") { padv() if pis("(") { let vars = new []int let nv = lp_varlist(vars) if pis("in") { padv() } if pis("query") { padv() } var here = pD.tk_start[ntok(pD) - 1] if pi < ntok(pD) { here = pD.tk_start[pi] } lp_query_terms(owner, vars, nv, here, scope_end) return } let nt = pname() if nt >= 0 { let sidx = sym_add(LS_LOCAL, nt, owner) pD.syms[sidx].ty = "int" pD.syms[sidx].bstart = pD.tk_start[nt]; pD.syms[sidx].bend = scope_end } return } if pis("state") { padv(); let nt = pname(); if nt >= 0 { let x = sym_add(LS_STATE, nt, owner) }; return } padv() } # panel id=Root w=288 { … } — each id= mints a UI_ handle function lp_widget(owner: int, uisym: int) -> void { let type_tok = pname() if type_tok < 0 { return } while pi < ntok(pD) and pk() == LT_ID { let ns = next_sig(pD, pi) var checkidx = ns if checkidx < 0 { checkidx = pi } if not tis(pD, checkidx, "=") { break } let kb = ttext(pD, pi) padv() if pis("=") { padv() } if kb == "id" { let nt = pname() if nt >= 0 { let sidx = sym_add(LS_WIDGET, nt, uisym) pD.syms[sidx].name = concat3("UI_", ttext(pD, nt), "") pD.syms[sidx].detail = concat3(ttext(pD, type_tok), " widget handle", "") pD.syms[sidx].ty = "int" } } else { while pi < ntok(pD) and pk() != LT_EOF and not pis("{") and not pis("}") { if pis("(") or pis("[") { skip_group(); continue } let nx = next_sig(pD, pi) if pk() == LT_ID and nx >= 0 and tis(pD, nx, "=") { break } padv() } } } if pis("{") { let close = cur_match() padv() while pi < ntok(pD) and pk() != LT_EOF and not pis("}") { if close >= 0 and pi >= close { break } let before = pi lp_widget(owner, uisym) if pi == before { padv() } } if close >= 0 { pi = close + 1 } else { padv() } pskipnl() } } # one top-level declaration function lp_decl(parent: int) -> void { let w = pword() let decl_tok = pi if w == "import" { padv() if pk() == LT_STR { let t = pi var n = tlen(pD, t) - 2 if n < 0 { n = 0 } let rel = pD.src[pD.tk_start[t] + 1..pD.tk_start[t] + 1 + n] push(pD.imports, rel) let sidx = sym_add(LS_IMPORT, t, parent) pD.syms[sidx].name = rel padv() } return } var wd = w if w == "export" and tis(pD, pi + 1, "state") { padv(); wd = "state" } if (wd == "property") or (wd == "model") or (wd == "state") { # a state: a record with one instance let is_comp = (wd == "property") or (wd == "state") padv(); let nt = pname(); if nt < 0 { return } var kind = LS_ARCHETYPE if is_comp { kind = LS_COMPONENT } let sidx = sym_add(kind, nt, parent) pD.syms[sidx].doc = collect_doc(pD, decl_tok) while pk() == LT_ANNO { padv() } let det = buf_new() buf_puts(det, w); buf_putc(det, ' '); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {") if not pis("{") { pD.syms[sidx].detail = buf_str(det); return } let close = cur_match() pD.syms[sidx].bstart = pD.tk_start[pi] if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) } padv() var first = true while pi < ntok(pD) and pk() != LT_EOF and not pis("}") { if close >= 0 and pi >= close { break } if pis(",") { padv(); continue } while pk() == LT_ANNO { padv() } # @Sync / @Computed on a field let ft = pname(); if ft < 0 { padv(); continue } let f = sym_add(LS_FIELD, ft, sidx) var fty = "" if pis(":") { padv(); fty = lp_type_text() } pD.syms[f].ty = fty pD.syms[f].detail = concat3(pD.syms[sidx].name, ".", concat3(pD.syms[f].name, ": ", fty)) if pis("=") { padv(); while pi < ntok(pD) and not pis(",") and not pis("}") and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { padv() } } } if not first { buf_putc(det, ',') } buf_putc(det, ' '); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty) first = false } buf_puts(det, " }") pD.syms[sidx].detail = buf_str(det) if close >= 0 { pi = close + 1 } else { padv() } pskipnl() return } if (w == "const") or (w == "var") { var kind = LS_VAR if w == "const" { kind = LS_CONST } padv(); let nt = pname(); if nt < 0 { return } let sidx = sym_add(kind, nt, parent) pD.syms[sidx].doc = collect_doc(pD, decl_tok) if pis(":") { padv(); pD.syms[sidx].ty = lp_type_text() } if pis("=") { padv(); pD.syms[sidx].init = pi } pD.syms[sidx].detail = concat3(w, " ", concat3(pD.syms[sidx].name, ": ", pD.syms[sidx].ty)) while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { pi += 1 } } pskipnl() return } if w == "extern" { padv(); if pis("function") { padv() } let nt = pname(); if nt < 0 { return } let sidx = sym_add(LS_EXTERN, nt, parent) pD.syms[sidx].doc = collect_doc(pD, decl_tok) let sig = buf_new(); buf_puts(sig, "extern function "); buf_puts(sig, pD.syms[sidx].name) lp_params(sidx, LS_PARAM, sig) if pis("->") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt); buf_puts(sig, " -> "); buf_puts(sig, pD.syms[sidx].ty) } } pD.syms[sidx].detail = buf_str(sig) while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF { pi += 1 } pskipnl() return } if (w == "function") or (w == "pure") or (w == "export") { let exported = (w == "export") if (w == "pure") or exported { padv(); if pis("pure") { padv() } } if pis("function") { padv() } let nt = pname(); if nt < 0 { return } let sidx = sym_add(LS_FN, nt, parent) if exported { pD.syms[sidx].exported = 1 } pD.syms[sidx].doc = collect_doc(pD, decl_tok) let sig = buf_new(); if exported { buf_puts(sig, "export ") }; buf_puts(sig, "fn "); buf_puts(sig, pD.syms[sidx].name) lp_params(sidx, LS_PARAM, sig) if pis("->") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt) } } buf_puts(sig, " -> "); if pD.syms[sidx].ty == "" { buf_puts(sig, "void") } else { buf_puts(sig, pD.syms[sidx].ty) } pD.syms[sidx].detail = buf_str(sig) while pis("requires") or pis("ensures") or pis("invariant") or pis("effects") { padv() while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF and not pis("{") { if pis("(") or pis("[") { skip_group() } else { pi += 1 } } pskipnl() } if pis("{") { let close = cur_match() pD.syms[sidx].bstart = pD.tk_start[pi] if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) } var q = 0 while q < len(pD.syms) { if pD.syms[q].parent == sidx and pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.syms[sidx].bstart; pD.syms[q].bend = pD.syms[sidx].bend }; q += 1 } lp_block(sidx, pD.syms[sidx].bend) } return } if w == "entry" { padv() let first_param = len(pD.syms) if pis("(") { lp_params(parent, LS_PARAM, buf_new()) } # entry (h: mut Hiker): its states if pis("{") { let close = cur_match() var endo = cstr_len(pD.text) if close >= 0 { endo = pD.tk_end[close] } var q = first_param while q < len(pD.syms) { if pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.tk_start[pi]; pD.syms[q].bend = endo }; q += 1 } lp_block(parent, endo) } return } if (w == "handler") or (w == "edge") { if w == "edge" { padv(); if not pis("handler") { return } } padv() let nt = pname(); if nt < 0 { return } let sidx = sym_add(LS_SYSTEM, nt, parent) pD.syms[sidx].doc = collect_doc(pD, decl_tok) pD.syms[sidx].ty = "Update" if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) } # handler Draw(h: Hiker): its states while pk() == LT_ANNO { padv() } let vars = new []int var nv = 0 var have_query = false while true { if pis("phase") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt) } } else { if pis("query") { padv(); have_query = true; nv = lp_varlist(vars); break } else { if pis("reads") or pis("writes") or pis("uses") or pis("effects") { padv(); if pis("[") { skip_group() } } else { if pis("needs") { padv(); let x = pname(); if pis("[") { skip_group() } } else { break } } } } pskipnl() } pD.syms[sidx].detail = concat3("system ", pD.syms[sidx].name, concat3(" phase ", pD.syms[sidx].ty, "")) if have_query { let save = pi var scan = pi while scan < ntok(pD) and not tis(pD, scan, "{") and pD.tk_kind[scan] != LT_EOF { scan += 1 } var cl = -1 if scan < ntok(pD) { cl = pD.tmatch[scan] } var bs = 0 if scan < ntok(pD) { bs = pD.tk_start[scan] } var be = cstr_len(pD.text) if cl >= 0 { be = pD.tk_end[cl] } pi = save lp_query_terms(sidx, vars, nv, bs, be) } if pis("{") { let close = cur_match() pD.syms[sidx].bstart = pD.tk_start[pi] if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) } var q = 0 while q < len(pD.syms) { if pD.syms[q].parent == sidx and pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.syms[sidx].bstart; pD.syms[q].bend = pD.syms[sidx].bend }; q += 1 } lp_block(sidx, pD.syms[sidx].bend) } return } if w == "ui" { padv(); let nt = pname(); if nt < 0 { return } let sidx = sym_add(LS_UI, nt, parent) pD.syms[sidx].doc = collect_doc(pD, decl_tok) pD.syms[sidx].detail = concat3("ui ", pD.syms[sidx].name, "") let h = sym_add(LS_WIDGET, nt, sidx) pD.syms[h].name = concat3("UI_", pD.syms[sidx].name, "") pD.syms[h].ty = "int" pD.syms[h].detail = concat3("root handle of ui ", pD.syms[sidx].name, "") if pis("{") { let close = cur_match() pD.syms[sidx].bstart = pD.tk_start[pi] if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) } padv() while pi < ntok(pD) and pk() != LT_EOF and not pis("}") { if close >= 0 and pi >= close { break } let before = pi lp_widget(sidx, sidx) if pi == before { padv() } } if close >= 0 { pi = close + 1 } else { padv() } pskipnl() } return } if w == "scene" { padv(); let nt = pname(); if nt < 0 { return } let sidx = sym_add(LS_SCENE, nt, parent) pD.syms[sidx].doc = collect_doc(pD, decl_tok) var startmod = "" if pis("start") { startmod = " start"; padv() } pD.syms[sidx].detail = concat3("scene ", pD.syms[sidx].name, startmod) if pis("{") { let close = cur_match() pD.syms[sidx].bstart = pD.tk_start[pi] if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) } padv() while pi < ntok(pD) and pk() != LT_EOF and not pis("}") { if close >= 0 and pi >= close { break } let before = pi if pis("on") { padv(); let x = pname(); lp_block(sidx, pD.syms[sidx].bend) } else { if pis("layer") { padv(); let lt = pname() var lsidx = -1 if lt >= 0 { lsidx = sym_add(LS_LAYER, lt, sidx); pD.syms[lsidx].detail = concat3("layer ", pD.syms[lsidx].name, concat3(" of scene ", pD.syms[sidx].name, "")) } if pis("{") { let lc = cur_match() if lsidx >= 0 { pD.syms[lsidx].bstart = pD.tk_start[pi]; if lc >= 0 { pD.syms[lsidx].bend = pD.tk_end[lc] } else { pD.syms[lsidx].bend = pD.syms[sidx].bend } } padv() while pi < ntok(pD) and pk() != LT_EOF and not pis("}") { if lc >= 0 and pi >= lc { break } let b2 = pi var lo = sidx if lsidx >= 0 { lo = lsidx } lp_decl(lo) if pi == b2 { padv() } } if lc >= 0 { pi = lc + 1 } else { padv() } pskipnl() } } else { padv() } } if pi == before { padv() } } if close >= 0 { pi = close + 1 } else { padv() } pskipnl() } return } padv() } function parse_doc(D: Doc) -> void { D.syms = new []Sym D.imports = new []pointer D.is_unit = 0; D.is_module = 0; D.unit = "" pD = D; pi = 0 pskipnl() while pis("import") { lp_decl(-1); pskipnl() } if pis("program") { D.is_module = 1; D.is_unit = 1 padv() let nt = pname() var up = -1 if nt >= 0 { up = sym_add(LS_UNIT, nt, -1) D.unit = pD.syms[up].name pD.syms[up].detail = concat3("program ", D.unit, "") } if pis("{") { let close = cur_match() if up >= 0 { pD.syms[up].bstart = pD.tk_start[pi]; if close >= 0 { pD.syms[up].bend = pD.tk_end[close] } else { pD.syms[up].bend = cstr_len(pD.text) } } padv() while pi < ntok(pD) and pk() != LT_EOF and not pis("}") { if close >= 0 and pi >= close { break } let before = pi; lp_decl(up) if pi == before { padv() } } } return } while pi < ntok(pD) and pk() != LT_EOF { let before = pi; lp_decl(-1) if pi == before { padv() } } } # ============================================================================ # workspace: many documents, one project # ============================================================================ function is_local_kind(k: int) -> bool { return k == LS_LOCAL or k == LS_PARAM or k == LS_QUERYVAR or k == LS_STATE } function is_toplevel_kind(k: int) -> bool { return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT } function is_outline_kind(k: int) -> bool { return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER } # ---- paths & URIs ---- function hexval(c: int) -> int { if c >= '0' and c <= '9' { return c - 48 } let l = c | 32 if l >= 97 and l <= 102 { return l - 97 + 10 } return -1 } function uri_to_path(uri: pointer) -> pointer { var s = uri if starts_with(uri, "file://") { s = uri[7..cstr_len(uri)] } let n = cstr_len(s) let out = bytes(n + 1) var j = 0; var i = 0 while i < n { if s[i] == '%' and hexval(s[i + 1]) >= 0 and hexval(s[i + 2]) >= 0 { out[j] = hexval(s[i + 1]) * 16 + hexval(s[i + 2]); j += 1; i += 3 } else { out[j] = s[i]; j += 1; i += 1 } } out[j] = 0 return out } function uri_safe(c: int) -> bool { if char_is_alnum(c) { return true } return c == '-' or c == '.' or c == '~' or c == '/' # - . ~ / } function hexch(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n } function path_to_uri(path: pointer) -> pointer { let out = buf_new() buf_puts(out, "file://") var i = 0 while path[i] != 0 { let c = path[i] if uri_safe(c) { buf_putc(out, c) } else { buf_putc(out, '%'); buf_putc(out, hexch((c & 255) / 16)); buf_putc(out, hexch((c & 255) % 16)) } i += 1 } return buf_str(out) } function dirname(p: pointer) -> pointer { var last = -1 var i = 0 while p[i] != 0 { if p[i] == '/' { last = i }; i += 1 } if last < 0 { return "." } if last == 0 { return "/" } return p[0..last] } function normpath(p: pointer) -> pointer { let n = cstr_len(p) let abs = (n > 0 and p[0] == '/') let list = new []pointer var cnt = 0 var i = 0 while i < n { while i < n and p[i] == '/' { i += 1 } let st = i while i < n and p[i] != '/' { i += 1 } if i > st { let c = p[st..i] if (c == ".") { } else { if (c == "..") { if cnt > 0 and not (list[cnt - 1] == "..") { cnt -= 1 } else { if not abs { if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt += 1 } } } else { if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt += 1 } } } } if cnt == 0 { if abs { return "/" }; return "." } let out = buf_new() if abs { buf_putc(out, '/') } var k = 0 while k < cnt { if k > 0 { buf_putc(out, '/') }; buf_puts(out, list[k]); k += 1 } return buf_str(out) } function join_path(dir: pointer, rel: pointer) -> pointer { if rel[0] == '/' { return normpath(rel) } return normpath(concat3(dir, "/", rel)) } # ---- markdown scrub: blank everything outside a ```ludic fence ---- function word_ludic_at(s: pointer, at: int) -> bool { return ((s[at] == 'l' or s[at] == 'L')) and ((s[at + 1] == 'u' or s[at + 1] == 'U')) and ((s[at + 2] == 'd' or s[at + 2] == 'D')) and ((s[at + 3] == 'i' or s[at + 3] == 'I')) and ((s[at + 4] == 'c' or s[at + 4] == 'C')) } function scrub_markdown(src: pointer) -> pointer { let n = cstr_len(src) let out = bytes(n + 1) var c0 = 0 while c0 < n { out[c0] = src[c0]; c0 += 1 } out[n] = 0 var i = 0 while i < n { let ls = i var le = i while le < n and src[le] != '\n' { le += 1 } var ind = 0 while ls + ind < le and src[ls + ind] == ' ' { ind += 1 } let j = ls + ind var m = 0 if j < le { m = src[j] } var run = 0 while j + run < le and src[j + run] == m { run += 1 } let fenced = (m == '`' or m == '~') and run >= 3 let info = j + run var is_ludic = false if fenced { var kk = info while kk < le and (src[kk] == ' ' or src[kk] == '\t') { kk += 1 } if le - kk >= 5 { if word_ludic_at(src, kk) and (le - kk == 5 or src[kk + 5] == ' ' or src[kk + 5] == '\t' or src[kk + 5] == '\r') { is_ludic = true } } } var b = ls while b < le { out[b] = ' '; b += 1 } i = le + 1; if le >= n { i = n } if not fenced { continue } while i < n { let bs = i var be = i while be < n and src[be] != '\n' { be += 1 } var bi = 0 while bs + bi < be and src[bs + bi] == ' ' { bi += 1 } let cj = bs + bi var crun = 0 while cj + crun < be and src[cj + crun] == m { crun += 1 } var only = true var k2 = cj + crun while k2 < be { if src[k2] != ' ' and src[k2] != '\r' { only = false; break }; k2 += 1 } if crun >= run and only { var bb = bs; while bb < be { out[bb] = ' '; bb += 1 }; i = be + 1; if be >= n { i = n }; break } if not is_ludic { var bb = bs; while bb < be { out[bb] = ' '; bb += 1 } } i = be + 1; if be >= n { i = n } } } return out } # ---- documents ---- function set_text(D: Doc, raw: pointer) -> void { D.raw = raw if D.is_md == 1 { D.text = scrub_markdown(raw) } else { D.text = raw } D.src = D.text } function reindex(D: Doc) -> void { lex_doc(D) match_braces(D) parse_doc(D) D.cls = new []int var i = 0 let N = ntok(D) while i < N { push(D.cls, SC_NONE); i += 1 } } function new_doc(path: pointer, text: pointer) -> Doc { let D = new Doc D.path = normpath(path) D.uri = path_to_uri(D.path) D.is_md = 0 if ends_with(D.path, ".md") or ends_with(D.path, ".markdown") { D.is_md = 1 } D.syms = new []Sym; D.imports = new []pointer; D.unit = "" set_text(D, text) reindex(D) return D } function by_path(path: pointer) -> Doc { let p = normpath(path) var i = 0 while i < len(g_docs) { if (g_docs[i].path == p) { return g_docs[i] }; i += 1 } return null } function by_uri(uri: pointer) -> Doc { let p = uri_to_path(uri) let D = by_path(p) if (D != null) { return D } var i = 0 while i < len(g_docs) { if (g_docs[i].uri == uri) { return g_docs[i] }; i += 1 } return null } function ensure_doc(path: pointer) -> Doc { let D = by_path(path) if (D != null) { return D } let text = read_file(path) if (text == null) { return null } let nd = new_doc(path, text) push(g_docs, nd) return nd } function scan_root(root: pointer) -> void { if (root == null) { return } let listfile = `{Os.temp_dir()}/ludic_lsp_scan_{Os.pid()}.txt` # per process: editors run several servers run(concat3(concat3("find '", root, "' -name '*.ludic' -not -path '*/build/*' -not -path '*/node_modules/*' -not -path '*/.*' > "), listfile, " 2>/dev/null")) let list = read_file(listfile) if (list == null) { return } var i = 0; var start = 0 while list[i] != 0 { if list[i] == '\n' { if i > start { let d = ensure_doc(list[start..i]) }; start = i + 1 } i += 1 } if i > start { let d = ensure_doc(list[start..i]) } } # ---- cross-file resolution ---- var g_owner: Doc = null var g_symidx: int = 0 function imports_of(D: Doc, out: []Doc) -> void { var i = 0 while i < len(D.imports) { let files = resolve_import_all(D.path, D.imports[i]) var f = 0 while f < len(files) { let I = ensure_doc(files[f]) if (I != null) { var seen = false var k = 0 while k < len(out) { if (out[k].path == I.path) { seen = true }; k += 1 } if not seen { push(out, I); imports_of(I, out) } } f += 1 } i += 1 } } function related(D: Doc) -> []Doc { let out = new []Doc if (D == null) { return out } push(out, D) imports_of(D, out) var i = 0 while i < len(g_docs) { let R = g_docs[i] if R.is_unit == 1 and not (R.path == D.path) { let reach = new []Doc imports_of(R, reach) var hits = false var k = 0 while k < len(reach) { if (reach[k].path == D.path) { hits = true }; k += 1 } if hits { var seen = false k = 0 while k < len(out) { if (out[k].path == R.path) { seen = true }; k += 1 } if not seen { push(out, R) } k = 0 while k < len(reach) { var s2 = false var q = 0 while q < len(out) { if (out[q].path == reach[k].path) { s2 = true }; q += 1 } if not s2 { push(out, reach[k]) } k += 1 } } } i += 1 } return out } function unit_root(D: Doc) -> Doc { if (D == null) or D.is_md == 1 { return null } if D.is_unit == 1 { return D } let rel = related(D) var i = 0 while i < len(rel) { if rel[i].is_unit == 1 and rel[i].is_md == 0 { return rel[i] }; i += 1 } return null } function find_local(D: Doc, name: pointer, off: int) -> int { var best = -1 var i = 0 while i < len(D.syms) { let s = D.syms[i] if is_local_kind(s.kind) and (s.name == name) { if off >= s.bstart and off <= s.bend { if best < 0 or s.bstart >= D.syms[best].bstart { best = i } } } i += 1 } return best } # returns sym index in g_owner (set), or -1 function find_top(D: Doc, name: pointer) -> int { let rel = related(D) var i = 0 while i < len(rel) { var k = 0 while k < len(rel[i].syms) { if is_toplevel_kind(rel[i].syms[k].kind) and (rel[i].syms[k].name == name) { g_owner = rel[i]; return k } k += 1 } i += 1 } i = 0 while i < len(g_docs) { var k = 0 while k < len(g_docs[i].syms) { if is_toplevel_kind(g_docs[i].syms[k].kind) and (g_docs[i].syms[k].name == name) { g_owner = g_docs[i]; return k } k += 1 } i += 1 } return -1 } # component sym index (in g_owner) for a dotted receiver, or -1 function receiver_component(D: Doc, dot_tok: int) -> int { let recv = prev_sig(D, dot_tok) if recv < 0 or D.tk_kind[recv] != LT_ID { return -1 } let name = ttext(D, recv) var ty = null let l = find_local(D, name, D.tk_start[recv]) if l >= 0 { ty = D.syms[l].ty } else { let t = find_top(D, name); if t >= 0 { ty = g_owner.syms[t].ty } } if (ty == null) { return -1 } if (ty == "") { return -1 } return find_top(D, ty) } # resolve the token: sets g_owner/g_symidx; returns the token index (or -1) function resolve_tok(D: Doc, tok: int) -> int { g_owner = null; g_symidx = -1 if tok < 0 { return -1 } if D.tk_kind[tok] != LT_ID and D.tk_kind[tok] != LT_STR { return -1 } let name = ttext(D, tok) var i = 0 while i < len(D.syms) { if D.syms[i].tok == tok { g_owner = D; g_symidx = i; return tok }; i += 1 } let prev = prev_sig(D, tok) if prev >= 0 and tis(D, prev, ".") { let comp = receiver_component(D, prev) if comp >= 0 and (g_owner != null) { let co = g_owner var k = 0 while k < len(co.syms) { if co.syms[k].kind == LS_FIELD and co.syms[k].parent == comp and (co.syms[k].name == name) { g_owner = co; g_symidx = k; return tok } k += 1 } } g_owner = null; g_symidx = -1 return tok } let l = find_local(D, name, D.tk_start[tok]) if l >= 0 { g_owner = D; g_symidx = l; return tok } let t = find_top(D, name) if t >= 0 { g_symidx = t; return tok } g_owner = null; g_symidx = -1 return tok } function resolve_at(D: Doc, off: int) -> int { return resolve_tok(D, tok_at(D, off)) } function first_on_line(D: Doc, tok: int) -> bool { let p = prev_sig(D, tok); return p < 0 or D.tk_line[p] != D.tk_line[tok] } function in_ui_body(D: Doc, off: int) -> bool { var i = 0 while i < len(D.syms) { if D.syms[i].kind == LS_UI and off > D.syms[i].bstart and off < D.syms[i].bend { return true }; i += 1 } return false } # ---- builtins / intrinsics (name -> signature) ---- var g_builtins: []pointer var g_bsigs: []pointer function badd(name: pointer, sig: pointer) -> void { push(g_builtins, name); push(g_bsigs, sig) } function is_builtin_name(name: pointer) -> bool { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return true }; i += 1 }; return false } function builtin_sig(name: pointer) -> pointer { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i += 1 }; return null } # The namespaced standard-library surface: `Namespace.method`, mirroring the # compiler's emit_ns_call (selfhost/backend/emit_call.ludic). Screen/Random/Input/Map # methods resolve to the underlying bare builtin's signature; Math/Text/List # are pure fixed-point / string / slice ops with their own signatures. Returns # the signature label for signature help, or null if it is not a known method. function ns_method_sig(ns: pointer, meth: pointer) -> pointer { if (ns == "Screen") { if (meth == "clear") { return builtin_sig("clear") } if (meth == "fill_rectangle") { return builtin_sig("fill_rect") } if (meth == "draw_rectangle") { return builtin_sig("frame_rect") } if (meth == "put_pixel") { return builtin_sig("put_px") } if (meth == "draw_text") { return builtin_sig("text") } if (meth == "draw_number") { return builtin_sig("text_int") } if (meth == "show") { return builtin_sig("present") } if (meth == "width") { return "Screen.width() -> int" } if (meth == "height") { return "Screen.height() -> int" } if (meth == "status") { return builtin_sig("status") } if (meth == "line") { return "Screen.line(x1, y1, x2, y2, color)" } if (meth == "circle") { return "Screen.circle(x, y, radius, color)" } if (meth == "fill_circle") { return "Screen.fill_circle(x, y, radius, color)" } if (meth == "triangle") { return "Screen.triangle(x1, y1, x2, y2, x3, y3, color)" } if (meth == "fill_triangle") { return "Screen.fill_triangle(x1, y1, x2, y2, x3, y3, color)" } if (meth == "sprite") { return "Screen.sprite(id, x, y)" } if (meth == "sprite_scaled") { return "Screen.sprite_scaled(id, x, y, scale)" } } if (ns == "Map") { if (meth == "size") { return builtin_sig("map_size") } if (meth == "row") { return builtin_sig("map_row") } if (meth == "tile") { return builtin_sig("tile") } } if (ns == "Random") { if (meth == "range") { return builtin_sig("rng_range") } if (meth == "chance") { return builtin_sig("rng_chance") } if (meth == "seed") { return builtin_sig("seed") } if (meth == "value") { return "Random.value() -> fixed" } if (meth == "int") { return "Random.int(max) -> int" } if (meth == "sign") { return "Random.sign() -> int" } } if (ns == "Color") { if (meth == "rgb") { return "Color.rgb(r, g, b) -> int" } if (meth == "rgba") { return "Color.rgba(r, g, b, a) -> int" } if (meth == "lerp") { return "Color.lerp(c0, c1, t) -> int" } if (meth == "darken") { return "Color.darken(c, amount) -> int" } if (meth == "lighten") { return "Color.lighten(c, amount) -> int" } if (meth == "with_alpha") { return "Color.with_alpha(c, a) -> int" } } if (ns == "Input") { if (meth == "key") { return builtin_sig("key") } } if (ns == "Math") { if (meth == "min") { return "Math.min(a, b) -> int" } if (meth == "max") { return "Math.max(a, b) -> int" } if (meth == "abs") { return "Math.abs(x) -> int" } if (meth == "clamp") { return "Math.clamp(v, lo, hi) -> int" } if (meth == "sign") { return "Math.sign(x) -> int" } if (meth == "floor") { return "Math.floor(x) -> int" } if (meth == "ceil") { return "Math.ceil(x) -> int" } if (meth == "round") { return "Math.round(x) -> int" } if (meth == "lerp") { return "Math.lerp(a, b, t) -> fixed" } if (meth == "inverse_lerp") { return "Math.inverse_lerp(a, b, v) -> fixed" } if (meth == "remap") { return "Math.remap(v, in0, in1, out0, out1) -> fixed" } if (meth == "sqrt") { return "Math.sqrt(x) -> fixed" } if (meth == "sin") { return "Math.sin(radians) -> fixed" } if (meth == "cos") { return "Math.cos(radians) -> fixed" } if (meth == "tan") { return "Math.tan(radians) -> fixed" } if (meth == "atan2") { return "Math.atan2(y, x) -> fixed" } if (meth == "asin") { return "Math.asin(x) -> fixed" } if (meth == "acos") { return "Math.acos(x) -> fixed" } if (meth == "hypot") { return "Math.hypot(x, y) -> fixed" } if (meth == "dist") { return "Math.dist(x0, y0, x1, y1) -> fixed" } if (meth == "dist2") { return "Math.dist2(x0, y0, x1, y1) -> fixed" } if (meth == "deg_to_rad") { return "Math.deg_to_rad(degrees) -> fixed" } if (meth == "rad_to_deg") { return "Math.rad_to_deg(radians) -> fixed" } if (meth == "posmod") { return "Math.posmod(a, m) -> int" } if (meth == "wrap") { return "Math.wrap(v, lo, hi) -> int" } if (meth == "ping_pong") { return "Math.ping_pong(t, len) -> int" } if (meth == "snapped") { return "Math.snapped(v, step) -> fixed" } if (meth == "move_toward") { return "Math.move_toward(from, to, delta) -> fixed" } if (meth == "smoothstep") { return "Math.smoothstep(e0, e1, x) -> fixed" } } if (ns == "Text") { if (meth == "length") { return "Text.length(s) -> int" } if (meth == "char_at") { return "Text.char_at(s, i) -> int" } if (meth == "slice") { return "Text.slice(s, a, b) -> string" } if (meth == "starts_with") { return "Text.starts_with(s, prefix) -> bool" } if (meth == "ends_with") { return "Text.ends_with(s, suffix) -> bool" } if (meth == "contains") { return "Text.contains(s, sub) -> bool" } if (meth == "index_of") { return "Text.index_of(s, sub) -> int" } if (meth == "equals") { return "Text.equals(a, b) -> bool" } if (meth == "concat") { return "Text.concat(a, b) -> string" } if (meth == "to_int") { return "Text.to_int(s) -> int" } if (meth == "to_float") { return "Text.to_float(s) -> float" } if (meth == "from_int") { return "Text.from_int(n) -> string" } if (meth == "upper") { return "Text.upper(s) -> string" } if (meth == "lower") { return "Text.lower(s) -> string" } if (meth == "trim") { return "Text.trim(s) -> string" } if (meth == "repeat") { return "Text.repeat(s, n) -> string" } if (meth == "pad_left") { return "Text.pad_left(s, width) -> string" } if (meth == "pad_right") { return "Text.pad_right(s, width) -> string" } if (meth == "split") { return "Text.split(s, sep) -> []string" } if (meth == "join") { return "Text.join(parts, sep) -> string" } if (meth == "replace") { return "Text.replace(s, from, to) -> string" } } if (ns == "Hash") { if (meth == "of") { return "Hash.of(s) -> int" } if (meth == "fnv1a") { return "Hash.fnv1a(s) -> int" } if (meth == "crc32") { return "Hash.crc32(s) -> int" } if (meth == "mix") { return "Hash.mix(x) -> int" } if (meth == "combine") { return "Hash.combine(...) -> int" } if (meth == "of64") { return "Hash.of64(s) -> long" } if (meth == "fnv1a_64") { return "Hash.fnv1a_64(s) -> long" } if (meth == "mix64") { return "Hash.mix64(x) -> long" } } if (ns == "Vector") { if (meth == "make") { return "Vector.make(x, y) -> Vector" } if (meth == "zero") { return "Vector.zero() -> Vector" } if (meth == "x") { return "Vector.x(v) -> fixed" } if (meth == "y") { return "Vector.y(v) -> fixed" } if (meth == "add") { return "Vector.add(a, b) -> Vector" } if (meth == "sub") { return "Vector.sub(a, b) -> Vector" } if (meth == "scale") { return "Vector.scale(v, s) -> Vector" } if (meth == "dot") { return "Vector.dot(a, b) -> fixed" } if (meth == "length") { return "Vector.length(v) -> fixed" } if (meth == "distance") { return "Vector.distance(a, b) -> fixed" } if (meth == "normalize") { return "Vector.normalize(v) -> Vector" } if (meth == "lerp") { return "Vector.lerp(a, b, t) -> Vector" } if (meth == "rotate") { return "Vector.rotate(v, angle) -> Vector" } if (meth == "angle") { return "Vector.angle(v) -> fixed" } if (meth == "from_angle") { return "Vector.from_angle(a) -> Vector" } } if (ns == "IVec2") { if (meth == "make") { return "IVec2.make(x, y) -> IVec2" } if (meth == "zero") { return "IVec2.zero() -> IVec2" } if (meth == "x") { return "IVec2.x(v) -> int" } if (meth == "y") { return "IVec2.y(v) -> int" } if (meth == "add") { return "IVec2.add(a, b) -> IVec2" } if (meth == "sub") { return "IVec2.sub(a, b) -> IVec2" } if (meth == "scale") { return "IVec2.scale(v, s) -> IVec2" } if (meth == "dot") { return "IVec2.dot(a, b) -> int" } if (meth == "equal") { return "IVec2.equal(a, b) -> bool" } if (meth == "manhattan") { return "IVec2.manhattan(a, b) -> int" } if (meth == "to_vector") { return "IVec2.to_vector(v) -> Vector" } } if (ns == "Rect") { if (meth == "make") { return "Rect.make(x, y, w, h) -> Rect" } if (meth == "x") { return "Rect.x(r) -> fixed" } if (meth == "y") { return "Rect.y(r) -> fixed" } if (meth == "w") { return "Rect.w(r) -> fixed" } if (meth == "h") { return "Rect.h(r) -> fixed" } if (meth == "right") { return "Rect.right(r) -> fixed" } if (meth == "bottom") { return "Rect.bottom(r) -> fixed" } if (meth == "center") { return "Rect.center(r) -> Vector" } if (meth == "contains") { return "Rect.contains(r, px, py) -> bool" } if (meth == "intersects") { return "Rect.intersects(a, b) -> bool" } } if (ns == "List") { if (meth == "len") { return "List.len(s) -> int" } if (meth == "push") { return "List.push(s, v)" } if (meth == "clear") { return "List.clear(s)" } if (meth == "first") { return "List.first(s) -> T" } if (meth == "last") { return "List.last(s) -> T" } if (meth == "pop") { return "List.pop(s) -> T" } if (meth == "swap") { return "List.swap(s, i, j)" } if (meth == "contains") { return "List.contains(s, v) -> bool" } if (meth == "index_of") { return "List.index_of(s, v) -> int" } if (meth == "reverse") { return "List.reverse(s)" } if (meth == "insert") { return "List.insert(s, i, v)" } if (meth == "remove_at") { return "List.remove_at(s, i)" } if (meth == "remove") { return "List.remove(s, v)" } if (meth == "sort") { return "List.sort(s)" } } if (ns == "Ease") { if (meth == "in") { return "Ease.in(t) -> fixed" } if (meth == "out") { return "Ease.out(t) -> fixed" } if (meth == "in_out") { return "Ease.in_out(t) -> fixed" } if (meth == "back") { return "Ease.back(t) -> fixed" } if (meth == "bounce") { return "Ease.bounce(t) -> fixed" } } if (ns == "Collision") { if (meth == "rects") { return "Collision.rects(ax, ay, aw, ah, bx, by, bw, bh) -> bool" } if (meth == "point_rect") { return "Collision.point_rect(px, py, rx, ry, rw, rh) -> bool" } if (meth == "circles") { return "Collision.circles(ax, ay, ar, bx, by, br) -> bool" } if (meth == "rect_circle") { return "Collision.rect_circle(rx, ry, rw, rh, cx, cy, cr) -> bool" } } if (ns == "World") { if (meth == "get") { return "World.get(prop_id, field_id, entity) -> int" } if (meth == "set") { return "World.set(prop_id, field_id, entity, value)" } if (meth == "has") { return "World.has(entity, prop_id) -> int" } if (meth == "count") { return "World.count() -> int" } if (meth == "size") { return "World.size() -> int" } if (meth == "spawn") { return "World.spawn(model_id) -> entity" } if (meth == "save") { return "World.save(buf) -> int" } if (meth == "load") { return "World.load(buf, len)" } if (meth == "prop_id") { return "World.prop_id(name) -> int" } if (meth == "field_id") { return "World.field_id(prop_id, name) -> int" } if (meth == "model_id") { return "World.model_id(name) -> int" } if (meth == "kind") { return "World.kind(entity) -> int" } if (meth == "register_prop") { return "World.register_prop(name, size) -> int" } if (meth == "attach") { return "World.attach(entity, prop_id)" } if (meth == "detach") { return "World.detach(entity, prop_id)" } if (meth == "query_next") { return "World.query_next(query_id, cursor) -> int" } } if (ns == "Network") { if (meth == "send") { return "Network.send(peer, buf, len)" } if (meth == "poll") { return "Network.poll(buf, cap) -> int" } if (meth == "serialize") { return "Network.serialize(entity, buf) -> int" } if (meth == "apply") { return "Network.apply(entity, buf) -> int" } if (meth == "owner") { return "Network.owner(entity) -> int" } if (meth == "set_owner") { return "Network.set_owner(entity, peer)" } if (meth == "is_server") { return "Network.is_server() -> bool" } if (meth == "is_owner") { return "Network.is_owner(entity) -> bool" } if (meth == "local_id") { return "Network.local_id() -> int" } } if (ns == "System") { if (meth == "run") { return "System.run(command) -> int" } if (meth == "read_char") { return "System.read_char() -> int" } if (meth == "file_open") { return "System.file_open(path, mode) -> pointer" } if (meth == "file_read") { return "System.file_read(file, buf, n) -> int" } if (meth == "file_write") { return "System.file_write(file, buf, n) -> int" } if (meth == "file_seek") { return "System.file_seek(file, offset, whence) -> int" } if (meth == "file_tell") { return "System.file_tell(file) -> int" } if (meth == "file_close") { return "System.file_close(file)" } } if (ns == "Save") { if (meth == "write") { return "Save.write()" } if (meth == "read") { return "Save.read() -> bool" } } if (ns == "Time") { if (meth == "frame") { return "Time.frame() -> int" } if (meth == "delta") { return "Time.delta() -> fixed" } if (meth == "elapsed") { return "Time.elapsed() -> fixed" } if (meth == "now") { return "Time.now() -> int" } if (meth == "since") { return "Time.since(past) -> int" } } if (ns == "Duration") { if (meth == "seconds") { return "Duration.seconds(n) -> int" } if (meth == "minutes") { return "Duration.minutes(n) -> int" } if (meth == "hours") { return "Duration.hours(n) -> int" } if (meth == "days") { return "Duration.days(n) -> int" } if (meth == "as_seconds") { return "Duration.as_seconds(span) -> int" } if (meth == "as_minutes") { return "Duration.as_minutes(span) -> int" } if (meth == "as_hours") { return "Duration.as_hours(span) -> int" } if (meth == "as_days") { return "Duration.as_days(span) -> int" } } if (ns == "Date") { if (meth == "new") { return "Date.new(year, month, day) -> int" } if (meth == "year") { return "Date.year(date) -> int" } if (meth == "month") { return "Date.month(date) -> int" } if (meth == "day") { return "Date.day(date) -> int" } if (meth == "weekday") { return "Date.weekday(date) -> int" } if (meth == "is_leap") { return "Date.is_leap(year) -> bool" } if (meth == "days_in_month") { return "Date.days_in_month(year, month) -> int" } if (meth == "to_epoch") { return "Date.to_epoch(date) -> int" } if (meth == "add_days") { return "Date.add_days(date, n) -> int" } if (meth == "diff_days") { return "Date.diff_days(a, b) -> int" } } if (ns == "DateTime") { if (meth == "from") { return "DateTime.from(year, month, day, hour, minute, second) -> int" } if (meth == "date") { return "DateTime.date(dt) -> int" } if (meth == "add") { return "DateTime.add(dt, span) -> int" } if (meth == "year") { return "DateTime.year(dt) -> int" } if (meth == "month") { return "DateTime.month(dt) -> int" } if (meth == "day") { return "DateTime.day(dt) -> int" } if (meth == "weekday") { return "DateTime.weekday(dt) -> int" } if (meth == "hour") { return "DateTime.hour(dt) -> int" } if (meth == "minute") { return "DateTime.minute(dt) -> int" } if (meth == "second") { return "DateTime.second(dt) -> int" } if (meth == "format") { return "DateTime.format(dt, pattern) -> string" } if (meth == "parse") { return "DateTime.parse(text, pattern) -> int" } } if (ns == "Clock") { if (meth == "now") { return "Clock.now() -> int" } if (meth == "set") { return "Clock.set(t)" } if (meth == "advance") { return "Clock.advance(span)" } if (meth == "reset") { return "Clock.reset()" } } if (ns == "Memory") { if (meth == "bytes") { return "Memory.bytes(n) -> pointer" } if (meth == "words") { return "Memory.words(n) -> words" } if (meth == "copy") { return "Memory.copy(dst, src, n)" } if (meth == "fill") { return "Memory.fill(buf, value, n)" } if (meth == "peek") { return "Memory.peek(buf, i) -> int" } if (meth == "poke") { return "Memory.poke(buf, i, value)" } } return null } function init_builtins() -> void { g_builtins = new []pointer; g_bsigs = new []pointer badd("min", "min(a: int, b: int) -> int") badd("max", "max(a: int, b: int) -> int") badd("abs", "abs(a: int) -> int") badd("clamp", "clamp(v: int, lo: int, hi: int) -> int") badd("seed", "seed(i: int)") badd("rng_range", "rng_range(lo: int, hi: int) -> int") badd("rng_chance", "rng_chance(pct: int) -> bool") badd("fx", "fixed(i: int) -> fixed") badd("flr", "floor(f: fixed) -> int") badd("map_size", "map_size(w: int, h: int)") badd("map_row", "map_row(y: int, row: string)") badd("tile", "tile(x: int, y: int) -> int") badd("clear", "clear(color: int)") badd("present", "present()") badd("fill_rect", "fill_rect(x: int, y: int, w: int, h: int, color: int)") badd("frame_rect", "frame_rect(x: int, y: int, w: int, h: int, color: int)") badd("put_px", "put_px(x: int, y: int, color: int)") badd("text", "text(x: int, y: int, s: string, color: int, scale: int)") badd("text_int", "text_int(x: int, y: int, n: int, color: int, scale: int)") badd("font_load", "font_load(path: string) -> int") badd("text_ttf", "text_ttf(font: int, x: int, y: int, utf8: string, color: int, px: int)") badd("text_w", "text_w(font: int, utf8: string, px: int) -> int") badd("text_h", "text_h(font: int, px: int) -> int") badd("image_load", "image_load(path: string) -> int") badd("draw_image", "draw_image(id: int, x: int, y: int)") badd("draw_image_scaled", "draw_image_scaled(id: int, x: int, y: int, w: int, h: int)") badd("draw_9slice", "draw_9slice(id: int, x: int, y: int, w: int, h: int, inset: int)") badd("png_load", "png_load(path: string) -> int") badd("sprites_load", "sprites_load(path: string)") badd("draw_sprite", "draw_sprite(id: int, x: int, y: int)") badd("draw_sprite_scaled", "draw_sprite_scaled(id: int, x: int, y: int, scale: int)") badd("ui_build", "ui_build()") badd("ui_open", "ui_open(id: int)") badd("ui_tick", "ui_tick(key: int)") badd("ui_render", "ui_render()") badd("ui_clicked", "ui_clicked(id: int) -> bool") badd("ui_set_text", "ui_set_text(id: int, s: string)") badd("ui_set_int", "ui_set_int(id: int, n: int)") badd("ui_focus", "ui_focus(id: int)") badd("ui_focused", "ui_focused() -> int") badd("ui_visible", "ui_visible(id: int, on: bool)") badd("key", "key() -> int") badd("reg", "reg(i: int) -> int") badd("set_reg", "set_reg(i: int, v: int)") badd("self", "self() -> entity") badd("save", "save()") badd("load", "load() -> bool") badd("status", "status(s: string)") badd("print", "print(x)") badd("string", "string(x) -> string") badd("quit", "quit()") badd("bytes", "bytes(n: int) -> pointer") badd("words", "words(n: int) -> words") badd("resize", "resize(p: pointer, n: int) -> pointer") badd("arg_count", "arg_count() -> int") badd("arg", "arg(i: int) -> string") badd("file_stderr", "file_stderr() -> pointer") badd("file_stdout", "file_stdout() -> pointer") badd("free", "free(p: pointer)") badd("fill", "fill(p: pointer, byte: int, n: int)") badd("offset", "offset(p: pointer, off: int) -> pointer") badd("as_fixed", "as_fixed(i: int) -> fixed") badd("as_int", "as_int(f: fixed) -> int") badd("file_open", "file_open(path: string, mode: string) -> pointer") badd("file_read", "file_read(f: pointer, buf: pointer, n: int) -> int") badd("file_write", "file_write(f: pointer, buf: pointer, n: int) -> int") badd("file_seek", "file_seek(f: pointer, off: int, whence: int) -> int") badd("file_tell", "file_tell(f: pointer) -> int") badd("file_close", "file_close(f: pointer)") badd("read_char", "read_char() -> int") badd("exit", "exit(code: int)") badd("is_windowed", "is_windowed() -> bool") badd("game_title", "game_title() -> string") badd("win_open", "win_open(title: string, w: int, h: int, scale: int)") badd("win_poll", "win_poll() -> int") badd("win_present", "win_present(px: pointer, w: int, h: int)") badd("win_running", "win_running() -> bool") badd("win_close", "win_close()") } # ---- classification ---- function class_of_symkind(k: int) -> int { if k == LS_COMPONENT { return SC_COMPONENT } if k == LS_ARCHETYPE { return SC_ARCHETYPE } if k == LS_FIELD { return SC_FIELD } if k == LS_CONST { return SC_CONST } if k == LS_VAR { return SC_MODVAR } if k == LS_FN or k == LS_EXTERN { return SC_FUNCTION } if k == LS_SYSTEM { return SC_SYSTEM } if k == LS_UI { return SC_UI } if k == LS_WIDGET { return SC_CONST } if k == LS_SCENE { return SC_SCENE } if k == LS_LAYER { return SC_LAYER } if k == LS_PARAM { return SC_PARAM } if k == LS_QUERYVAR or k == LS_LOCAL or k == LS_STATE { return SC_VARIABLE } if k == LS_UNIT { return SC_UI } return SC_UNKNOWN } function classify(D: Doc) -> void { let N = ntok(D) if len(D.cls) < N { D.cls = new []int; var z = 0; while z < N { push(D.cls, SC_NONE); z += 1 } } D.clsmods = new []int var zm = 0 while zm < N { push(D.clsmods, 0); zm += 1 } var i = 0 while i < N { let k = D.tk_kind[i] var c = SC_NONE if k == LT_COMMENT { c = SC_COMMENT } else { if k == LT_STR or k == LT_CHAR { c = SC_STRING } else { if k == LT_INT or k == LT_FLOAT { c = SC_NUMBER } else { if k == LT_BOOL or k == LT_KW { c = SC_KEYWORD } else { if k == LT_TYPE { c = SC_TYPE } else { if k == LT_PHASE { c = SC_PHASE } else { if k == LT_ANNO { c = SC_ANNOTATION } else { if k == LT_OP { c = SC_OPERATOR } else { if k == LT_ID { c = SC_UNKNOWN } } } } } } } } } if k != LT_ID { D.cls[i] = c; i += 1; continue } let name = ttext(D, i) var decl = -1 var s = 0 while s < len(D.syms) { if D.syms[s].tok == i { decl = s; break }; s += 1 } if decl >= 0 { D.cls[i] = class_of_symkind(D.syms[decl].kind) D.clsmods[i] = SEM_DECLARATION | vendor_mod(D) i += 1; continue } let prev = prev_sig(D, i) if prev >= 0 and tis(D, prev, ".") { # Namespace.member: a standard-library call or constant let recv = prev_sig(D, prev) if recv >= 0 and D.tk_kind[recv] == LT_ID and is_namespace_name(ttext(D, recv)) { let nx = next_sig(D, i) if nx >= 0 and tis(D, nx, "(") { D.cls[i] = SC_BUILTIN } else { D.cls[i] = SC_CONST; D.clsmods[i] = SEM_DEFAULT_LIBRARY } i += 1; continue } D.cls[i] = SC_FIELD; i += 1; continue } if is_namespace_name(name) and find_top(D, name) < 0 { let nx = next_sig(D, i) if nx >= 0 and tis(D, nx, ".") { D.cls[i] = SC_NAMESPACE; i += 1; continue } } let nxt = next_sig(D, i) var followed_eq = false if nxt >= 0 and tis(D, nxt, "=") { followed_eq = true } if in_ui_body(D, D.tk_start[i]) { if followed_eq { D.cls[i] = SC_PROP; i += 1; continue } if first_on_line(D, i) or (prev >= 0 and (tis(D, prev, "{") or tis(D, prev, "}"))) { if is_widget_word(name) { D.cls[i] = SC_WIDGET } else { D.cls[i] = SC_UI } i += 1; continue } } let l = find_local(D, name, D.tk_start[i]) if l >= 0 { if D.syms[l].kind == LS_PARAM { D.cls[i] = SC_PARAM } else { D.cls[i] = SC_VARIABLE }; i += 1; continue } let top = find_top(D, name) if top >= 0 { D.cls[i] = class_of_symkind(g_owner.syms[top].kind); D.clsmods[i] = vendor_mod(g_owner); i += 1; continue } if is_builtin_name(name) or docs_index(name) >= 0 { D.cls[i] = SC_BUILTIN; i += 1; continue } if followed_eq { D.cls[i] = SC_FIELD; i += 1; continue } D.cls[i] = SC_UNKNOWN i += 1 } } # semantic token type index + modifiers (via g_semmods) var g_semmods: int = 0 function sem_type_of(cls: int) -> int { g_semmods = 0 if cls == SC_KEYWORD { return 0 } if cls == SC_TYPE { g_semmods = 4; return 1 } if cls == SC_COMPONENT { return 2 } if cls == SC_ARCHETYPE { return 3 } if cls == SC_SCENE { return 4 } if cls == SC_LAYER { return 4 } if cls == SC_UI { return 3 } if cls == SC_WIDGET { g_semmods = 4; return 3 } if cls == SC_SYSTEM { g_semmods = 2; return 5 } # a handler: readonly function if cls == SC_FUNCTION { return 5 } if cls == SC_BUILTIN { g_semmods = 4; return 5 } if cls == SC_PARAM { return 6 } if cls == SC_VARIABLE { return 7 } if cls == SC_MODVAR { g_semmods = 16; return 7 } if cls == SC_CONST { g_semmods = 18; return 7 } if cls == SC_PROP { return 8 } if cls == SC_FIELD { return 8 } if cls == SC_PHASE { g_semmods = 6; return 9 } if cls == SC_NUMBER { return 10 } if cls == SC_STRING { return 11 } if cls == SC_COMMENT { return 12 } if cls == SC_OPERATOR { return 13 } if cls == SC_ANNOTATION { return 14 } if cls == SC_NAMESPACE { g_semmods = 4; return 4 } return -1 } # ============================================================================ # JSON (just enough for LSP) # ============================================================================ const JNULL: int = 0 const JBOOL: int = 1 const JNUM: int = 2 const JSTR: int = 3 const JARR: int = 4 const JOBJ: int = 5 property JVal { t: int = 0, num: int = 0, b: int = 0, s: pointer = null, kids: []JVal, keys: []pointer } function jval(t: int) -> JVal { let v = new JVal; v.t = t; v.kids = new []JVal; v.keys = new []pointer; return v } var jsrc: pointer = null var jp: int = 0 function jskip() -> void { while jsrc[jp] == ' ' or jsrc[jp] == '\t' or jsrc[jp] == '\n' or jsrc[jp] == '\r' { jp += 1 } } function jhex4(s: pointer, at: int) -> int { var v = 0; var i = 0 while i < 4 { let h = hexval(s[at + i]); if h < 0 { return v }; v = v * 16 + h; i += 1 } return v } function utf8_emit(o: Buf, cp: int) -> void { if cp < 128 { buf_putc(o, cp) } else { if cp < 2048 { buf_putc(o, 192 | (cp >> 6)); buf_putc(o, 128 | (cp & 63)) } else { if cp < 65536 { buf_putc(o, 224 | (cp >> 12)); buf_putc(o, 128 | ((cp >> 6) & 63)); buf_putc(o, 128 | (cp & 63)) } else { buf_putc(o, 240 | (cp >> 18)); buf_putc(o, 128 | ((cp >> 12) & 63)); buf_putc(o, 128 | ((cp >> 6) & 63)); buf_putc(o, 128 | (cp & 63)) } } } } function jparse_string() -> pointer { jp += 1 # skip opening quote let o = buf_new() while jsrc[jp] != 0 and jsrc[jp] != '"' { if jsrc[jp] == '\\' { jp += 1 let c = jsrc[jp]; jp += 1 if c == 'n' { buf_putc(o, '\n') } else { if c == 't' { buf_putc(o, '\t') } else { if c == 'r' { buf_putc(o, '\r') } else { if c == 'b' { buf_putc(o, 8) } else { if c == 'f' { buf_putc(o, 12) } else { if c == 'u' { var cp = jhex4(jsrc, jp); jp += 4 if cp >= 55296 and cp < 56320 and jsrc[jp] == '\\' and jsrc[jp + 1] == 'u' { let lo = jhex4(jsrc, jp + 2); jp += 6 cp = 65536 + ((cp - 55296) << 10) + (lo - 56320) } utf8_emit(o, cp) } else { buf_putc(o, c) } } } } } } } else { buf_putc(o, jsrc[jp]); jp += 1 } } if jsrc[jp] == '"' { jp += 1 } return buf_str(o) } function jparse() -> JVal { jskip() let c = jsrc[jp] if c == '{' { # '{' jp += 1 let v = jval(JOBJ) while true { jskip() if jsrc[jp] == '}' { jp += 1; break } var key = "" if jsrc[jp] == '"' { key = jparse_string() } jskip(); if jsrc[jp] == ':' { jp += 1 } let kid = jparse() push(v.keys, key); push(v.kids, kid) jskip() if jsrc[jp] == ',' { jp += 1 } else { if jsrc[jp] == '}' { jp += 1; break } else { if jsrc[jp] == 0 { break } } } } return v } if c == '[' { # '[' jp += 1 let v = jval(JARR) while true { jskip() if jsrc[jp] == ']' { jp += 1; break } let kid = jparse() push(v.keys, ""); push(v.kids, kid) jskip() if jsrc[jp] == ',' { jp += 1 } else { if jsrc[jp] == ']' { jp += 1; break } else { if jsrc[jp] == 0 { break } } } } return v } if c == '"' { let v = jval(JSTR); v.s = jparse_string(); return v } if jsrc[jp] == 't' and jsrc[jp + 1] == 'r' { jp += 4; let v = jval(JBOOL); v.b = 1; return v } # true if jsrc[jp] == 'f' and jsrc[jp + 1] == 'a' { jp += 5; let v = jval(JBOOL); v.b = 0; return v } # false if jsrc[jp] == 'n' and jsrc[jp + 1] == 'u' { jp += 4; return jval(JNULL) } # null # number var neg = false if jsrc[jp] == '-' { neg = true; jp += 1 } if char_is_digit(jsrc[jp]) { var n = 0 while char_is_digit(jsrc[jp]) { n = n * 10 + (jsrc[jp] - 48); jp += 1 } if jsrc[jp] == '.' { jp += 1; while char_is_digit(jsrc[jp]) { jp += 1 } } # skip fraction let v = jval(JNUM); if neg { v.num = -n } else { v.num = n }; return v } jp += 1 return jval(JNULL) } function jget(v: JVal, key: pointer) -> JVal { if (v == null) { return null } if v.t != JOBJ { return null } var i = 0 while i < len(v.keys) { if (v.keys[i] == key) { return v.kids[i] }; i += 1 } return null } function jat(v: JVal, i: int) -> JVal { if (v == null) { return null }; if i < 0 or i >= len(v.kids) { return null }; return v.kids[i] } function jlen(v: JVal) -> int { if (v == null) { return 0 }; return len(v.kids) } function jstr(v: JVal, def: pointer) -> pointer { if (v != null) and v.t == JSTR and (v.s != null) { return v.s }; return def } function jint(v: JVal, def: int) -> int { if (v != null) and v.t == JNUM { return v.num }; return def } function jpath(v: JVal, path: pointer) -> JVal { var cur = v let n = cstr_len(path) var start = 0; var i = 0 while i <= n { if i == n or path[i] == '.' { let key = path[start..i] cur = jget(cur, key) if (cur == null) { return null } start = i + 1 } i += 1 } return cur } # ============================================================================ # the formatter (Doc-based; reused for LSP formatting) — mirrors fmt.ludic # ============================================================================ function f_name_like(k: int) -> bool { return k == LT_ID or k == LT_KW or k == LT_TYPE or k == LT_PHASE or k == LT_BOOL } function f_dot_tight(F: Doc, t: int) -> bool { if f_name_like(F.tk_kind[t]) { return true } if F.tk_kind[t] != LT_OP { return false } let n = tlen(F, t); let c0 = F.src[F.tk_start[t]] if n == 1 and (c0 == ')' or c0 == ']' or c0 == '.') { return true } if n == 2 and c0 == '.' and F.src[F.tk_start[t] + 1] == 46 { return true } return false } function f_is_unary(F: Doc, i: int) -> bool { if F.tk_kind[i] != LT_OP { return false } let n = tlen(F, i) if not (n == 1 and (F.src[F.tk_start[i]] == 45 or F.src[F.tk_start[i]] == 33)) { return false } let p = prev_sig(F, i) if p < 0 { return true } let pk2 = F.tk_kind[p] if pk2 == LT_ID or pk2 == LT_INT or pk2 == LT_FLOAT or pk2 == LT_STR or pk2 == LT_CHAR or pk2 == LT_BOOL or pk2 == LT_TYPE or pk2 == LT_PHASE { return false } if pk2 == LT_OP { let c = F.src[F.tk_start[p]]; return not (tlen(F, p) == 1 and (c == ')' or c == ']' or c == '}')) } return true } function f_space_before(F: Doc, prev: int, cur: int) -> int { if prev < 0 { return 0 } let pk2 = F.tk_kind[prev]; let ck = F.tk_kind[cur] let plen = tlen(F, prev); let clen = tlen(F, cur) let p0 = F.src[F.tk_start[prev]]; let c0 = F.src[F.tk_start[cur]] let p1 = (plen == 1); let c1 = (clen == 1) if pk2 == LT_ERR or ck == LT_ERR { return F.tk_start[cur] - F.tk_end[prev] } if c1 and (c0 == ')' or c0 == ']' or c0 == ',' or c0 == ':' or c0 == ';') { return 0 } if c1 and c0 == '.' and ck == LT_OP and f_dot_tight(F, prev) { return 0 } if p1 and p0 == '.' and pk2 == LT_OP and f_dot_tight(F, cur) { return 0 } if p1 and (p0 == '(' or p0 == '[') and pk2 == LT_OP { return 0 } if pk2 == LT_OP and f_is_unary(F, prev) { return 0 } if pk2 == LT_ANNO and c1 and c0 == '(' { return 0 } if c1 and c0 == '(' and ck == LT_OP { if pk2 == LT_ID or pk2 == LT_TYPE or pk2 == LT_PHASE { return 0 } if pk2 == LT_OP { if p1 and (p0 == ')' or p0 == ']') { return 1 }; return 0 } return 1 } return 1 } function f_is_clause(w: pointer) -> bool { return (w == "phase") or (w == "query") or (w == "reads") or (w == "writes") or (w == "needs") or (w == "uses") or (w == "requires") or (w == "ensures") or (w == "invariant") or (w == "effects") } function make_text_doc(text: pointer) -> Doc { let F = new Doc F.raw = text; F.text = text; F.src = text; F.is_md = 0 lex_doc(F) return F } function format_doc(F: Doc, iw: int) -> pointer { let o = buf_new() let stack = words(600) let hang = words(600) var sp = 0; var cur = 0; var open = 0; var brack = 0; var ui_depth = -1 var pending_blank = 0; var wrote_any = 0; var prev_line_had_comment = 0 let N = ntok(F) var i = 0 while i < N and F.tk_kind[i] != LT_EOF { let a = i while i < N and F.tk_kind[i] != LT_NL and F.tk_kind[i] != LT_EOF { i += 1 } let b = i if i < N and F.tk_kind[i] == LT_NL { i += 1 } if a == b { if wrote_any == 1 { pending_blank = 1 }; continue } var line_level = cur var tsp = sp; var t = a while t < b and F.tk_kind[t] == LT_OP and tlen(F, t) == 1 and F.src[F.tk_start[t]] == 125 { if tsp > 0 { tsp -= 1; line_level = stack[tsp] }; t += 1 } var orig_ind = 0 var kk = F.linestart[F.tk_line[a]] while kk < F.tk_start[a] { if F.src[kk] == '\t' { orig_ind += 4 } else { orig_ind += 1 }; kk += 1 } var comment_run = 0 if F.tk_kind[a] == LT_COMMENT and b == a + 1 and prev_line_had_comment == 1 { comment_run = 1 } var ind = 0 var hangprev = 0 if sp > 0 { hangprev = hang[sp - 1] } if open > 0 or (sp > 0 and hangprev == 1) { var k2 = F.linestart[F.tk_line[a]] ind = 0 while k2 < F.tk_start[a] { if F.src[k2] == '\t' { ind += 4 } else { ind += 1 }; k2 += 1 } } else { var extra = 0 if f_is_clause(ttext(F, a)) and F.tk_kind[a] == LT_KW { extra = iw } ind = line_level * iw + extra if comment_run == 1 and orig_ind > ind { ind = orig_ind } } if pending_blank == 1 and wrote_any == 1 { buf_putc(o, '\n') } pending_blank = 0 var wi = 0 while wi < ind { buf_putc(o, ' '); wi += 1 } var prev = -1 var line_brack = brack var line_ui_open = 0 if ui_depth >= 0 and sp > ui_depth { line_ui_open = 1 } t = a while t < b { var want = 0 if F.tk_kind[t] == LT_COMMENT { if prev >= 0 { want = 2 } else { want = 0 } } else { want = f_space_before(F, prev, t) if line_brack > 0 { if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 and F.src[F.tk_start[t]] == 125 { want = 0 } if prev >= 0 and F.tk_kind[prev] == LT_OP and tlen(F, prev) == 1 and F.src[F.tk_start[prev]] == 123 { want = 0 } } if line_ui_open == 1 { var eqh = 0 if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 and F.src[F.tk_start[t]] == 61 { eqh = 1 } var eqp = 0 if prev >= 0 and F.tk_kind[prev] == LT_OP and tlen(F, prev) == 1 and F.src[F.tk_start[prev]] == 61 { eqp = 1 } if eqh == 1 or eqp == 1 { want = 0 } } } var gap = 0 if prev >= 0 { gap = F.tk_start[t] - F.tk_end[prev] } if gap >= 2 and want >= 1 { if gap > 60 { gap = 60 }; want = gap } if want < 0 { want = 0 } var w2 = 0 while w2 < want { buf_putc(o, ' '); w2 += 1 } buf_addrange(o, F.src, F.tk_start[t], F.tk_end[t]) prev = t if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 { let c = F.src[F.tk_start[t]] if c == '[' { line_brack += 1 } else { if c == ']' { line_brack -= 1; if line_brack < 0 { line_brack = 0 } } } } t += 1 } buf_putc(o, '\n') wrote_any = 1 prev_line_had_comment = 0 if prev >= 0 and F.tk_kind[prev] == LT_COMMENT { prev_line_had_comment = 1 } if F.tk_kind[a] == LT_KW and (ttext(F, a) == "ui") and ui_depth < 0 { ui_depth = sp } var level = cur t = a while t < b { if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 { let c = F.src[F.tk_start[t]] if c == '{' { if sp < 512 { let nxt = next_sig(F, t); stack[sp] = line_level; var h = 0; if nxt >= 0 and nxt < b { h = 1 }; hang[sp] = h; sp += 1 } level = line_level + 1 } else { if c == '}' { if sp > 0 { sp -= 1; level = stack[sp] } } else { if c == '(' or c == '[' { open += 1; if c == '[' { brack += 1 } } else { if c == ')' or c == ']' { open -= 1; if open < 0 { open = 0 }; if c == ']' { brack -= 1; if brack < 0 { brack = 0 } } } } } } } t += 1 } cur = level if ui_depth >= 0 and sp <= ui_depth { ui_depth = -1 } } return buf_str(o) } # markdown fence formatting (mirrors fmt.ludic) var mf_flen: int = 0 var mf_info: int = 0 var mf_marker: int = 0 function mf_fence_at(s: pointer, i: int) -> bool { var j = i; while s[j] == ' ' { j += 1 } let m = s[j] if m != '`' and m != '~' { return false } var n = 0; while s[j] == m { j += 1; n += 1 } if n < 3 { return false } mf_flen = n; mf_info = j; mf_marker = m return true } function f_line_end(s: pointer, i: int) -> int { var e = i; while s[e] != 0 and s[e] != '\n' { e += 1 }; return e } function format_md(s: pointer, iw: int) -> pointer { let o = buf_new() var i = 0 while s[i] != 0 { let ls = i let le = f_line_end(s, ls) var indent = 0; while s[ls + indent] == ' ' { indent += 1 } var is_ludic_fence = false if mf_fence_at(s, ls) { var kk = mf_info; while s[kk] == ' ' or s[kk] == '\t' { kk += 1 } if word_ludic_at(s, kk) { is_ludic_fence = true } } if is_ludic_fence { var e0 = le; if s[le] != 0 { e0 = le + 1 } buf_addrange(o, s, ls, e0) i = e0 let bs = i var be = bs let openm = mf_marker; let openlen = mf_flen while true { if s[be] == 0 { break } let ps = be; let pe = f_line_end(s, ps) if mf_fence_at(s, ps) { if mf_marker == openm and mf_flen >= openlen { var only = 1; var k = mf_info while k < pe { if s[k] != ' ' and s[k] != '\r' { only = 0; break }; k += 1 } if only == 1 { be = ps; break } } } if s[pe] != 0 { be = pe + 1 } else { be = pe } } let body = buf_new() var p = bs while p < be { let q = f_line_end(s, p) var skip = 0 while skip < indent and (p + skip) < q and s[p + skip] == ' ' { skip += 1 } buf_addrange(body, s, p + skip, q) buf_putc(body, '\n') if s[q] != 0 { p = q + 1 } else { p = q } } let F = make_text_doc(buf_str(body)) let ftext = format_doc(F, iw) var fp = 0 while ftext[fp] != 0 { var fq = fp; while ftext[fq] != 0 and ftext[fq] != '\n' { fq += 1 } if fq > fp { var wi = 0; while wi < indent { buf_putc(o, ' '); wi += 1 } } buf_addrange(o, ftext, fp, fq) buf_putc(o, '\n') if ftext[fq] != 0 { fp = fq + 1 } else { fp = fq } } i = be continue } var e1 = le; if s[le] != 0 { e1 = le + 1 } buf_addrange(o, s, ls, e1) i = e1 } return buf_str(o) } function skip_ws2(s: pointer, at: int) -> int { var a = at; while s[a] == ' ' or s[a] == '\t' { a += 1 }; return a } # ============================================================================ # framing + output # ============================================================================ extern function fflush(f: pointer) -> int = "fflush" function jstr_out(o: Buf, z: pointer) -> void { buf_putc(o, '"') if (z == null) { buf_putc(o, '"'); return } var i = 0 while z[i] != 0 { let c = z[i] if c == '"' { buf_putc(o, '\\'); buf_putc(o, '"') } else { if c == '\\' { buf_putc(o, '\\'); buf_putc(o, '\\') } else { if c == '\n' { buf_putc(o, '\\'); buf_putc(o, 'n') } else { if c == '\r' { buf_putc(o, '\\'); buf_putc(o, 'r') } else { if c == '\t' { buf_putc(o, '\\'); buf_putc(o, 't') } else { if c >= 0 and c < ' ' { buf_putc(o, '\\'); buf_putc(o, 'u'); buf_putc(o, '0'); buf_putc(o, '0'); buf_putc(o, hexch(c / 16)); buf_putc(o, hexch(c % 16)) } else { buf_putc(o, c) } } } } } } i += 1 } buf_putc(o, '"') } function send_raw(body: pointer) -> void { let hdr = buf_new() buf_puts(hdr, "Content-Length: "); buf_puti(hdr, cstr_len(body)); buf_putc(hdr, '\r'); buf_putc(hdr, '\n'); buf_putc(hdr, '\r'); buf_putc(hdr, '\n') let hs = buf_str(hdr) let out = file_stdout() file_write(out, hs, cstr_len(hs)) file_write(out, body, cstr_len(body)) let z = fflush(out) } function send_result(idv: JVal, result: pointer) -> void { let o = buf_new() buf_puts(o, "{\"jsonrpc\":\"2.0\",\"id\":") if (idv == null) { buf_puts(o, "null") } else { if idv.t == JSTR { jstr_out(o, idv.s) } else { buf_puti(o, idv.num) } } buf_puts(o, ",\"result\":") if (result == null) or (result[0] == 0) { buf_puts(o, "null") } else { buf_puts(o, result) } buf_putc(o, '}') send_raw(buf_str(o)) } function send_notify(method: pointer, params: pointer) -> void { let o = buf_new() buf_puts(o, "{\"jsonrpc\":\"2.0\",\"method\":"); jstr_out(o, method); buf_puts(o, ",\"params\":"); buf_puts(o, params); buf_putc(o, '}') send_raw(buf_str(o)) } function lower(c: int) -> int { if c >= 'A' and c <= 'Z' { return c + 32 }; return c } function starts_with_ci(s: pointer, pre: pointer) -> bool { var i = 0 while pre[i] != 0 { if s[i] == 0 or lower(s[i]) != lower(pre[i]) { return false }; i += 1 } return true } function read_message() -> pointer { var length = -1 let line = buf_new() while true { line.len = 0 var c = read_char() if c < 0 { return null } while c >= 0 and c != '\n' { buf_putc(line, c); c = read_char() } let s = buf_str(line) var L = cstr_len(s) if L > 0 and s[L - 1] == '\r' { s[L - 1] = 0; L -= 1 } if L == 0 { break } if starts_with_ci(s, "content-length:") { length = atoi(s[15..cstr_len(s)]) } } if length <= 0 { return null } let buf = bytes(length + 1) var got = 0 while got < length { let c = read_char(); if c < 0 { break }; buf[got] = c; got += 1 } buf[got] = 0 return buf } # ---- position output ---- function put_pos(o: Buf, D: Doc, off: int) -> void { buf_puts(o, "{\"line\":"); buf_puti(o, line_of(D, off)); buf_puts(o, ",\"character\":"); buf_puti(o, col_of(D, off)); buf_putc(o, '}') } function put_range(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"start\":"); put_pos(o, D, a); buf_puts(o, ",\"end\":"); put_pos(o, D, b); buf_putc(o, '}') } function put_location(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"range\":"); put_range(o, D, a, b); buf_putc(o, '}') } function doc_of(msg: JVal) -> Doc { let uri = jstr(jpath(msg, "params.textDocument.uri"), null) if (uri == null) { return null } let D = by_uri(uri) if (D != null) { return D } return ensure_doc(uri_to_path(uri)) } function off_of(msg: JVal, D: Doc, base: pointer) -> int { let line = jint(jpath(msg, concat3(base, ".line", "")), 0) let ch = jint(jpath(msg, concat3(base, ".character", "")), 0) return offset_of(D, line, ch) } function basename(p: pointer) -> pointer { var last = -1; var i = 0; while p[i] != 0 { if p[i] == '/' { last = i }; i += 1 }; return p[last + 1..i] } # ---- LSP symbol kinds & completion kinds ---- function lsp_symbol_kind(k: int) -> int { if k == LS_UNIT { return 2 } if k == LS_COMPONENT { return 23 } if k == LS_FIELD { return 8 } if k == LS_ARCHETYPE { return 5 } if k == LS_CONST { return 14 } if k == LS_VAR { return 13 } if k == LS_FN or k == LS_EXTERN { return 12 } if k == LS_SYSTEM { return 6 } if k == LS_UI { return 5 } if k == LS_WIDGET { return 14 } if k == LS_SCENE { return 3 } if k == LS_LAYER { return 3 } return 13 } function comp_kind_of(k: int) -> int { if k == LS_COMPONENT { return 22 } if k == LS_ARCHETYPE { return 7 } if k == LS_CONST { return 21 } if k == LS_VAR { return 6 } if k == LS_FN or k == LS_EXTERN { return 3 } if k == LS_SYSTEM { return 23 } if k == LS_UI { return 7 } if k == LS_WIDGET { return 21 } if k == LS_SCENE { return 11 } if k == LS_LAYER { return 11 } if k == LS_FIELD { return 5 } return 6 } # ---- diagnostics (structural) ---- function diag_structural(D: Doc, o: Buf) -> void { let ds_s = new []int; let ds_e = new []int; let ds_m = new []pointer let stack = words(1200); var top = 0 var i = 0; let N = ntok(D) while i < N { if D.tk_kind[i] != LT_OP or tlen(D, i) != 1 { i += 1; continue } let c = D.src[D.tk_start[i]] if c == '{' or c == '(' or c == '[' { if top < 1200 { stack[top] = i; top += 1 } } else { if c == '}' or c == ')' or c == ']' { if top == 0 { push(ds_s, D.tk_start[i]); push(ds_e, D.tk_end[i]); push(ds_m, concat3("unmatched '", D.src[D.tk_start[i]..D.tk_end[i]], "'")) } else { top -= 1; let oi = stack[top]; let oc = D.src[D.tk_start[oi]] var wantc = ']' if oc == '{' { wantc = '}' } else if oc == '(' { wantc = ')' } if wantc != c { push(ds_s, D.tk_start[i]); push(ds_e, D.tk_end[i]); push(ds_m, "mismatched bracket") } } } } i += 1 } var k = 0 while k < top { let oi = stack[k]; push(ds_s, D.tk_start[oi]); push(ds_e, D.tk_end[oi]); push(ds_m, concat3("unclosed '", D.src[D.tk_start[oi]..D.tk_end[oi]], "'")); k += 1 } var j = 0 while j < len(ds_s) { if j > 0 { buf_putc(o, ',') } buf_puts(o, "{\"range\":"); put_range(o, D, ds_s[j], ds_e[j]); buf_puts(o, ",\"severity\":1,\"source\":\"ludic\",\"message\":"); jstr_out(o, ds_m[j]); buf_putc(o, '}') j += 1 } } # the compiler's error, as one whole-line diagnostic from source "ludicc" function diag_compiler(D: Doc, o: Buf, first: bool) -> void { if (D.cc_msg == null) or D.cc_line < 1 or D.cc_line > len(D.linestart) { return } let a = D.linestart[D.cc_line - 1] var b = cstr_len(D.src) if D.cc_line < len(D.linestart) { b = D.linestart[D.cc_line] - 1 } if not first { buf_putc(o, ',') } buf_puts(o, "{\"range\":"); put_range(o, D, a, b); buf_puts(o, ",\"severity\":1,\"source\":\"ludicc\",\"message\":"); jstr_out(o, D.cc_msg); buf_putc(o, '}') } function publish_diag(D: Doc) -> void { let o = buf_new() buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"diagnostics\":[") if D.is_md == 0 { let before = o.len diag_structural(D, o) diag_compiler(D, o, o.len == before) } buf_puts(o, "]}") send_notify("textDocument/publishDiagnostics", buf_str(o)) } # ---- compiler diagnostics, on save ---- # ludicc stops at its first error and reports it as `path:line: error: msg`; # run it over the saved document's compilation unit, clear the previous # compiler error from every document of that unit, and pin the new one (if # any) on the document it names — publishing both so a fixed error clears. function compiler_diags(D: Doc) -> void { if (g_compiler == null) or D.is_md == 1 { return } let root = unit_root(D) if (root == null) { return } var i = 0 while i < len(g_docs) { if (g_docs[i].unit == root.unit) { g_docs[i].cc_msg = null; g_docs[i].cc_line = 0 }; i += 1 } let errf = `{Os.temp_dir()}/ludic_lsp_cc_{Os.pid()}.txt` run(`'{g_compiler}' --emit-llvm '{root.path}' -o /dev/null > /dev/null 2> '{errf}'`) let out = read_file(errf) run(`rm -f '{errf}'`) if (out == null) { return } # the first `path:line: error: msg` line var a = 0 while out[a] != 0 { var b = a while out[b] != 0 and out[b] != '\n' { b += 1 } let line = out[a..b] let e = Text.index_of(line, ": error: ") if e > 0 { var c = e - 1 while c > 0 and line[c - 1] >= '0' and line[c - 1] <= '9' { c -= 1 } if c > 1 and line[c - 1] == ':' { let target = by_path(line[0..c - 1]) if (target != null) { target.cc_line = Text.to_int(line[c..e]) target.cc_msg = line[e + 9..cstr_len(line)] if (target != D) and target.isopen == 1 { publish_diag(target) } } } return } a = b if out[a] == '\n' { a += 1 } } } function on_did_save(msg: JVal) -> void { let D = doc_of(msg) if (D == null) { return } compiler_diags(D) publish_diag(D) } # ---- document symbols ---- function put_doc_symbols(o: Buf, D: Doc, parent: int) -> void { buf_putc(o, '[') var first = 1 var i = 0 while i < len(D.syms) { let y = D.syms[i] if y.parent == parent and is_outline_kind(y.kind) and (y.name != null) and (cstr_len(y.name) > 0) { if first == 0 { buf_putc(o, ',') } first = 0 buf_puts(o, "{\"name\":"); jstr_out(o, y.name) buf_puts(o, ",\"detail\":"); jstr_out(o, y.detail) buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind)) buf_puts(o, ",\"range\":") var a = y.start; var b = y.end if y.bend > y.end and y.bstart >= y.start { b = y.bend } put_range(o, D, a, b) buf_puts(o, ",\"selectionRange\":"); put_range(o, D, y.start, y.end) buf_puts(o, ",\"children\":"); put_doc_symbols(o, D, i) buf_putc(o, '}') } i += 1 } buf_putc(o, ']') } # ---- hover ---- function hover_md(o: Buf, code: pointer, docstr: pointer) -> void { let m = buf_new() buf_puts(m, "```ludic\n"); buf_puts(m, code); buf_puts(m, "\n```") if (docstr != null) and (docstr[0] != 0) { buf_puts(m, "\n\n"); buf_puts(m, docstr) } buf_puts(o, "{\"contents\":{\"kind\":\"markdown\",\"value\":"); jstr_out(o, buf_str(m)); buf_puts(o, "}}") } function on_hover(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "null"); return } let off = off_of(msg, D, "params.position") let tok = tok_at(D, off) if tok < 0 { send_result(id, "null"); return } if is_import_string(D, tok) { let files = resolve_import_all(D.path, import_rel_of(D, tok)) let o = buf_new() if len(files) == 0 { hover_md(o, "import " + ttext(D, tok), "_not found: not beside this file, in ludic_modules/, or in the toolchain's packages/_") } else { hover_md(o, "import " + ttext(D, tok), "`" + files[0] + "`") } send_result(id, buf_str(o)); return } resolve_at(D, off) if (g_owner != null) and g_symidx >= 0 { let ow = g_owner let si = g_symidx let y = ow.syms[si] let code = sym_hover_code(ow, si) var docstr = y.doc if not (ow.path == D.path) { let where = concat3("_declared in ", basename(ow.path), "_") if (docstr == "") { docstr = where } else { docstr = concat3(docstr, "\n\n", where) } } let o = buf_new(); hover_md(o, code, docstr); send_result(id, buf_str(o)); return } let dh = docs_hover(D, tok) if (dh != null) { send_result(id, dh); return } let name = ttext(D, tok) let sig = builtin_sig(name) if (sig != null) { let o = buf_new(); hover_md(o, sig, ""); send_result(id, buf_str(o)); return } if D.tk_kind[tok] == LT_PHASE { let o = buf_new(); hover_md(o, name, "A system phase."); send_result(id, buf_str(o)); return } if D.tk_kind[tok] == LT_TYPE { let o = buf_new(); hover_md(o, name, "A built-in type."); send_result(id, buf_str(o)); return } send_result(id, "null") } # ---- definition ---- function on_definition(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "null"); return } let off = off_of(msg, D, "params.position") let tok = tok_at(D, off) if is_import_string(D, tok) { let r = import_definition(D, tok) if (r != null) { send_result(id, r); return } send_result(id, "null"); return } resolve_at(D, off) if (g_owner == null) or g_symidx < 0 { # a built-in, a built-in type or a namespace: its reference page if tok >= 0 { let dd = docs_definition(D, tok); if (dd != null) { send_result(id, dd); return } } send_result(id, "null"); return } let ow = g_owner let y = ow.syms[g_symidx] let o = buf_new(); put_location(o, ow, y.start, y.end); send_result(id, buf_str(o)) } # ---- occurrences (references / rename) ---- var g_occ_doc: []Doc var g_occ_tok: []int function find_occurrences(from: Doc, tdoc: Doc, tsym: int) -> void { g_occ_doc = new []Doc; g_occ_tok = new []int let want = tdoc.syms[tsym].name var docs = new []Doc if is_local_kind(tdoc.syms[tsym].kind) { push(docs, from) } else { docs = related(from) } var i = 0 while i < len(docs) { let T = docs[i] var k = 0 while k < ntok(T) { if T.tk_kind[k] == LT_ID and (ttext(T, k) == want) { resolve_tok(T, k) if (g_owner != null) and (g_owner.path == tdoc.path) and g_symidx == tsym { push(g_occ_doc, T); push(g_occ_tok, k) } } k += 1 } i += 1 } } function on_references(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "[]"); return } let off = off_of(msg, D, "params.position") let tok = resolve_at(D, off) if tok < 0 or (g_owner == null) or g_symidx < 0 { send_result(id, "[]"); return } let tdoc = g_owner; let tsym = g_symidx find_occurrences(D, tdoc, tsym) let o = buf_new(); buf_putc(o, '[') var i = 0 while i < len(g_occ_doc) { if i > 0 { buf_putc(o, ',') } let T = g_occ_doc[i]; let k = g_occ_tok[i] put_location(o, T, T.tk_start[k], T.tk_end[k]) i += 1 } buf_putc(o, ']'); send_result(id, buf_str(o)) } # highlight usages: only the occurrences in THIS document — a highlight has no # uri, so a range from another file would be painted onto this one function on_document_highlight(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "[]"); return } let off = off_of(msg, D, "params.position") let tok = resolve_at(D, off) if tok < 0 or (g_owner == null) or g_symidx < 0 { send_result(id, "[]"); return } let tdoc = g_owner; let tsym = g_symidx find_occurrences(D, tdoc, tsym) let o = buf_new(); buf_putc(o, '[') var first = true var i = 0 while i < len(g_occ_doc) { if (g_occ_doc[i].path == D.path) { let k = g_occ_tok[i] if not first { buf_putc(o, ',') } first = false buf_puts(o, "{\"range\":"); put_range(o, D, D.tk_start[k], D.tk_end[k]) var kind = 1 let nx = next_sig(D, k) if D.tk_start[k] == tdoc.syms[tsym].start and (tdoc.path == D.path) { kind = 3 } else if nx >= 0 and (tis(D, nx, "=") or tis(D, nx, "+=") or tis(D, nx, "-=") or tis(D, nx, "*=") or tis(D, nx, "/=")) { kind = 3 } buf_puts(o, ",\"kind\":"); buf_puti(o, kind); buf_putc(o, '}') } i += 1 } buf_putc(o, ']'); send_result(id, buf_str(o)) } function on_rename(msg: JVal, id: JVal) -> void { let D = doc_of(msg) let newname = jstr(jpath(msg, "params.newName"), null) if (D == null) or (newname == null) or (newname[0] == 0) { send_result(id, "null"); return } let off = off_of(msg, D, "params.position") let tok = resolve_at(D, off) if tok < 0 or (g_owner == null) or g_symidx < 0 { send_result(id, "null"); return } let tdoc = g_owner; let tsym = g_symidx find_occurrences(D, tdoc, tsym) # group by doc let seen = new []Doc let o = buf_new(); buf_puts(o, "{\"changes\":{") var gi = 0 while gi < len(g_occ_doc) { var already = false var s = 0 while s < len(seen) { if (seen[s].path == g_occ_doc[gi].path) { already = true }; s += 1 } if not already { push(seen, g_occ_doc[gi]) if len(seen) > 1 { buf_putc(o, ',') } jstr_out(o, g_occ_doc[gi].uri); buf_putc(o, ':'); buf_putc(o, '[') var first = 1 var k = 0 while k < len(g_occ_doc) { if (g_occ_doc[k].path == g_occ_doc[gi].path) { if first == 0 { buf_putc(o, ',') } first = 0 let T = g_occ_doc[k]; let tk = g_occ_tok[k] buf_puts(o, "{\"range\":"); put_range(o, T, T.tk_start[tk], T.tk_end[tk]); buf_puts(o, ",\"newText\":"); jstr_out(o, newname); buf_putc(o, '}') } k += 1 } buf_putc(o, ']') } gi += 1 } buf_puts(o, "}}"); send_result(id, buf_str(o)) } # ---- completion ---- var g_first: int = 1 # the rank prefix for the next items: in-scope names before globals before # builtins before keywords var g_rank: pointer = "5" function comp_item(o: Buf, label: pointer, kind: int, detail: pointer) -> void { comp_item_full(o, label, kind, detail, g_rank + label, "") } function comp_item_full(o: Buf, label: pointer, kind: int, detail: pointer, sort: pointer, insert: pointer) -> void { if g_first == 0 { buf_putc(o, ',') } g_first = 0 buf_puts(o, "{\"label\":"); jstr_out(o, label); buf_puts(o, ",\"kind\":"); buf_puti(o, kind) if (detail != null) and (detail[0] != 0) { buf_puts(o, ",\"detail\":"); jstr_out(o, detail) } buf_puts(o, ",\"sortText\":"); jstr_out(o, sort) if (insert != null) and (insert[0] != 0) { buf_puts(o, ",\"insertText\":"); jstr_out(o, insert) } buf_putc(o, '}') } function comp_scope(o: Buf, D: Doc, off: int, types_only: int) -> void { if types_only == 0 { g_rank = "1" var i = 0 while i < len(D.syms) { let y = D.syms[i] if is_local_kind(y.kind) and off >= y.bstart and off <= y.bend and y.bstart <= off { comp_item(o, y.name, 6, sym_type(D, i, 0)) } i += 1 } } g_rank = "2" let rel = related(D) var r = 0 while r < len(rel) { var k = 0 while k < len(rel[r].syms) { let y = rel[r].syms[k] if is_toplevel_kind(y.kind) and y.kind != LS_UNIT { var okk = true if types_only == 1 and not (y.kind == LS_COMPONENT or y.kind == LS_ARCHETYPE) { okk = false } if okk { comp_item(o, y.name, comp_kind_of(y.kind), y.detail) } } k += 1 } r += 1 } } function comp_types(o: Buf) -> void { comp_item(o, "int", 14, "built-in type"); comp_item(o, "long", 14, "built-in type"); comp_item(o, "fixed", 14, "built-in type"); comp_item(o, "bool", 14, "built-in type") comp_item(o, "entity", 14, "built-in type"); comp_item(o, "string", 14, "built-in type"); comp_item(o, "pointer", 14, "built-in type"); comp_item(o, "Vector", 14, "built-in type"); comp_item(o, "IVec2", 14, "built-in type"); comp_item(o, "Rect", 14, "built-in type"); comp_item(o, "void", 14, "built-in type") } function comp_builtins(o: Buf) -> void { g_rank = "3"; var i = 0; while i < len(g_builtins) { comp_item(o, g_builtins[i], 3, g_bsigs[i]); i += 1 } } function on_completion(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "{\"isIncomplete\":false,\"items\":[]}"); return } let off = off_of(msg, D, "params.position") let o = buf_new() buf_puts(o, "{\"isIncomplete\":false,\"items\":[") g_first = 1 var tok = -1 var i = ntok(D) - 1 while i >= 0 { let k = D.tk_kind[i] if k != LT_NL and k != LT_EOF and k != LT_COMMENT { if D.tk_end[i] <= off { tok = i; break } } i -= 1 } var anchor = tok if anchor >= 0 and D.tk_kind[anchor] == LT_ID and D.tk_end[anchor] >= off { anchor = prev_sig(D, anchor) } var in_qt = 0 var depth = 0 var j = anchor while j >= 0 { if D.tk_kind[j] == LT_OP and tlen(D, j) == 1 { let ch = D.src[D.tk_start[j]] if ch == ']' { depth += 1 } else { if ch == '[' { if depth == 0 { in_qt = 1; j = -1 } else { depth -= 1 } } else { if ch == '{' or ch == '}' { j = -1 } } } } j -= 1 } var done = 0 if in_qt == 1 { comp_scope(o, D, off, 1); done = 1 } else { if anchor >= 0 { if tis(D, anchor, ".") { let recv = prev_sig(D, anchor) if recv >= 0 and D.tk_kind[recv] == LT_ID { if comp_ns_members(o, ttext(D, recv)) { done = 1 } } let comp = receiver_component(D, anchor) if comp >= 0 and (g_owner != null) { let co = g_owner var k = 0 while k < len(co.syms) { if co.syms[k].kind == LS_FIELD and co.syms[k].parent == comp { comp_item(o, co.syms[k].name, 5, co.syms[k].detail) }; k += 1 } } done = 1 } else { if tis(D, anchor, "phase") { comp_item(o, "Start", 13, ""); comp_item(o, "Input", 13, ""); comp_item(o, "FixedUpdate", 13, ""); comp_item(o, "Update", 13, ""); comp_item(o, "LateUpdate", 13, ""); comp_item(o, "Render", 13, ""); done = 1 } else { if tis(D, anchor, ":") or tis(D, anchor, "->") { comp_types(o); comp_scope(o, D, off, 1); done = 1 } else { if tis(D, anchor, "spawn") { comp_scope(o, D, off, 1); done = 1 } else { if D.tk_kind[anchor] == LT_OP and tlen(D, anchor) == 1 and D.src[D.tk_start[anchor]] == 64 { comp_item(o, "deterministic", 14, "@deterministic"); done = 1 } } } } } } } if done == 0 and in_ui_body(D, off) { comp_item(o, "panel", 7, "widget"); comp_item(o, "col", 7, "widget"); comp_item(o, "row", 7, "widget"); comp_item(o, "label", 7, "widget"); comp_item(o, "button", 7, "widget"); comp_item(o, "image", 7, "widget"); comp_item(o, "spacer", 7, "widget") } var in_args = false if done == 0 and find_call_at(D, off) { in_args = true if anchor >= 0 and (tis(D, anchor, "(") or tis(D, anchor, ",")) { if comp_call_args(o, D, off) { } } } if done == 0 and in_args { comp_builtins(o) comp_scope(o, D, off, 0) done = 1 } if done == 0 { g_rank = "4" comp_item(o, "let", 14, "statement"); comp_item(o, "var", 14, "statement"); comp_item(o, "if", 14, "statement"); comp_item(o, "for", 14, "statement"); comp_item(o, "while", 14, "statement"); comp_item(o, "return", 14, "statement"); comp_item(o, "spawn", 14, "statement"); comp_item(o, "handler", 14, "declaration"); comp_item(o, "property", 14, "declaration"); comp_item(o, "model", 14, "declaration") comp_types(o) comp_builtins(o) comp_scope(o, D, off, 0) g_rank = "5" } buf_puts(o, "]}") send_result(id, buf_str(o)) } # ---- inlay hints, signature help, semantic tokens, folding ---- function on_inlay_hint(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "[]"); return } let o = buf_new(); buf_putc(o, '[') var first = 1 var i = 0 while i < len(D.syms) { let y = D.syms[i] if y.kind == LS_QUERYVAR and (y.ty != null) and (y.ty[0] != 0) { if first == 0 { buf_putc(o, ',') } first = 0 buf_puts(o, "{\"position\":"); put_pos(o, D, y.end); buf_puts(o, ",\"label\":"); jstr_out(o, concat3(": ", y.ty, "")); buf_puts(o, ",\"kind\":1,\"paddingLeft\":false}") } i += 1 } buf_putc(o, ']'); send_result(id, buf_str(o)) } function on_semantic_tokens(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "null"); return } classify(D) let o = buf_new(); buf_puts(o, "{\"data\":[") var prev_line = 0; var prev_char = 0; var first = 1 var i = 0; let N = ntok(D) while i < N { let k = D.tk_kind[i] if k != LT_NL and k != LT_EOF { let ty = sem_type_of(D.cls[i]) if ty >= 0 { let line = D.tk_line[i] let ch = col_of(D, D.tk_start[i]) let ln = utf16_len(D.text, D.tk_start[i], tlen(D, i)) if ln > 0 { let dl = line - prev_line var dc = ch if dl == 0 { dc = ch - prev_char } if first == 0 { buf_putc(o, ',') } first = 0 var mods = g_semmods if i < len(D.clsmods) { mods = mods | D.clsmods[i] } buf_puti(o, dl); buf_putc(o, ','); buf_puti(o, dc); buf_putc(o, ','); buf_puti(o, ln); buf_putc(o, ','); buf_puti(o, ty); buf_putc(o, ','); buf_puti(o, mods) prev_line = line; prev_char = ch } } } i += 1 } buf_puts(o, "]}"); send_result(id, buf_str(o)) } function on_folding(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "[]"); return } let o = buf_new(); buf_putc(o, '['); var first = 1 var i = 0; let N = ntok(D) while i < N { if D.tk_kind[i] == LT_OP and tlen(D, i) == 1 and D.src[D.tk_start[i]] == 123 { let m = D.tmatch[i] if m >= 0 { let a = D.tk_line[i]; let b = D.tk_line[m] if b > a { if first == 0 { buf_putc(o, ',') } first = 0 buf_puts(o, "{\"startLine\":"); buf_puti(o, a); buf_puts(o, ",\"endLine\":"); buf_puti(o, b - 1); buf_puts(o, ",\"kind\":\"region\"}") } } } i += 1 } buf_putc(o, ']'); send_result(id, buf_str(o)) } function on_workspace_symbol(msg: JVal, id: JVal) -> void { let q = jstr(jpath(msg, "params.query"), "") let o = buf_new(); buf_putc(o, '['); var first = 1 var d = 0 while d < len(g_docs) { let D = g_docs[d] var k = 0 while k < len(D.syms) { let y = D.syms[k] if is_outline_kind(y.kind) and (y.name != null) and (cstr_len(y.name) > 0) { var keep = true if q[0] != 0 { keep = contains_ci(y.name, q) } if keep { if first == 0 { buf_putc(o, ',') } first = 0 buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, '}') } } k += 1 } d += 1 } buf_putc(o, ']'); send_result(id, buf_str(o)) } function contains_ci(hay: pointer, needle: pointer) -> bool { let hn = cstr_len(hay); let nn = cstr_len(needle) if nn == 0 { return true } var i = 0 while i + nn <= hn { var m = true var j = 0 while j < nn { if lower(hay[i + j]) != lower(needle[j]) { m = false; break }; j += 1 } if m { return true } i += 1 } return false } function on_formatting(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "null"); return } var tab = jint(jpath(msg, "params.options.tabSize"), 2) if tab < 1 or tab > 8 { tab = 2 } var out = null if D.is_md == 1 { out = format_md(D.raw, tab) } else { let F = make_text_doc(D.raw); out = format_doc(F, tab) } let endline = len(D.linestart) - 1 let ls0 = D.linestart[endline] let e0 = cstr_len(D.text) let endchar = utf16_len(D.text, ls0, e0 - ls0) let o = buf_new() buf_puts(o, "[{\"range\":{\"start\":{\"line\":0,\"character\":0},\"end\":{\"line\":"); buf_puti(o, endline); buf_puts(o, ",\"character\":"); buf_puti(o, endchar); buf_puts(o, "}},\"newText\":"); jstr_out(o, out); buf_puts(o, "}]") send_result(id, buf_str(o)) } # ---- document lifecycle ---- function on_did_open(msg: JVal) -> void { let uri = jstr(jpath(msg, "params.textDocument.uri"), null) let text = jstr(jpath(msg, "params.textDocument.text"), "") if (uri == null) { return } let path = uri_to_path(uri) var D = by_path(path) if (D == null) { D = new_doc(path, text); push(g_docs, D) } else { set_text(D, text); reindex(D) } D.isopen = 1 publish_diag(D) } function on_did_change(msg: JVal) -> void { let D = doc_of(msg) if (D == null) { return } let changes = jpath(msg, "params.contentChanges") let last = jat(changes, jlen(changes) - 1) let text = jstr(jget(last, "text"), null) if (text == null) { return } set_text(D, text) reindex(D) publish_diag(D) } function on_did_close(msg: JVal) -> void { let D = doc_of(msg) if (D == null) { return } D.isopen = 0 let disk = read_file(D.path) if (disk != null) { set_text(D, disk); reindex(D) } } function caps() -> pointer { let o = buf_new() buf_puts(o, "{\"capabilities\":{") buf_puts(o, "\"positionEncoding\":\"utf-16\"") buf_puts(o, ",\"textDocumentSync\":{\"openClose\":true,\"change\":1,\"save\":{\"includeText\":false}}") buf_puts(o, ",\"documentSymbolProvider\":true") buf_puts(o, ",\"workspaceSymbolProvider\":true") buf_puts(o, ",\"definitionProvider\":true") buf_puts(o, ",\"typeDefinitionProvider\":true") buf_puts(o, ",\"declarationProvider\":true") buf_puts(o, ",\"referencesProvider\":true") buf_puts(o, ",\"documentHighlightProvider\":true") buf_puts(o, ",\"hoverProvider\":true") buf_puts(o, ",\"renameProvider\":{\"prepareProvider\":true}") buf_puts(o, ",\"documentFormattingProvider\":true") buf_puts(o, ",\"documentRangeFormattingProvider\":true") buf_puts(o, ",\"foldingRangeProvider\":true") buf_puts(o, ",\"documentLinkProvider\":{\"resolveProvider\":false}") buf_puts(o, ",\"inlayHintProvider\":true") buf_puts(o, ",\"completionProvider\":{\"triggerCharacters\":[\".\",\"@\",\"[\",\" \"],\"resolveProvider\":false}") buf_puts(o, ",\"signatureHelpProvider\":{\"triggerCharacters\":[\"(\",\",\"]}") buf_puts(o, ",\"semanticTokensProvider\":{\"legend\":{\"tokenTypes\":[\"keyword\",\"type\",\"struct\",\"class\",\"namespace\",\"function\",\"parameter\",\"variable\",\"property\",\"enumMember\",\"number\",\"string\",\"comment\",\"operator\",\"decorator\"],\"tokenModifiers\":[\"declaration\",\"readonly\",\"defaultLibrary\",\"vendor\",\"static\"]},\"full\":true,\"range\":true}") buf_puts(o, "},\"serverInfo\":{\"name\":\"ludic-lsp\",\"version\":\"1.0.0\"}}") return buf_str(o) } # the ludicc that ships beside this server binary, when there is one function compiler_beside_me() -> pointer { let me = arg(0) var i = cstr_len(me) while i > 0 and me[i - 1] != '/' { i -= 1 } let cand = me[0..i] + "ludicc" if run(`test -x '{cand}'`) == 0 { return cand } return null } function on_initialize(msg: JVal, id: JVal) -> void { let root = jpath(msg, "params.rootUri") if (root != null) and root.t == JSTR { g_root = uri_to_path(root.s) } if (g_root != null) { scan_root(g_root) } let want = jpath(msg, "params.initializationOptions.compilerDiagnostics") if (want != null) and want.t == JBOOL and want.b == 1 { g_compiler = jstr(jpath(msg, "params.initializationOptions.compilerPath"), null) if (g_compiler == null) or cstr_len(g_compiler) == 0 { g_compiler = compiler_beside_me() } } send_result(id, caps()) } function handle(msg: JVal) -> void { let method = jstr(jget(msg, "method"), null) let id = jget(msg, "id") if (method == null) { return } if (method == "initialize") { on_initialize(msg, id); return } if (method == "initialized") { return } if (method == "shutdown") { g_shutdown = 1; send_result(id, "null"); return } if (method == "exit") { if g_shutdown == 1 { exit(0) }; exit(1) } if (method == "textDocument/didOpen") { on_did_open(msg); return } if (method == "textDocument/didChange") { on_did_change(msg); return } if (method == "textDocument/didClose") { on_did_close(msg); return } if (method == "textDocument/didSave") { on_did_save(msg); return } if (method == "textDocument/documentSymbol") { let D = doc_of(msg); if (D == null) { send_result(id, "[]"); return }; let o = buf_new(); put_doc_symbols(o, D, -1); send_result(id, buf_str(o)); return } if (method == "workspace/symbol") { on_workspace_symbol(msg, id); return } if (method == "textDocument/definition") or (method == "textDocument/declaration") { on_definition(msg, id); return } if (method == "textDocument/typeDefinition") { on_type_definition(msg, id); return } if (method == "textDocument/hover") { on_hover(msg, id); return } if (method == "textDocument/references") { on_references(msg, id); return } if (method == "textDocument/documentHighlight") { on_document_highlight(msg, id); return } if (method == "textDocument/rename") { on_rename(msg, id); return } if (method == "textDocument/prepareRename") { let D = doc_of(msg); if (D == null) { send_result(id, "null"); return }; let off = off_of(msg, D, "params.position"); let tok = resolve_at(D, off); if tok < 0 or (g_owner == null) or g_symidx < 0 or D.tk_kind[tok] != LT_ID { send_result(id, "null"); return }; let o = buf_new(); buf_puts(o, "{\"range\":"); put_range(o, D, D.tk_start[tok], D.tk_end[tok]); buf_puts(o, ",\"placeholder\":"); jstr_out(o, ttext(D, tok)); buf_putc(o, '}'); send_result(id, buf_str(o)); return } if (method == "textDocument/formatting") or (method == "textDocument/rangeFormatting") { on_formatting(msg, id); return } if (method == "textDocument/foldingRange") { on_folding(msg, id); return } if (method == "textDocument/inlayHint") { on_inlay_hint(msg, id); return } if (method == "textDocument/signatureHelp") { on_signature_help2(msg, id); return } if (method == "textDocument/completion") { on_completion(msg, id); return } if (method == "textDocument/documentLink") { on_document_link(msg, id); return } if (method == "textDocument/semanticTokens/full") or (method == "textDocument/semanticTokens/range") { on_semantic_tokens(msg, id); return } if (id != null) { send_result(id, "null") } } entry { init_builtins() g_docs = new []Doc while true { let raw = read_message() if (raw == null) { break } jsrc = raw; jp = 0 let msg = jparse() handle(msg) } } }