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:
parent
96d01e45ab
commit
bca8f126fc
67 changed files with 24066 additions and 9309 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
563
tools/ludic-tools/fmt.ludic
Normal 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
2409
tools/ludic-tools/lsp.ludic
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 */
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue