Updated the LSP indexer (ludic_index.h), completion snippets (ludic_lsp.c), check-docs wrapper, and the fmt/LSP test fixtures (test-tools.sh, test-lsp.py) to the new keywords. Fixed two pre-existing bugs surfaced along the way: build-tools.sh had a set -e bug (a failing find|while pipeline in a command substitution) that silently prevented ALL rebuilds since the first build, so the editor binaries were stale; and an LSP test offset was calibrated to the old keyword length. test.sh 14/14, test-tools 28/0, check-docs + check-vocabulary green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
672 lines
31 KiB
C
672 lines
31 KiB
C
/* ============================================================================
|
|
* 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 */
|