Networking N2–N6, and a fully C-free toolchain

Implement the rest of NETWORKING-DESIGN.md (N2–N6) and eliminate every
`.c` file from the repo. clang remains only the LLVM-IR assembler; no C
is compiled anywhere.

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-29 15:08:23 +03:00
parent 96d01e45ab
commit bca8f126fc
67 changed files with 24066 additions and 9309 deletions

View file

@ -5,41 +5,39 @@
# ./tools/build-tools.sh --test build, then run tools/test-tools.sh
# ./tools/build-tools.sh --install also symlink both into ~/.local/bin
#
# Both binaries are plain C with no dependencies, same as the compiler. They
# share the lexer and vocabulary in tools/ludic-tools/ludic_syntax.h, so a
# keyword added there reaches every editor at once.
# Both binaries are written in Ludic (tools/ludic-tools/fmt.ludic, lsp.ludic) and
# compiled by the Ludic compiler itself — no C is compiled. clang only assembles
# the emitted LLVM IR, the same floor the compiler stands on. The vocabulary the
# grammar/lexer share still lives in tools/ludic-tools/ludic_syntax.h (data, read
# by check-vocabulary.py); the tools carry their own copy of it in Ludic.
set -e
cd "$(dirname "$0")/.."
CC="${LUDIC_CC:-clang}"
CFLAGS="-O2 -Wall -Wno-unused-function"
SRC=tools/ludic-tools
OUT=build
mkdir -p "$OUT"
build_one() {
local name="$1" main="$2"
local newest
# rebuild when any header or the entry point is newer than the binary
if [ -x "$OUT/$name" ]; then
newest=$(find "$SRC" -name '*.h' -o -name "$(basename "$main")" | while read -r f; do
[ "$f" -nt "$OUT/$name" ] && echo new
done) || true
if [ -z "$newest" ]; then echo "$name: up to date"; return; fi
# the Ludic compiler, assembled from the checked-in IR seed (C-free)
if [ ! -x "$OUT/ludicc" ] || [ selfhost/ludicc.seed.ll -nt "$OUT/ludicc" ]; then
echo "cc: selfhost/ludicc.seed.ll -> $OUT/ludicc (from the IR seed, no C compiler)"
$CC selfhost/ludicc.seed.ll -o "$OUT/ludicc"
fi
build_ludic() {
local name="$1" src="$2"
if [ -x "$OUT/$name" ] && [ ! "$src" -nt "$OUT/$name" ] && [ ! "$OUT/ludicc" -nt "$OUT/$name" ]; then
echo "$name: up to date"; return
fi
echo "cc: $main -> $OUT/$name"
$CC $CFLAGS "$main" -o "$OUT/$name"
echo "ludicc: $src -> $OUT/$name (Ludic -> LLVM IR -> binary, no C)"
"$OUT/ludicc" "$src" --emit-llvm -o "$OUT/$name.ll" >/dev/null 2>&1 \
&& $CC -O2 "$OUT/$name.ll" -o "$OUT/$name" \
&& rm -f "$OUT/$name.ll" || { echo "build failed: $name"; exit 1; }
}
build_one ludic-fmt "$SRC/ludic_fmt_main.c"
build_one ludic-lsp "$SRC/ludic_lsp.c"
# The language server shells out to the compiler for real diagnostics, so having
# it around is most of the value.
if [ ! -x "$OUT/ludicc" ]; then
echo "note: $OUT/ludicc not built yet — run ./build.sh for compiler diagnostics"
fi
build_ludic ludic-fmt "$SRC/fmt.ludic"
build_ludic ludic-lsp "$SRC/lsp.ludic"
# VS Code needs the grammar inside its own extension directory, so it gets a
# copy. A copy that can drift is the whole failure mode this toolchain is built

View file

@ -42,15 +42,15 @@ object LudicTokens {
object LudicVocabulary {
val DECL = setOf(
"program", "import", "property", "model", "enum", "ui",
"const", "var", "fn", "extern", "handler", "entry"
"const", "var", "fn", "extern", "handler", "entry", "event", "scene"
)
val CLAUSE = setOf(
"phase", "query", "on"
"phase", "query", "on", "cancellable", "public", "layer", "start"
)
val STMT = setOf(
"let", "return", "if", "else", "while", "for", "in", "spawn", "despawn",
"enable", "disable", "match", "machine", "state", "become", "where",
"and", "or", "not", "break", "continue", "new"
"and", "or", "not", "break", "continue", "new", "emit", "cancel"
)
val PRIMITIVES = setOf("int", "fixed", "bool", "entity", "str", "ptr", "byte", "words", "fixeds", "ptrs", "void")
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render")

563
tools/ludic-tools/fmt.ludic Normal file
View file

@ -0,0 +1,563 @@
# fmt.ludic — the canonical Ludic formatter, written in Ludic (replaces the C
# ludic_fmt_main.c + ludic_fmt.h). Works on the token stream, so comments and
# blank lines survive and nothing is ever dropped or reordered — only the
# whitespace between tokens is normalized. Lines are re-indented and respaced but
# never joined or split. Mirrors tools/ludic-tools/ludic_fmt.h exactly.
#
# ludic-fmt a.ludic print the formatted text
# ludic-fmt -w a.ludic rewrite in place
# ludic-fmt --check a.ludic exit 1 if unformatted
# ludic-fmt a.md format the ```ludic fences in a document
# cat a.ludic | ludic-fmt - filter mode (stdin -> stdout)
program LudicFmt {
# ---- token kinds (mirror ludic_syntax.h) ----
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
# ---- tiny stdio + string helpers (self-contained) ----
fn read_file(path: str) -> ptr {
let f = file_open(path, "rb")
if (f == null) { return null }
file_seek(f, 0, 2)
let n = file_tell(f)
file_seek(f, 0, 0)
let buf = bytes(n + 1)
file_read(f, buf, n)
buf[n] = 0
file_close(f)
return buf
}
fn cstr_len(s: ptr) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n }
fn char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 }
fn char_is_alpha(c: int) -> bool {
if c >= 65 and c <= 90 { return true }
if c >= 97 and c <= 122 { return true }
return c == 95
}
fn char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) }
fn char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 97 and c <= 102) or (c >= 65 and c <= 70) }
fn itoa(v: int) -> ptr {
if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z }
var neg = false; var x = v
if x < 0 { neg = true; x = 0 - x }
let tmp = bytes(16); var n = 0
while x > 0 { tmp[n] = 48 + x % 10; x = x / 10; n = n + 1 }
var total = n
if neg { total = total + 1 }
let out = bytes(total + 1); var k = 0
if neg { out[0] = 45; k = 1 }
var i = 0
while i < n { out[k + i] = tmp[n - 1 - i]; i = i + 1 }
out[total] = 0
return out
}
# ---- a growable byte buffer ----
property Buf { data: ptr = null, len: int = 0, cap: int = 0 }
fn buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b }
fn buf_ensure(b: Buf, extra: int) -> void {
if b.len + extra + 1 <= b.cap { return }
while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 }
b.data = resize(b.data, b.cap)
}
fn buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len = b.len + 1 }
fn buf_puts(b: Buf, s: ptr) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } }
fn buf_indent(b: Buf, n: int) -> void { var i = 0; while i < n { buf_putc(b, 32); i = i + 1 } }
fn buf_str(b: Buf) -> ptr { b.data[b.len] = 0; return b.data }
# append src[a..b) raw
fn buf_addrange(b: Buf, s: ptr, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k = k + 1 } }
# ---- the token stream (parallel slices) ----
var src: ptr = null
var tk_kind: []int
var tk_start: []int
var tk_end: []int
var tk_line: []int
var linestart: []int
fn ntok() -> int { return len(tk_kind) }
fn tok_len(i: int) -> int { return tk_end[i] - tk_start[i] }
fn tok_text(i: int) -> ptr { return src[tk_start[i]..tk_end[i]] }
fn push_tok(k: int, st: int, en: int, ln: int) -> void {
push(tk_kind, k); push(tk_start, st); push(tk_end, en); push(tk_line, ln)
}
# ---- vocabulary classifiers ----
fn is_type_word(w: ptr) -> bool {
return (w == "int") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void")
}
fn is_phase_word(w: ptr) -> bool {
return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render")
}
fn is_keyword_word(w: ptr) -> bool {
if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true }
if (w == "const") or (w == "var") or (w == "fn") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") { return true }
if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "let") or (w == "return") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "spawn") or (w == "despawn") { return true }
if (w == "enable") or (w == "disable") or (w == "match") or (w == "machine") or (w == "state") or (w == "become") or (w == "where") { return true }
if (w == "and") or (w == "or") or (w == "not") or (w == "break") or (w == "continue") or (w == "new") or (w == "emit") or (w == "cancel") { return true }
return false
}
fn is_clause_word(w: ptr) -> bool {
return (w == "phase") or (w == "query") or (w == "reads") or (w == "writes") or (w == "needs") or (w == "uses") or (w == "requires") or (w == "ensures") or (w == "invariant") or (w == "effects")
}
# ---- the lexer: keeps comments, newlines, byte spans; never exits on bad input ----
fn is_op2(c0: int, c1: int) -> bool {
if c0 == 45 and c1 == 62 { return true } # ->
if c1 == 61 and (c0 == 43 or c0 == 45 or c0 == 42 or c0 == 47 or c0 == 61 or c0 == 33 or c0 == 60 or c0 == 62) { return true } # += -= *= /= == != <= >=
if c0 == 38 and c1 == 38 { return true } # &&
if c0 == 124 and c1 == 124 { return true } # ||
if c0 == 46 and c1 == 46 { return true } # ..
if c0 == 61 and c1 == 62 { return true } # =>
return false
}
fn is_op1(c: int) -> bool {
return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59
}
fn lex(s: ptr) -> void {
src = s
tk_kind = new []int; tk_start = new []int; tk_end = new []int; tk_line = new []int
linestart = new []int
push(linestart, 0)
var i = 0; var line = 0
while s[i] != 0 {
let c = s[i]
if c == 10 { push_tok(LT_NL, i, i + 1, line); i = i + 1; line = line + 1; push(linestart, i); continue }
if c == 32 or c == 9 or c == 13 { i = i + 1; continue }
if c == 35 { # '#' comment to end of line
let st = i; while s[i] != 0 and s[i] != 10 { i = i + 1 }; push_tok(LT_COMMENT, st, i, line); continue
}
if c == 34 { # "string"
let st = i; i = i + 1
while s[i] != 0 and s[i] != 34 and s[i] != 10 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } }
if s[i] == 34 { i = i + 1 }
push_tok(LT_STR, st, i, line); continue
}
if c == 96 { # `interpolated`
let st = i; i = i + 1
while s[i] != 0 and s[i] != 96 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } }
if s[i] == 96 { i = i + 1 }
push_tok(LT_STR, st, i, line); continue
}
if c == 39 { # 'c'
let st = i; i = i + 1
if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { if s[i] != 0 and s[i] != 10 { i = i + 1 } }
if s[i] == 39 { i = i + 1 }
push_tok(LT_CHAR, st, i, line); continue
}
if char_is_digit(c) {
let st = i
if c == 48 and (s[i + 1] == 120 or s[i + 1] == 88) { # 0x hex
i = i + 2; while char_is_hex(s[i]) { i = i + 1 }; push_tok(LT_INT, st, i, line); continue
}
while char_is_digit(s[i]) { i = i + 1 }
if s[i] == 46 and char_is_digit(s[i + 1]) {
i = i + 1; while char_is_digit(s[i]) { i = i + 1 }; push_tok(LT_FLOAT, st, i, line); continue
}
push_tok(LT_INT, st, i, line); continue
}
if c == 64 and (char_is_alpha(s[i + 1]) or s[i + 1] == 95) { # @name annotation
let st = i; i = i + 1; while char_is_alnum(s[i]) { i = i + 1 }; push_tok(LT_ANNO, st, i, line); continue
}
if char_is_alpha(c) {
let st = i; while char_is_alnum(s[i]) { i = i + 1 }
let w = s[st..i]
var k = LT_ID
if (w == "true") or (w == "false") or (w == "null") { k = LT_BOOL }
else { if is_type_word(w) { k = LT_TYPE }
else { if is_phase_word(w) { k = LT_PHASE }
else { if is_keyword_word(w) { k = LT_KW } } } }
push_tok(k, st, i, line); continue
}
if is_op2(c, s[i + 1]) { push_tok(LT_OP, i, i + 2, line); i = i + 2; continue }
if is_op1(c) { push_tok(LT_OP, i, i + 1, line); i = i + 1; continue }
# anything else: one UTF-8 character's worth as an LT_ERR token
var ln = 1
if c >= 240 { ln = 4 } else { if c >= 224 { ln = 3 } else { if c >= 128 { ln = 2 } } }
var kk = 1
while kk < ln { if s[i + kk] == 0 or (s[i + kk] & 192) != 128 { ln = kk }; kk = kk + 1 }
push_tok(LT_ERR, i, i + ln, line); i = i + ln
}
push_tok(LT_EOF, i, i, line)
}
# ---- token-stream helpers ----
fn next_sig(i: int) -> int {
var j = i + 1
while j < ntok() { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j + 1 }
return 0 - 1
}
fn prev_sig(i: int) -> int {
var j = i - 1
while j >= 0 { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j - 1 }
return 0 - 1
}
fn 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 }
# a '.' hugs an operand, a closing bracket, and another dot
fn dot_tight(t: int) -> bool {
if name_like(tk_kind[t]) { return true }
if tk_kind[t] != LT_OP { return false }
let n = tok_len(t); let c0 = src[tk_start[t]]
if n == 1 and (c0 == 41 or c0 == 93 or c0 == 46) { return true }
if n == 2 and c0 == 46 and src[tk_start[t] + 1] == 46 { return true }
return false
}
# is the token at index i a unary '-'/'!' rather than a binary operator?
fn is_unary(i: int) -> bool {
if tk_kind[i] != LT_OP { return false }
let n = tok_len(i)
if not (n == 1 and (src[tk_start[i]] == 45 or src[tk_start[i]] == 33)) { return false }
let p = prev_sig(i)
if p < 0 { return true }
let pk = tk_kind[p]
if pk == LT_ID or pk == LT_INT or pk == LT_FLOAT or pk == LT_STR or pk == LT_CHAR or pk == LT_BOOL or pk == LT_TYPE or pk == LT_PHASE { return false }
if pk == LT_OP {
let c = src[tk_start[p]]
return not (tok_len(p) == 1 and (c == 41 or c == 93 or c == 125)) # a closing bracket ends an operand
}
return true # keyword/annotation/comment: operand starts here
}
# whitespace between the previous emitted token (prev) and the current one (cur)
fn space_before(prev: int, cur: int) -> int {
if prev < 0 { return 0 }
let pk = tk_kind[prev]; let ck = tk_kind[cur]
let plen = tok_len(prev); let clen = tok_len(cur)
let p0 = src[tk_start[prev]]; let c0 = src[tk_start[cur]]
let p1 = (plen == 1); let c1 = (clen == 1)
if pk == LT_ERR or ck == LT_ERR { return tk_start[cur] - tk_end[prev] } # preserve an error token's spacing
if c1 and (c0 == 41 or c0 == 93 or c0 == 44 or c0 == 58 or c0 == 59) { return 0 } # ) ] , : ;
if c1 and c0 == 46 and ck == LT_OP and dot_tight(prev) { return 0 }
if p1 and p0 == 46 and pk == LT_OP and dot_tight(cur) { return 0 }
if p1 and (p0 == 40 or p0 == 91) and pk == LT_OP { return 0 } # nothing hugs an opener from the right
if pk == LT_OP and is_unary(prev) { return 0 }
if pk == LT_ANNO and c1 and c0 == 40 { return 0 } # @anno(
if c1 and c0 == 40 and ck == LT_OP {
if pk == LT_ID or pk == LT_TYPE or pk == LT_PHASE { return 0 } # fn move( / clear(
if pk == LT_OP { if p1 and (p0 == 41 or p0 == 93) { return 1 }; return 0 }
return 1
}
return 1
}
# ---- the formatting pass ----
fn format(indent_width: int) -> Buf {
let o = buf_new()
let stack = words(600)
let hang = words(600)
var sp = 0
var cur = 0
var open = 0
var brack = 0
var ui_depth = 0 - 1
var pending_blank = 0
var wrote_any = 0
var prev_line_had_comment = 0
let N = ntok()
var i = 0
while i < N and tk_kind[i] != LT_EOF {
let a = i
while i < N and tk_kind[i] != LT_NL and tk_kind[i] != LT_EOF { i = i + 1 }
let b = i
if i < N and tk_kind[i] == LT_NL { i = i + 1 }
if a == b { # a blank line
if wrote_any == 1 { pending_blank = 1 }
continue
}
# where does this line start?
var line_level = cur
var tsp = sp
var t = a
while t < b and tk_kind[t] == LT_OP and tok_len(t) == 1 and src[tk_start[t]] == 125 { # leading '}'
if tsp > 0 { tsp = tsp - 1; line_level = stack[tsp] }
t = t + 1
}
# original column of this line
var orig_ind = 0
var kk = linestart[tk_line[a]]
while kk < tk_start[a] { if src[kk] == 9 { orig_ind = orig_ind + 4 } else { orig_ind = orig_ind + 1 }; kk = kk + 1 }
# a comment continuing the previous comment line keeps the author's column
var comment_run = 0
if 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) { # author owns alignment inside open call/brace
let ls = linestart[tk_line[a]]
ind = 0
var k2 = ls
while k2 < tk_start[a] { if src[k2] == 9 { ind = ind + 4 } else { ind = ind + 1 }; k2 = k2 + 1 }
} else {
var extra = 0
if is_clause_word(tok_text(a)) and tk_kind[a] == LT_KW { extra = indent_width }
ind = line_level * indent_width + extra
if comment_run == 1 and orig_ind > ind { ind = orig_ind }
}
if pending_blank == 1 and wrote_any == 1 { buf_putc(o, 10) }
pending_blank = 0
buf_indent(o, ind)
# emit the tokens
var prev = 0 - 1
var line_brack = brack
var line_ui_open = 0
if ui_depth >= 0 and sp > ui_depth { line_ui_open = 1 }
t = a
while t < b {
var want = 0
if tk_kind[t] == LT_COMMENT { if prev >= 0 { want = 2 } else { want = 0 } }
else {
want = space_before(prev, t)
if line_brack > 0 { # query {Tag} filter is one word
if tk_kind[t] == LT_OP and tok_len(t) == 1 and src[tk_start[t]] == 125 { want = 0 }
if prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 1 and src[tk_start[prev]] == 123 { want = 0 }
}
if line_ui_open == 1 { # widget props are k=v
var eq_here = 0
if tk_kind[t] == LT_OP and tok_len(t) == 1 and src[tk_start[t]] == 61 { eq_here = 1 }
var eq_prev = 0
if prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 1 and src[tk_start[prev]] == 61 { eq_prev = 1 }
if eq_here == 1 or eq_prev == 1 { want = 0 }
}
}
var gap = 0
if prev >= 0 { gap = tk_start[t] - tk_end[prev] }
if gap >= 2 and want >= 1 { if gap > 60 { gap = 60 }; want = gap } # hand alignment wins
if want < 0 { want = 0 }
var w2 = 0
while w2 < want { buf_putc(o, 32); w2 = w2 + 1 }
buf_addrange(o, src, tk_start[t], tk_end[t])
prev = t
if tk_kind[t] == LT_OP and tok_len(t) == 1 {
let c = src[tk_start[t]]
if c == 91 { line_brack = line_brack + 1 }
else { if c == 93 { line_brack = line_brack - 1; if line_brack < 0 { line_brack = 0 } } }
}
t = t + 1
}
buf_putc(o, 10)
wrote_any = 1
prev_line_had_comment = 0
if prev >= 0 and tk_kind[prev] == LT_COMMENT { prev_line_had_comment = 1 }
# carry the nesting into the next line
if tk_kind[a] == LT_KW and (tok_text(a) == "ui") and ui_depth < 0 { ui_depth = sp }
var level = cur
t = a
while t < b {
if tk_kind[t] == LT_OP and tok_len(t) == 1 {
let c = src[tk_start[t]]
if c == 123 { # '{'
if sp < 512 {
let nxt = next_sig(t)
stack[sp] = line_level
var h = 0
if nxt >= 0 and nxt < b { h = 1 }
hang[sp] = h
sp = sp + 1
}
level = line_level + 1
}
else { if c == 125 { if sp > 0 { sp = sp - 1; level = stack[sp] } }
else { if c == 40 or c == 91 { open = open + 1; if c == 91 { brack = brack + 1 } }
else { if c == 41 or c == 93 { open = open - 1; if open < 0 { open = 0 }; if c == 93 { brack = brack - 1; if brack < 0 { brack = 0 } } } } } }
}
t = t + 1
}
cur = level
if ui_depth >= 0 and sp <= ui_depth { ui_depth = 0 - 1 }
}
return o
}
# ---- markdown: format the body of every ```ludic fence, leave prose alone ----
var g_flen: int = 0
var g_info: int = 0
var g_marker: int = 0
# detect a fence at line offset i; sets g_flen/g_info/g_marker; returns bool
fn md_fence_at(s: ptr, i: int) -> bool {
var j = i; while s[j] == 32 { j = j + 1 }
let m = s[j]
if m != 96 and m != 126 { return false } # ` or ~
var n = 0; while s[j] == m { j = j + 1; n = n + 1 }
if n < 3 { return false }
g_flen = n; g_info = j; g_marker = m
return true
}
fn md_info_is_ludic(s: ptr, at: int) -> bool {
var a = at; while s[a] == 32 or s[a] == 9 { a = a + 1 }
# case-insensitive "ludic"
if not (((s[a] == 108 or s[a] == 76)) and ((s[a + 1] == 117 or s[a + 1] == 85)) and ((s[a + 2] == 100 or s[a + 2] == 68)) and ((s[a + 3] == 105 or s[a + 3] == 73)) and ((s[a + 4] == 99 or s[a + 4] == 67))) { return false }
let af = s[a + 5]
return af == 0 or af == 10 or af == 32 or af == 9 or af == 13
}
fn line_end(s: ptr, i: int) -> int { var e = i; while s[e] != 0 and s[e] != 10 { e = e + 1 }; return e }
fn format_markdown(s: ptr, indent_width: int) -> Buf {
let o = buf_new()
var i = 0
while s[i] != 0 {
let ls = i
let le = line_end(s, ls)
var indent = 0; while s[ls + indent] == 32 { indent = indent + 1 }
if md_fence_at(s, ls) and md_info_is_ludic(s, g_info) {
# copy the opening fence line verbatim (with its newline)
var e0 = le; if s[le] != 0 { e0 = le + 1 }
buf_addrange(o, s, ls, e0)
i = e0
# gather the body up to the closing fence
let bs = i
var be = bs
while true {
if s[be] == 0 { break }
let ps = be; let pe = line_end(s, ps)
if md_fence_at(s, ps) and g_marker == g_marker and g_flen >= g_flen {
# re-run detection for THIS line (md_fence_at set globals for ps)
let cmark = g_marker; let clen = g_flen; let cinfo = g_info
if md_fence_at(s, ps) {
if g_marker == cmark and g_flen >= clen {
var only = 1
var k = g_info
while k < pe { if s[k] != 32 and s[k] != 13 { only = 0; break }; k = k + 1 }
if only == 1 { be = ps; break }
}
}
}
if s[pe] != 0 { be = pe + 1 } else { be = pe }
}
# de-indent the body, format it, re-indent it
let body = buf_new()
var p = bs
while p < be {
var q = line_end(s, p)
var skip = 0
while skip < indent and (p + skip) < q and s[p + skip] == 32 { skip = skip + 1 }
buf_addrange(body, s, p + skip, q)
buf_putc(body, 10)
if s[q] != 0 { p = q + 1 } else { p = q }
}
# feed the body through the ludic formatter
lex(buf_str(body))
let f = format(indent_width)
let ftext = buf_str(f)
var fp = 0
while ftext[fp] != 0 {
var fq = fp; while ftext[fq] != 0 and ftext[fq] != 10 { fq = fq + 1 }
if fq > fp { buf_indent(o, indent) }
buf_addrange(o, ftext, fp, fq)
buf_putc(o, 10)
if ftext[fq] != 0 { fp = fq + 1 } else { fp = fq }
}
i = be
continue
}
var e1 = le; if s[le] != 0 { e1 = le + 1 }
buf_addrange(o, s, ls, e1)
i = e1
}
return o
}
# ---- ends-with helpers for extension detection ----
fn ends_with(s: ptr, suf: ptr) -> bool {
let n = cstr_len(s); let m = cstr_len(suf)
if m > n { return false }
return (s[n - m..n] == suf)
}
# ---- stdin slurp (for `-`) ----
fn slurp_stdin() -> ptr {
let b = buf_new()
var c = read_char()
while c >= 0 { buf_putc(b, c); c = read_char() }
return buf_str(b)
}
# format one source string according to its kind (markdown vs ludic)
fn format_source(text: ptr, is_md: bool, indent_width: int) -> ptr {
if is_md { let m = format_markdown(text, indent_width); return buf_str(m) }
lex(text)
let f = format(indent_width)
return buf_str(f)
}
fn streq(a: ptr, b: ptr) -> bool { return (a == b) }
entry {
var write = false
var check = false
var indent = 2
var quiet = false
var changed = false
var failed = false
let files = new []ptr
var ai = 1
while ai < arg_count() {
let a = arg(ai)
if (a == "-w") or (a == "--write") { write = true }
else { if (a == "--check") or (a == "-l") { check = true }
else { if (a == "-q") or (a == "--quiet") { quiet = true }
else { if (a == "--indent") { ai = ai + 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di = di + 1 } } }
else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source"); return }
else { push(files, a) } } } } }
ai = ai + 1
}
if indent < 1 or indent > 8 { indent = 2 }
# stdin filter mode
if len(files) == 0 or (len(files) == 1 and (files[0] == "-")) {
let text = slurp_stdin()
let out = format_source(text, false, indent)
file_write(file_stdout(), out, cstr_len(out))
return
}
var fi = 0
while fi < len(files) {
let path = files[fi]
let text = read_file(path)
if (text == null) {
let m = `ludic-fmt: cannot open {path}\n`
file_write(file_stderr(), m, cstr_len(m)); failed = true
} else {
let is_md = ends_with(path, ".md") or ends_with(path, ".markdown")
let out = format_source(text, is_md, indent)
let same = (out == text)
if check {
if not same { changed = true; if not quiet { print(path) } }
} else { if write {
if not same {
let f = file_open(path, "wb")
if (f == null) { let m = `ludic-fmt: cannot write {path}\n`; file_write(file_stderr(), m, cstr_len(m)); failed = true }
else { file_write(f, out, cstr_len(out)); file_close(f); if not quiet { let m = `formatted {path}\n`; file_write(file_stderr(), m, cstr_len(m)) } }
changed = true
}
} else {
file_write(file_stdout(), out, cstr_len(out))
} }
}
fi = fi + 1
}
if failed { exit(2) }
if check and changed { exit(1) }
}
}

2409
tools/ludic-tools/lsp.ludic Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,339 +0,0 @@
/* ============================================================================
* ludic_fmt.h — the canonical Ludic formatter.
*
* This is deliberately NOT `ludicc --fmt`. The compiler's printer walks the AST
* after import splicing, so it drops every comment and inlines every imported
* file into whichever file you pointed it at — fine for inspecting what the
* compiler saw, catastrophic as an editor's "format on save".
*
* This formatter works on the token stream instead:
* - comments and blank lines survive, because they are tokens;
* - nothing is ever dropped or reordered, because every token is re-emitted
* in order — the only freedom taken is the whitespace between them;
* - lines are re-indented and respaced but never joined or split, so the
* author keeps control of line structure and a format-on-save never
* rewrites a file out from under someone mid-edit.
* ==========================================================================*/
#ifndef LUDIC_FMT_H
#define LUDIC_FMT_H
#include "ludic_syntax.h"
typedef struct { char* b; size_t n, cap; } FSB;
static void fsb_ensure(FSB* s, size_t add){
if (s->n + add + 1 > s->cap){ s->cap = (s->n + add + 1) * 2; s->b = realloc(s->b, s->cap); }
}
static void fsb_add(FSB* s, const char* z, size_t l){ fsb_ensure(s, l); memcpy(s->b + s->n, z, l); s->n += l; s->b[s->n] = 0; }
static void fsb_puts(FSB* s, const char* z){ fsb_add(s, z, strlen(z)); }
static void fsb_putc(FSB* s, char c){ fsb_add(s, &c, 1); }
static void fsb_indent(FSB* s, int n){ for (int i = 0; i < n; i++) fsb_putc(s, ' '); }
/* Can this token be one side of a member access? */
static int lud_name_like(int kind){
return kind == LT_ID || kind == LT_KW || kind == LT_TYPE || kind == LT_PHASE || kind == LT_BOOL;
}
/* A '.' hugs an operand, a closing bracket, and another dot — the last so that
* a run of dots stays a run of dots instead of being spaced into pieces. */
static int lud_dot_tight(const LLex* L, const LTok* t){
if (lud_name_like(t->kind)) return 1;
if (t->kind != LT_OP) return 0;
int n = ltok_len(t);
char c0 = L->src[t->start];
if (n == 1 && (c0 == ')' || c0 == ']' || c0 == '.')) return 1;
if (n == 2 && c0 == '.' && L->src[t->start + 1] == '.') return 1;
return 0;
}
/* Is the token at index i a unary '-' / '!' rather than a binary operator?
* Unary iff nothing that can end an operand precedes it. */
static int fmt_is_unary(const LLex* L, int i){
const LTok* t = &L->v[i];
if (t->kind != LT_OP) return 0;
int n = ltok_len(t);
if (!(n == 1 && (L->src[t->start] == '-' || L->src[t->start] == '!'))) return 0;
int p = ltok_prev_sig(L, i);
if (p < 0) return 1;
const LTok* pt = &L->v[p];
switch (pt->kind){
case LT_ID: case LT_INT: case LT_FLOAT: case LT_STR: case LT_CHAR:
case LT_BOOL: case LT_TYPE: case LT_PHASE:
return 0;
case LT_OP: {
char c = L->src[pt->start];
/* a closing bracket ends an operand; every other operator does not */
return !(ltok_len(pt) == 1 && (c == ')' || c == ']' || c == '}'));
}
default: return 1; /* keyword, annotation, comment: operand starts here */
}
}
/* Whitespace between the previous emitted token (prev) and the current one. */
static int fmt_space_before(const LLex* L, int prev, int cur){
if (prev < 0) return 0;
const LTok* p = &L->v[prev];
const LTok* c = &L->v[cur];
const char* src = L->src;
int plen = ltok_len(p), clen = ltok_len(c);
char p0 = src[p->start], c0 = src[c->start];
int p1 = (plen == 1), c1 = (clen == 1);
/* An error token is bytes we did not understand. Whatever spacing it had
* is the only spacing we can justify, so it is preserved verbatim. */
if (p->kind == LT_ERR || c->kind == LT_ERR) return c->start - p->end;
/* nothing hugs a closer, a separator or a member dot from the left */
if (c1 && (c0 == ')' || c0 == ']' || c0 == ',' || c0 == ':' || c0 == ';')) return 0;
/* `.` binds tight only when it really is member access — `a.b`. A lone dot
* next to a brace is something else (or a typo) and gets normal spacing. */
if (c1 && c0 == '.' && c->kind == LT_OP && lud_dot_tight(L, p)) return 0;
if (p1 && p0 == '.' && p->kind == LT_OP && lud_dot_tight(L, c)) return 0;
/* nothing hugs an opener from the right */
if (p1 && (p0 == '(' || p0 == '[') && p->kind == LT_OP) return 0;
/* a unary sign binds to its operand */
if (p->kind == LT_OP && fmt_is_unary(L, prev)) return 0;
/* @anno(args) */
if (p->kind == LT_ANNO && c1 && c0 == '(') return 0;
if (c1 && c0 == '(' && c->kind == LT_OP){
/* `fn move(` and `clear(` hug; `if (`, `return (`, `x * (` do not */
switch (p->kind){
case LT_ID: case LT_TYPE: case LT_PHASE: return 0;
case LT_OP: return !(p1 && (p0 == ')' || p0 == ']'));
default: return 1;
}
}
return 1;
}
/* Clause keywords hang under the declaration they qualify: a system's
* `phase`/`query` lines and a function's contract lines are indented one level
* past the `system`/`fn` they belong to, with the body brace back at the
* declaration's own level. Every example in the tree is written that way. */
static int fmt_is_clause_word(const LLex* L, int i){
static const char* CLAUSES[] = { "phase","query","reads","writes","needs","uses",
"requires","ensures","invariant","effects", 0 };
if (i < 0 || i >= L->n || L->v[i].kind != LT_KW) return 0;
char b[32]; ltok_text(L, i, b, sizeof(b));
return lud_in(CLAUSES, b);
}
/* One pass over the token stream, re-emitting it with canonical whitespace.
*
* Two carve-outs keep the result idiomatic rather than merely uniform:
* - a run of two or more spaces is preserved verbatim, so hand-aligned
* columns (`const R_DIR: int = 0`) and deliberately set-off trailing
* comments survive a format-on-save;
* - `id=Root` inside a `ui` block and `{Enemy}` inside a query stay tight,
* because those are the spellings the language documents and uses.
*/
static char* ludic_format(const char* src, int indent_width){
LLex L; lud_lex(&L, src);
FSB o = {0};
/* One entry per open brace, holding the indent level to return to. Every
* brace opened on the same line shares that line's level, so a line like
* `if a { if b { if c {` steps in by ONE level, not three — and the line
* that closes them all lands back where it started. */
int stack[512]; int hang[512]; int sp = 0;
int cur = 0;
int open = 0; /* unclosed ( or [ : the author owns the alignment */
int brack = 0; /* unclosed [ only: query-term context */
int ui_depth = -1; /* brace depth just outside the innermost `ui` block */
int pending_blank = 0;
int wrote_any = 0;
int prev_line_had_comment = 0;
int i = 0;
while (i < L.n && L.v[i].kind != LT_EOF){
int a = i;
while (i < L.n && L.v[i].kind != LT_NL && L.v[i].kind != LT_EOF) i++;
int b = i; /* [a,b) are this line's tokens */
if (i < L.n && L.v[i].kind == LT_NL) i++;
if (a == b){ /* a blank line */
if (wrote_any) pending_blank = 1;
continue;
}
/* ---- where does this line start? --------------------------------- */
int line_level = cur;
{ /* leading closers belong to the level of the line that opened them */
int tsp = sp, t = a;
while (t < b && L.v[t].kind == LT_OP && ltok_len(&L.v[t]) == 1 && src[L.v[t].start] == '}'){
if (tsp > 0) line_level = stack[--tsp];
t++;
}
}
/* Original column of this line, for the cases where the author's
* alignment is the only sensible answer. */
int orig_ind = 0;
for (int k = L.linestart[L.v[a].line]; k < L.v[a].start; k++) orig_ind += (src[k] == '\t') ? 4 : 1;
/* A comment on its own line, indented past its block and following a
* line that itself ended in a comment, is the continuation of that
* comment — a column the author chose, not stray indentation. */
int comment_run = (L.v[a].kind == LT_COMMENT && b == a + 1 && prev_line_had_comment);
int ind;
if (open > 0 || (sp > 0 && hang[sp - 1])){
/* Inside an unclosed call, query, or a brace that was opened with
* content trailing it, the author is aligning to a column the
* formatter cannot see. Leave those lines exactly as written. */
int ls = L.linestart[L.v[a].line];
ind = 0;
for (int k = ls; k < L.v[a].start; k++) ind += (src[k] == '\t') ? 4 : 1;
} else {
ind = line_level * indent_width + (fmt_is_clause_word(&L, a) ? indent_width : 0);
if (comment_run && orig_ind > ind) ind = orig_ind;
}
if (pending_blank && wrote_any) fsb_putc(&o, '\n');
pending_blank = 0;
fsb_indent(&o, ind);
/* ---- the tokens --------------------------------------------------- */
int prev = -1;
int line_brack = brack, line_ui_open = (ui_depth >= 0 && sp > ui_depth);
for (int t = a; t < b; t++){
const LTok* tk = &L.v[t];
int gap = (prev >= 0) ? tk->start - L.v[prev].end : 0;
int want;
if (tk->kind == LT_COMMENT){
want = (prev >= 0) ? 2 : 0; /* set a trailing comment off */
} else {
want = fmt_space_before(&L, prev, t);
/* a query's {Tag} filter is one word, not a record literal */
if (line_brack > 0){
if (tk->kind == LT_OP && ltok_len(tk) == 1 && src[tk->start] == '}') want = 0;
if (prev >= 0 && L.v[prev].kind == LT_OP && ltok_len(&L.v[prev]) == 1 && src[L.v[prev].start] == '{') want = 0;
}
/* widget props are written k=v */
if (line_ui_open){
int eq_here = tk->kind == LT_OP && ltok_len(tk) == 1 && src[tk->start] == '=';
int eq_prev = prev >= 0 && L.v[prev].kind == LT_OP &&
ltok_len(&L.v[prev]) == 1 && src[L.v[prev].start] == '=';
if (eq_here || eq_prev) want = 0;
}
}
/* hand alignment wins over the canonical single space */
if (gap >= 2 && want >= 1){ if (gap > 60) gap = 60; want = gap; }
if (want < 0) want = 0;
for (int k = 0; k < want; k++) fsb_putc(&o, ' ');
fsb_add(&o, src + tk->start, ltok_len(tk));
prev = t;
if (tk->kind == LT_OP && ltok_len(tk) == 1){
char c = src[tk->start];
if (c == '[') line_brack++;
else if (c == ']'){ line_brack--; if (line_brack < 0) line_brack = 0; }
}
}
fsb_putc(&o, '\n');
wrote_any = 1;
prev_line_had_comment = (prev >= 0 && L.v[prev].kind == LT_COMMENT);
/* ---- carry the nesting into the next line ------------------------- */
if (ltok_is(&L, a, "ui") && L.v[a].kind == LT_KW && ui_depth < 0) ui_depth = sp;
{
int level = cur;
for (int t = a; t < b; t++){
const LTok* tk = &L.v[t];
if (tk->kind != LT_OP || ltok_len(tk) != 1) continue;
char c = src[tk->start];
if (c == '{'){
if (sp < 512){
int nxt = ltok_next_sig(&L, t);
stack[sp] = line_level;
hang[sp] = (nxt >= 0 && nxt < b); /* content follows on this line */
sp++;
}
level = line_level + 1;
}
else if (c == '}'){ if (sp > 0) level = stack[--sp]; }
else if (c == '(' || c == '['){ open++; if (c == '[') brack++; }
else if (c == ')' || c == ']'){ open--; if (open < 0) open = 0; if (c == ']'){ brack--; if (brack < 0) brack = 0; } }
}
cur = level;
}
if (ui_depth >= 0 && sp <= ui_depth) ui_depth = -1;
}
lud_lex_free(&L);
if (!o.b) { o.b = malloc(1); o.b[0] = 0; }
return o.b;
}
/* ---------- markdown ------------------------------------------------------
* Fenced Ludic in prose is still Ludic. This rewrites the body of every
* ```ludic fence in a Markdown document and leaves the prose untouched, so
* LANGUAGE.md and README.md can be kept honest by the same formatter as the
* source tree. Fence indentation (a fence inside a list item) is preserved. */
static int md_fence_at(const char* s, int i, int* fence_len, int* info_at, char* marker){
int j = i, n = 0;
while (s[j] == ' ') j++; /* leading indent */
char m = s[j];
if (m != '`' && m != '~') return 0;
while (s[j] == m){ j++; n++; }
if (n < 3) return 0;
*fence_len = n; *info_at = j; *marker = m;
return 1;
}
static int md_info_is_ludic(const char* s, int at){
while (s[at] == ' ' || s[at] == '\t') at++;
if (strncasecmp(s + at, "ludic", 5)) return 0;
char after = s[at + 5];
return after == 0 || after == '\n' || after == ' ' || after == '\t' || after == '\r';
}
static char* ludic_format_markdown(const char* src, int indent_width){
FSB o = {0};
int i = 0;
while (src[i]){
int ls = i; /* line start */
int le = ls; while (src[le] && src[le] != '\n') le++;
int flen, info, indent = 0; char marker;
while (src[ls + indent] == ' ') indent++;
if (md_fence_at(src, ls, &flen, &info, &marker) && md_info_is_ludic(src, info)){
/* copy the opening fence line verbatim */
fsb_add(&o, src + ls, le - ls + (src[le] ? 1 : 0));
i = src[le] ? le + 1 : le;
/* gather the body up to the closing fence */
int bs = i;
int be = bs;
for (;;){
if (!src[be]) break;
int ps = be, pe = ps; while (src[pe] && src[pe] != '\n') pe++;
int clen, cinfo; char cmark;
if (md_fence_at(src, ps, &clen, &cinfo, &cmark) && cmark == marker && clen >= flen){
int only = 1;
for (int k = cinfo; k < pe; k++) if (src[k] != ' ' && src[k] != '\r'){ only = 0; break; }
if (only) break;
}
be = src[pe] ? pe + 1 : pe;
}
/* de-indent the body, format it, re-indent it */
FSB body = {0};
for (int p = bs; p < be; ){
int q = p; while (src[q] && src[q] != '\n') q++;
int skip = 0; while (skip < indent && p + skip < q && src[p + skip] == ' ') skip++;
fsb_add(&body, src + p + skip, q - (p + skip));
fsb_putc(&body, '\n');
p = src[q] ? q + 1 : q;
}
char* f = ludic_format(body.b ? body.b : "", indent_width);
for (char* p = f; *p; ){
char* q = strchr(p, '\n'); if (!q) q = p + strlen(p);
if (q > p) fsb_indent(&o, indent);
fsb_add(&o, p, q - p);
fsb_putc(&o, '\n');
p = *q ? q + 1 : q;
}
free(f); free(body.b);
i = be;
continue;
}
fsb_add(&o, src + ls, le - ls + (src[le] ? 1 : 0));
i = src[le] ? le + 1 : le;
}
if (!o.b){ o.b = malloc(1); o.b[0] = 0; }
return o.b;
}
#endif /* LUDIC_FMT_H */

View file

@ -1,118 +0,0 @@
/* ============================================================================
* ludic-fmt — the Ludic formatter, as a plain CLI.
*
* Editors call this through the language server, but CI and pre-commit hooks
* want a binary they can run:
*
* ludic-fmt a.ludic b.ludic print the formatted text
* ludic-fmt -w examples rewrite every .ludic under examples/
* ludic-fmt --check . exit 1 if anything is unformatted
* ludic-fmt LANGUAGE.md -w reformat the ```ludic fences in a document
* cat x.ludic | ludic-fmt - filter mode, for editors without LSP
* ==========================================================================*/
#include "ludic_fmt.h"
#include <dirent.h>
#include <sys/stat.h>
#include <limits.h>
static int g_write = 0, g_check = 0, g_indent = 2, g_quiet = 0;
static int g_changed = 0, g_failed = 0;
static char* slurp(FILE* f){
FSB s = {0};
char buf[65536]; size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) fsb_add(&s, buf, n);
if (!s.b){ s.b = malloc(1); s.b[0] = 0; }
return s.b;
}
static int is_md(const char* p){
size_t n = strlen(p);
return (n > 3 && !strcmp(p + n - 3, ".md")) || (n > 9 && !strcmp(p + n - 9, ".markdown"));
}
static int is_ludic(const char* p){
size_t n = strlen(p);
return n > 6 && !strcmp(p + n - 6, ".ludic");
}
static void do_file(const char* path){
FILE* f = fopen(path, "rb");
if (!f){ fprintf(stderr, "ludic-fmt: cannot open %s\n", path); g_failed = 1; return; }
char* src = slurp(f); fclose(f);
char* out = is_md(path) ? ludic_format_markdown(src, g_indent) : ludic_format(src, g_indent);
int same = !strcmp(src, out);
if (g_check){
if (!same){ g_changed = 1; if (!g_quiet) printf("%s\n", path); }
} else if (g_write){
if (!same){
FILE* w = fopen(path, "wb");
if (!w){ fprintf(stderr, "ludic-fmt: cannot write %s\n", path); g_failed = 1; }
else { fwrite(out, 1, strlen(out), w); fclose(w); if (!g_quiet) fprintf(stderr, "formatted %s\n", path); }
g_changed = 1;
}
} else {
fwrite(out, 1, strlen(out), stdout);
}
free(src); free(out);
}
static void do_dir(const char* dir, int depth){
if (depth > 16) return;
DIR* d = opendir(dir);
if (!d){ fprintf(stderr, "ludic-fmt: cannot open %s\n", dir); g_failed = 1; return; }
struct dirent* e;
while ((e = readdir(d))){
if (e->d_name[0] == '.') continue;
if (!strcmp(e->d_name, "build") || !strcmp(e->d_name, "node_modules")) continue;
char p[PATH_MAX]; snprintf(p, sizeof(p), "%s/%s", dir, e->d_name);
struct stat st; if (stat(p, &st)) continue;
if (S_ISDIR(st.st_mode)) do_dir(p, depth + 1);
else if (is_ludic(p)) do_file(p);
}
closedir(d);
}
static const char* USAGE =
"ludic-fmt — format Ludic source\n"
"\n"
"usage: ludic-fmt [options] [file|dir ...]\n"
" ludic-fmt - read stdin, write stdout\n"
"\n"
"options:\n"
" -w, --write rewrite files in place\n"
" --check list unformatted files; exit 1 if any (implies no output)\n"
" --indent N spaces per level (default 2)\n"
" -q, --quiet no per-file chatter\n"
" -h, --help this text\n"
"\n"
"Directories are walked for *.ludic. A .md/.markdown file has the body of every\n"
"```ludic fence formatted and the prose left alone.\n";
int main(int argc, char** argv){
const char* files[4096]; int nf = 0;
for (int i = 1; i < argc; i++){
const char* a = argv[i];
if (!strcmp(a, "-w") || !strcmp(a, "--write")) g_write = 1;
else if (!strcmp(a, "--check") || !strcmp(a, "-l")) g_check = 1;
else if (!strcmp(a, "-q") || !strcmp(a, "--quiet")) g_quiet = 1;
else if (!strcmp(a, "--indent") && i + 1 < argc) g_indent = atoi(argv[++i]);
else if (!strcmp(a, "-h") || !strcmp(a, "--help")){ fputs(USAGE, stdout); return 0; }
else if (a[0] == '-' && a[1] && strcmp(a, "-")){ fprintf(stderr, "ludic-fmt: unknown option %s\n", a); return 2; }
else if (nf < 4096) files[nf++] = a;
}
if (g_indent < 1 || g_indent > 8) g_indent = 2;
if (nf == 0 || (nf == 1 && !strcmp(files[0], "-"))){
char* src = slurp(stdin);
char* out = ludic_format(src, g_indent);
fwrite(out, 1, strlen(out), stdout);
free(src); free(out);
return 0;
}
for (int i = 0; i < nf; i++){
struct stat st;
if (stat(files[i], &st)){ fprintf(stderr, "ludic-fmt: no such file %s\n", files[i]); g_failed = 1; continue; }
if (S_ISDIR(st.st_mode)) do_dir(files[i], 0);
else do_file(files[i]);
}
if (g_failed) return 2;
return (g_check && g_changed) ? 1 : 0;
}

View file

@ -1,672 +0,0 @@
/* ============================================================================
* ludic_index.h — an error-tolerant model of a Ludic workspace.
*
* The compiler's parser is the wrong tool for an editor: it stops at the first
* error and it splices imports into one flat program. A language server needs
* the opposite — keep going after a syntax error (a file being typed into is
* broken most of the time), keep every file separate, and remember where each
* name came from so it can be jumped to, renamed and completed.
*
* So this is a second, deliberately shallow reader of the same grammar. It
* recognises declarations and bindings and records their spans; it does not
* type-check. Ground-truth errors still come from `ludicc` itself (see
* ludic_lsp.c) — this layer supplies structure, not judgement.
* ==========================================================================*/
#ifndef LUDIC_INDEX_H
#define LUDIC_INDEX_H
#include "ludic_syntax.h"
#include <stdarg.h>
/* ---------- symbol kinds --------------------------------------------------*/
enum {
LS_UNIT, LS_COMPONENT, LS_FIELD, LS_ARCHETYPE, LS_CONST, LS_VAR,
LS_FN, LS_PARAM, LS_EXTERN, LS_SYSTEM, LS_UI, LS_WIDGET, LS_SCENE,
LS_LAYER, LS_LOCAL, LS_QUERYVAR, LS_STATE, LS_IMPORT
};
/* semantic classes, one per token — the source for semantic highlighting */
enum {
SC_NONE, SC_KEYWORD, SC_TYPE, SC_COMPONENT, SC_ARCHETYPE, SC_SCENE,
SC_LAYER, SC_UI, SC_WIDGET, SC_PROP, SC_FIELD, SC_SYSTEM, SC_FUNCTION,
SC_BUILTIN, SC_PARAM, SC_VARIABLE, SC_CONST, SC_MODVAR, SC_PHASE,
SC_NUMBER, SC_STRING, SC_COMMENT, SC_OPERATOR, SC_ANNOTATION, SC_UNKNOWN
};
typedef struct {
int kind;
char name[96];
char type[96]; /* declared type; for a query var, its component */
char detail[224]; /* one-line signature, shown in hover and outlines */
char doc[640]; /* the comment block sitting directly above */
int tok; /* token index of the NAME */
int start, end; /* byte span of the name */
int body_start, body_end; /* span the symbol governs (a block, or the
* whole file for a top-level declaration) */
int parent; /* enclosing symbol index, -1 at the top level */
int exported;
} LSym;
typedef struct {
int line, col, endline, endcol, severity; /* 1 = error, 2 = warning */
char msg[512];
char path[1024]; /* empty = this document */
} LDiag;
typedef struct LDoc {
char* path; /* filesystem path */
char* uri; /* file:// URI */
char* raw; /* the buffer exactly as the editor has it */
char* text; /* what the lexer sees: `raw`, or — for Markdown — a
* copy with every byte outside a ```ludic fence blanked
* out, so offsets still line up with the real file */
int version;
int open; /* the editor holds it (so `text` beats the disk) */
int is_markdown; /* a .md file: only its ```ludic fences are Ludic */
LLex lex;
int* match; /* per token: matching brace/bracket token, else -1 */
unsigned char* cls; /* per token: semantic class */
LSym* sym; int nsym, symcap;
char** imports; int nimport;
int had_diags; /* we published a non-empty list for it last time */
int is_unit; /* declares `game` or `module` */
int is_module;
char unit[96];
} LDoc;
typedef struct {
LDoc** d; int n, cap;
char* root; /* workspace root directory */
} LIndex;
/* ---------- small helpers -------------------------------------------------*/
static void lsym_reserve(LDoc* D){
if (D->nsym >= D->symcap){ D->symcap = D->symcap ? D->symcap * 2 : 128; D->sym = realloc(D->sym, D->symcap * sizeof(LSym)); }
}
static void lcpy(char* dst, int cap, const char* s, int n){
if (n >= cap) n = cap - 1; if (n < 0) n = 0;
memcpy(dst, s, n); dst[n] = 0;
}
static void lcatf(char* dst, int cap, const char* fmt, ...){
int used = (int)strlen(dst); if (used >= cap - 1) return;
va_list ap; va_start(ap, fmt); vsnprintf(dst + used, cap - used, fmt, ap); va_end(ap);
}
/* ---------- positions -----------------------------------------------------
* LSP counts characters in UTF-16 code units by default. Ludic source is UTF-8
* and string literals really do carry non-ASCII (the language ships a Unicode
* TrueType path), so the conversion is not optional. */
static int lutf16_len(const char* s, int nbytes){
int u = 0;
for (int i = 0; i < nbytes; ){
unsigned char c = (unsigned char)s[i];
if (c < 0x80){ i += 1; u += 1; }
else if (c < 0xE0){ i += 2; u += 1; }
else if (c < 0xF0){ i += 3; u += 1; }
else { i += 4; u += 2; } /* astral planes are a surrogate pair */
}
return u;
}
static int ldoc_line_of(const LDoc* D, int off){
int lo = 0, hi = D->lex.nline - 1;
while (lo < hi){ int mid = (lo + hi + 1) / 2; if (D->lex.linestart[mid] <= off) lo = mid; else hi = mid - 1; }
return lo;
}
static int ldoc_col_of(const LDoc* D, int off){
int line = ldoc_line_of(D, off);
return lutf16_len(D->text + D->lex.linestart[line], off - D->lex.linestart[line]);
}
static int ldoc_offset_of(const LDoc* D, int line, int character){
if (line < 0) return 0;
if (line >= D->lex.nline) return (int)strlen(D->text);
int off = D->lex.linestart[line];
int end = (line + 1 < D->lex.nline) ? D->lex.linestart[line + 1] : (int)strlen(D->text);
int u = 0;
while (off < end && u < character){
unsigned char c = (unsigned char)D->text[off];
if (c < 0x80){ off += 1; u += 1; }
else if (c < 0xE0){ off += 2; u += 1; }
else if (c < 0xF0){ off += 3; u += 1; }
else { off += 4; u += 2; }
}
return off;
}
/* The token the cursor is on. A caret sitting exactly between two tokens
* belongs to the one it is *inside*; only when it is inside none of them does
* the token ending there win — otherwise `st.|guard` resolves to the dot. */
static int ldoc_tok_at(const LDoc* D, int off){
int touching = -1;
for (int i = 0; i < D->lex.n; i++){
const LTok* t = &D->lex.v[i];
if (t->kind == LT_NL || t->kind == LT_EOF) continue;
if (off >= t->start && off < t->end) return i;
if (off == t->end && touching < 0) touching = i;
if (t->start > off) break;
}
return touching;
}
/* ---------- brace matching ------------------------------------------------*/
static void ldoc_match_braces(LDoc* D){
free(D->match);
D->match = malloc(sizeof(int) * (D->lex.n + 1));
for (int i = 0; i < D->lex.n; i++) D->match[i] = -1;
int stack[512], top = 0;
for (int i = 0; i < D->lex.n; i++){
const LTok* t = &D->lex.v[i];
if (t->kind != LT_OP || ltok_len(t) != 1) continue;
char c = D->text[t->start];
if (c == '{' || c == '(' || c == '['){ if (top < 512) stack[top++] = i; }
else if (c == '}' || c == ')' || c == ']'){
if (top > 0){ int o = stack[--top]; D->match[o] = i; D->match[i] = o; }
}
}
}
/* ---------- doc comments --------------------------------------------------
* The comment block immediately above a declaration is its documentation —
* the convention the runtime and examples already follow. */
static void ldoc_collect_doc(LDoc* D, int decl_tok, char* out, int cap){
out[0] = 0;
int line = D->lex.v[decl_tok].line;
/* walk backwards over comment-only lines */
int first = -1;
for (int i = decl_tok - 1; i >= 0; i--){
const LTok* t = &D->lex.v[i];
if (t->kind == LT_NL) continue;
if (t->kind != LT_COMMENT) break;
if (t->line >= line) break; /* trailing, not leading */
/* the comment must own its line */
int p = ltok_prev_sig(&D->lex, i);
if (p >= 0 && D->lex.v[p].line == t->line) break;
if (t->line < line - 1 && first >= 0) break;/* a blank line ends it */
if (first >= 0 && D->lex.v[first].line != t->line + 1) break;
first = i; line = t->line;
}
if (first < 0) return;
for (int i = first; i < decl_tok; i++){
const LTok* t = &D->lex.v[i];
if (t->kind != LT_COMMENT) continue;
int s = t->start + 1; /* skip '#' */
while (s < t->end && (D->text[s] == ' ' || D->text[s] == '\t' || D->text[s] == '#' || D->text[s] == '*')) s++;
int used = (int)strlen(out);
int n = t->end - s;
if (used + n + 2 >= cap) break;
memcpy(out + used, D->text + s, n); out[used + n] = '\n'; out[used + n + 1] = 0;
}
}
/* ---------- the shallow parser -------------------------------------------*/
typedef struct { LDoc* D; int i; } LP;
static int lp_kind(LP* p){ return p->i < p->D->lex.n ? p->D->lex.v[p->i].kind : LT_EOF; }
static int lp_is(LP* p, const char* s){ return ltok_is(&p->D->lex, p->i, s); }
static void lp_skipnl(LP* p){ while (p->i < p->D->lex.n && (lp_kind(p) == LT_NL || lp_kind(p) == LT_COMMENT)) p->i++; }
static void lp_adv(LP* p){ if (p->i < p->D->lex.n) p->i++; lp_skipnl(p); }
static char* lp_word(LP* p, char* buf, int cap){
buf[0] = 0;
if (p->i < p->D->lex.n) lcpy(buf, cap, p->D->text + p->D->lex.v[p->i].start, ltok_len(&p->D->lex.v[p->i]));
return buf;
}
/* Consume an identifier-ish token, returning its index (or -1). */
static int lp_name(LP* p){
int k = lp_kind(p);
if (k == LT_ID || k == LT_TYPE || k == LT_PHASE || k == LT_KW || k == LT_BOOL){ int t = p->i; lp_adv(p); return t; }
return -1;
}
static int lsym_add(LDoc* D, int kind, int nametok, int parent){
lsym_reserve(D);
LSym* s = &D->sym[D->nsym];
memset(s, 0, sizeof(*s));
s->kind = kind; s->tok = nametok; s->parent = parent;
if (nametok >= 0){
const LTok* t = &D->lex.v[nametok];
s->start = t->start; s->end = t->end;
lcpy(s->name, sizeof(s->name), D->text + t->start, ltok_len(t));
}
s->body_start = 0; s->body_end = (int)strlen(D->text);
return D->nsym++;
}
/* Skip a balanced group starting at the current opener; leaves p after it. */
static void lp_skip_group(LP* p){
int m = (p->i < p->D->lex.n) ? p->D->match[p->i] : -1;
if (m < 0){ lp_adv(p); return; }
p->i = m + 1; lp_skipnl(p);
}
/* Read `name: Type` pairs inside the parentheses at p->i, recording params. */
static void lp_params(LP* p, int owner, int kind, char* sig, int sigcap){
if (!lp_is(p, "(")) return;
int close = p->D->match[p->i];
lcatf(sig, sigcap, "(");
lp_adv(p);
int first = 1;
while (p->i < p->D->lex.n && (close < 0 || p->i < close)){
if (lp_is(p, ")")) break;
if (lp_is(p, ",")){ lp_adv(p); continue; }
int nt = lp_name(p); if (nt < 0){ lp_adv(p); continue; }
char ty[96]; ty[0] = 0;
if (lp_is(p, ":")){ lp_adv(p); int tt = lp_name(p); if (tt >= 0) lcpy(ty, sizeof(ty), p->D->text + p->D->lex.v[tt].start, ltok_len(&p->D->lex.v[tt])); }
int s = lsym_add(p->D, kind, nt, owner);
lcpy(p->D->sym[s].type, sizeof(p->D->sym[s].type), ty, (int)strlen(ty));
lcatf(sig, sigcap, "%s%s: %s", first ? "" : ", ", p->D->sym[s].name, ty[0] ? ty : "?");
first = 0;
}
if (close >= 0) p->i = close + 1;
lp_skipnl(p);
lcatf(sig, sigcap, ")");
}
/* `[Pos, Vel, {Enemy}]` — bind the listed components to the named variables in
* order, skipping {Tag} terms, which filter without binding. */
static void lp_query_terms(LP* p, int owner, int* vars, int nvars, int scope_start, int scope_end){
if (!lp_is(p, "[")) return;
int close = p->D->match[p->i];
lp_adv(p);
int bind = 0;
while (p->i < p->D->lex.n && (close < 0 || p->i < close)){
if (lp_is(p, "]")) break;
if (lp_is(p, ",")){ lp_adv(p); continue; }
if (lp_is(p, "{")){ /* a filter term */
int cb = p->D->match[p->i]; lp_adv(p);
int nt = lp_name(p); (void)nt;
if (cb >= 0) p->i = cb + 1; else lp_adv(p);
lp_skipnl(p); continue;
}
int nt = lp_name(p); if (nt < 0){ lp_adv(p); continue; }
if (bind < nvars && vars[bind] >= 0){
int s = lsym_add(p->D, LS_QUERYVAR, vars[bind], owner);
lcpy(p->D->sym[s].type, sizeof(p->D->sym[s].type), p->D->text + p->D->lex.v[nt].start, ltok_len(&p->D->lex.v[nt]));
p->D->sym[s].body_start = scope_start; p->D->sym[s].body_end = scope_end;
lcatf(p->D->sym[s].detail, sizeof(p->D->sym[s].detail), "%s: %s (query binding)", p->D->sym[s].name, p->D->sym[s].type);
}
bind++;
}
if (close >= 0) p->i = close + 1;
lp_skipnl(p);
if (lp_is(p, "where")) lp_adv(p); /* the condition is ordinary expression */
}
/* `(a, b)` variable list before `in query` / after the `query` clause. */
static int lp_varlist(LP* p, int* out, int max){
int n = 0;
if (!lp_is(p, "(")) return 0;
int close = p->D->match[p->i];
lp_adv(p);
while (p->i < p->D->lex.n && (close < 0 || p->i < close)){
if (lp_is(p, ")")) break;
if (lp_is(p, ",")){ lp_adv(p); continue; }
int nt = lp_name(p); if (nt < 0){ lp_adv(p); continue; }
if (n < max) out[n++] = nt;
}
if (close >= 0) p->i = close + 1;
lp_skipnl(p);
return n;
}
static void lp_block(LP* p, int owner, int scope_end);
/* statements — we only care about what BINDS a name or opens a scope */
static void lp_stmt(LP* p, int owner, int scope_end){
LDoc* D = p->D;
if (lp_is(p, "let")){
lp_adv(p); int nt = lp_name(p); if (nt < 0) return;
char ty[96]; ty[0] = 0;
if (lp_is(p, ":")){ lp_adv(p); int tt = lp_name(p); if (tt >= 0) lcpy(ty, sizeof(ty), D->text + D->lex.v[tt].start, ltok_len(&D->lex.v[tt])); }
int s = lsym_add(D, LS_LOCAL, nt, owner);
lcpy(D->sym[s].type, sizeof(D->sym[s].type), ty, (int)strlen(ty));
D->sym[s].body_start = D->lex.v[nt].start; D->sym[s].body_end = scope_end;
return;
}
if (lp_is(p, "for")){
lp_adv(p);
if (lp_is(p, "(")){ /* for (a, b) in query [...] */
int vars[16]; int nv = lp_varlist(p, vars, 16);
if (lp_is(p, "in")) lp_adv(p);
if (lp_is(p, "query")) lp_adv(p);
int body_end = scope_end;
/* the loop body is the next {...}; bindings live there */
lp_query_terms(p, owner, vars, nv, D->lex.v[p->i < D->lex.n ? p->i : D->lex.n - 1].start, body_end);
return;
}
int nt = lp_name(p); /* for i in a .. b */
if (nt >= 0){
int s = lsym_add(D, LS_LOCAL, nt, owner);
lcpy(D->sym[s].type, sizeof(D->sym[s].type), "int", 3);
D->sym[s].body_start = D->lex.v[nt].start; D->sym[s].body_end = scope_end;
}
return;
}
if (lp_is(p, "state")){ /* machine { state Idle = 0 {…} } */
lp_adv(p); int nt = lp_name(p);
if (nt >= 0) lsym_add(D, LS_STATE, nt, owner);
return;
}
lp_adv(p);
}
static void lp_block(LP* p, int owner, int scope_end){
if (!lp_is(p, "{")) return;
int close = p->D->match[p->i];
int end = close >= 0 ? p->D->lex.v[close].start : scope_end;
lp_adv(p);
while (p->i < p->D->lex.n && lp_kind(p) != LT_EOF){
if (close >= 0 && p->i >= close) break;
if (lp_is(p, "}")) break;
if (lp_is(p, "{")){ lp_block(p, owner, end); continue; }
int before = p->i;
lp_stmt(p, owner, end);
if (p->i == before) lp_adv(p);
}
if (close >= 0) p->i = close + 1; else lp_adv(p);
lp_skipnl(p);
}
/* `panel id=Root w=288 { … }` — each id= mints a UI_<Name> handle. */
static void lp_widget(LP* p, int owner, int uisym){
LDoc* D = p->D;
int type_tok = lp_name(p); if (type_tok < 0) return;
while (p->i < D->lex.n && lp_kind(p) == LT_ID && ltok_is(&D->lex, ltok_next_sig(&D->lex, p->i) < 0 ? p->i : ltok_next_sig(&D->lex, p->i), "=")){
int key = p->i; char kb[64]; lcpy(kb, sizeof(kb), D->text + D->lex.v[key].start, ltok_len(&D->lex.v[key]));
lp_adv(p); /* key */
if (lp_is(p, "=")) lp_adv(p); /* '=' */
if (!strcmp(kb, "id")){
int nt = lp_name(p);
if (nt >= 0){
int s = lsym_add(D, LS_WIDGET, nt, uisym);
snprintf(D->sym[s].name, sizeof(D->sym[s].name), "UI_%.*s",
ltok_len(&D->lex.v[nt]), D->text + D->lex.v[nt].start);
lcatf(D->sym[s].detail, sizeof(D->sym[s].detail), "%s widget handle",
(const char*)(D->text + D->lex.v[type_tok].start));
lcpy(D->sym[s].type, sizeof(D->sym[s].type), "int", 3);
}
} else {
/* skip one value expression: stop at the next `key=` or at a brace */
while (p->i < D->lex.n && lp_kind(p) != LT_EOF && !lp_is(p, "{") && !lp_is(p, "}")){
if (lp_is(p, "(") || lp_is(p, "[")){ lp_skip_group(p); continue; }
int nx = ltok_next_sig(&D->lex, p->i);
if (lp_kind(p) == LT_ID && nx >= 0 && ltok_is(&D->lex, nx, "=")) break;
lp_adv(p);
}
}
}
if (lp_is(p, "{")){
int close = D->match[p->i];
lp_adv(p);
while (p->i < D->lex.n && lp_kind(p) != LT_EOF && !lp_is(p, "}")){
if (close >= 0 && p->i >= close) break;
int before = p->i;
lp_widget(p, owner, uisym);
if (p->i == before) lp_adv(p);
}
if (close >= 0) p->i = close + 1; else lp_adv(p);
lp_skipnl(p);
}
}
/* one top-level declaration */
static void lp_decl(LP* p, int parent){
LDoc* D = p->D;
char w[96]; lp_word(p, w, sizeof(w));
int decl_tok = p->i;
if (!strcmp(w, "import")){
lp_adv(p);
if (lp_kind(p) == LT_STR){
const LTok* t = &D->lex.v[p->i];
int n = ltok_len(t) - 2; if (n < 0) n = 0;
char* rel = malloc(n + 1); memcpy(rel, D->text + t->start + 1, n); rel[n] = 0;
D->imports = realloc(D->imports, (D->nimport + 1) * sizeof(char*));
D->imports[D->nimport++] = rel;
int s = lsym_add(D, LS_IMPORT, p->i, parent);
lcpy(D->sym[s].name, sizeof(D->sym[s].name), rel, (int)strlen(rel));
lp_adv(p);
}
return;
}
if (!strcmp(w, "property") || !strcmp(w, "model")){
int is_comp = !strcmp(w, "property");
lp_adv(p); int nt = lp_name(p); if (nt < 0) return;
int s = lsym_add(D, is_comp ? LS_COMPONENT : LS_ARCHETYPE, nt, parent);
ldoc_collect_doc(D, decl_tok, D->sym[s].doc, sizeof(D->sym[s].doc));
while (lp_kind(p) == LT_ANNO) lp_adv(p);
snprintf(D->sym[s].detail, sizeof(D->sym[s].detail), "%s %s {",
is_comp ? "property" : "model", D->sym[s].name);
if (!lp_is(p, "{")) return;
int close = D->match[p->i];
D->sym[s].body_start = D->lex.v[p->i].start;
D->sym[s].body_end = close >= 0 ? D->lex.v[close].end : (int)strlen(D->text);
lp_adv(p);
int first = 1;
while (p->i < D->lex.n && lp_kind(p) != LT_EOF && !lp_is(p, "}")){
if (close >= 0 && p->i >= close) break;
if (lp_is(p, ",")){ lp_adv(p); continue; }
int ft = lp_name(p); if (ft < 0){ lp_adv(p); continue; }
if (is_comp){
int f = lsym_add(D, LS_FIELD, ft, s);
if (lp_is(p, ":")){
lp_adv(p); int tt = lp_name(p);
if (tt >= 0) lcpy(D->sym[f].type, sizeof(D->sym[f].type), D->text + D->lex.v[tt].start, ltok_len(&D->lex.v[tt]));
}
lcatf(D->sym[f].detail, sizeof(D->sym[f].detail), "%s.%s: %s", D->sym[s].name, D->sym[f].name, D->sym[f].type);
if (lp_is(p, "=")){ lp_adv(p); while (p->i < D->lex.n && !lp_is(p, ",") && !lp_is(p, "}") && lp_kind(p) != LT_EOF){ if (lp_is(p, "(") || lp_is(p, "{") || lp_is(p, "[")) lp_skip_group(p); else lp_adv(p); } }
lcatf(D->sym[s].detail, sizeof(D->sym[s].detail), "%s %s: %s", first ? "" : ",", D->sym[f].name, D->sym[f].type);
} else {
lcatf(D->sym[s].detail, sizeof(D->sym[s].detail), "%s %.*s", first ? "" : ",",
ltok_len(&D->lex.v[ft]), D->text + D->lex.v[ft].start);
}
first = 0;
}
lcatf(D->sym[s].detail, sizeof(D->sym[s].detail), " }");
if (close >= 0) p->i = close + 1; else lp_adv(p);
lp_skipnl(p);
return;
}
if (!strcmp(w, "const") || !strcmp(w, "var")){
int kind = !strcmp(w, "const") ? LS_CONST : LS_VAR;
lp_adv(p); int nt = lp_name(p); if (nt < 0) return;
int s = lsym_add(D, kind, nt, parent);
ldoc_collect_doc(D, decl_tok, D->sym[s].doc, sizeof(D->sym[s].doc));
if (lp_is(p, ":")){ lp_adv(p); int tt = lp_name(p); if (tt >= 0) lcpy(D->sym[s].type, sizeof(D->sym[s].type), D->text + D->lex.v[tt].start, ltok_len(&D->lex.v[tt])); }
lcatf(D->sym[s].detail, sizeof(D->sym[s].detail), "%s %s: %s", w, D->sym[s].name, D->sym[s].type);
while (p->i < D->lex.n && lp_kind(p) != LT_NL && lp_kind(p) != LT_EOF){
if (lp_is(p, "(") || lp_is(p, "{") || lp_is(p, "[")) lp_skip_group(p); else p->i++;
}
lp_skipnl(p);
return;
}
if (!strcmp(w, "extern")){
lp_adv(p); if (lp_is(p, "fn")) lp_adv(p);
int nt = lp_name(p); if (nt < 0) return;
int s = lsym_add(D, LS_EXTERN, nt, parent);
ldoc_collect_doc(D, decl_tok, D->sym[s].doc, sizeof(D->sym[s].doc));
char sig[224]; snprintf(sig, sizeof(sig), "extern fn %s", D->sym[s].name);
lp_params(p, s, LS_PARAM, sig, sizeof(sig));
if (lp_is(p, "->")){ lp_adv(p); int tt = lp_name(p); if (tt >= 0){ lcpy(D->sym[s].type, sizeof(D->sym[s].type), D->text + D->lex.v[tt].start, ltok_len(&D->lex.v[tt])); lcatf(sig, sizeof(sig), " -> %s", D->sym[s].type); } }
lcpy(D->sym[s].detail, sizeof(D->sym[s].detail), sig, (int)strlen(sig));
while (p->i < D->lex.n && lp_kind(p) != LT_NL && lp_kind(p) != LT_EOF) p->i++;
lp_skipnl(p);
return;
}
if (!strcmp(w, "fn") || !strcmp(w, "pure") || !strcmp(w, "export")){
int exported = !strcmp(w, "export");
if (!strcmp(w, "pure") || exported){ lp_adv(p); if (lp_is(p, "pure")) lp_adv(p); }
if (lp_is(p, "fn")) lp_adv(p);
int nt = lp_name(p); if (nt < 0) return;
int s = lsym_add(D, LS_FN, nt, parent);
D->sym[s].exported = exported;
ldoc_collect_doc(D, decl_tok, D->sym[s].doc, sizeof(D->sym[s].doc));
char sig[224]; snprintf(sig, sizeof(sig), "%sfn %s", exported ? "export " : "", D->sym[s].name);
lp_params(p, s, LS_PARAM, sig, sizeof(sig));
if (lp_is(p, "->")){ lp_adv(p); int tt = lp_name(p); if (tt >= 0) lcpy(D->sym[s].type, sizeof(D->sym[s].type), D->text + D->lex.v[tt].start, ltok_len(&D->lex.v[tt])); }
lcatf(sig, sizeof(sig), " -> %s", D->sym[s].type[0] ? D->sym[s].type : "void");
lcpy(D->sym[s].detail, sizeof(D->sym[s].detail), sig, (int)strlen(sig));
/* contracts sit between the signature and the body */
while (lp_is(p, "requires") || lp_is(p, "ensures") || lp_is(p, "invariant") || lp_is(p, "effects")){
lp_adv(p);
while (p->i < D->lex.n && lp_kind(p) != LT_NL && lp_kind(p) != LT_EOF && !lp_is(p, "{")){
if (lp_is(p, "(") || lp_is(p, "[")) lp_skip_group(p); else p->i++;
}
lp_skipnl(p);
}
if (lp_is(p, "{")){
int close = D->match[p->i];
D->sym[s].body_start = D->lex.v[p->i].start;
D->sym[s].body_end = close >= 0 ? D->lex.v[close].end : (int)strlen(D->text);
/* params are visible for the whole body */
for (int q = 0; q < D->nsym; q++) if (D->sym[q].parent == s && D->sym[q].kind == LS_PARAM){
D->sym[q].body_start = D->sym[s].body_start; D->sym[q].body_end = D->sym[s].body_end;
}
lp_block(p, s, D->sym[s].body_end);
}
return;
}
if (!strcmp(w, "handler") || !strcmp(w, "edge")){
if (!strcmp(w, "edge")){ lp_adv(p); if (!lp_is(p, "handler")) return; }
lp_adv(p);
int nt = lp_name(p); if (nt < 0) return;
int s = lsym_add(D, LS_SYSTEM, nt, parent);
ldoc_collect_doc(D, decl_tok, D->sym[s].doc, sizeof(D->sym[s].doc));
lcpy(D->sym[s].type, sizeof(D->sym[s].type), "Update", 6);
while (lp_kind(p) == LT_ANNO) lp_adv(p);
int vars[16]; int nv = 0; int have_query = 0;
for (;;){
if (lp_is(p, "phase")){ lp_adv(p); int tt = lp_name(p); if (tt >= 0) lcpy(D->sym[s].type, sizeof(D->sym[s].type), D->text + D->lex.v[tt].start, ltok_len(&D->lex.v[tt])); }
else if (lp_is(p, "query")){ lp_adv(p); have_query = 1; nv = lp_varlist(p, vars, 16); break; }
else if (lp_is(p, "reads") || lp_is(p, "writes") || lp_is(p, "uses") || lp_is(p, "effects")){ lp_adv(p); if (lp_is(p, "[")) lp_skip_group(p); }
else if (lp_is(p, "needs")){ lp_adv(p); lp_name(p); if (lp_is(p, "[")) lp_skip_group(p); }
else break;
lp_skipnl(p);
}
lcatf(D->sym[s].detail, sizeof(D->sym[s].detail), "system %s phase %s", D->sym[s].name, D->sym[s].type);
/* find the body first, so query bindings can be scoped to it */
int body_open = p->i;
if (have_query){
int save = p->i;
/* the query terms come before the body */
int scan = p->i;
while (scan < D->lex.n && !ltok_is(&D->lex, scan, "{") && D->lex.v[scan].kind != LT_EOF) scan++;
int close = (scan < D->lex.n) ? D->match[scan] : -1;
int bs = (scan < D->lex.n) ? D->lex.v[scan].start : 0;
int be = close >= 0 ? D->lex.v[close].end : (int)strlen(D->text);
p->i = save;
lp_query_terms(p, s, vars, nv, bs, be);
body_open = p->i;
}
(void)body_open;
if (lp_is(p, "{")){
int close = D->match[p->i];
D->sym[s].body_start = D->lex.v[p->i].start;
D->sym[s].body_end = close >= 0 ? D->lex.v[close].end : (int)strlen(D->text);
lp_block(p, s, D->sym[s].body_end);
}
return;
}
if (!strcmp(w, "ui")){
lp_adv(p); int nt = lp_name(p); if (nt < 0) return;
int s = lsym_add(D, LS_UI, nt, parent);
ldoc_collect_doc(D, decl_tok, D->sym[s].doc, sizeof(D->sym[s].doc));
lcatf(D->sym[s].detail, sizeof(D->sym[s].detail), "ui %s", D->sym[s].name);
/* the block name is a handle too */
{ int h = lsym_add(D, LS_WIDGET, nt, s);
snprintf(D->sym[h].name, sizeof(D->sym[h].name), "UI_%s", D->sym[s].name);
lcpy(D->sym[h].type, sizeof(D->sym[h].type), "int", 3);
lcatf(D->sym[h].detail, sizeof(D->sym[h].detail), "root handle of ui %s", D->sym[s].name); }
if (lp_is(p, "{")){
int close = D->match[p->i];
D->sym[s].body_start = D->lex.v[p->i].start;
D->sym[s].body_end = close >= 0 ? D->lex.v[close].end : (int)strlen(D->text);
lp_adv(p);
while (p->i < D->lex.n && lp_kind(p) != LT_EOF && !lp_is(p, "}")){
if (close >= 0 && p->i >= close) break;
int before = p->i;
lp_widget(p, s, s);
if (p->i == before) lp_adv(p);
}
if (close >= 0) p->i = close + 1; else lp_adv(p);
lp_skipnl(p);
}
return;
}
if (!strcmp(w, "scene")){
lp_adv(p); int nt = lp_name(p); if (nt < 0) return;
int s = lsym_add(D, LS_SCENE, nt, parent);
ldoc_collect_doc(D, decl_tok, D->sym[s].doc, sizeof(D->sym[s].doc));
int start = 0;
if (lp_is(p, "start")){ start = 1; lp_adv(p); }
lcatf(D->sym[s].detail, sizeof(D->sym[s].detail), "scene %s%s", D->sym[s].name, start ? " start" : "");
if (lp_is(p, "{")){
int close = D->match[p->i];
D->sym[s].body_start = D->lex.v[p->i].start;
D->sym[s].body_end = close >= 0 ? D->lex.v[close].end : (int)strlen(D->text);
lp_adv(p);
while (p->i < D->lex.n && lp_kind(p) != LT_EOF && !lp_is(p, "}")){
if (close >= 0 && p->i >= close) break;
int before = p->i;
if (lp_is(p, "on")){ lp_adv(p); lp_name(p); lp_block(p, s, D->sym[s].body_end); }
else if (lp_is(p, "layer")){
lp_adv(p); int lt = lp_name(p);
int ls = lt >= 0 ? lsym_add(D, LS_LAYER, lt, s) : -1;
if (ls >= 0) lcatf(D->sym[ls].detail, sizeof(D->sym[ls].detail), "layer %s of scene %s", D->sym[ls].name, D->sym[s].name);
if (lp_is(p, "{")){
int lc = D->match[p->i];
if (ls >= 0){ D->sym[ls].body_start = D->lex.v[p->i].start; D->sym[ls].body_end = lc >= 0 ? D->lex.v[lc].end : D->sym[s].body_end; }
lp_adv(p);
while (p->i < D->lex.n && lp_kind(p) != LT_EOF && !lp_is(p, "}")){
if (lc >= 0 && p->i >= lc) break;
int b2 = p->i; lp_decl(p, ls >= 0 ? ls : s);
if (p->i == b2) lp_adv(p);
}
if (lc >= 0) p->i = lc + 1; else lp_adv(p);
lp_skipnl(p);
}
} else lp_adv(p);
if (p->i == before) lp_adv(p);
}
if (close >= 0) p->i = close + 1; else lp_adv(p);
lp_skipnl(p);
}
return;
}
lp_adv(p);
}
static void ldoc_parse(LDoc* D){
D->nsym = 0;
for (int i = 0; i < D->nimport; i++) free(D->imports[i]);
free(D->imports); D->imports = 0; D->nimport = 0;
D->is_unit = 0; D->is_module = 0; D->unit[0] = 0;
LP p = { D, 0 };
lp_skipnl(&p);
while (lp_is(&p, "import")) { lp_decl(&p, -1); lp_skipnl(&p); }
int unit_parent = -1;
if (lp_is(&p, "program") || lp_is(&p, "program")){
D->is_module = lp_is(&p, "program");
D->is_unit = 1;
lp_adv(&p);
int nt = lp_name(&p);
if (nt >= 0){
unit_parent = lsym_add(D, LS_UNIT, nt, -1);
lcpy(D->unit, sizeof(D->unit), D->sym[unit_parent].name, (int)strlen(D->sym[unit_parent].name));
lcatf(D->sym[unit_parent].detail, sizeof(D->sym[unit_parent].detail), "%s %s", D->is_module ? "program" : "program", D->unit);
}
if (lp_is(&p, "{")){
int close = D->match[p.i];
if (unit_parent >= 0){
D->sym[unit_parent].body_start = D->lex.v[p.i].start;
D->sym[unit_parent].body_end = close >= 0 ? D->lex.v[close].end : (int)strlen(D->text);
}
lp_adv(&p);
while (p.i < D->lex.n && lp_kind(&p) != LT_EOF && !lp_is(&p, "}")){
if (close >= 0 && p.i >= close) break;
int before = p.i; lp_decl(&p, unit_parent);
if (p.i == before) lp_adv(&p);
}
}
return;
}
/* A fragment: an imported file is a bare list of declarations. */
while (p.i < D->lex.n && lp_kind(&p) != LT_EOF){
int before = p.i; lp_decl(&p, -1);
if (p.i == before) lp_adv(&p);
}
}
#endif /* LUDIC_INDEX_H */

View file

@ -1,191 +0,0 @@
/* ============================================================================
* ludic_json.h — just enough JSON for LSP, and no dependencies.
*
* The rest of this toolchain is C with no third-party libraries, and the
* language server should not be the thing that drags a package manager into a
* project whose whole premise is a self-contained native pipeline. This is a
* recursive-descent reader and an escaping writer; nothing more.
* ==========================================================================*/
#ifndef LUDIC_JSON_H
#define LUDIC_JSON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <math.h>
enum { JNULL, JBOOL, JNUM, JSTR, JARR, JOBJ };
typedef struct JVal JVal;
struct JVal {
int t;
double num;
int b;
char* s; /* JSTR: decoded UTF-8 */
JVal** kids; char** keys; int n, cap;
};
static JVal* jnew(int t){ JVal* v = calloc(1, sizeof(JVal)); v->t = t; return v; }
static void jfree(JVal* v){
if (!v) return;
for (int i = 0; i < v->n; i++){ jfree(v->kids[i]); free(v->keys ? v->keys[i] : 0); }
free(v->kids); free(v->keys); free(v->s); free(v);
}
static void jpush(JVal* v, const char* key, JVal* kid){
if (v->n >= v->cap){
v->cap = v->cap ? v->cap * 2 : 8;
v->kids = realloc(v->kids, v->cap * sizeof(JVal*));
v->keys = realloc(v->keys, v->cap * sizeof(char*));
}
v->keys[v->n] = key ? strdup(key) : 0;
v->kids[v->n++] = kid;
}
static void jskip(const char** p){ while (**p == ' ' || **p == '\t' || **p == '\n' || **p == '\r') (*p)++; }
static JVal* jparse_at(const char** p);
static void jutf8(char** o, unsigned cp){
if (cp < 0x80) *(*o)++ = (char)cp;
else if (cp < 0x800){ *(*o)++ = (char)(0xC0 | (cp >> 6)); *(*o)++ = (char)(0x80 | (cp & 63)); }
else if (cp < 0x10000){ *(*o)++ = (char)(0xE0 | (cp >> 12)); *(*o)++ = (char)(0x80 | ((cp >> 6) & 63)); *(*o)++ = (char)(0x80 | (cp & 63)); }
else { *(*o)++ = (char)(0xF0 | (cp >> 18)); *(*o)++ = (char)(0x80 | ((cp >> 12) & 63)); *(*o)++ = (char)(0x80 | ((cp >> 6) & 63)); *(*o)++ = (char)(0x80 | (cp & 63)); }
}
static char* jparse_string(const char** p){
if (**p != '"') return 0;
(*p)++;
const char* s = *p;
size_t cap = strlen(s) + 1;
char* out = malloc(cap); char* o = out;
while (**p && **p != '"'){
if (**p == '\\'){
(*p)++;
char c = **p; (*p)++;
switch (c){
case 'n': *o++ = '\n'; break;
case 't': *o++ = '\t'; break;
case 'r': *o++ = '\r'; break;
case 'b': *o++ = '\b'; break;
case 'f': *o++ = '\f'; break;
case 'u': {
unsigned cp = (unsigned)strtoul((char[5]){ (*p)[0], (*p)[1], (*p)[2], (*p)[3], 0 }, 0, 16);
*p += 4;
if (cp >= 0xD800 && cp < 0xDC00 && (*p)[0] == '\\' && (*p)[1] == 'u'){
unsigned lo = (unsigned)strtoul((char[5]){ (*p)[2], (*p)[3], (*p)[4], (*p)[5], 0 }, 0, 16);
*p += 6;
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
}
jutf8(&o, cp);
break;
}
default: *o++ = c;
}
} else *o++ = *(*p)++;
}
if (**p == '"') (*p)++;
*o = 0;
return out;
}
static JVal* jparse_at(const char** p){
jskip(p);
char c = **p;
if (c == '{'){
(*p)++; JVal* v = jnew(JOBJ);
for (;;){
jskip(p);
if (**p == '}'){ (*p)++; break; }
char* k = jparse_string(p);
jskip(p); if (**p == ':') (*p)++;
JVal* kid = jparse_at(p);
jpush(v, k ? k : "", kid);
free(k);
jskip(p);
if (**p == ',') (*p)++;
else if (**p == '}'){ (*p)++; break; }
else if (!**p) break;
}
return v;
}
if (c == '['){
(*p)++; JVal* v = jnew(JARR);
for (;;){
jskip(p);
if (**p == ']'){ (*p)++; break; }
jpush(v, 0, jparse_at(p));
jskip(p);
if (**p == ',') (*p)++;
else if (**p == ']'){ (*p)++; break; }
else if (!**p) break;
}
return v;
}
if (c == '"'){ JVal* v = jnew(JSTR); v->s = jparse_string(p); return v; }
if (!strncmp(*p, "true", 4)){ *p += 4; JVal* v = jnew(JBOOL); v->b = 1; return v; }
if (!strncmp(*p, "false", 5)){ *p += 5; JVal* v = jnew(JBOOL); v->b = 0; return v; }
if (!strncmp(*p, "null", 4)){ *p += 4; return jnew(JNULL); }
{ char* end; double d = strtod(*p, &end); if (end != *p){ *p = end; JVal* v = jnew(JNUM); v->num = d; return v; } }
(*p)++; /* unparsable: step past it */
return jnew(JNULL);
}
static JVal* jparse(const char* text){ const char* p = text; return jparse_at(&p); }
static JVal* jget(JVal* v, const char* key){
if (!v || v->t != JOBJ) return 0;
for (int i = 0; i < v->n; i++) if (v->keys[i] && !strcmp(v->keys[i], key)) return v->kids[i];
return 0;
}
static JVal* jat(JVal* v, int i){ return (v && i >= 0 && i < v->n) ? v->kids[i] : 0; }
static const char* jstr(JVal* v, const char* def){ return (v && v->t == JSTR && v->s) ? v->s : def; }
static int jint(JVal* v, int def){ return (v && v->t == JNUM) ? (int)v->num : def; }
static int jbool(JVal* v, int def){ return v ? (v->t == JBOOL ? v->b : (v->t == JNUM ? v->num != 0 : def)) : def; }
/* dotted lookup: jpath(msg, "params.textDocument.uri") */
static JVal* jpath(JVal* v, const char* path){
char buf[256]; snprintf(buf, sizeof(buf), "%s", path);
char* save = 0;
for (char* tok = strtok_r(buf, ".", &save); tok; tok = strtok_r(0, ".", &save)){
v = jget(v, tok);
if (!v) return 0;
}
return v;
}
/* ---------- writing -------------------------------------------------------*/
typedef struct { char* b; size_t n, cap; } JSB;
static void jsb_ensure(JSB* s, size_t add){
if (s->n + add + 1 > s->cap){ s->cap = (s->n + add + 1) * 2; s->b = realloc(s->b, s->cap); }
}
static void jsb_add(JSB* s, const char* z, size_t l){ jsb_ensure(s, l); memcpy(s->b + s->n, z, l); s->n += l; s->b[s->n] = 0; }
static void jsb_puts(JSB* s, const char* z){ jsb_add(s, z, strlen(z)); }
static void jsb_putc(JSB* s, char c){ jsb_add(s, &c, 1); }
static void jsb_putf(JSB* s, const char* fmt, ...){
char tmp[1024];
va_list ap; va_start(ap, fmt);
int n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
va_end(ap);
if (n < (int)sizeof(tmp)){ jsb_add(s, tmp, n); return; }
char* big = malloc(n + 1);
va_start(ap, fmt); vsnprintf(big, n + 1, fmt, ap); va_end(ap);
jsb_add(s, big, n); free(big);
}
/* Escape a UTF-8 string as a JSON string literal, quotes included. */
static void jsb_str(JSB* s, const char* z){
jsb_putc(s, '"');
if (!z) z = "";
for (const unsigned char* p = (const unsigned char*)z; *p; p++){
switch (*p){
case '"': jsb_puts(s, "\\\""); break;
case '\\': jsb_puts(s, "\\\\"); break;
case '\n': jsb_puts(s, "\\n"); break;
case '\r': jsb_puts(s, "\\r"); break;
case '\t': jsb_puts(s, "\\t"); break;
default:
if (*p < 0x20) jsb_putf(s, "\\u%04x", *p);
else jsb_putc(s, (char)*p);
}
}
jsb_putc(s, '"');
}
static void jsb_kv_str(JSB* s, const char* k, const char* v){ jsb_str(s, k); jsb_putc(s, ':'); jsb_str(s, v); }
static void jsb_kv_int(JSB* s, const char* k, long v){ jsb_str(s, k); jsb_putf(s, ":%ld", v); }
static void jsb_kv_bool(JSB* s, const char* k, int v){ jsb_str(s, k); jsb_puts(s, v ? ":true" : ":false"); }
#endif /* LUDIC_JSON_H */

File diff suppressed because it is too large Load diff

View file

@ -53,22 +53,22 @@ typedef struct {
* declaration keyword, anything stmt() dispatches on is a statement keyword. */
static const char* LUDIC_KW_DECL[] = {
"program","import","property","model","enum","ui",
"const","var","fn","extern","handler","entry", 0
"const","var","fn","extern","handler","entry","event","scene", 0
};
static const char* LUDIC_KW_CLAUSE[] = {
"phase","query","on", 0
"phase","query","on","cancellable","public","layer","start", 0
};
/* Documented design targets the self-hosted parser does not accept yet. Kept
* out of the highlighted vocabulary (they would read as working keywords) until
* they are implemented; check-vocabulary.py verifies the lists above are a
* subset of what selfhost/parse*.ludic actually dispatches on. */
static const char* LUDIC_KW_RESERVED[] = {
"scene","layer","start", 0
0
};
static const char* LUDIC_KW_STMT[] = {
"let","return","if","else","while","for","in","spawn","despawn",
"enable","disable","match","machine","state","become","where",
"and","or","not","break","continue","new", 0
"and","or","not","break","continue","new","emit","cancel", 0
};
static const char* LUDIC_TYPES[] = {
"int","fixed","bool","entity","str","ptr","byte","words","fixeds","ptrs","void", 0

View file

@ -1,327 +0,0 @@
/* ============================================================================
* ludic_syntax.h — the lexical layer shared by every Ludic editor tool.
*
* One lexer, one vocabulary. `ludic-fmt`, `ludic-lsp` and the generated
* TextMate grammar all read their keyword/builtin/type tables from here, so an
* addition to the language shows up in every editor at once instead of drifting
* across six hand-maintained copies.
*
* Unlike the compiler's lexer (compiler/ludicc.c) this one is written for
* editors: it keeps comments, keeps newlines, records byte spans for every
* token, and never exits on bad input — a stray character becomes an LT_ERR
* token and lexing continues, because a file being typed into is malformed most
* of the time.
* ==========================================================================*/
#ifndef LUDIC_SYNTAX_H
#define LUDIC_SYNTAX_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/* ---------- token kinds ---------------------------------------------------*/
enum {
LT_EOF, LT_NL, LT_COMMENT,
LT_ID, /* a plain identifier */
LT_KW, /* a reserved word (see KEYWORDS) */
LT_TYPE, /* a built-in type name: int fixed bool entity str ptr void */
LT_PHASE, /* Start Input FixedUpdate Update LateUpdate Render */
LT_BOOL, /* true false */
LT_INT, LT_FLOAT, LT_STR, LT_CHAR,
LT_ANNO, /* @deterministic — the '@' and the name as one token */
LT_OP,
LT_ERR
};
typedef struct {
int kind;
int start, end; /* byte offsets into the source buffer */
int line; /* 0-based */
int bad; /* set on an unterminated string / stray character */
} LTok;
typedef struct {
LTok* v; int n, cap;
const char* src;
int* linestart; int nline, caplin;
} LLex;
/* ---------- the vocabulary ------------------------------------------------*/
/* Reserved words, grouped so editors can colour them differently. The groups
* mirror the compiler's parser: anything parse_decl() dispatches on is a
* declaration keyword, anything stmt() dispatches on is a statement keyword. */
static const char* LUDIC_KW_DECL[] = {
"game","module","import","component","struct","archetype","enum","ui",
"const","var","fn","extern","system","main", 0
};
static const char* LUDIC_KW_CLAUSE[] = {
"phase","query","reads","writes", 0
};
/* Documented design targets the self-hosted parser does not accept yet. Kept
* out of the highlighted vocabulary (they would read as working keywords) until
* they are implemented; check-vocabulary.py verifies the lists above are a
* subset of what selfhost/parse*.ludic actually dispatches on. */
static const char* LUDIC_KW_RESERVED[] = {
"scene","layer","on","start", 0
};
static const char* LUDIC_KW_STMT[] = {
"let","return","if","else","when","while","for","in","spawn","despawn",
"match","machine","state","become","enter","where","and","or","not",
"break","continue","new", 0
};
static const char* LUDIC_TYPES[] = {
"int","fixed","bool","entity","str","ptr","void", 0
};
static const char* LUDIC_PHASES[] = {
"Start","Input","FixedUpdate","Update","LateUpdate","Render", 0
};
static const char* LUDIC_WIDGETS[] = {
"panel","col","row","label","button","image","spacer", 0
};
static const char* LUDIC_WIDGET_PROPS[] = {
"id","text","skin","image","align","w","h","pad","gap","size","font",
"inset","grow","bg","fg","border","focus","x","y", 0
};
/* A builtin is a name the runtime provides (`clear(c)` -> rt_clear). An
* intrinsic is a name the compiler lowers directly to libc/OS. Editors treat
* both as "standard library", but the signatures differ, so they stay apart. */
typedef struct { const char* name; const char* sig; const char* doc; } LBuiltin;
static const LBuiltin LUDIC_BUILTINS[] = {
{"min","min(a: int, b: int) -> int","Smaller of two integers."},
{"max","max(a: int, b: int) -> int","Larger of two integers."},
{"abs","abs(a: int) -> int","Absolute value."},
{"clamp","clamp(v: int, lo: int, hi: int) -> int","Constrain v to [lo, hi]."},
{"seed","seed(i: int)","Seed the deterministic RNG."},
{"rng_range","rng_range(lo: int, hi: int) -> int","Deterministic integer in [lo, hi]."},
{"rng_chance","rng_chance(pct: int) -> bool","True pct% of the time, deterministically."},
{"fx","fx(i: int) -> fixed","Widen an int to Q16.16 fixed-point."},
{"flr","flr(f: fixed) -> int","Truncate a fixed-point value toward zero."},
{"map_size","map_size(w: int, h: int)","Set the tilemap dimensions."},
{"map_row","map_row(y: int, row: str)","Fill one tilemap row from a string."},
{"tile","tile(x: int, y: int) -> int","Tile code at a map cell."},
{"clear","clear(color: int)","Clear the framebuffer to a 0xRRGGBB colour."},
{"present","present()","Push the framebuffer to the window (or out.ppm when headless)."},
{"fill_rect","fill_rect(x: int, y: int, w: int, h: int, color: int)","Filled rectangle."},
{"frame_rect","frame_rect(x: int, y: int, w: int, h: int, color: int)","One-pixel rectangle outline."},
{"put_px","put_px(x: int, y: int, color: int)","Write a single pixel."},
{"text","text(x: int, y: int, s: str, color: int, scale: int)","Draw text with the built-in 5x7 bitmap font."},
{"text_int","text_int(x: int, y: int, n: int, color: int, scale: int)","Draw an integer with the 5x7 bitmap font."},
{"font_load","font_load(path: str) -> int","Load a TrueType .ttf/.ttc; returns a font id."},
{"text_ttf","text_ttf(font: int, x: int, y: int, utf8: str, color: int, px: int)","Draw UTF-8 text with a TrueType font."},
{"text_w","text_w(font: int, utf8: str, px: int) -> int","Advance width of the string, in pixels."},
{"text_h","text_h(font: int, px: int) -> int","Line height of the font, in pixels."},
{"image_load","image_load(path: str) -> int","Decode a PNG into an image id."},
{"draw_image","draw_image(id: int, x: int, y: int)","Blit an image at its natural size."},
{"draw_image_scaled","draw_image_scaled(id: int, x: int, y: int, w: int, h: int)","Blit an image stretched to w x h."},
{"draw_9slice","draw_9slice(id: int, x: int, y: int, w: int, h: int, inset: int)","Nine-slice an image across a w x h box."},
{"load_png","load_png(path: str) -> int","Decode a PNG as a 16x16 sprite sheet; returns the first sprite id."},
{"load_sprites","load_sprites(path: str)","Load the sprite sheet used by draw_sprite."},
{"draw_sprite","draw_sprite(id: int, x: int, y: int)","Blit a sprite."},
{"draw_sprite_scaled","draw_sprite_scaled(id: int, x: int, y: int, scale: int)","Blit a sprite at an integer scale."},
{"ui_build","ui_build()","Construct every declared `ui` tree (loads skins and images)."},
{"ui_open","ui_open(id: int)","Make a ui tree active and focus its first button."},
{"ui_tick","ui_tick(key: int)","Feed a key to the UI: w/s move focus, space/enter activate."},
{"ui_render","ui_render()","Lay out and draw the active ui tree."},
{"ui_clicked","ui_clicked(id: int) -> bool","True on the frame a widget was activated."},
{"ui_set_text","ui_set_text(id: int, s: str)","Replace a widget's text."},
{"ui_set_int","ui_set_int(id: int, n: int)","Replace a widget's text with a number."},
{"ui_focus","ui_focus(id: int)","Move keyboard focus to a widget."},
{"ui_focused","ui_focused() -> int","Id of the focused widget."},
{"ui_visible","ui_visible(id: int, on: bool)","Show or hide a widget subtree."},
{"key","key() -> int","Key code pressed this frame, 0 if none."},
{"reg","reg(i: int) -> int","Read one of the 64 integer resources shared by systems."},
{"setreg","setreg(i: int, v: int)","Write one of the 64 integer resources."},
{"self","self() -> entity","The entity of the innermost query loop."},
{"save","save()","Write a binary snapshot of the whole ECS world."},
{"load","load() -> bool","Restore the snapshot; false if there is none."},
{"status","status(s: str)","Set the one-line status message."},
{"print_int","print_int(i: int)","Print an integer to stdout."},
{"quit","quit()","Stop the frame loop and exit."},
{0,0,0}
};
static const LBuiltin LUDIC_INTRINSICS[] = {
{"mem_alloc","mem_alloc(n: int) -> ptr","Allocate n bytes (malloc)."},
{"mem_realloc","mem_realloc(p: ptr, n: int) -> ptr","Resize a block to n bytes, preserving its contents (realloc)."},
{"os_argc","os_argc() -> int","Number of command-line arguments, argv[0] included."},
{"os_arg","os_arg(i: int) -> str","The i-th command-line argument."},
{"file_stderr","file_stderr() -> ptr","The standard error stream, for file_write."},
{"mem_free","mem_free(p: ptr)","Release an allocation."},
{"mem_copy","mem_copy(dst: ptr, src: ptr, n: int)","memcpy."},
{"mem_set","mem_set(p: ptr, byte: int, n: int)","memset."},
{"peek8","peek8(p: ptr, off: int) -> int","Read one byte."},
{"poke8","poke8(p: ptr, off: int, v: int)","Write one byte."},
{"peek32","peek32(p: ptr, off: int) -> int","Read a 32-bit word."},
{"poke32","poke32(p: ptr, off: int, v: int)","Write a 32-bit word."},
{"peekp","peekp(p: ptr, off: int) -> ptr","Read a pointer-sized word."},
{"pokep","pokep(p: ptr, off: int, v: ptr)","Write a pointer-sized word."},
{"peekf","peekf(p: ptr, off: int) -> fixed","Read a fixed-point word."},
{"pokef","pokef(p: ptr, off: int, v: fixed)","Write a fixed-point word."},
{"ptr_add","ptr_add(p: ptr, off: int) -> ptr","Offset a pointer by bytes."},
{"ptr_null","ptr_null() -> ptr","The null pointer."},
{"ptr_is_null","ptr_is_null(p: ptr) -> bool","Null test."},
{"as_fixed","as_fixed(i: int) -> fixed","Reinterpret an int as fixed (no conversion)."},
{"as_int","as_int(f: fixed) -> int","Reinterpret a fixed as int (no conversion)."},
{"file_open","file_open(path: str, mode: str) -> ptr","fopen."},
{"file_read","file_read(f: ptr, buf: ptr, n: int) -> int","fread."},
{"file_write","file_write(f: ptr, buf: ptr, n: int) -> int","fwrite."},
{"file_seek","file_seek(f: ptr, off: int, whence: int) -> int","fseek."},
{"file_tell","file_tell(f: ptr) -> int","ftell."},
{"file_close","file_close(f: ptr)","fclose."},
{"read_byte","read_byte() -> int","Read one byte from stdin, -1 at EOF."},
{"write_byte","write_byte(b: int)","Write one byte to stdout."},
{"print_str","print_str(s: str)","Write a string to stdout."},
{"str_len","str_len(s: str) -> int","Length of a string in bytes."},
{"shl","shl(v: int, n: int) -> int","Shift left."},
{"shr","shr(v: int, n: int) -> int","Logical shift right."},
{"band","band(a: int, b: int) -> int","Bitwise and."},
{"bor","bor(a: int, b: int) -> int","Bitwise or."},
{"bxor","bxor(a: int, b: int) -> int","Bitwise xor."},
{"bnot","bnot(a: int) -> int","Bitwise not."},
{"os_exit","os_exit(code: int)","Terminate the process."},
{"os_time","os_time() -> int","Seconds since the epoch."},
{"is_windowed","is_windowed() -> bool","True when the build has a window."},
{"game_title","game_title() -> str","The name from the `game` declaration."},
{"win_open","win_open(title: str, w: int, h: int, scale: int)","Open the platform window."},
{"win_poll","win_poll() -> int","Pump the event queue; returns a key code."},
{"win_present","win_present(px: ptr, w: int, h: int)","Blit a framebuffer to the window."},
{"win_running","win_running() -> bool","False once the window has been closed."},
{"win_close","win_close()","Close the platform window."},
{0,0,0}
};
static int lud_in(const char** set, const char* s){
for (int i = 0; set[i]; i++) if (!strcmp(set[i], s)) return 1;
return 0;
}
static const LBuiltin* lud_lookup(const LBuiltin* t, const char* s){
for (int i = 0; t[i].name; i++) if (!strcmp(t[i].name, s)) return &t[i];
return 0;
}
static int lud_is_keyword(const char* s){
return lud_in(LUDIC_KW_DECL, s) || lud_in(LUDIC_KW_CLAUSE, s) || lud_in(LUDIC_KW_STMT, s);
}
/* ---------- lexing --------------------------------------------------------*/
static void ltok_push(LLex* L, int kind, int start, int end, int line, int bad){
if (L->n >= L->cap){ L->cap = L->cap ? L->cap * 2 : 512; L->v = realloc(L->v, L->cap * sizeof(LTok)); }
L->v[L->n++] = (LTok){ kind, start, end, line, bad };
}
static void lline_push(LLex* L, int off){
if (L->nline >= L->caplin){ L->caplin = L->caplin ? L->caplin * 2 : 256; L->linestart = realloc(L->linestart, L->caplin * sizeof(int)); }
L->linestart[L->nline++] = off;
}
static const char* LUDIC_OPS2[] = { "->","+=","-=","*=","/=","==","!=","<=",">=","&&","||","..","=>", 0 };
/* Lex the whole buffer. Comments and newlines are kept — the formatter needs
* both, and a highlighter needs the comments. */
static void lud_lex(LLex* L, const char* src){
memset(L, 0, sizeof(*L));
L->src = src;
lline_push(L, 0);
int i = 0, line = 0;
while (src[i]){
char c = src[i];
if (c == '\n'){ ltok_push(L, LT_NL, i, i + 1, line, 0); i++; line++; lline_push(L, i); continue; }
if (c == ' ' || c == '\t' || c == '\r'){ i++; continue; }
if (c == '#'){ int s = i; while (src[i] && src[i] != '\n') i++; ltok_push(L, LT_COMMENT, s, i, line, 0); continue; }
if (c == '"'){
int s = i; i++; int bad = 0;
while (src[i] && src[i] != '"' && src[i] != '\n'){ if (src[i] == '\\' && src[i+1]) i += 2; else i++; }
if (src[i] == '"') i++; else bad = 1;
ltok_push(L, LT_STR, s, i, line, bad); continue;
}
if (c == '\''){
int s = i; i++; int bad = 0;
if (src[i] == '\\' && src[i+1]) i += 2; else if (src[i] && src[i] != '\n') i++;
if (src[i] == '\'') i++; else bad = 1;
ltok_push(L, LT_CHAR, s, i, line, bad); continue;
}
if (isdigit((unsigned char)c)){
int s = i;
if (c == '0' && (src[i+1] == 'x' || src[i+1] == 'X')){
i += 2; while (isxdigit((unsigned char)src[i])) i++;
ltok_push(L, LT_INT, s, i, line, 0); continue;
}
while (isdigit((unsigned char)src[i])) i++;
if (src[i] == '.' && isdigit((unsigned char)src[i+1])){
i++; while (isdigit((unsigned char)src[i])) i++;
ltok_push(L, LT_FLOAT, s, i, line, 0); continue;
}
ltok_push(L, LT_INT, s, i, line, 0); continue;
}
/* @name is one token: an annotation reads as a unit, and the formatter
* must never put a space between the sigil and the name. */
if (c == '@' && (isalpha((unsigned char)src[i+1]) || src[i+1] == '_')){
int s = i; i++; while (isalnum((unsigned char)src[i]) || src[i] == '_') i++;
ltok_push(L, LT_ANNO, s, i, line, 0); continue;
}
if (isalpha((unsigned char)c) || c == '_'){
int s = i; while (isalnum((unsigned char)src[i]) || src[i] == '_') i++;
int len = i - s; char w[128];
if (len < (int)sizeof(w)){ memcpy(w, src + s, len); w[len] = 0; } else { w[0] = 0; }
int k = LT_ID;
if (!strcmp(w, "true") || !strcmp(w, "false")) k = LT_BOOL;
else if (lud_in(LUDIC_TYPES, w)) k = LT_TYPE;
else if (lud_in(LUDIC_PHASES, w)) k = LT_PHASE;
else if (lud_is_keyword(w)) k = LT_KW;
ltok_push(L, k, s, i, line, 0); continue;
}
{
int matched = 0;
for (int k = 0; LUDIC_OPS2[k]; k++)
if (src[i] == LUDIC_OPS2[k][0] && src[i+1] == LUDIC_OPS2[k][1]){
ltok_push(L, LT_OP, i, i + 2, line, 0); i += 2; matched = 1; break;
}
if (matched) continue;
}
if (c == ';'){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; }
if (strchr("+-*/%<>=(){}[],:.!@", c)){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; }
/* Anything else is an error token — but a whole UTF-8 character's worth
* at a time. Splitting a multi-byte character into one token per byte
* would let a consumer that re-emits tokens (the formatter) put spaces
* inside it and corrupt the file. */
{
unsigned char u = (unsigned char)c;
int len = u < 0x80 ? 1 : u < 0xE0 ? 2 : u < 0xF0 ? 3 : 4;
for (int k = 1; k < len; k++) if (!src[i + k] || ((unsigned char)src[i + k] & 0xC0) != 0x80){ len = k; break; }
ltok_push(L, LT_ERR, i, i + len, line, 1); i += len;
}
}
ltok_push(L, LT_EOF, i, i, line, 0);
}
static void lud_lex_free(LLex* L){ free(L->v); free(L->linestart); memset(L, 0, sizeof(*L)); }
/* ---------- helpers over the token stream --------------------------------*/
static int ltok_len(const LTok* t){ return t->end - t->start; }
static int ltok_is(const LLex* L, int i, const char* s){
if (i < 0 || i >= L->n) return 0;
const LTok* t = &L->v[i]; int n = ltok_len(t);
return (int)strlen(s) == n && !strncmp(L->src + t->start, s, n);
}
/* Copy a token's text into a caller buffer; returns buf. */
static char* ltok_text(const LLex* L, int i, char* buf, int cap){
const LTok* t = &L->v[i]; int n = ltok_len(t);
if (n >= cap) n = cap - 1;
memcpy(buf, L->src + t->start, n); buf[n] = 0; return buf;
}
/* Index of the next token that is not a newline or comment, or -1. */
static int ltok_next_sig(const LLex* L, int i){
for (int j = i + 1; j < L->n; j++){
int k = L->v[j].kind;
if (k != LT_NL && k != LT_COMMENT) return j;
}
return -1;
}
static int ltok_prev_sig(const LLex* L, int i){
for (int j = i - 1; j >= 0; j--){
int k = L->v[j].kind;
if (k != LT_NL && k != LT_COMMENT) return j;
}
return -1;
}
#endif /* LUDIC_SYNTAX_H */

View file

@ -1,405 +0,0 @@
/* ============================================================================
* ludic_workspace.h — many documents, one project.
*
* Ludic programs are multi-file: `import "rules.ludic"` splices a fragment into
* whichever `game`/`module` file pulled it in. An imported fragment is NOT a
* standalone program — it has no `game` block — so running the compiler on it
* directly is meaningless. This layer keeps that straight: it knows which files
* form a compilation unit, which file is its root, and it resolves a name
* against the whole unit rather than one buffer.
* ==========================================================================*/
#ifndef LUDIC_WORKSPACE_H
#define LUDIC_WORKSPACE_H
#include "ludic_index.h"
#include <dirent.h>
#include <limits.h>
#include <sys/stat.h>
#include <unistd.h>
/* ---------- paths & URIs --------------------------------------------------*/
static char* lw_readfile(const char* path, long* out_n){
FILE* f = fopen(path, "rb"); if (!f) return 0;
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
char* b = malloc(n + 1);
if (fread(b, 1, n, f) != (size_t)n){ fclose(f); free(b); return 0; }
b[n] = 0; fclose(f);
if (out_n) *out_n = n;
return b;
}
static int lw_hex(int c){ return c >= '0' && c <= '9' ? c - '0' : (c | 32) >= 'a' && (c | 32) <= 'f' ? (c | 32) - 'a' + 10 : -1; }
static char* lw_uri_to_path(const char* uri){
if (strncmp(uri, "file://", 7)) return strdup(uri);
const char* s = uri + 7;
/* file://host/path is not something an editor sends for local files */
char* out = malloc(strlen(s) + 1); int j = 0;
for (int i = 0; s[i]; ){
if (s[i] == '%' && lw_hex(s[i+1]) >= 0 && lw_hex(s[i+2]) >= 0){
out[j++] = (char)(lw_hex(s[i+1]) * 16 + lw_hex(s[i+2])); i += 3;
} else out[j++] = s[i++];
}
out[j] = 0;
return out;
}
static char* lw_path_to_uri(const char* path){
static const char* safe = "-_.~/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
size_t cap = strlen(path) * 3 + 16;
char* out = malloc(cap);
strcpy(out, "file://");
int j = 7;
for (int i = 0; path[i]; i++){
unsigned char c = (unsigned char)path[i];
if (strchr(safe, c)) out[j++] = c;
else j += sprintf(out + j, "%%%02X", c);
}
out[j] = 0;
return out;
}
static char* lw_dirname(const char* p){
char* d = strdup(p); char* s = strrchr(d, '/');
if (s) *s = 0; else { free(d); d = strdup("."); }
return d;
}
static char* lw_join(const char* dir, const char* rel){
if (rel[0] == '/') return strdup(rel);
size_t n = strlen(dir) + strlen(rel) + 2;
char* p = malloc(n); snprintf(p, n, "%s/%s", dir, rel);
char real[PATH_MAX];
if (realpath(p, real)){ free(p); return strdup(real); }
return p;
}
static int lw_ends(const char* s, const char* suf){
size_t a = strlen(s), b = strlen(suf);
return a >= b && !strcmp(s + a - b, suf);
}
/* ---------- Markdown ------------------------------------------------------
* Fenced Ludic inside prose is real Ludic and deserves the same treatment. The
* trick that makes it free: copy the document and blank out every byte that is
* not inside a ```ludic fence. Offsets, lines and columns still match the file
* on disk exactly, so every feature built on top — highlighting, hover,
* go-to-definition, diagnostics — works on a .md without knowing about it. */
static char* lw_scrub_markdown(const char* src){
char* out = strdup(src);
int i = 0, n = (int)strlen(src);
while (i < n){
int ls = i, le = i;
while (le < n && src[le] != '\n') le++;
int ind = 0; while (ls + ind < le && src[ls + ind] == ' ') ind++;
int j = ls + ind; char m = j < le ? src[j] : 0;
int run = 0; while (j + run < le && src[j + run] == m) run++;
int fenced = (m == '`' || m == '~') && run >= 3;
int info = j + run;
int is_ludic = 0;
if (fenced){
int k = info; while (k < le && (src[k] == ' ' || src[k] == '\t')) k++;
is_ludic = (le - k >= 5) && !strncasecmp(src + k, "ludic", 5) &&
(le - k == 5 || src[k+5] == ' ' || src[k+5] == '\t' || src[k+5] == '\r');
}
/* blank the fence line itself either way */
for (int k = ls; k < le; k++) out[k] = ' ';
i = le < n ? le + 1 : n;
if (!fenced) continue;
/* inside a fence: keep the body only when the info string says ludic */
while (i < n){
int bs = i, be = i;
while (be < n && src[be] != '\n') be++;
int bi = 0; while (bs + bi < be && src[bs + bi] == ' ') bi++;
int cj = bs + bi, crun = 0;
while (cj + crun < be && src[cj + crun] == m) crun++;
int only = 1;
for (int k = cj + crun; k < be; k++) if (src[k] != ' ' && src[k] != '\r'){ only = 0; break; }
if (crun >= run && only){
for (int k = bs; k < be; k++) out[k] = ' ';
i = be < n ? be + 1 : n;
break;
}
if (!is_ludic) for (int k = bs; k < be; k++) out[k] = ' ';
i = be < n ? be + 1 : n;
}
}
return out;
}
/* ---------- documents -----------------------------------------------------*/
static void ldoc_settext(LDoc* D, char* raw){
if (D->text != D->raw) free(D->text);
free(D->raw);
D->raw = raw;
D->text = D->is_markdown ? lw_scrub_markdown(raw) : raw;
}
static void ldoc_reindex(LDoc* D){
lud_lex_free(&D->lex);
lud_lex(&D->lex, D->text);
ldoc_match_braces(D);
ldoc_parse(D);
free(D->cls);
D->cls = calloc(D->lex.n + 1, 1);
}
static LDoc* lw_new_doc(const char* path, char* text){
LDoc* D = calloc(1, sizeof(LDoc));
char real[PATH_MAX];
D->path = realpath(path, real) ? strdup(real) : strdup(path);
D->uri = lw_path_to_uri(D->path);
D->is_markdown = lw_ends(D->path, ".md") || lw_ends(D->path, ".markdown");
ldoc_settext(D, text);
ldoc_reindex(D);
return D;
}
static void lw_add(LIndex* X, LDoc* D){
if (X->n >= X->cap){ X->cap = X->cap ? X->cap * 2 : 32; X->d = realloc(X->d, X->cap * sizeof(LDoc*)); }
X->d[X->n++] = D;
}
static LDoc* lw_by_path(LIndex* X, const char* path){
char real[PATH_MAX];
const char* p = realpath(path, real) ? real : path;
for (int i = 0; i < X->n; i++) if (!strcmp(X->d[i]->path, p)) return X->d[i];
return 0;
}
static LDoc* lw_by_uri(LIndex* X, const char* uri){
char* p = lw_uri_to_path(uri);
LDoc* D = lw_by_path(X, p);
if (!D) for (int i = 0; i < X->n; i++) if (!strcmp(X->d[i]->uri, uri)){ D = X->d[i]; break; }
free(p);
return D;
}
/* Load a file into the index if it is not there yet. */
static LDoc* lw_ensure(LIndex* X, const char* path){
LDoc* D = lw_by_path(X, path);
if (D) return D;
char* text = lw_readfile(path, 0);
if (!text) return 0;
D = lw_new_doc(path, text);
lw_add(X, D);
return D;
}
/* Walk the workspace once at startup so that go-to-definition works across
* files the editor has never opened. Skips the usual noise directories. */
static void lw_scan(LIndex* X, const char* dir, int depth){
if (depth > 8) return;
DIR* d = opendir(dir); if (!d) return;
struct dirent* e;
while ((e = readdir(d))){
if (e->d_name[0] == '.') continue;
if (!strcmp(e->d_name, "build") || !strcmp(e->d_name, "node_modules") ||
!strcmp(e->d_name, "target") || !strcmp(e->d_name, "out")) continue;
char p[PATH_MAX]; snprintf(p, sizeof(p), "%s/%s", dir, e->d_name);
struct stat st; if (stat(p, &st)) continue;
if (S_ISDIR(st.st_mode)) lw_scan(X, p, depth + 1);
else if (lw_ends(e->d_name, ".ludic")) lw_ensure(X, p);
}
closedir(d);
}
/* ---------- compilation units --------------------------------------------
* `related` = every document that shares a compilation unit with D: the unit
* root that (transitively) imports D, plus everything that root imports. */
static void lw_imports_of(LIndex* X, LDoc* D, LDoc** out, int* n, int max){
for (int i = 0; i < D->nimport; i++){
char* dir = lw_dirname(D->path);
char* full = lw_join(dir, D->imports[i]);
free(dir);
LDoc* I = lw_ensure(X, full);
free(full);
if (!I) continue;
int seen = 0; for (int k = 0; k < *n; k++) if (out[k] == I) seen = 1;
if (seen || *n >= max) continue;
out[(*n)++] = I;
lw_imports_of(X, I, out, n, max);
}
}
static int lw_related(LIndex* X, LDoc* D, LDoc** out, int max){
int n = 0;
if (!D) return 0;
out[n++] = D;
lw_imports_of(X, D, out, &n, max);
/* pull in any unit root that reaches D, and that root's other imports */
for (int i = 0; i < X->n && n < max; i++){
LDoc* R = X->d[i];
if (!R->is_unit || R == D) continue;
LDoc* reach[128]; int rn = 0;
lw_imports_of(X, R, reach, &rn, 128);
int hits = 0; for (int k = 0; k < rn; k++) if (reach[k] == D) hits = 1;
if (!hits) continue;
int seen = 0; for (int k = 0; k < n; k++) if (out[k] == R) seen = 1;
if (!seen && n < max) out[n++] = R;
for (int k = 0; k < rn && n < max; k++){
int s2 = 0; for (int q = 0; q < n; q++) if (out[q] == reach[k]) s2 = 1;
if (!s2) out[n++] = reach[k];
}
}
return n;
}
/* The file `ludicc` should actually be pointed at when checking D. */
static LDoc* lw_unit_root(LIndex* X, LDoc* D){
if (!D || D->is_markdown) return 0;
if (D->is_unit) return D;
LDoc* rel[128]; int n = lw_related(X, D, rel, 128);
/* a `game` beats a `module`: it is the one that actually compiles */
for (int i = 0; i < n; i++) if (rel[i]->is_unit && !rel[i]->is_module && !rel[i]->is_markdown) return rel[i];
for (int i = 0; i < n; i++) if (rel[i]->is_unit && !rel[i]->is_markdown) return rel[i];
return 0;
}
/* ---------- name resolution ----------------------------------------------*/
static int lsym_is_toplevel(int kind){
switch (kind){
case LS_COMPONENT: case LS_ARCHETYPE: case LS_CONST: case LS_VAR:
case LS_FN: case LS_EXTERN: case LS_SYSTEM: case LS_UI: case LS_WIDGET:
case LS_SCENE: case LS_LAYER: case LS_UNIT:
return 1;
default: return 0;
}
}
static int lsym_is_local(int kind){
return kind == LS_LOCAL || kind == LS_PARAM || kind == LS_QUERYVAR || kind == LS_STATE;
}
/* The innermost local binding of `name` visible at byte offset `off`. */
static int lw_find_local(LDoc* D, const char* name, int off){
int best = -1;
for (int i = 0; i < D->nsym; i++){
LSym* s = &D->sym[i];
if (!lsym_is_local(s->kind) || strcmp(s->name, name)) continue;
if (off < s->body_start || off > s->body_end) continue;
if (best < 0 || s->body_start >= D->sym[best].body_start) best = i;
}
return best;
}
static int lw_find_top(LIndex* X, LDoc* D, const char* name, LDoc** owner){
LDoc* rel[128]; int n = lw_related(X, D, rel, 128);
for (int i = 0; i < n; i++)
for (int k = 0; k < rel[i]->nsym; k++)
if (lsym_is_toplevel(rel[i]->sym[k].kind) && !strcmp(rel[i]->sym[k].name, name)){
*owner = rel[i]; return k;
}
/* fall back to the whole workspace — better a cross-unit jump than none */
for (int i = 0; i < X->n; i++)
for (int k = 0; k < X->d[i]->nsym; k++)
if (lsym_is_toplevel(X->d[i]->sym[k].kind) && !strcmp(X->d[i]->sym[k].name, name)){
*owner = X->d[i]; return k;
}
return -1;
}
/* The component a dotted receiver has, e.g. `p` in `p.x` when p came from a
* query binding or a typed parameter. Returns the component symbol or -1. */
static int lw_receiver_component(LIndex* X, LDoc* D, int dot_tok, LDoc** owner){
int recv = ltok_prev_sig(&D->lex, dot_tok);
if (recv < 0 || D->lex.v[recv].kind != LT_ID) return -1;
char name[96]; ltok_text(&D->lex, recv, name, sizeof(name));
int l = lw_find_local(D, name, D->lex.v[recv].start);
const char* ty = 0;
if (l >= 0) ty = D->sym[l].type;
else {
LDoc* o; int t = lw_find_top(X, D, name, &o);
if (t >= 0) ty = o->sym[t].type;
}
if (!ty || !ty[0]) return -1;
return lw_find_top(X, D, ty, owner);
}
/* ---------- semantic classification --------------------------------------*/
static int lw_first_on_line(LDoc* D, int tok){
int p = ltok_prev_sig(&D->lex, tok);
return p < 0 || D->lex.v[p].line != D->lex.v[tok].line;
}
static int lw_in_ui_body(LDoc* D, int off){
for (int i = 0; i < D->nsym; i++)
if (D->sym[i].kind == LS_UI && off > D->sym[i].body_start && off < D->sym[i].body_end) return 1;
return 0;
}
static void lw_classify(LIndex* X, LDoc* D){
if (!D->cls) D->cls = calloc(D->lex.n + 1, 1);
for (int i = 0; i < D->lex.n; i++){
const LTok* t = &D->lex.v[i];
int c = SC_NONE;
switch (t->kind){
case LT_COMMENT: c = SC_COMMENT; break;
case LT_STR: case LT_CHAR: c = SC_STRING; break;
case LT_INT: case LT_FLOAT: c = SC_NUMBER; break;
case LT_BOOL: case LT_KW: c = SC_KEYWORD; break;
case LT_TYPE: c = SC_TYPE; break;
case LT_PHASE: c = SC_PHASE; break;
case LT_ANNO: c = SC_ANNOTATION; break;
case LT_OP: c = SC_OPERATOR; break;
case LT_ID: c = SC_UNKNOWN; break;
default: c = SC_NONE; break;
}
if (t->kind != LT_ID){ D->cls[i] = (unsigned char)c; continue; }
char name[96]; ltok_text(&D->lex, i, name, sizeof(name));
/* 1. the token IS a declaration's name */
int decl = -1;
for (int s = 0; s < D->nsym; s++) if (D->sym[s].tok == i){ decl = s; break; }
if (decl >= 0){
switch (D->sym[decl].kind){
case LS_COMPONENT: c = SC_COMPONENT; break;
case LS_ARCHETYPE: c = SC_ARCHETYPE; break;
case LS_FIELD: c = SC_FIELD; break;
case LS_CONST: c = SC_CONST; break;
case LS_VAR: c = SC_MODVAR; break;
case LS_FN: case LS_EXTERN: c = SC_FUNCTION; break;
case LS_SYSTEM: c = SC_SYSTEM; break;
case LS_UI: c = SC_UI; break;
case LS_WIDGET: c = SC_CONST; break; /* UI_Foo is a handle */
case LS_SCENE: c = SC_SCENE; break;
case LS_LAYER: c = SC_LAYER; break;
case LS_PARAM: c = SC_PARAM; break;
case LS_QUERYVAR: case LS_LOCAL: case LS_STATE: c = SC_VARIABLE; break;
case LS_UNIT: c = SC_UI; break;
default: c = SC_UNKNOWN;
}
D->cls[i] = (unsigned char)c; continue;
}
/* 2. member access: `p.x` */
int prev = ltok_prev_sig(&D->lex, i);
if (prev >= 0 && ltok_is(&D->lex, prev, ".")){ D->cls[i] = SC_FIELD; continue; }
int next = ltok_next_sig(&D->lex, i);
int followed_by_eq = next >= 0 && ltok_is(&D->lex, next, "=");
/* 3. inside a `ui` block a bare word is a widget type and `k=` a prop —
* and those names (text, image, size) collide with builtins, so this
* has to be decided before the builtin table is consulted. */
if (lw_in_ui_body(D, t->start)){
if (followed_by_eq){ D->cls[i] = SC_PROP; continue; }
if (lw_first_on_line(D, i) || (prev >= 0 && (ltok_is(&D->lex, prev, "{") || ltok_is(&D->lex, prev, "}")))){
D->cls[i] = lud_in(LUDIC_WIDGETS, name) ? SC_WIDGET : SC_UI; continue;
}
}
/* 4. a local binding in scope */
int l = lw_find_local(D, name, t->start);
if (l >= 0){ D->cls[i] = D->sym[l].kind == LS_PARAM ? SC_PARAM : SC_VARIABLE; continue; }
/* 5. a declaration somewhere in the compilation unit */
LDoc* owner = 0; int top = lw_find_top(X, D, name, &owner);
if (top >= 0){
switch (owner->sym[top].kind){
case LS_COMPONENT: c = SC_COMPONENT; break;
case LS_ARCHETYPE: c = SC_ARCHETYPE; break;
case LS_CONST: c = SC_CONST; break;
case LS_VAR: c = SC_MODVAR; break;
case LS_FN: case LS_EXTERN: c = SC_FUNCTION; break;
case LS_SYSTEM: c = SC_SYSTEM; break;
case LS_UI: c = SC_UI; break;
case LS_WIDGET: c = SC_CONST; break;
case LS_SCENE: c = SC_SCENE; break;
case LS_LAYER: c = SC_LAYER; break;
default: c = SC_UNKNOWN;
}
D->cls[i] = (unsigned char)c; continue;
}
/* 6. the runtime surface */
if (lud_lookup(LUDIC_BUILTINS, name) || lud_lookup(LUDIC_INTRINSICS, name)){ D->cls[i] = SC_BUILTIN; continue; }
/* 7. a record field initialiser: `Pos = { x = 10 }` */
if (followed_by_eq){ D->cls[i] = SC_FIELD; continue; }
D->cls[i] = SC_UNKNOWN;
}
}
#endif /* LUDIC_WORKSPACE_H */

View file

@ -1,59 +0,0 @@
/* migrate_records.c — one-time migration for Rule A (named fields use ':').
* Records appear ONLY inside `spawn` blocks, so we track spawn context and, for
* every single '=' inside one:
* - '=' followed by '{' is a component init `Comp = { … }` -> delete it (`Comp { … }`)
* - otherwise is a field binding `field = value` -> rewrite to ':'
* A '=' at a real boundary is a pure re-spelling; the driver's IR-identity check
* (migrated program compiles to the same IR) is the proof.
* usage: migrate_records <file> -> migrated source to stdout
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ludic_syntax.h"
int main(int argc, char** argv){
if (argc < 2){ fprintf(stderr, "usage: migrate_records <file>\n"); return 2; }
FILE* f = fopen(argv[1], "rb");
if (!f){ fprintf(stderr, "cannot open %s\n", argv[1]); return 2; }
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
char* src = malloc(n + 1); fread(src, 1, n, f); src[n] = 0; fclose(f);
LLex L; lud_lex(&L, src);
char* rep = calloc(L.n, 1); /* '=' -> ':' */
char* del = calloc(L.n, 1); /* drop the '=' (component init) */
int sp_stack[1024]; int bp = 0; int spawn_active = 0; int pendingSpawn = 0;
for (int i = 0; i < L.n; i++){
int k = L.v[i].kind; if (k == LT_EOF) break;
if (k == LT_NL || k == LT_COMMENT) continue;
if (k == LT_KW && ltok_is(&L, i, "spawn")) pendingSpawn = 1;
int op1 = (k == LT_OP && ltok_len(&L.v[i]) == 1);
char c0 = L.src[L.v[i].start];
if (op1 && c0 == '{'){
if (bp < 1024){ sp_stack[bp] = (pendingSpawn || spawn_active > 0) ? 1 : 0; if (sp_stack[bp]) spawn_active++; bp++; }
pendingSpawn = 0;
} else if (op1 && c0 == '}'){
if (bp > 0){ bp--; if (sp_stack[bp] && spawn_active > 0) spawn_active--; }
} else if (op1 && c0 == '='){
if (spawn_active > 0){
int nx = ltok_next_sig(&L, i);
if (nx >= 0 && L.v[nx].kind == LT_OP && ltok_len(&L.v[nx]) == 1 && L.src[L.v[nx].start] == '{') del[i] = 1;
else rep[i] = 1;
}
}
}
/* map flags onto byte positions so the rebuild can peek one char ahead */
char* repb = calloc(n + 1, 1); char* delb = calloc(n + 1, 1);
for (int i = 0; i < L.n; i++){ if (rep[i]) repb[L.v[i].start] = 1; if (del[i]) delb[L.v[i].start] = 1; }
for (int b = 0; b < n; ){
if (repb[b]){ putchar(':'); b++; continue; } /* '=' -> ':' */
if (delb[b]){ b++; if (b < n && src[b] == ' ') b++; continue; } /* drop '=' and one space */
if (src[b] == ' ' && b + 1 < n && repb[b + 1]){ b++; continue; } /* drop the space before ':' -> `field: v` */
putchar(src[b]); b++;
}
free(repb); free(delb);
lud_lex_free(&L); free(rep); free(del); free(src);
return 0;
}

View file

@ -1,119 +0,0 @@
/* sepfix.c — one-time migration for Rule B: insert `;` at statement boundaries
* that are currently spelled with only whitespace. Reuses the toolchain lexer.
*
* A boundary is inserted between two significant, same-line tokens prev,cur when
* - we are at paren/bracket depth 0 (not inside a call's args or an index), AND
* - we are NOT inside a `ui` block (widget props are `k=v`, space-separated,
* and the parser does not skip newlines between them), AND
* - prev can END an operand/statement, AND cur can START a statement.
* Inserting `;` (which the lexer maps to a newline token) at a REAL boundary is
* a semantic no-op under today's permissive parser — the IR-identity check in
* the driver is the proof. Wrong insertions change the IR and are rejected.
*
* usage: sepfix <in.ludic> -> writes migrated source to stdout
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ludic_syntax.h"
static const char* STMT_START[] = {
"let","return","if","when","while","for","spawn","despawn",
"match","machine","become","enter","break","continue", 0
};
static int ender(const LLex* L, int i){
const LTok* t = &L->v[i];
switch (t->kind){
case LT_ID: case LT_INT: case LT_FLOAT: case LT_STR: case LT_CHAR:
case LT_BOOL: case LT_TYPE: case LT_PHASE: return 1;
case LT_OP: {
if (ltok_len(t) != 1) return 0;
char c = L->src[t->start];
if (c == ']'){
/* `]` closing an empty `[]` is a slice-TYPE marker (`[]Node`),
* not an operand end — the type name that follows continues it. */
int p = ltok_prev_sig(L, i);
if (p >= 0 && L->v[p].kind == LT_OP && ltok_len(&L->v[p]) == 1 && L->src[L->v[p].start] == '[') return 0;
return 1;
}
return (c == ')' || c == '}'); }
case LT_KW: { char b[32]; ltok_text(L, i, b, sizeof b);
return !strcmp(b, "break") || !strcmp(b, "continue"); }
default: return 0;
}
}
static int starter(const LLex* L, int i){
const LTok* t = &L->v[i];
if (t->kind == LT_ID || t->kind == LT_INT || t->kind == LT_FLOAT ||
t->kind == LT_STR || t->kind == LT_CHAR || t->kind == LT_BOOL) return 1;
if (t->kind == LT_KW){ char b[32]; ltok_text(L, i, b, sizeof b); return lud_in(STMT_START, b); }
return 0; /* an operator never starts a statement here */
}
int main(int argc, char** argv){
if (argc < 2){ fprintf(stderr, "usage: sepfix <file>\n"); return 2; }
FILE* f = fopen(argv[1], "rb");
if (!f){ fprintf(stderr, "sepfix: cannot open %s\n", argv[1]); return 2; }
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
char* src = malloc(n + 1); fread(src, 1, n, f); src[n] = 0; fclose(f);
LLex L; lud_lex(&L, src);
/* insert[k] = 1 means: emit a ';' immediately after token k's bytes */
char* insert = calloc(L.n, 1);
int paren = 0; /* unclosed ( or [ */
/* ui suppression: brace depth stack, marking which open braces are ui/widget */
int ui_stack[512]; int bp = 0; int ui_active = 0;
int pendingUiOpen = 0; /* saw `ui` or a widget word; the next { is a ui brace */
int prev = -1; /* previous significant token index */
for (int i = 0; i < L.n; i++){
int k = L.v[i].kind;
if (k == LT_EOF) break;
if (k == LT_NL || k == LT_COMMENT) continue;
char c0 = L.src[L.v[i].start];
int op1 = (k == LT_OP && ltok_len(&L.v[i]) == 1);
/* track ui context by the `ui` KEYWORD only — it brackets the whole
* widget tree via its root brace, so every nested widget brace is
* already inside ui_active. (Do NOT key off widget words like col/row/
* image/label: those are also ordinary variable names, and matching them
* would wrongly suppress real statement boundaries.) */
if (k == LT_KW && ltok_is(&L, i, "ui")) pendingUiOpen = 1;
if (op1 && c0 == '{'){
if (bp < 512){ ui_stack[bp] = pendingUiOpen; bp++; if (pendingUiOpen) ui_active++; }
pendingUiOpen = 0;
} else if (op1 && c0 == '}'){
if (bp > 0){ bp--; if (ui_stack[bp]) { if (ui_active) ui_active--; } }
} else if (op1 && (c0 == '(' || c0 == '[')){ paren++; if (getenv("SEPFIX_TRACE")) fprintf(stderr, " L%d '%c' -> paren=%d\n", L.v[i].line + 1, c0, paren); }
else if (op1 && (c0 == ')' || c0 == ']')){ if (paren > 0) paren--; if (getenv("SEPFIX_TRACE")) fprintf(stderr, " L%d '%c' -> paren=%d\n", L.v[i].line + 1, c0, paren); }
/* candidate boundary between prev and this token */
if (prev >= 0 && paren == 0 && !ui_active &&
L.v[prev].line == L.v[i].line &&
ender(&L, prev) && starter(&L, i)){
insert[prev] = 1;
}
prev = i;
}
if (getenv("SEPFIX_DEBUG")) fprintf(stderr, "END STATE %s: paren=%d ui_active=%d bp=%d\n", argv[1], paren, ui_active, bp);
/* rebuild: copy bytes, dropping in ';' right after any token flagged */
for (int i = 0, t = 0; i < n; ){
/* find if a token ends exactly at i and is flagged */
putchar(src[i]);
i++;
/* emit ';' after the last byte of a flagged token */
for (; t < L.n; t++){
if (L.v[t].end == i && insert[t]){ putchar(';'); break; }
if (L.v[t].end > i) break;
}
}
lud_lex_free(&L); free(insert); free(src);
return 0;
}

View file

@ -1,52 +0,0 @@
/* migrate_ui.c — one-time migration for Rule A in `ui` blocks: widget props go
* from `key=value` to `key: value`. Every '=' inside a `ui` block is a prop
* separator (values are expressions, never contain a top-level '='), so we track
* ui context (armed by the `ui` keyword's root brace) and rewrite each '=' to a
* ':' with canonical spacing (`id: Root`). The driver's IR-identity check proves
* it is a pure re-spelling (the parser builds the same E_FINIT nodes).
* usage: migrate_ui <file> -> migrated source to stdout
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ludic_syntax.h"
int main(int argc, char** argv){
if (argc < 2){ fprintf(stderr, "usage: migrate_ui <file>\n"); return 2; }
FILE* f = fopen(argv[1], "rb");
if (!f){ fprintf(stderr, "cannot open %s\n", argv[1]); return 2; }
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
char* src = malloc(n + 1); fread(src, 1, n, f); src[n] = 0; fclose(f);
LLex L; lud_lex(&L, src);
char* repb = calloc(n + 1, 1); /* byte position of a '=' to rewrite as ':' */
int ui_stack[1024]; int bp = 0; int ui_active = 0; int pendingUi = 0;
for (int i = 0; i < L.n; i++){
int k = L.v[i].kind; if (k == LT_EOF) break;
if (k == LT_NL || k == LT_COMMENT) continue;
if (k == LT_KW && ltok_is(&L, i, "ui")) pendingUi = 1;
int op1 = (k == LT_OP && ltok_len(&L.v[i]) == 1);
char c0 = L.src[L.v[i].start];
if (op1 && c0 == '{'){
if (bp < 1024){ ui_stack[bp] = (pendingUi || ui_active > 0) ? 1 : 0; if (ui_stack[bp]) ui_active++; bp++; }
pendingUi = 0;
} else if (op1 && c0 == '}'){
if (bp > 0){ bp--; if (ui_stack[bp] && ui_active > 0) ui_active--; }
} else if (op1 && c0 == '=' && ui_active > 0){
repb[L.v[i].start] = 1;
}
}
for (int b = 0; b < n; ){
if (repb[b]){ /* '=' -> ': ' with a single trailing space */
putchar(':'); b++;
if (b < n && src[b] != ' ' && src[b] != '\n') putchar(' ');
continue;
}
if (src[b] == ' ' && b + 1 < n && repb[b + 1]){ b++; continue; } /* drop the space before ':' */
putchar(src[b]); b++;
}
lud_lex_free(&L); free(repb); free(src);
return 0;
}