ludic/tools/ludic-tools/ludic_workspace.h
Orkuncakilkaya 985f9ad8f2 Baseline: Ludic compiler + toolchain, Phase 1 syntax fixes complete
Self-hosted compiler (selfhost/*.ludic), runtime, examples, editor tooling,
and docs. Phase 1 of the syntax-redesign cohesion pass has landed:
edge-system fix, signature-query, when-alias, and the documentation truth-pass.
Suite green (14/14), C-free bootstrap fixpoint holds.

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

405 lines
17 KiB
C

/* ============================================================================
* 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 */