ludic/tools/ludic-tools/lsp.ludic
Orkuncakilkaya bca8f126fc Networking N2–N6, and a fully C-free toolchain
Implement the rest of NETWORKING-DESIGN.md (N2–N6) and eliminate every
`.c` file from the repo. clang remains only the LLVM-IR assembler; no C
is compiled anywhere.

Networking (selfhost/emit_net.ludic + parser/emit changes):
- N2 @Sync: per-model serialize/apply + by-kind dispatchers; POD-scalar
  compile error and empty-participation warning; selective replication.
- N3 @Owned: @L_owner array + owner/set_owner/is_owner; owners snapshot.
- N4 @ToServer/@ToClients remote events: framed net_send + net_pump re-emit.
- N5 @Server/@Predicted role guards + drivable sim (tick_fixed/tick_render,
  entry-owns-the-loop).
- Built-in loopback transport so multiplayer runs with zero foreign code;
  extern fn net_send/net_poll still overrides it for a real socket.
- N6 blessed runtime (examples/net_rt.ludic) + end-to-end demo (net_demo).
- Fix: llty("entity") is now i32 (entities are i32 handles), so let e = self().

C elimination:
- Networking + foreign-mod-ABI tests rewritten as self-contained pure-Ludic
  programs (examples/net_*, world_*, mod_events, scoped); tests/ removed.
- Reflection ABI exposed to Ludic as world_* builtins (Ludic-to-Ludic modding).
- Formatter rewritten C→Ludic: tools/ludic-tools/fmt.ludic.
- Language server rewritten C→Ludic: tools/ludic-tools/lsp.ludic (lexer, index
  parser, cross-file workspace resolver, JSON, all LSP handlers).
- Obsolete migrate_*.c codemods deleted; ludic_syntax.h kept as vocabulary data.

Suites: ./test.sh 44/44, ./tools/test-tools.sh 28/28 (LSP 42/42), fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 15:08:23 +03:00

2409 lines
98 KiB
Text

# 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 {
# ---- 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
# ---- tiny helpers ----
fn read_file(path: ptr) -> ptr {
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
}
fn cstr_len(s: ptr) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n }
fn char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 }
fn char_is_alpha(c: int) -> bool {
if c >= 65 and c <= 90 { return true }
if c >= 97 and c <= 122 { return true }
return c == 95
}
fn char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) }
fn char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 97 and c <= 102) or (c >= 65 and c <= 70) }
fn itoa(v: int) -> ptr {
if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z }
var neg = false; var x = v
if x < 0 { neg = true; x = 0 - x }
let tmp = bytes(16); var n = 0
while x > 0 { tmp[n] = 48 + x % 10; x = x / 10; n = n + 1 }
var total = n
if neg { total = total + 1 }
let out = bytes(total + 1); var k = 0
if neg { out[0] = 45; k = 1 }
var i = 0
while i < n { out[k + i] = tmp[n - 1 - i]; i = i + 1 }
out[total] = 0
return out
}
fn atoi(s: ptr) -> int {
var n = 0; var i = 0; var neg = false
while s[i] == 32 or s[i] == 9 { i = i + 1 }
if s[i] == 45 { neg = true; i = i + 1 }
while char_is_digit(s[i]) { n = n * 10 + (s[i] - 48); i = i + 1 }
if neg { return 0 - n }
return n
}
# ---- growable byte buffer ----
property Buf { data: ptr = null, len: int = 0, cap: int = 0 }
fn buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b }
fn buf_ensure(b: Buf, extra: int) -> void {
if b.len + extra + 1 <= b.cap { return }
while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 }
b.data = resize(b.data, b.cap)
}
fn buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len = b.len + 1 }
fn buf_puts(b: Buf, s: ptr) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } }
fn buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) }
fn buf_addrange(b: Buf, s: ptr, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k = k + 1 } }
fn buf_str(b: Buf) -> ptr { b.data[b.len] = 0; return b.data }
# ---- string helpers ----
fn streq(a: ptr, b: ptr) -> bool { return (a == b) }
fn concat3(a: ptr, b: ptr, c: ptr) -> ptr { return ((a + b) + c) }
fn ends_with(s: ptr, suf: ptr) -> bool {
let n = cstr_len(s); let m = cstr_len(suf)
if m > n { return false }
return (s[n - m..n] == suf)
}
fn starts_with(s: ptr, pre: ptr) -> 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: ptr = null
ty: ptr = null
detail: ptr = null
doc: ptr = null
tok: int = 0
start: int = 0
end: int = 0
bstart: int = 0
bend: int = 0
parent: int = 0
exported: int = 0
}
property Doc {
path: ptr = null
uri: ptr = null
raw: ptr = null
text: ptr = null
is_md: int = 0
is_unit: int = 0
is_module: int = 0
unit: ptr = null
src: ptr = null
tk_kind: []int
tk_start: []int
tk_end: []int
tk_line: []int
linestart: []int
tmatch: []int
cls: []int
syms: []Sym
imports: []ptr
had_diags: int = 0
isopen: int = 0
version: int = 0
}
var g_docs: []Doc
var g_root: ptr = null
var g_shutdown: int = 0
# ---- the lexer, over a Doc ----
fn is_op2(c0: int, c1: int) -> bool {
if c0 == 45 and c1 == 62 { return true }
if c1 == 61 and (c0 == 43 or c0 == 45 or c0 == 42 or c0 == 47 or c0 == 61 or c0 == 33 or c0 == 60 or c0 == 62) { return true }
if c0 == 38 and c1 == 38 { return true }
if c0 == 124 and c1 == 124 { return true }
if c0 == 46 and c1 == 46 { return true }
if c0 == 61 and c1 == 62 { return true }
return false
}
fn is_op1(c: int) -> bool {
return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59
}
fn is_type_word(w: ptr) -> bool {
return (w == "int") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void")
}
fn is_phase_word(w: ptr) -> bool {
return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render")
}
fn is_keyword_word(w: ptr) -> bool {
if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true }
if (w == "const") or (w == "var") or (w == "fn") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") { return true }
if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "let") or (w == "return") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "spawn") or (w == "despawn") { return true }
if (w == "enable") or (w == "disable") or (w == "match") or (w == "machine") or (w == "state") or (w == "become") or (w == "where") { return true }
if (w == "and") or (w == "or") or (w == "not") or (w == "break") or (w == "continue") or (w == "new") or (w == "emit") or (w == "cancel") { return true }
return false
}
fn is_widget_word(w: ptr) -> bool {
return (w == "panel") or (w == "col") or (w == "row") or (w == "label") or (w == "button") or (w == "image") or (w == "spacer")
}
fn 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
while s[i] != 0 {
let c = s[i]
if c == 10 { push(D.tk_kind, LT_NL); push(D.tk_start, i); push(D.tk_end, i + 1); push(D.tk_line, line); i = i + 1; line = line + 1; push(D.linestart, i); continue }
if c == 32 or c == 9 or c == 13 { i = i + 1; continue }
var st = i
if c == 35 { while s[i] != 0 and s[i] != 10 { i = 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 == 34 { i = i + 1; while s[i] != 0 and s[i] != 34 and s[i] != 10 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } }; if s[i] == 34 { 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 }
if c == 96 { i = i + 1; while s[i] != 0 and s[i] != 96 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } }; if s[i] == 96 { 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 }
if c == 39 { i = i + 1; if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { if s[i] != 0 and s[i] != 10 { i = i + 1 } }; if s[i] == 39 { 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 == 48 and (s[i + 1] == 120 or s[i + 1] == 88) { i = i + 2; while char_is_hex(s[i]) { 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 = i + 1 }
if s[i] == 46 and char_is_digit(s[i + 1]) { i = i + 1; while char_is_digit(s[i]) { 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 == 64 and (char_is_alpha(s[i + 1]) or s[i + 1] == 95) { i = i + 1; while char_is_alnum(s[i]) { 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 = 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 = 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 = 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 = 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 = i + ln
}
push(D.tk_kind, LT_EOF); push(D.tk_start, i); push(D.tk_end, i); push(D.tk_line, line)
}
fn ntok(D: Doc) -> int { return len(D.tk_kind) }
fn tlen(D: Doc, i: int) -> int { return D.tk_end[i] - D.tk_start[i] }
fn ttext(D: Doc, i: int) -> ptr { return D.src[D.tk_start[i]..D.tk_end[i]] }
fn tis(D: Doc, i: int, s: ptr) -> bool { if i < 0 or i >= ntok(D) { return false }; return (ttext(D, i) == s) }
fn 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 = j + 1 }; return 0 - 1 }
fn 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 = j - 1 }; return 0 - 1 }
# ---- positions (UTF-16 for LSP) ----
fn utf16_len(s: ptr, a: int, nbytes: int) -> int {
var u = 0; var i = 0
while i < nbytes {
let c = s[a + i]
if c < 128 { i = i + 1; u = u + 1 }
else { if c < 224 { i = i + 2; u = u + 1 }
else { if c < 240 { i = i + 3; u = u + 1 }
else { i = i + 4; u = u + 2 } } }
}
return u
}
fn 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
}
fn 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]) }
fn 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 = off + 1; u = u + 1 }
else { if c < 224 { off = off + 2; u = u + 1 }
else { if c < 240 { off = off + 3; u = u + 1 }
else { off = off + 4; u = u + 2 } } }
}
return off
}
fn tok_at(D: Doc, off: int) -> int {
var touching = 0 - 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 = i + 1
}
return touching
}
# ---- brace matching ----
fn match_braces(D: Doc) -> void {
let N = ntok(D)
D.tmatch = new []int
var i = 0
while i < N { push(D.tmatch, 0 - 1); i = 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 == 123 or c == 40 or c == 91 { if top < 1200 { stack[top] = i; top = top + 1 } }
else { if c == 125 or c == 41 or c == 93 { if top > 0 { top = top - 1; let o = stack[top]; D.tmatch[o] = i; D.tmatch[i] = o } } }
}
i = i + 1
}
}
# ---- doc comment: the comment block directly above a declaration ----
fn collect_doc(D: Doc, decl_tok: int) -> ptr {
var line = D.tk_line[decl_tok]
var first = 0 - 1
var i = decl_tok - 1
while i >= 0 {
let k = D.tk_kind[i]
if k == LT_NL { i = 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 = 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] == 32 or D.text[s] == 9 or D.text[s] == 35 or D.text[s] == 42) { s = s + 1 }
buf_addrange(out, D.text, s, D.tk_end[i])
buf_putc(out, 10)
}
i = i + 1
}
return buf_str(out)
}
# ============================================================================
# the shallow parser — records declarations and bindings with their spans
# ============================================================================
var pD: Doc = null
var pi: int = 0
fn pk() -> int { if pi < ntok(pD) { return pD.tk_kind[pi] }; return LT_EOF }
fn pis(s: ptr) -> bool { return tis(pD, pi, s) }
fn pskipnl() -> void { while pi < ntok(pD) and (pk() == LT_NL or pk() == LT_COMMENT) { pi = pi + 1 } }
fn padv() -> void { if pi < ntok(pD) { pi = pi + 1 }; pskipnl() }
fn pword() -> ptr { if pi < ntok(pD) { return ttext(pD, pi) }; return "" }
fn 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 0 - 1
}
fn 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 = ""
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
}
fn cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return 0 - 1 }
fn 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
fn lp_params(owner: int, kind: int, sig: Buf) -> void {
if not pis("(") { return }
let close = cur_match()
buf_putc(sig, 40) # '('
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 = ""
if pis(":") { padv(); let tt = pname(); if tt >= 0 { ty = ttext(pD, tt) } }
let sidx = sym_add(kind, nt, owner)
pD.syms[sidx].ty = 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, 41) # ')'
}
# (a, b) variable list -> token indices into out; returns count
fn 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 = n + 1
}
if close >= 0 { pi = close + 1 }
pskipnl()
return n
}
# [Pos, Vel, {Enemy}] — bind listed components to vars in order (skip {Tag})
fn 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 = bind + 1
}
if close >= 0 { pi = close + 1 }
pskipnl()
if pis("where") { padv() }
}
fn 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()
}
fn lp_stmt(owner: int, scope_end: int) -> void {
if pis("let") {
padv(); let nt = pname(); if nt < 0 { return }
var ty = ""
if pis(":") { padv(); let tt = pname(); if tt >= 0 { ty = ttext(pD, tt) } }
let sidx = sym_add(LS_LOCAL, nt, owner)
pD.syms[sidx].ty = ty
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_<Name> handle
fn 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
fn 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
}
if (w == "property") or (w == "model") {
let is_comp = (w == "property")
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, 32); 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 }
if is_comp {
let f = sym_add(LS_FIELD, ft, sidx)
var fty = ""
if pis(":") { padv(); let tt = pname(); if tt >= 0 { fty = ttext(pD, tt) } }
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, 44) }
buf_putc(det, 32); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty)
} else {
if not first { buf_putc(det, 44) }
buf_putc(det, 32); buf_puts(det, ttext(pD, ft))
}
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(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt) } }
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 = pi + 1 } }
pskipnl()
return
}
if w == "extern" {
padv(); if pis("fn") { 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 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, " -> "); 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 = pi + 1 }
pskipnl()
return
}
if (w == "fn") or (w == "pure") or (w == "export") {
let exported = (w == "export")
if (w == "pure") or exported { padv(); if pis("pure") { padv() } }
if pis("fn") { 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 = 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 = q + 1 }
lp_block(sidx, pD.syms[sidx].bend)
}
return
}
if w == "entry" {
padv()
if pis("{") { let close = cur_match(); var endo = cstr_len(pD.text); if close >= 0 { endo = pD.tk_end[close] }; 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"
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 = scan + 1 }
var cl = 0 - 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) }
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 = 0 - 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()
}
fn parse_doc(D: Doc) -> void {
D.syms = new []Sym
D.imports = new []ptr
D.is_unit = 0; D.is_module = 0; D.unit = ""
pD = D; pi = 0
pskipnl()
while pis("import") { lp_decl(0 - 1); pskipnl() }
if pis("program") {
D.is_module = 1; D.is_unit = 1
padv()
let nt = pname()
var up = 0 - 1
if nt >= 0 {
up = sym_add(LS_UNIT, nt, 0 - 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(0 - 1)
if pi == before { padv() }
}
}
# ============================================================================
# workspace: many documents, one project
# ============================================================================
fn is_local_kind(k: int) -> bool { return k == LS_LOCAL or k == LS_PARAM or k == LS_QUERYVAR or k == LS_STATE }
fn 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
}
fn 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 ----
fn hexval(c: int) -> int {
if c >= 48 and c <= 57 { return c - 48 }
let l = c | 32
if l >= 97 and l <= 102 { return l - 97 + 10 }
return 0 - 1
}
fn uri_to_path(uri: ptr) -> ptr {
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] == 37 and hexval(s[i + 1]) >= 0 and hexval(s[i + 2]) >= 0 { out[j] = hexval(s[i + 1]) * 16 + hexval(s[i + 2]); j = j + 1; i = i + 3 }
else { out[j] = s[i]; j = j + 1; i = i + 1 }
}
out[j] = 0
return out
}
fn uri_safe(c: int) -> bool {
if char_is_alnum(c) { return true }
return c == 45 or c == 46 or c == 126 or c == 47 # - . ~ /
}
fn hexch(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n }
fn path_to_uri(path: ptr) -> ptr {
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, 37); buf_putc(out, hexch((c & 255) / 16)); buf_putc(out, hexch((c & 255) % 16)) }
i = i + 1
}
return buf_str(out)
}
fn dirname(p: ptr) -> ptr {
var last = 0 - 1
var i = 0
while p[i] != 0 { if p[i] == 47 { last = i }; i = i + 1 }
if last < 0 { return "." }
if last == 0 { return "/" }
return p[0..last]
}
fn normpath(p: ptr) -> ptr {
let n = cstr_len(p)
let abs = (n > 0 and p[0] == 47)
let list = new []ptr
var cnt = 0
var i = 0
while i < n {
while i < n and p[i] == 47 { i = i + 1 }
let st = i
while i < n and p[i] != 47 { 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 = cnt - 1 }
else { if not abs { if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt = cnt + 1 } }
} else {
if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt = cnt + 1
} }
}
}
if cnt == 0 { if abs { return "/" }; return "." }
let out = buf_new()
if abs { buf_putc(out, 47) }
var k = 0
while k < cnt { if k > 0 { buf_putc(out, 47) }; buf_puts(out, list[k]); k = k + 1 }
return buf_str(out)
}
fn join_path(dir: ptr, rel: ptr) -> ptr {
if rel[0] == 47 { return normpath(rel) }
return normpath(concat3(dir, "/", rel))
}
# ---- markdown scrub: blank everything outside a ```ludic fence ----
fn word_ludic_at(s: ptr, at: int) -> bool {
return ((s[at] == 108 or s[at] == 76)) and ((s[at + 1] == 117 or s[at + 1] == 85)) and ((s[at + 2] == 100 or s[at + 2] == 68)) and ((s[at + 3] == 105 or s[at + 3] == 73)) and ((s[at + 4] == 99 or s[at + 4] == 67))
}
fn scrub_markdown(src: ptr) -> ptr {
let n = cstr_len(src)
let out = bytes(n + 1)
var c0 = 0
while c0 < n { out[c0] = src[c0]; c0 = c0 + 1 }
out[n] = 0
var i = 0
while i < n {
let ls = i
var le = i
while le < n and src[le] != 10 { le = le + 1 }
var ind = 0
while ls + ind < le and src[ls + ind] == 32 { 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 = run + 1 }
let fenced = (m == 96 or m == 126) and run >= 3
let info = j + run
var is_ludic = false
if fenced {
var kk = info
while kk < le and (src[kk] == 32 or src[kk] == 9) { kk = kk + 1 }
if le - kk >= 5 { if word_ludic_at(src, kk) and (le - kk == 5 or src[kk + 5] == 32 or src[kk + 5] == 9 or src[kk + 5] == 13) { is_ludic = true } }
}
var b = ls
while b < le { out[b] = 32; 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] != 10 { be = be + 1 }
var bi = 0
while bs + bi < be and src[bs + bi] == 32 { bi = bi + 1 }
let cj = bs + bi
var crun = 0
while cj + crun < be and src[cj + crun] == m { crun = crun + 1 }
var only = true
var k2 = cj + crun
while k2 < be { if src[k2] != 32 and src[k2] != 13 { only = false; break }; k2 = k2 + 1 }
if crun >= run and only { var bb = bs; while bb < be { out[bb] = 32; bb = bb + 1 }; i = be + 1; if be >= n { i = n }; break }
if not is_ludic { var bb = bs; while bb < be { out[bb] = 32; bb = bb + 1 } }
i = be + 1; if be >= n { i = n }
}
}
return out
}
# ---- documents ----
fn set_text(D: Doc, raw: ptr) -> void {
D.raw = raw
if D.is_md == 1 { D.text = scrub_markdown(raw) } else { D.text = raw }
D.src = D.text
}
fn 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 = i + 1 }
}
fn new_doc(path: ptr, text: ptr) -> 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 []ptr; D.unit = ""
set_text(D, text)
reindex(D)
return D
}
fn by_path(path: ptr) -> Doc {
let p = normpath(path)
var i = 0
while i < len(g_docs) { if (g_docs[i].path == p) { return g_docs[i] }; i = i + 1 }
return null
}
fn by_uri(uri: ptr) -> 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 = i + 1 }
return null
}
fn ensure_doc(path: ptr) -> 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
}
fn scan_root(root: ptr) -> void {
if (root == null) { return }
let listfile = "/tmp/ludic_lsp_scan.txt"
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] == 10 { if i > start { let d = ensure_doc(list[start..i]) }; start = i + 1 }
i = 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
fn imports_of(D: Doc, out: []Doc) -> void {
var i = 0
while i < len(D.imports) {
let full = join_path(dirname(D.path), D.imports[i])
let I = ensure_doc(full)
if (I != null) {
var seen = false
var k = 0
while k < len(out) { if (out[k].path == I.path) { seen = true }; k = k + 1 }
if not seen { push(out, I); imports_of(I, out) }
}
i = i + 1
}
}
fn 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 = k + 1 }
if hits {
var seen = false
k = 0
while k < len(out) { if (out[k].path == R.path) { seen = true }; k = 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 = q + 1 }
if not s2 { push(out, reach[k]) }
k = k + 1
}
}
}
i = i + 1
}
return out
}
fn 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 = i + 1 }
return null
}
fn find_local(D: Doc, name: ptr, off: int) -> int {
var best = 0 - 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 = i + 1
}
return best
}
# returns sym index in g_owner (set), or -1
fn find_top(D: Doc, name: ptr) -> 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 = k + 1
}
i = 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 = k + 1
}
i = i + 1
}
return 0 - 1
}
# component sym index (in g_owner) for a dotted receiver, or -1
fn 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 0 - 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 0 - 1 }
if (ty == "") { return 0 - 1 }
return find_top(D, ty)
}
# resolve the token: sets g_owner/g_symidx; returns the token index (or -1)
fn resolve_tok(D: Doc, tok: int) -> int {
g_owner = null; g_symidx = 0 - 1
if tok < 0 { return 0 - 1 }
if D.tk_kind[tok] != LT_ID and D.tk_kind[tok] != LT_STR { return 0 - 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 = 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 = k + 1
}
}
g_owner = null; g_symidx = 0 - 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 = 0 - 1
return tok
}
fn resolve_at(D: Doc, off: int) -> int { return resolve_tok(D, tok_at(D, off)) }
fn 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] }
fn 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 = i + 1 }
return false
}
# ---- builtins / intrinsics (name -> signature) ----
var g_builtins: []ptr
var g_bsigs: []ptr
fn badd(name: ptr, sig: ptr) -> void { push(g_builtins, name); push(g_bsigs, sig) }
fn is_builtin_name(name: ptr) -> bool { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return true }; i = i + 1 }; return false }
fn builtin_sig(name: ptr) -> ptr { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i = i + 1 }; return null }
fn init_builtins() -> void {
g_builtins = new []ptr; g_bsigs = new []ptr
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", "fx(i: int) -> fixed")
badd("flr", "flr(f: fixed) -> int")
badd("map_size", "map_size(w: int, h: int)")
badd("map_row", "map_row(y: int, row: str)")
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: str, 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: str) -> int")
badd("text_ttf", "text_ttf(font: int, x: int, y: int, utf8: str, color: int, px: int)")
badd("text_w", "text_w(font: int, utf8: str, px: int) -> int")
badd("text_h", "text_h(font: int, px: int) -> int")
badd("image_load", "image_load(path: str) -> 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: str) -> int")
badd("sprites_load", "sprites_load(path: str)")
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: str)")
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: str)")
badd("print", "print(x)")
badd("str", "str(x) -> str")
badd("quit", "quit()")
badd("bytes", "bytes(n: int) -> ptr")
badd("words", "words(n: int) -> words")
badd("resize", "resize(p: ptr, n: int) -> ptr")
badd("arg_count", "arg_count() -> int")
badd("arg", "arg(i: int) -> str")
badd("file_stderr", "file_stderr() -> ptr")
badd("file_stdout", "file_stdout() -> ptr")
badd("free", "free(p: ptr)")
badd("fill", "fill(p: ptr, byte: int, n: int)")
badd("offset", "offset(p: ptr, off: int) -> ptr")
badd("as_fixed", "as_fixed(i: int) -> fixed")
badd("as_int", "as_int(f: fixed) -> int")
badd("file_open", "file_open(path: str, mode: str) -> ptr")
badd("file_read", "file_read(f: ptr, buf: ptr, n: int) -> int")
badd("file_write", "file_write(f: ptr, buf: ptr, n: int) -> int")
badd("file_seek", "file_seek(f: ptr, off: int, whence: int) -> int")
badd("file_tell", "file_tell(f: ptr) -> int")
badd("file_close", "file_close(f: ptr)")
badd("read_char", "read_char() -> int")
badd("exit", "exit(code: int)")
badd("is_windowed", "is_windowed() -> bool")
badd("game_title", "game_title() -> str")
badd("win_open", "win_open(title: str, w: int, h: int, scale: int)")
badd("win_poll", "win_poll() -> int")
badd("win_present", "win_present(px: ptr, w: int, h: int)")
badd("win_running", "win_running() -> bool")
badd("win_close", "win_close()")
}
# ---- classification ----
fn 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
}
fn 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 = z + 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 = i + 1; continue }
let name = ttext(D, i)
var decl = 0 - 1
var s = 0
while s < len(D.syms) { if D.syms[s].tok == i { decl = s; break }; s = s + 1 }
if decl >= 0 { D.cls[i] = class_of_symkind(D.syms[decl].kind); i = i + 1; continue }
let prev = prev_sig(D, i)
if prev >= 0 and tis(D, prev, ".") { D.cls[i] = SC_FIELD; i = 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 = 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 = 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 = i + 1; continue }
let top = find_top(D, name)
if top >= 0 { D.cls[i] = class_of_symkind(g_owner.syms[top].kind); i = i + 1; continue }
if is_builtin_name(name) { D.cls[i] = SC_BUILTIN; i = i + 1; continue }
if followed_eq { D.cls[i] = SC_FIELD; i = i + 1; continue }
D.cls[i] = SC_UNKNOWN
i = i + 1
}
}
# semantic token type index + modifiers (via g_semmods)
var g_semmods: int = 0
fn 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 { return 5 }
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 { return 7 }
if cls == SC_CONST { g_semmods = 2; 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 }
return 0 - 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: ptr = null, kids: []JVal, keys: []ptr }
fn jval(t: int) -> JVal { let v = new JVal; v.t = t; v.kids = new []JVal; v.keys = new []ptr; return v }
var jsrc: ptr = null
var jp: int = 0
fn jskip() -> void { while jsrc[jp] == 32 or jsrc[jp] == 9 or jsrc[jp] == 10 or jsrc[jp] == 13 { jp = jp + 1 } }
fn jhex4(s: ptr, 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 = i + 1 }
return v
}
fn 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)) } } }
}
fn jparse_string() -> ptr {
jp = jp + 1 # skip opening quote
let o = buf_new()
while jsrc[jp] != 0 and jsrc[jp] != 34 {
if jsrc[jp] == 92 {
jp = jp + 1
let c = jsrc[jp]; jp = jp + 1
if c == 110 { buf_putc(o, 10) }
else { if c == 116 { buf_putc(o, 9) }
else { if c == 114 { buf_putc(o, 13) }
else { if c == 98 { buf_putc(o, 8) }
else { if c == 102 { buf_putc(o, 12) }
else { if c == 117 {
var cp = jhex4(jsrc, jp); jp = jp + 4
if cp >= 55296 and cp < 56320 and jsrc[jp] == 92 and jsrc[jp + 1] == 117 {
let lo = jhex4(jsrc, jp + 2); jp = 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 = jp + 1 }
}
if jsrc[jp] == 34 { jp = jp + 1 }
return buf_str(o)
}
fn jparse() -> JVal {
jskip()
let c = jsrc[jp]
if c == 123 { # '{'
jp = jp + 1
let v = jval(JOBJ)
while true {
jskip()
if jsrc[jp] == 125 { jp = jp + 1; break }
var key = ""
if jsrc[jp] == 34 { key = jparse_string() }
jskip(); if jsrc[jp] == 58 { jp = jp + 1 }
let kid = jparse()
push(v.keys, key); push(v.kids, kid)
jskip()
if jsrc[jp] == 44 { jp = jp + 1 }
else { if jsrc[jp] == 125 { jp = jp + 1; break } else { if jsrc[jp] == 0 { break } } }
}
return v
}
if c == 91 { # '['
jp = jp + 1
let v = jval(JARR)
while true {
jskip()
if jsrc[jp] == 93 { jp = jp + 1; break }
let kid = jparse()
push(v.keys, ""); push(v.kids, kid)
jskip()
if jsrc[jp] == 44 { jp = jp + 1 }
else { if jsrc[jp] == 93 { jp = jp + 1; break } else { if jsrc[jp] == 0 { break } } }
}
return v
}
if c == 34 { let v = jval(JSTR); v.s = jparse_string(); return v }
if jsrc[jp] == 116 and jsrc[jp + 1] == 114 { jp = jp + 4; let v = jval(JBOOL); v.b = 1; return v } # true
if jsrc[jp] == 102 and jsrc[jp + 1] == 97 { jp = jp + 5; let v = jval(JBOOL); v.b = 0; return v } # false
if jsrc[jp] == 110 and jsrc[jp + 1] == 117 { jp = jp + 4; return jval(JNULL) } # null
# number
var neg = false
if jsrc[jp] == 45 { neg = true; jp = jp + 1 }
if char_is_digit(jsrc[jp]) {
var n = 0
while char_is_digit(jsrc[jp]) { n = n * 10 + (jsrc[jp] - 48); jp = jp + 1 }
if jsrc[jp] == 46 { jp = jp + 1; while char_is_digit(jsrc[jp]) { jp = jp + 1 } } # skip fraction
let v = jval(JNUM); if neg { v.num = 0 - n } else { v.num = n }; return v
}
jp = jp + 1
return jval(JNULL)
}
fn jget(v: JVal, key: ptr) -> 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 = i + 1 }
return null
}
fn 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] }
fn jlen(v: JVal) -> int { if (v == null) { return 0 }; return len(v.kids) }
fn jstr(v: JVal, def: ptr) -> ptr { if (v != null) and v.t == JSTR and (v.s != null) { return v.s }; return def }
fn jint(v: JVal, def: int) -> int { if (v != null) and v.t == JNUM { return v.num }; return def }
fn jpath(v: JVal, path: ptr) -> JVal {
var cur = v
let n = cstr_len(path)
var start = 0; var i = 0
while i <= n {
if i == n or path[i] == 46 {
let key = path[start..i]
cur = jget(cur, key)
if (cur == null) { return null }
start = i + 1
}
i = i + 1
}
return cur
}
# ============================================================================
# the formatter (Doc-based; reused for LSP formatting) — mirrors fmt.ludic
# ============================================================================
fn 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 }
fn 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 == 41 or c0 == 93 or c0 == 46) { return true }
if n == 2 and c0 == 46 and F.src[F.tk_start[t] + 1] == 46 { return true }
return false
}
fn 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 == 41 or c == 93 or c == 125)) }
return true
}
fn 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 == 41 or c0 == 93 or c0 == 44 or c0 == 58 or c0 == 59) { return 0 }
if c1 and c0 == 46 and ck == LT_OP and f_dot_tight(F, prev) { return 0 }
if p1 and p0 == 46 and pk2 == LT_OP and f_dot_tight(F, cur) { return 0 }
if p1 and (p0 == 40 or p0 == 91) 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 == 40 { return 0 }
if c1 and c0 == 40 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 == 41 or p0 == 93) { return 1 }; return 0 }
return 1
}
return 1
}
fn f_is_clause(w: ptr) -> 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")
}
fn make_text_doc(text: ptr) -> Doc {
let F = new Doc
F.raw = text; F.text = text; F.src = text; F.is_md = 0
lex_doc(F)
return F
}
fn format_doc(F: Doc, iw: int) -> ptr {
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 = 0 - 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 = i + 1 }
let b = i
if i < N and F.tk_kind[i] == LT_NL { i = 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 = tsp - 1; line_level = stack[tsp] }; t = t + 1 }
var orig_ind = 0
var kk = F.linestart[F.tk_line[a]]
while kk < F.tk_start[a] { if F.src[kk] == 9 { orig_ind = orig_ind + 4 } else { orig_ind = orig_ind + 1 }; kk = 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] == 9 { ind = ind + 4 } else { ind = ind + 1 }; k2 = 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, 10) }
pending_blank = 0
var wi = 0
while wi < ind { buf_putc(o, 32); wi = wi + 1 }
var prev = 0 - 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, 32); w2 = 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 == 91 { line_brack = line_brack + 1 }
else { if c == 93 { line_brack = line_brack - 1; if line_brack < 0 { line_brack = 0 } } }
}
t = t + 1
}
buf_putc(o, 10)
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 == 123 {
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 = sp + 1 }
level = line_level + 1
}
else { if c == 125 { if sp > 0 { sp = sp - 1; level = stack[sp] } }
else { if c == 40 or c == 91 { open = open + 1; if c == 91 { brack = brack + 1 } }
else { if c == 41 or c == 93 { open = open - 1; if open < 0 { open = 0 }; if c == 93 { brack = brack - 1; if brack < 0 { brack = 0 } } } } } }
}
t = t + 1
}
cur = level
if ui_depth >= 0 and sp <= ui_depth { ui_depth = 0 - 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
fn mf_fence_at(s: ptr, i: int) -> bool {
var j = i; while s[j] == 32 { j = j + 1 }
let m = s[j]
if m != 96 and m != 126 { return false }
var n = 0; while s[j] == m { j = j + 1; n = n + 1 }
if n < 3 { return false }
mf_flen = n; mf_info = j; mf_marker = m
return true
}
fn f_line_end(s: ptr, i: int) -> int { var e = i; while s[e] != 0 and s[e] != 10 { e = e + 1 }; return e }
fn format_md(s: ptr, iw: int) -> ptr {
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] == 32 { indent = indent + 1 }
var is_ludic_fence = false
if mf_fence_at(s, ls) {
var kk = mf_info; while s[kk] == 32 or s[kk] == 9 { kk = 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] != 32 and s[k] != 13 { only = 0; break }; k = 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] == 32 { skip = skip + 1 }
buf_addrange(body, s, p + skip, q)
buf_putc(body, 10)
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] != 10 { fq = fq + 1 }
if fq > fp { var wi = 0; while wi < indent { buf_putc(o, 32); wi = wi + 1 } }
buf_addrange(o, ftext, fp, fq)
buf_putc(o, 10)
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)
}
fn skip_ws2(s: ptr, at: int) -> int { var a = at; while s[a] == 32 or s[a] == 9 { a = a + 1 }; return a }
# ============================================================================
# framing + output
# ============================================================================
extern fn fflush(f: ptr) -> int = "fflush"
fn jstr_out(o: Buf, z: ptr) -> void {
buf_putc(o, 34)
if (z == null) { buf_putc(o, 34); return }
var i = 0
while z[i] != 0 {
let c = z[i]
if c == 34 { buf_putc(o, 92); buf_putc(o, 34) }
else { if c == 92 { buf_putc(o, 92); buf_putc(o, 92) }
else { if c == 10 { buf_putc(o, 92); buf_putc(o, 110) }
else { if c == 13 { buf_putc(o, 92); buf_putc(o, 114) }
else { if c == 9 { buf_putc(o, 92); buf_putc(o, 116) }
else { if c >= 0 and c < 32 { buf_putc(o, 92); buf_putc(o, 117); buf_putc(o, 48); buf_putc(o, 48); buf_putc(o, hexch(c / 16)); buf_putc(o, hexch(c % 16)) }
else { buf_putc(o, c) } } } } } }
i = i + 1
}
buf_putc(o, 34)
}
fn send_raw(body: ptr) -> void {
let hdr = buf_new()
buf_puts(hdr, "Content-Length: "); buf_puti(hdr, cstr_len(body)); buf_putc(hdr, 13); buf_putc(hdr, 10); buf_putc(hdr, 13); buf_putc(hdr, 10)
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)
}
fn send_result(idv: JVal, result: ptr) -> 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, 125)
send_raw(buf_str(o))
}
fn send_notify(method: ptr, params: ptr) -> 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, 125)
send_raw(buf_str(o))
}
fn lower(c: int) -> int { if c >= 65 and c <= 90 { return c + 32 }; return c }
fn starts_with_ci(s: ptr, pre: ptr) -> bool {
var i = 0
while pre[i] != 0 { if s[i] == 0 or lower(s[i]) != lower(pre[i]) { return false }; i = i + 1 }
return true
}
fn read_message() -> ptr {
var length = 0 - 1
let line = buf_new()
while true {
line.len = 0
var c = read_char()
if c < 0 { return null }
while c >= 0 and c != 10 { buf_putc(line, c); c = read_char() }
let s = buf_str(line)
var L = cstr_len(s)
if L > 0 and s[L - 1] == 13 { s[L - 1] = 0; L = 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 = got + 1 }
buf[got] = 0
return buf
}
# ---- position output ----
fn 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, 125) }
fn 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, 125) }
fn 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, 125) }
fn 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))
}
fn off_of(msg: JVal, D: Doc, base: ptr) -> 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)
}
fn basename(p: ptr) -> ptr { var last = 0 - 1; var i = 0; while p[i] != 0 { if p[i] == 47 { last = i }; i = i + 1 }; return p[last + 1..i] }
# ---- LSP symbol kinds & completion kinds ----
fn 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
}
fn 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) ----
fn diag_structural(D: Doc, o: Buf) -> void {
let ds_s = new []int; let ds_e = new []int; let ds_m = new []ptr
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 = i + 1; continue }
let c = D.src[D.tk_start[i]]
if c == 123 or c == 40 or c == 91 { if top < 1200 { stack[top] = i; top = top + 1 } }
else { if c == 125 or c == 41 or c == 93 {
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 = top - 1; let oi = stack[top]; let oc = D.src[D.tk_start[oi]]
var wantc = 93
if oc == 123 { wantc = 125 } else { if oc == 40 { wantc = 41 } }
if wantc != c { push(ds_s, D.tk_start[i]); push(ds_e, D.tk_end[i]); push(ds_m, "mismatched bracket") }
}
} }
i = 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 = k + 1 }
var j = 0
while j < len(ds_s) {
if j > 0 { buf_putc(o, 44) }
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, 125)
j = j + 1
}
}
fn 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 { diag_structural(D, o) }
buf_puts(o, "]}")
send_notify("textDocument/publishDiagnostics", buf_str(o))
}
# ---- document symbols ----
fn put_doc_symbols(o: Buf, D: Doc, parent: int) -> void {
buf_putc(o, 91)
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, 44) }
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, 125)
}
i = i + 1
}
buf_putc(o, 93)
}
# ---- hover ----
fn hover_md(o: Buf, code: ptr, docstr: ptr) -> 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, "}}")
}
fn 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 }
resolve_at(D, off)
if (g_owner != null) and g_symidx >= 0 {
let ow = g_owner
let y = ow.syms[g_symidx]
var code = y.detail
if code == "" { code = y.name }
var docstr = y.doc
if not (ow.path == D.path) { docstr = concat3(docstr, "\n\n_declared in ", concat3(basename(ow.path), "_", "")) }
let o = buf_new(); hover_md(o, code, docstr); send_result(id, buf_str(o)); 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 ----
fn 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 tok >= 0 and D.tk_kind[tok] == LT_STR {
let p = prev_sig(D, tok)
if p >= 0 and tis(D, p, "import") {
var n = tlen(D, tok) - 2; if n < 0 { n = 0 }
let rel = D.src[D.tk_start[tok] + 1..D.tk_start[tok] + 1 + n]
let full = join_path(dirname(D.path), rel)
let T = ensure_doc(full)
if (T != null) { let o = buf_new(); put_location(o, T, 0, 0); send_result(id, buf_str(o)); return }
}
}
resolve_at(D, off)
if (g_owner == null) or g_symidx < 0 { 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
fn 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 = k + 1
}
i = i + 1
}
}
fn 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, 91)
var i = 0
while i < len(g_occ_doc) {
if i > 0 { buf_putc(o, 44) }
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 = i + 1
}
buf_putc(o, 93); send_result(id, buf_str(o))
}
fn 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 = s + 1 }
if not already {
push(seen, g_occ_doc[gi])
if len(seen) > 1 { buf_putc(o, 44) }
jstr_out(o, g_occ_doc[gi].uri); buf_putc(o, 58); buf_putc(o, 91)
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, 44) }
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, 125)
}
k = k + 1
}
buf_putc(o, 93)
}
gi = gi + 1
}
buf_puts(o, "}}"); send_result(id, buf_str(o))
}
# ---- completion ----
var g_first: int = 1
fn comp_item(o: Buf, label: ptr, kind: int, detail: ptr) -> void {
if g_first == 0 { buf_putc(o, 44) }
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_putc(o, 125)
}
fn comp_scope(o: Buf, D: Doc, off: int, types_only: int) -> void {
if types_only == 0 {
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 {
var det = y.detail; if det == "" { det = y.ty }
comp_item(o, y.name, 6, det)
}
i = i + 1
}
}
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 = k + 1
}
r = r + 1
}
}
fn comp_types(o: Buf) -> void {
comp_item(o, "int", 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, "str", 14, "built-in type"); comp_item(o, "ptr", 14, "built-in type"); comp_item(o, "void", 14, "built-in type")
}
fn comp_builtins(o: Buf) -> void { var i = 0; while i < len(g_builtins) { comp_item(o, g_builtins[i], 3, g_bsigs[i]); i = i + 1 } }
fn 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 = 0 - 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 = 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 == 93 { depth = depth + 1 }
else { if ch == 91 { if depth == 0 { in_qt = 1; j = 0 - 1 } else { depth = depth - 1 } }
else { if ch == 123 or ch == 125 { j = 0 - 1 } } }
}
j = 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 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 = 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")
}
if done == 0 {
comp_item(o, "let", 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)
}
buf_puts(o, "]}")
send_result(id, buf_str(o))
}
# ---- inlay hints, signature help, semantic tokens, folding ----
fn 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, 91)
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, 44) }
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 = i + 1
}
buf_putc(o, 93); send_result(id, buf_str(o))
}
fn on_signature_help(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")
var depth = 0; var callee = 0 - 1; var arg = 0
var i = ntok(D) - 1
while i >= 0 {
let t = i
if D.tk_start[t] >= off { i = i - 1; continue }
if D.tk_kind[t] == LT_OP and tlen(D, t) == 1 {
let c = D.src[D.tk_start[t]]
if c == 41 { depth = depth + 1 }
else { if c == 40 {
if depth == 0 {
callee = prev_sig(D, t)
var k = t + 1
while k < ntok(D) and D.tk_start[k] < off { if tis(D, k, ",") { arg = arg + 1 }; k = k + 1 }
i = 0 - 1
} else { depth = depth - 1 }
} }
}
i = i - 1
}
if callee < 0 or D.tk_kind[callee] != LT_ID { send_result(id, "null"); return }
let name = ttext(D, callee)
var sig = builtin_sig(name)
var docstr = ""
if (sig == null) {
let t = find_top(D, name)
if t >= 0 and (g_owner.syms[t].kind == LS_FN or g_owner.syms[t].kind == LS_EXTERN) { sig = g_owner.syms[t].detail; docstr = g_owner.syms[t].doc }
}
if (sig == null) { send_result(id, "null"); return }
let o = buf_new()
buf_puts(o, "{\"signatures\":[{\"label\":"); jstr_out(o, sig)
if (docstr != null) and (docstr[0] != 0) { buf_puts(o, ",\"documentation\":"); jstr_out(o, docstr) }
buf_puts(o, "}],\"activeSignature\":0,\"activeParameter\":"); buf_puti(o, arg); buf_putc(o, 125)
send_result(id, buf_str(o))
}
fn 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, 44) }
first = 0
buf_puti(o, dl); buf_putc(o, 44); buf_puti(o, dc); buf_putc(o, 44); buf_puti(o, ln); buf_putc(o, 44); buf_puti(o, ty); buf_putc(o, 44); buf_puti(o, g_semmods)
prev_line = line; prev_char = ch
}
}
}
i = i + 1
}
buf_puts(o, "]}"); send_result(id, buf_str(o))
}
fn 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, 91); 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, 44) }
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 = i + 1
}
buf_putc(o, 93); send_result(id, buf_str(o))
}
fn on_workspace_symbol(msg: JVal, id: JVal) -> void {
let q = jstr(jpath(msg, "params.query"), "")
let o = buf_new(); buf_putc(o, 91); 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, 44) }
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, 125)
}
}
k = k + 1
}
d = d + 1
}
buf_putc(o, 93); send_result(id, buf_str(o))
}
fn contains_ci(hay: ptr, needle: ptr) -> 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 = j + 1 }
if m { return true }
i = i + 1
}
return false
}
fn 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 ----
fn 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)
}
fn 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)
}
fn 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) }
}
fn caps() -> ptr {
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, ",\"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\"]},\"full\":true,\"range\":true}")
buf_puts(o, "},\"serverInfo\":{\"name\":\"ludic-lsp\",\"version\":\"1.0.0\"}}")
return buf_str(o)
}
fn 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) }
send_result(id, caps())
}
fn 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_change(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, 0 - 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") or (method == "textDocument/typeDefinition") { on_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_references(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, 125); 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_help(msg, id); return }
if (method == "textDocument/completion") { on_completion(msg, id); return }
if (method == "textDocument/documentLink") { send_result(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)
}
}
}