ludic/tools/ludic-tools/lsp.ludic
Orkuncakilkaya 2ad6edaae3 ludic syntax, and ludic-dev syntax: every grammar written from the compiler's vocabulary and checked against it
`ludic syntax [--json] [-o FILE]` prints what `ludicc --emit-syntax` does (a line
per entry, or the JSON). `ludic-dev syntax` writes, between "ludic-dev syntax:
begin" / "end" lines, the keyword, type, phase and attribute tables of
ludic_syntax.h, LudicVocabulary's sets (JetBrains), ludic-mode.el's lists and the
language server's word tests (is_keyword_word and the rest; is_contextual_word is
every word the parser does not reserve, and every declaring or modifying one),
and every TextMate pattern marked "comment": "ludic-dev syntax: <group>" (shared
and the VS Code copy). The grammars gain module uses port bind action reducer
dispatch registry def open component prop view alias friend unsafe numbers of as
from mut system; import and extern colour as declarations; the phase clause
knows Overlay; the bitwise pattern matches | and ^ on their own again.

`ludic-dev syntax --check` - and check-vocabulary, whose old parser comparison it
replaces, and the regression suite (syntax_cases, one line per file) - fails when
a written list is behind, when a grammar lacks a keyword, type or phase, when
docs/language has no page for a keyword, type, phase or attribute, or when the
parser (a scan of selfhost/frontend: is_id / text == words, a == / ann ==
attributes) tests a word or reads an attribute vocab.ludic lacks, or the reverse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:59:30 +03:00

2928 lines
127 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 {
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_<Name> 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)
}
}
}