/* ============================================================================ * 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 (selfhost/frontend/lex.ludic) 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 #include #include #include /* ---------- 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 string pointer 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[] = { "program","import","property","model","enum","ui","namespace", "const","var","function","extern","handler","entry","event","scene","test", 0 }; static const char* LUDIC_KW_CLAUSE[] = { "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[] = { 0 }; static const char* LUDIC_KW_STMT[] = { "let","return","if","else","while","for","in","spawn","despawn", "enable","disable","match","machine","state","become","where","prefab", "and","or","not","break","continue","new","emit","cancel","try", 0 }; static const char* LUDIC_TYPES[] = { "int","long","fixed","float","double","countdown","bool","entity","string","pointer","byte","words","fixeds","floats","doubles","pointers","Vector","IVec2","Rect","void", 0 }; static const char* LUDIC_PHASES[] = { "Start","Input","FixedUpdate","Update","LateUpdate","Render","Overlay", 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."}, {"fixed","fixed(i: int) -> fixed","Widen an int to Q16.16 fixed-point."}, {"floor","floor(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: string)","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: string, 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: string) -> int","Load a TrueType .ttf/.ttc; returns a font id."}, {"text_ttf","text_ttf(font: int, x: int, y: int, utf8: string, color: int, px: int)","Draw UTF-8 text with a TrueType font."}, {"text_w","text_w(font: int, utf8: string, 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: string) -> 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."}, {"png_load","png_load(path: string) -> int","Decode a PNG as a 16x16 sprite sheet; returns the first sprite id."}, {"sprites_load","sprites_load(path: string)","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: string)","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."}, {"set_reg","set_reg(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: string)","Set the one-line status message."}, {"print","print(x)","Print a value (int or string) and a newline to stdout."}, {"string","string(x) -> string","Convert an int/bool/fixed to text (a string passes through)."}, {"quit","quit()","Stop the frame loop and exit."}, {"panic","panic(msg: string)","Print `file:line: panic: msg` to stderr and abort (exit 1) — a located error, not a crash."}, {"assert","assert(cond: bool, msg: string)","Abort with `file:line: assertion failed: msg` when cond is false — for programmer-bug invariants."}, {"ok","ok(v) -> result","Wrap a success payload (any i32-width scalar) in a result — the happy half of `try`/`else`."}, {"err","err(msg: string) -> result","Wrap a failure message in a result — the sad half, recovered by `try`/`else`."}, {"is_ok","is_ok(r: result) -> bool","True when a result carries a success payload."}, {"is_err","is_err(r: result) -> bool","True when a result carries a failure."}, {"some","some(v) -> option","Wrap a present value (any i32-width scalar) in an option — the `has a value` case."}, {"none","none() -> option","The empty option — a missing value with no magic -1 sentinel."}, {"is_some","is_some(o: option) -> bool","True when an option holds a value."}, {"is_none","is_none(o: option) -> bool","True when an option is empty."}, {"unwrap_or","unwrap_or(o: option, fallback) -> int","The option's value, or fallback when it is empty."}, {0,0,0} }; static const LBuiltin LUDIC_INTRINSICS[] = { {"bytes","bytes(n: int) -> ptr","Allocate a buffer of n bytes."}, {"words","words(n: int) -> words","Allocate a buffer of n 32-bit words."}, {"resize","resize(p: ptr, n: int) -> ptr","Resize a block to n bytes, preserving its contents (realloc)."}, {"arg_count","arg_count() -> int","Number of command-line arguments, argv[0] included."}, {"arg","arg(i: int) -> str","The i-th command-line argument."}, {"file_stderr","file_stderr() -> ptr","The standard error stream, for file_write."}, {"file_stdout","file_stdout() -> ptr","The standard output stream, for file_write."}, {"free","free(p: ptr)","Release an allocation."}, {"fill","fill(p: ptr, byte: int, n: int)","memset."}, {"offset","offset(p: ptr, off: int) -> ptr","Offset a pointer by bytes."}, {"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: string, mode: string) -> 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_char","read_char() -> int","Read one byte from stdin, -1 at EOF."}, {"exit","exit(code: int)","Terminate the process."}, {"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: string, 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 == '`'){ /* `interpolated {expr}` string */ int s = i; i++; int bad = 0; while (src[i] && src[i] != '`'){ 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") || !strcmp(w, "null")) 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 */