A registry marked `@Machine(Deer.mood)` is the transitions of a machine over that enum field of the records a state's Table<Deer> holds. Its record has from and to (the enum's variants), on: string (an action's name, "" for a transition the tick asks), guard: fn(Row<Deer>, reads...) -> bool and enter: fn(Row<Deer>, reads...) -> void; the states are the enum's variants and the start is the field's default. The rows are data (an .lres or defs), the names the studio already edits. Written by the compiler (machines.ludic, machines_write.ludic): for each action an `on` names, a row reducer in the registry's file (named ..__machine__DeerSteps, so it sits beside the program's own row reducer on the same action, after it): the row's state, the first transition from it on that action whose guard passes, the field set, enter run - guards and enters called by name. When a row leaves a state on a guard alone, `state DeerStepsMachine` (the kept row view) and deer_steps_tick(m: mut DeerStepsMachine, s: mut Herd, reads...), one transition a row a tick. Nothing allocates. The table is the whole machine: the field written anywhere else - an assignment, or a `machine` block's become over it - is a type error (check_stmt.ludic, ck_machine_write). Guards and enters take the row first, are the record's module's, keep a row reducer's rules (and may be handed the row); a guard writes nothing through it. The graph is checked, each error at its row (in the .lres when the rows are there): a state never reached from the start, a state with no way out, an `on` naming no action or an action with no @Target, a self-transition with no guard, two ways out of a state on one trigger behind an unguarded first. Also refused: @Machine off a registry, a field that is not a plain enum with a default, a @Column field, no table (or two) of the record, a transitions record of another shape, a machine outside its table's state's module. ludic schema's code section gains `machines` (registry, record, field, enum, table, start, states, actions, tick, module, at); ludic deps names a machine's reducer `reducer Deer in Herd.deer on Spook (machine DeerSteps)`. vocab @Machine; docs annot-machine, kw-machine; LANGUAGE.md "A machine as data"; examples actions/machine (+ deer_steps.lres) and ten rejects; test.ludic feat, reject and schema cases (not run); changes/machines.md. Reseeded; bootstrap-cfree fixpoint holds (317642 lines); Maroon Lake's `ludic build --check` is clean against this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1764 lines
84 KiB
Text
1764 lines
84 KiB
Text
# parse.ludic — recursive-descent parser: the token slice -> an AST.
|
|
# Core grammar: properties, vars/lets, consts, fns, entry; the ECS/ui/match
|
|
# constructs live in parse_game.ludic. Uses the global `toks` and a cursor `pi`.
|
|
|
|
var pi: int = 0
|
|
var prog: []Node # the top-level declarations
|
|
var g_game_name: pointer # the `program` name
|
|
|
|
# ---- diagnostics -----------------------------------------------------------
|
|
# Every compiler error is `file:line: error: message` — the form editors and
|
|
# build tools parse. While parsing, the location is the current token's; while
|
|
# lowering, it is the statement being emitted (emit_stmt records it).
|
|
var g_parse_file: pointer = "" # the file whose tokens are being parsed
|
|
# `numbers float`: files whose bare decimal literals are float, not fixed. A file that says
|
|
# so is listed, and so is every non-runtime file it imports (a barrel passes it on).
|
|
var g_float_files: []pointer = new []pointer
|
|
# L3 modules: `module NAME` at the top of a file names the module it and everything it imports
|
|
# belong to, until an import names its own; `friend module NAME` may see every module's private
|
|
# names (a test harness). A file in no module - the runtime, a program's root - is public.
|
|
# L7: the files that may write `unsafe` - the runtime, a package from the toolchain or
|
|
# ludic_modules, and what those import from beside them. A project's own files may only
|
|
# with --unsafe (g_unsafe_all).
|
|
var g_uses_bytes: bool = false # text_of / Fs.read_bytes / Fs.write_bytes: splice bytes.ludic
|
|
var g_trusted_files: []pointer = new []pointer
|
|
var g_unsafe_all: bool = false
|
|
function unsafe_trusted(f: pointer) -> bool {
|
|
if g_unsafe_all { return true }
|
|
return not (fm_get(g_fm_trusted, f) == "")
|
|
}
|
|
# a file's facts - its module, its package, whether it is trusted - asked of every reference the
|
|
# checker and the emitter resolve: a table by file name, not a list walked each time (with a game's
|
|
# nine hundred files that walk was most of a check-only build)
|
|
const FM_SLOTS: int = 16384
|
|
property FileMap {
|
|
keys: []pointer = null
|
|
vals: []pointer = null
|
|
}
|
|
function fm_new() -> FileMap {
|
|
let m = new FileMap
|
|
m.keys = new []pointer
|
|
m.vals = new []pointer
|
|
var i = 0
|
|
while i < FM_SLOTS {
|
|
push(m.keys, null)
|
|
push(m.vals, null)
|
|
i += 1
|
|
}
|
|
return m
|
|
}
|
|
function fm_slot(m: FileMap, f: pointer) -> int {
|
|
var h = 5381
|
|
var i = 0
|
|
let n = len(f)
|
|
while i < n {
|
|
h = (h * 33 + f[i]) % 1000003
|
|
i += 1
|
|
}
|
|
h = h % FM_SLOTS
|
|
while m.keys[h] != null and not (m.keys[h] == f) { h = (h + 1) % FM_SLOTS }
|
|
return h
|
|
}
|
|
# the latest value set for f wins, as the lists' walk from the end had it
|
|
function fm_put(m: FileMap, f: pointer, v: pointer) -> void {
|
|
if f == null { return }
|
|
let h = fm_slot(m, f)
|
|
m.keys[h] = f
|
|
m.vals[h] = v
|
|
}
|
|
function fm_get(m: FileMap, f: pointer) -> pointer {
|
|
if f == null { return "" }
|
|
let h = fm_slot(m, f)
|
|
if m.keys[h] == null { return "" }
|
|
return m.vals[h]
|
|
}
|
|
var g_fm_mod: FileMap = fm_new()
|
|
var g_fm_pkg: FileMap = fm_new()
|
|
var g_fm_trusted: FileMap = fm_new()
|
|
var g_fm_src: FileMap = fm_new() # every file read, by its path: the schema's docs and spans
|
|
function trust_file(f: pointer) -> void {
|
|
push(g_trusted_files, f)
|
|
fm_put(g_fm_trusted, f, "1")
|
|
}
|
|
var g_mod_file: []pointer = new []pointer
|
|
var g_mod_name: []pointer = new []pointer
|
|
var g_mod_friends: []pointer = new []pointer
|
|
function module_of(f: pointer) -> pointer { return fm_get(g_fm_mod, f) }
|
|
function module_set(f: pointer, name: pointer) -> void {
|
|
push(g_mod_file, f)
|
|
push(g_mod_name, name)
|
|
fm_put(g_fm_mod, f, name)
|
|
}
|
|
function is_float_file(f: pointer) -> bool {
|
|
if (f == null) { return false }
|
|
return not (fm_get(g_fm_float, f) == "")
|
|
}
|
|
var g_fm_float: FileMap = fm_new()
|
|
function float_file_add(f: pointer) -> void {
|
|
push(g_float_files, f)
|
|
fm_put(g_fm_float, f, "1")
|
|
}
|
|
var g_parsing: bool = true # false once lowering starts
|
|
var g_err_file: pointer = "" # the statement being lowered
|
|
var g_err_line: int = 0
|
|
|
|
function cur() -> Tok { return toks[pi] }
|
|
function pk(o: int) -> Tok { return toks[pi + o] }
|
|
function is_op(v: pointer) -> bool { let t = toks[pi]; return t.kind == TK_OP and (t.text == v) }
|
|
function is_id(v: pointer) -> bool { let t = toks[pi]; return t.kind == TK_ID and (t.text == v) }
|
|
function is_kw(v: pointer) -> bool { return is_id(v) }
|
|
|
|
function perr(msg: pointer) -> void {
|
|
var file = g_err_file
|
|
var line = g_err_line
|
|
var col = 0
|
|
if g_parsing {
|
|
file = g_parse_file
|
|
line = 0
|
|
if pi < len(toks) { line = toks[pi].line; col = toks[pi].col }
|
|
}
|
|
if file == null { file = "" }
|
|
if g_diag_json {
|
|
diag_add(file, line, col, "error", msg)
|
|
diag_exit(1)
|
|
}
|
|
let m = `{file}:{line}: error: {msg}\n`
|
|
file_write(file_stderr(), m, len(m))
|
|
exit(1)
|
|
}
|
|
# an error the pass reporting it can go on from (every @Ref, every module rule broken): reported,
|
|
# counted, and the build stops once the pass is done (diag_stop_if_errors)
|
|
function perr_at(file: pointer, line: int, col: int, msg: pointer) -> void {
|
|
var f = file
|
|
if f == null { f = "" }
|
|
if g_diag_json { diag_add(f, line, col, "error", msg) }
|
|
else {
|
|
let m = `{f}:{itoa(line)}: error: {msg}\n`
|
|
file_write(file_stderr(), m, len(m))
|
|
g_dg_errors += 1
|
|
}
|
|
}
|
|
# a warning: kept in the JSON array, or said on stderr; the build goes on
|
|
function pwarn_at(file: pointer, line: int, col: int, msg: pointer) -> void {
|
|
var f = file
|
|
if f == null { f = "" }
|
|
if g_diag_json { diag_add(f, line, col, "warning", msg) }
|
|
else {
|
|
let m = `{f}:{itoa(line)}: warning: {msg}\n`
|
|
file_write(file_stderr(), m, len(m))
|
|
}
|
|
}
|
|
function diag_stop_if_errors() -> void {
|
|
if g_dg_errors > 0 { diag_exit(1) }
|
|
}
|
|
|
|
# the current token, described for a diagnostic
|
|
function tok_desc() -> pointer {
|
|
let t = toks[pi]
|
|
if t.kind == TK_EOF { return "end of file" }
|
|
if t.kind == TK_NL { return "end of line" }
|
|
if t.kind == TK_INT or t.kind == TK_FLOAT { return `number {itoa(t.ival)}` }
|
|
if t.kind == TK_STR { return `string \"{t.text}\"` }
|
|
return `'{t.text}'`
|
|
}
|
|
function eat_op(v: pointer) -> void { if not is_op(v) { perr(`expected '{v}', got {tok_desc()}`) }; pi += 1 }
|
|
function eat_id() -> pointer {
|
|
let t = toks[pi]
|
|
if t.kind != TK_ID { perr(`expected an identifier, got {tok_desc()}`) }
|
|
pi += 1
|
|
return t.text
|
|
}
|
|
function skipnl() -> void { while toks[pi].kind == TK_NL { pi += 1 } }
|
|
|
|
# a type: `[]T` slice, `fn(T, U) -> R` function, or a plain name (int/ptr/str/bool/struct)
|
|
# L7: the typed buffers are slices - a length, a bounds check on every index, and a place in the
|
|
# checker - so `floats` is `[]float`, `words` is `[]int`, and so on. `bytes()` stays raw.
|
|
function buffer_slice_ty(t: pointer) -> pointer {
|
|
if (t == "floats") { return "[]float" }
|
|
if (t == "words") { return "[]int" }
|
|
if (t == "fixeds") { return "[]fixed" }
|
|
if (t == "doubles") { return "[]double" }
|
|
if (t == "pointers") { return "[]pointer" }
|
|
return t
|
|
}
|
|
function ptype() -> pointer {
|
|
if (toks[pi].text == "fn") and (toks[pi + 1].text == "(") {
|
|
pi += 1
|
|
eat_op("(")
|
|
var out = "fn("
|
|
var first = true
|
|
while not is_op(")") {
|
|
if not first { out = out + "," }
|
|
out = out + ptype()
|
|
first = false
|
|
if is_op(",") { pi += 1 }
|
|
}
|
|
eat_op(")")
|
|
var r: pointer = "void"
|
|
if is_op("->") {
|
|
pi += 1
|
|
r = ptype()
|
|
}
|
|
return out + ")->" + r
|
|
}
|
|
if is_op("[") {
|
|
pi += 1
|
|
eat_op("]")
|
|
let el = ptype()
|
|
let out = bytes(len(el) + 3)
|
|
out[0] = '['; out[1] = ']' # "[]"
|
|
var i = 0
|
|
while el[i] != 0 { out[2 + i] = el[i]; i += 1 }
|
|
out[2 + i] = 0
|
|
return out
|
|
}
|
|
let tn = eat_id()
|
|
if is_op("<") { return gen_type_args(tn) }
|
|
return buffer_slice_ty(tn)
|
|
}
|
|
|
|
# ---- expressions -----------------------------------------------------------
|
|
function expr() -> Node { return p_or() }
|
|
|
|
# Call arguments. Each argument is either positional (`expr`) or named
|
|
# (`name: expr`) — a named argument is an identifier immediately followed by a
|
|
# colon, which is unambiguous inside a call. Named args are stored as E_FINIT
|
|
# (s=label, a=value) and reordered to the callee's parameter order at emit time.
|
|
function args_call(call: Node) -> void {
|
|
eat_op("("); skipnl()
|
|
while not is_op(")") {
|
|
let t = toks[pi]
|
|
let nx = toks[pi + 1]
|
|
if t.kind == TK_ID and nx.kind == TK_OP and (nx.text == ":") {
|
|
let fi = node(E_FINIT); fi.s = eat_id(); eat_op(":"); skipnl(); fi.a = expr(); push(call.kids, fi)
|
|
} else {
|
|
push(call.kids, expr())
|
|
}
|
|
skipnl(); if is_op(",") { pi += 1; skipnl() }
|
|
}
|
|
eat_op(")")
|
|
}
|
|
|
|
# ---- string interpolation --------------------------------------------------
|
|
# `text {expr} text` desugars to a `+` chain of string literals and `string(expr)`
|
|
# holes, so it reuses the string-concat operator and needs no new runtime.
|
|
function interp_lit(buf: pointer, len: int) -> Node { let n = node(E_STR); n.s = buf[0..0 + len]; return n }
|
|
function interp_add(acc: Node, part: Node) -> Node {
|
|
if acc == null { return part }
|
|
return mkbin("+", acc, part)
|
|
}
|
|
function interp_str(e: Node) -> Node { # wrap a hole in string(...)
|
|
let c = node(E_CALL); let id = node(E_ID); id.s = "string"; id.uns = TPL_HOLE; c.a = id; push(c.kids, e); return c
|
|
}
|
|
var g_interp_line: int = 1 # the line the interpolated string sits on
|
|
var g_interp_pos: int = -1 # the backtick's place in its file
|
|
var g_hole_base: int = -1
|
|
function parse_hole(inner: pointer) -> Node { # re-lex+parse an embedded expression
|
|
let saved_toks = toks; let saved_pi = pi
|
|
let saved_base = g_lex_base
|
|
g_lex_base = g_hole_base # the hole's place in its file, or -1
|
|
lex_at(inner, g_interp_line); pi = 0; skipnl()
|
|
g_lex_base = saved_base
|
|
let e = expr()
|
|
toks = saved_toks; pi = saved_pi
|
|
return e
|
|
}
|
|
function parse_interp(raw: pointer) -> Node {
|
|
let my_pos = g_interp_pos # a hole's own templates move g_interp_pos
|
|
let n = len(raw)
|
|
var acc: Node = null
|
|
let lit = bytes(n + 1)
|
|
var lj = 0
|
|
var i = 0
|
|
while i < n {
|
|
let c = raw[i]
|
|
if c == '{' { # '{'
|
|
if raw[i + 1] == '{' { lit[lj] = '{'; lj += 1; i += 2; continue } # {{ -> {
|
|
if lj > 0 { acc = interp_add(acc, interp_lit(lit, lj)); lj = 0 }
|
|
i += 1
|
|
let hs = i
|
|
# find the hole's closing brace: braces nest, and a brace inside a "string", a 'char' or a
|
|
# `template` in the hole is text, not structure (lex.ludic, hole_end)
|
|
i = hole_end(raw, i, n)
|
|
g_hole_base = -1
|
|
if my_pos >= 0 { g_hole_base = my_pos + 1 + hs }
|
|
acc = interp_add(acc, interp_str(parse_hole(raw[hs..i])))
|
|
i += 1 # skip the closing '}'
|
|
} else {
|
|
if c == '}' and raw[i + 1] == '}' { lit[lj] = '}'; lj += 1; i += 2; continue } # }} -> }
|
|
if c == CH_BACKSLASH { # backslash escape, same table as "strings"
|
|
lit[lj] = unescape(raw[i + 1]); lj += 1; i += 2; continue
|
|
}
|
|
lit[lj] = c; lj += 1; i += 1
|
|
}
|
|
}
|
|
if lj > 0 or (acc == null) { acc = interp_add(acc, interp_lit(lit, lj)) }
|
|
if acc.kind == E_CALL { acc.a.uns = TPL_COPY } # `{s}` alone is a new string, never s itself (emit_call.ludic)
|
|
return acc
|
|
}
|
|
const TPL_COPY: int = 91 # on the callee of a template's lone string(hole)
|
|
const TPL_HOLE: int = 92 # on the callee of every other string(hole) (a Key is refused there)
|
|
|
|
# emit E(field: v, ...) — shared by the statement form and the expression form.
|
|
# As an expression it yields a cancellable event's cancelled flag (0/1); a
|
|
# non-cancellable event yields 0.
|
|
function parse_emit() -> Node {
|
|
pi += 1; let n = node(S_EMIT); n.s = eat_id()
|
|
cx_emit(n) # the schema's code map (emit_schema_code.ludic)
|
|
let r = node(E_REC)
|
|
eat_op("("); skipnl()
|
|
while not is_op(")") {
|
|
let fi = node(E_FINIT); fi.s = eat_id(); eat_op(":"); fi.a = expr(); push(r.kids, fi)
|
|
skipnl(); if is_op(",") { pi += 1; skipnl() }
|
|
}
|
|
eat_op(")")
|
|
n.a = r
|
|
return n
|
|
}
|
|
|
|
# `[a, b, c]` — a slice literal (an empty `[]` needs a type: `new []T`)
|
|
function parse_list() -> Node {
|
|
let n = node(E_LIST); n.line = toks[pi].line
|
|
eat_op("["); skipnl()
|
|
while not is_op("]") {
|
|
push(n.kids, expr())
|
|
skipnl(); if is_op(",") { pi += 1; skipnl() }
|
|
}
|
|
eat_op("]")
|
|
return n
|
|
}
|
|
|
|
function p_primary() -> Node {
|
|
let t = toks[pi]
|
|
if t.kind == TK_INTERP { pi += 1; g_interp_line = t.line; g_interp_pos = t.pos; return parse_interp(t.text) }
|
|
if is_op("[") { return parse_list() }
|
|
if t.kind == TK_ID and (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() }
|
|
if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; n.s = t.text; pi += 1; return n } # s: a long literal's digits
|
|
if t.kind == TK_FLOAT { let n = node(E_FLOAT); n.ival = t.ival; n.s = t.text; pi += 1; return n }
|
|
if t.kind == TK_STR { let n = node(E_STR); n.s = t.text; pi += 1; return n }
|
|
if t.kind == TK_KEY { pi += 1; return key_lit_node(t) } # k"..." / kn"..." (i18n.ludic)
|
|
if t.kind == TK_ID {
|
|
if (t.text == "true") { let n = node(E_BOOL); n.ival = 1; pi += 1; return n }
|
|
if (t.text == "false") { let n = node(E_BOOL); n.ival = 0; pi += 1; return n }
|
|
if (t.text == "null") { pi += 1; return node(E_NULL) }
|
|
if (t.text == "new") { pi += 1; let n = node(E_NEW); n.s = ptype(); if is_op("{") { n.a = record() }; return n }
|
|
if (t.text == "spawn") { return parse_spawn() } # spawn as an expression: the new entity
|
|
# fn name — a top-level function as a value, for a worker entry point (Job.parallel_for)
|
|
# `fn` stays an ordinary name before an operator word (`p < fn and ...`) or at the end of a line
|
|
if (t.text == "fn") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 1].line == t.line) and (toks[pi + 1].text != "and") and (toks[pi + 1].text != "or") and (toks[pi + 1].text != "not") and (toks[pi + 1].text != "in") and (toks[pi + 1].text != "is") { pi += 1; let n = node(E_FNREF); n.line = t.line; n.s = eat_id(); cx_fnref(n); return n }
|
|
# try EXPR else { ... } — evaluate a fallible (result-typed) expression; on
|
|
# `ok` the whole expression is its payload, on `err` the else block runs (with
|
|
# the message bound to `error`) and its trailing expression is the fallback.
|
|
if (t.text == "try") {
|
|
pi += 1
|
|
let n = node(E_TRY); n.line = t.line
|
|
n.a = expr()
|
|
skipnl()
|
|
if not is_id("else") { perr("try needs an `else { ... }` fallback") }
|
|
pi += 1; skipnl()
|
|
n.b = block()
|
|
return n
|
|
}
|
|
if (t.text == "text_of") { g_uses_bytes = true }
|
|
let n = node(E_ID); n.s = t.text; pi += 1; return n
|
|
}
|
|
if is_op("(") { pi += 1; skipnl(); let e = expr(); skipnl(); eat_op(")"); return e }
|
|
if g_res_mode and is_op("{") { return record() } # L9: a resource's nested record, typed by its field
|
|
perr("expected expression")
|
|
return node(E_INT)
|
|
}
|
|
|
|
function p_postfix() -> Node {
|
|
var e = p_primary()
|
|
while true {
|
|
if is_op(".") { pi += 1; let m = node(E_MEMBER); m.a = e; m.s = eat_id(); e = m
|
|
if e.a.kind == E_ID and e.a.s == "Regex" { g_uses_regex = true } # splice the regex runtime on demand
|
|
if e.a.kind == E_ID and e.a.s == "Random" { g_uses_rng = true } # splice the seeded random numbers on demand
|
|
if e.a.kind == E_ID and e.a.s == "Fs" and (e.s == "read_bytes" or e.s == "write_bytes") { g_uses_bytes = true }
|
|
if e.a.kind == E_ID and e.a.s == "Build" and e.s == "schema_hash" { g_uses_schema_hash = true } # the schema hashed after the checker
|
|
if e.a.kind == E_ID and (e.a.s == "BigInt" or e.a.s == "Decimal") { g_uses_bignum = true } # splice the bignum runtime on demand
|
|
if e.a.kind == E_ID and (e.a.s == "Dict" or e.a.s == "Set") { g_uses_dict = true } # splice the hash-table runtime on demand
|
|
if e.a.kind == E_ID and (e.a.s == "Huge" or e.a.s == "Angle" or e.a.s == "Percent") { g_uses_numeric = true } # splice the huge/angle/percent runtime on demand
|
|
if e.a.kind == E_ID and (e.a.s == "Job" or e.a.s == "Promise" or e.a.s == "Sync") { g_uses_jobs = true } # splice the jobs/promise/sync runtime on demand
|
|
if e.a.kind == E_ID and e.a.s == "Query" { g_uses_query = true } # splice the ECS spatial-query runtime on demand
|
|
if e.a.kind == E_ID and e.a.s == "Reflect" { g_uses_reflect = true } # force-emit the reflection ABI (Reflect.* reads the world schema)
|
|
if e.a.kind == E_ID and e.a.s == "Light" { g_uses_light = true } # splice the 2D light-accumulation pass on demand
|
|
if e.a.kind == E_ID and (e.a.s == "Value" or e.a.s == "Json") { g_uses_value = true } # splice the value tree + JSON on demand (#44)
|
|
if e.a.kind == E_ID and e.a.s == "Xml" { g_uses_xml = true } # splice the XML reader on demand (Tiled #67)
|
|
if e.a.kind == E_ID and e.a.s == "Base64" { g_uses_base64 = true } # splice base64 codec + inflate on demand (Tiled #67)
|
|
if e.a.kind == E_ID and e.a.s == "Tiled" { g_uses_tiled = true } # splice the Tiled map runtime on demand (#69)
|
|
# #81 — Sprite.sheet/cell/… or Assets.* splice the spritesheet/atlas runtime
|
|
# (it reads the framebuffer + the image loader, so it links against core).
|
|
if e.a.kind == E_ID and e.a.s == "Sprite" and (e.s == "sheet" or e.s == "cell" or e.s == "cell_span" or e.s == "define" or e.s == "named" or e.s == "draw" or e.s == "draw_scaled" or e.s == "width" or e.s == "height") { g_uses_atlas = true }
|
|
if e.a.kind == E_ID and e.a.s == "Assets" { g_uses_atlas = true }
|
|
if e.a.kind == E_ID and e.a.s == "Reflect" and (e.s == "serialize" or e.s == "apply") { g_uses_value = true; g_uses_reflect_io = true } # Reflect.serialize/apply -> value tree + world table
|
|
# Input.* action-map / record-replay methods (#7) -> splice input.ludic.
|
|
# Input.key stays bare (no runtime), so gate on the new methods only.
|
|
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "bind" or e.s == "rebind" or e.s == "poll" or e.s == "down" or e.s == "pressed" or e.s == "record" or e.s == "replay" or e.s == "action" or e.s == "bind_pad" or e.s == "active" or e.s == "just_pressed" or e.s == "just_released") { g_uses_input = true }
|
|
# #50 device layer — any of the multi-key / analog / mouse / gamepad / touch
|
|
# methods also splices input.ludic (Input.key stays bare, no runtime).
|
|
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "key_down" or e.s == "key_pressed" or e.s == "key_released" or e.s == "key_label" or e.s == "press" or e.s == "release" or e.s == "axis" or e.s == "axis_i" or e.s == "vector" or e.s == "strength" or e.s == "mouse_x" or e.s == "mouse_y" or e.s == "mouse_dx" or e.s == "mouse_dy" or e.s == "mouse_down" or e.s == "wheel" or e.s == "set_mouse" or e.s == "pad_connected" or e.s == "pad_button" or e.s == "pad_axis" or e.s == "move_i" or e.s == "set_pad" or e.s == "touch_count" or e.s == "touch_x" or e.s == "touch_y" or e.s == "set_touch" or e.s == "cursor_mode") { g_uses_input = true }
|
|
# Anim.play/clip/on_frame/fired + Motion.to (#48): the ergonomic writes over
|
|
# the SpriteAnim/Motion components live in systems.ludic and use the world
|
|
# table, so splice it and force the reflection ABI even if the game leaves
|
|
# the engine auto-advance to do the ticking.
|
|
if e.a.kind == E_ID and e.a.s == "Anim" and (e.s == "play" or e.s == "clip" or e.s == "on_frame" or e.s == "fired") { g_uses_anim_rt = true }
|
|
if e.a.kind == E_ID and e.a.s == "Motion" and e.s == "to" { g_uses_anim_rt = true }
|
|
# Audio.* (#22) — any Audio method splices the audio runtime.
|
|
if e.a.kind == E_ID and e.a.s == "Audio" { g_uses_audio = true }
|
|
# Http.* (#6) — any Http method splices the HTTP client runtime.
|
|
if e.a.kind == E_ID and e.a.s == "Http" { g_uses_http = true }
|
|
# Udp.* — any Udp method splices the datagram runtime (and links udp.ll / udp_win.ll).
|
|
if e.a.kind == E_ID and e.a.s == "Udp" { g_uses_udp = true }
|
|
# Process.* — any Process method splices the child-process runtime (process.ll / process_win.ll).
|
|
if e.a.kind == E_ID and e.a.s == "Process" { g_uses_process = true }
|
|
# Gl.* — any Gl method splices the OpenGL runtime (and links the GL backend).
|
|
if e.a.kind == E_ID and e.a.s == "Gl" { g_uses_gl = true }
|
|
# Vk.* — any Vk method splices the Vulkan runtime (and links its loader)
|
|
if e.a.kind == E_ID and e.a.s == "Vk" { g_uses_vk = true }
|
|
# Tween.to/chain/delay/value/stop/parallel (#48): the fluent stateful handles
|
|
# live in tween.ludic, advanced by an engine-owned system each Update tick.
|
|
if e.a.kind == E_ID and e.a.s == "Tween" and (e.s == "to" or e.s == "chain" or e.s == "delay" or e.s == "value" or e.s == "stop" or e.s == "parallel") { g_uses_tween_rt = true }
|
|
if e.a.kind == E_ID and e.a.s == "Fx" { g_uses_fx = true } # Fx.* -> splice fx.ludic + tick/draw it each frame
|
|
}
|
|
else if is_op("[") { pi += 1; let lo = expr()
|
|
if is_op("..") { pi += 1; let sl = node(E_SLICE); sl.a = e; sl.b = lo; sl.c = expr(); eat_op("]"); e = sl } # s[a..b] substring
|
|
else { let ix = node(E_INDEX); ix.a = e; ix.b = lo; eat_op("]"); e = ix }
|
|
}
|
|
else if is_op("(") { let c = node(E_CALL); c.a = e; c.line = toks[pi].line; args_call(c); e = c
|
|
if (c.a.kind == E_ID) and rt_wanted_name(c.a.s) { push(g_rt_wants, c.a.s) } # a runtime called by its own name
|
|
if (c.a.kind == E_ID) and ((c.a.s == "draw_sprite") or (c.a.s == "draw_sprite_scaled")) { warn_draw_sprite(c.a.s) } # #85 deprecate the bare aliases
|
|
}
|
|
else { break }
|
|
}
|
|
return e
|
|
}
|
|
|
|
function p_unary() -> Node {
|
|
if is_op("-") { pi += 1; let n = node(E_UN); n.s = "-"; n.a = p_unary(); return n }
|
|
if is_op("~") { pi += 1; let n = node(E_UN); n.s = "~"; n.a = p_unary(); return n } # bitwise not
|
|
if is_id("not") { pi += 1; let n = node(E_UN); n.s = "not"; n.a = p_unary(); return n }
|
|
return p_postfix()
|
|
}
|
|
|
|
function mkbin(op: pointer, l: Node, r: Node) -> Node { let b = node(E_BIN); b.s = op; b.a = l; b.b = r; return b }
|
|
|
|
# precedence (Go-style, so `flags & MASK == 0` needs no parens): shifts and `&`
|
|
# bind like `*`; `|` and `^` bind like `+`; both tighter than comparison.
|
|
function p_mul() -> Node {
|
|
var l = p_unary()
|
|
while is_op("*") or is_op("/") or is_op("%") or is_op("<<") or is_op(">>") or is_op("&") {
|
|
let op = toks[pi].text; pi += 1; l = mkbin(op, l, p_unary()) }
|
|
return l
|
|
}
|
|
function p_add() -> Node {
|
|
var l = p_mul()
|
|
while is_op("+") or is_op("-") or is_op("|") or is_op("^") {
|
|
let op = toks[pi].text; pi += 1; l = mkbin(op, l, p_mul()) }
|
|
return l
|
|
}
|
|
function p_cmp() -> Node {
|
|
var l = p_add()
|
|
while is_op("<") or is_op("<=") or is_op(">") or is_op(">=") or is_op("==") or is_op("!=") {
|
|
let op = toks[pi].text; pi += 1; l = mkbin(op, l, p_add())
|
|
}
|
|
return l
|
|
}
|
|
function p_and() -> Node {
|
|
var l = p_cmp()
|
|
while is_id("and") { pi += 1; l = mkbin("and", l, p_cmp()) }
|
|
return l
|
|
}
|
|
function p_or() -> Node {
|
|
var l = p_and()
|
|
while is_id("or") { pi += 1; l = mkbin("or", l, p_and()) }
|
|
return l
|
|
}
|
|
|
|
# a record literal `{ field: value, ... }` — used by spawn component inits.
|
|
# Rule A: a named part uses `:` (`=` is assignment/binding only).
|
|
function record() -> Node {
|
|
eat_op("{")
|
|
let r = node(E_REC)
|
|
while true { skipnl(); if is_op("}") { break }
|
|
let fi = node(E_FINIT); fi.s = eat_id(); eat_op(":")
|
|
let v0 = pi
|
|
fi.a = expr()
|
|
fi.sp0 = toks[v0].off; fi.sp1 = toks[pi - 1].oend # the schema's text of the value
|
|
push(r.kids, fi)
|
|
if is_op(",") { pi += 1 } }
|
|
eat_op("}"); return r
|
|
}
|
|
|
|
# ---- statements ------------------------------------------------------------
|
|
function block() -> Node {
|
|
skipnl(); eat_op("{")
|
|
let b = node(N_BLOCK)
|
|
while true { skipnl(); if is_op("}") { break }
|
|
push(b.kids, stmt())
|
|
# Rule B: statements are separated by a newline or ';' (both lex to TK_NL).
|
|
# After a statement the next token must be that separator or the block's end —
|
|
# two statements may not sit adjacent with only spaces between them.
|
|
var sep = toks[pi].kind == TK_NL
|
|
if is_op("}") { sep = true }
|
|
if not sep { perr("expected newline or ';' between statements") }
|
|
}
|
|
eat_op("}")
|
|
return b
|
|
}
|
|
|
|
# parse a statement and stamp it with its source line (the first token's line),
|
|
# unless the specific rule already set one. The line drives --coverage and the
|
|
# panic/expect file:line messages.
|
|
function stmt() -> Node {
|
|
var aok = false
|
|
if is_op("@") and (toks[pi + 1].text == "alloc_ok") { # 25.2: @alloc_ok("why") before one statement
|
|
pi += 2
|
|
eat_op("(")
|
|
let why = expr()
|
|
if why.kind != E_STR or len(why.s) == 0 { perr("@alloc_ok needs its reason: @alloc_ok(\"a memo miss, bounded by MM_CAP\")") }
|
|
eat_op(")")
|
|
skipnl()
|
|
aok = true
|
|
}
|
|
let ln = toks[pi].line
|
|
let n = stmt_body()
|
|
if n.line == 0 { n.line = ln }
|
|
if aok { n.uns = FR_AOK_STMT; g_aok_seen = true } # on the node, so a generic's clone carries it (gen_clone copies uns)
|
|
return n
|
|
}
|
|
|
|
function stmt_body() -> Node {
|
|
let t = toks[pi]
|
|
if t.kind == TK_ID {
|
|
if (t.text == "let") or (t.text == "var") {
|
|
var mut = 0; if (t.text == "var") { mut = 1 } # let = immutable, var = mutable
|
|
pi += 1; let n = node(S_LET); n.ival = mut; n.line = toks[pi].line; n.s = eat_id()
|
|
if is_op(":") { pi += 1; n.ty = ptype() }
|
|
if is_op("=") { pi += 1; n.a = expr() }
|
|
return n
|
|
}
|
|
if (t.text == "return") {
|
|
pi += 1; let n = node(S_RETURN)
|
|
if toks[pi].kind != TK_NL and not is_op("}") { n.a = expr() }
|
|
return n
|
|
}
|
|
if (t.text == "if") {
|
|
pi += 1; let n = node(S_IF); n.a = expr(); n.b = block()
|
|
let save = pi; skipnl() # peek past newlines for a trailing `else`
|
|
if is_id("else") { pi += 1; skipnl()
|
|
if is_id("if") { n.c = stmt() } else { n.c = block() } }
|
|
else { pi = save } # no else: keep the separator for block()'s Rule-B check
|
|
return n
|
|
}
|
|
if (t.text == "while") { pi += 1; let n = node(S_WHILE); n.a = expr(); n.b = block(); return n }
|
|
if (t.text == "for") {
|
|
pi += 1
|
|
if is_op("(") { return parse_query_for() }
|
|
let n = node(S_FOR); n.s = eat_id()
|
|
let inkw = eat_id() # 'in'
|
|
n.a = expr(); eat_op(".."); n.b = expr(); n.c = block()
|
|
return n
|
|
}
|
|
if (t.text == "spawn") { return parse_spawn() }
|
|
if (t.text == "dispatch") and toks[pi + 1].kind == TK_ID { return parse_dispatch() } # 0.R
|
|
if (t.text == "machine") {
|
|
pi += 1; let n = node(S_MACHINE); n.a = expr(); skipnl(); eat_op("{")
|
|
var sidx = 0 # states auto-number by declaration order
|
|
while true { skipnl(); if is_op("}") { break }
|
|
let stkw = eat_id() # 'state'
|
|
let s = node(S_STATE); s.s = eat_id()
|
|
if is_op("=") { pi += 1; s.b = expr() } # explicit value (still allowed)
|
|
s.ival = sidx # else: the enum variant of the same name
|
|
# when the store is enum-typed, or this ordinal
|
|
skipnl(); s.a = block()
|
|
push(n.kids, s); sidx += 1 }
|
|
eat_op("}"); return n
|
|
}
|
|
# `emit E(...)` fires an event, but a bare `emit(...)` is an ordinary call
|
|
# (the compiler dogfoods a function named `emit`), so require an event name.
|
|
if (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() }
|
|
if (t.text == "become") { pi += 1; let n = node(S_BECOME); n.s = eat_id(); return n }
|
|
if (t.text == "despawn") { pi += 1; let n = node(S_DESPAWN); n.a = expr(); return n }
|
|
if (t.text == "enable") or (t.text == "disable") {
|
|
var en = 0; if (t.text == "enable") { en = 1 }
|
|
pi += 1; let n = node(S_TOGGLE); n.ival = en
|
|
if is_id("layer") { pi += 1; n.ty = "layer"; n.s = eat_id(); note_toggled_layer(n.s); return n } # enable/disable layer L
|
|
if is_id("system") { pi += 1; n.ty = "system"; n.s = eat_id(); if en == 0 { push(g_disabled_sys, n.s) }; return n } # disable system <esys_fn> (lever 5, compile-time)
|
|
n.s = eat_id() # `enable P on e` / `disable Model` / `disable Handler`
|
|
if is_id("on") { pi += 1; n.a = expr() } # property on an entity
|
|
return n
|
|
}
|
|
if (t.text == "attach") { # attach P on e [{ field: val, ... }]
|
|
pi += 1; let n = node(S_ATTACH); n.s = eat_id()
|
|
if not is_id("on") { perr("attach needs 'on <entity>'") }
|
|
pi += 1; n.a = expr()
|
|
if is_op("{") { n.b = record() } # optional field overrides (same-line)
|
|
return n
|
|
}
|
|
if (t.text == "detach") { # detach P on e
|
|
pi += 1; let n = node(S_DETACH); n.s = eat_id()
|
|
if not is_id("on") { perr("detach needs 'on <entity>'") }
|
|
pi += 1; n.a = expr()
|
|
return n
|
|
}
|
|
if (t.text == "unsafe") and (toks[pi + 1].text == "{") { # L7: raw memory allowed inside
|
|
let un = node(S_UNSAFE)
|
|
pi += 1
|
|
un.a = block()
|
|
return un
|
|
}
|
|
if (t.text == "break") { pi += 1; return node(S_BREAK) }
|
|
if (t.text == "continue") { pi += 1; return node(S_CONTINUE) }
|
|
if (t.text == "cancel") { pi += 1; return node(S_CANCEL) } # veto a cancellable event
|
|
if (t.text == "match") {
|
|
pi += 1; let n = node(S_MATCH); n.a = expr(); skipnl(); eat_op("{")
|
|
while true {
|
|
skipnl(); if is_op("}") { break }
|
|
let arm = node(S_MARM)
|
|
while true { push(arm.kids, expr()); if is_op(",") { pi += 1; skipnl(); continue }; break }
|
|
eat_op("=>"); skipnl()
|
|
if is_op("{") { arm.a = block() } else { let b = node(N_BLOCK); push(b.kids, stmt()); arm.a = b }
|
|
push(n.kids, arm)
|
|
}
|
|
eat_op("}"); return n
|
|
}
|
|
}
|
|
let e = expr()
|
|
if is_op("=") or is_op("+=") or is_op("-=") or is_op("*=") or is_op("/=") {
|
|
let n = node(S_ASSIGN); n.line = toks[pi].line; n.s = toks[pi].text; pi += 1; n.a = e; n.b = expr(); return n
|
|
}
|
|
let n = node(S_EXPR); n.a = e; return n
|
|
}
|
|
|
|
# ---- declarations ----------------------------------------------------------
|
|
function parse_var() -> Node {
|
|
pi += 1; let n = node(N_VAR); n.s = eat_id(); eat_op(":"); n.ty = ptype()
|
|
if is_op("=") { pi += 1; n.a = expr() }
|
|
n.pos2 = toks[pi - 1].end # 0.S: where the declaration ends
|
|
return n
|
|
}
|
|
function parse_const() -> Node {
|
|
pi += 1; let n = node(N_CONST); n.s = eat_id(); eat_op(":"); n.ty = ptype(); eat_op("=")
|
|
let v0 = pi
|
|
n.a = expr()
|
|
n.sp0 = toks[v0].off; n.sp1 = toks[pi - 1].oend # the schema's text of the value
|
|
return n
|
|
}
|
|
# the language's statement / declaration words cannot name a function (`new`,
|
|
# `match`, `spawn` ...). Contextual words the parser only recognises in position
|
|
# (`emit`, `on`, `phase`, `start`, `export`, ...) stay usable — the compiler itself
|
|
# has a function called `emit`.
|
|
function is_reserved_word(w: pointer) -> bool {
|
|
if (w == "new") or (w == "match") or (w == "machine") or (w == "become") or (w == "spawn") or (w == "despawn") { return true }
|
|
if (w == "query") or (w == "cancel") or (w == "scene") or (w == "layer") or (w == "handler") or (w == "property") { return true }
|
|
if (w == "model") or (w == "enum") or (w == "event") or (w == "namespace") or (w == "function") or (w == "let") { return true }
|
|
if (w == "prefab") { return true }
|
|
if (w == "var") or (w == "const") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "return") { return true }
|
|
if (w == "import") or (w == "program") or (w == "entry") or (w == "ui") or (w == "break") or (w == "continue") { return true }
|
|
if (w == "and") or (w == "or") or (w == "not") or (w == "true") or (w == "false") or (w == "null") { return true }
|
|
return false
|
|
}
|
|
|
|
function parse_fn() -> Node {
|
|
pi += 1; let n = node(N_FN); n.s = eat_id()
|
|
if is_op("<") { n.tps = gen_params() } # L5: function first<T>(xs: []T) -> T
|
|
gen_enter(n.tps)
|
|
n.pos2 = toks[pi].pos # 0.S: the parameter list's '('
|
|
eat_op("(")
|
|
if is_reserved_word(n.s) { perr(`'{n.s}' is a reserved word and cannot name a function`) }
|
|
while not is_op(")") {
|
|
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); parse_param_mut(p); p.ty = ptype()
|
|
if is_op("=") { pi += 1; p.a = expr() } # L11: a default, for a call that leaves it out
|
|
push(n.kids, p)
|
|
if is_op(",") { pi += 1 }
|
|
}
|
|
eat_op(")")
|
|
n.ty = "void"
|
|
if is_op("->") { pi += 1; n.ty = ptype() }
|
|
n.a = block()
|
|
gen_enter(null)
|
|
return n
|
|
}
|
|
function parse_main() -> Node {
|
|
pi += 1
|
|
let n = node(N_MAIN)
|
|
let ps = parse_entry_params() # 0.S: entry (h: mut Hiker) - state.ludic
|
|
n.a = block()
|
|
entry_bind(n.a, ps)
|
|
return n
|
|
}
|
|
|
|
# issue #76 — rewrite calls to a namespace-block sibling (a bare short-name call)
|
|
# to the prefixed function name, so a body can call `pending()` where the function
|
|
# is emitted as `combat_pending`. Only E_CALL callees that are bare identifiers are
|
|
# rewritten (Ludic has no function values, and Name.method calls are E_MEMBER).
|
|
function ns_short_in(names: []pointer, s: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(names) { if (names[i] == s) { return true }; i += 1 }
|
|
return false
|
|
}
|
|
function ns_rewrite_calls(n: Node, shorts: []pointer, prefix: pointer) -> void {
|
|
if (n == null) { return }
|
|
if (n.kind == E_CALL) and (n.a != null) and (n.a.kind == E_ID) {
|
|
if ns_short_in(shorts, n.a.s) { n.a.s = prefix + n.a.s }
|
|
}
|
|
ns_rewrite_calls(n.a, shorts, prefix)
|
|
ns_rewrite_calls(n.b, shorts, prefix)
|
|
ns_rewrite_calls(n.c, shorts, prefix)
|
|
var i = 0
|
|
while i < len(n.kids) { ns_rewrite_calls(n.kids[i], shorts, prefix); i += 1 }
|
|
}
|
|
|
|
# issue #76 — `namespace Name { [export|internal] function short(…) … }`. Declares
|
|
# the namespace once; each function is emitted as `<namelower>_short` and, when
|
|
# exported (the default; `internal` opts out), registered so `Name.short(…)`
|
|
# dispatches to it — the same generic path @Namespace(Name) uses, so it is the
|
|
# block sugar for it. Sibling calls inside the block are rewritten to the prefixed
|
|
# name. `internal` keeps a function as a private helper: it is emitted but not part
|
|
# of the Name.* surface.
|
|
function parse_namespace() -> void {
|
|
pi += 1 # eat "namespace"
|
|
let nsname = eat_id()
|
|
eat_op("{"); skipnl()
|
|
let fns = new []Node
|
|
let shorts = new []pointer
|
|
let exps = new []int
|
|
while not is_op("}") {
|
|
var is_exp = 1 # default: exported (public)
|
|
if is_id("export") { pi += 1 }
|
|
else { if is_id("internal") { pi += 1; is_exp = 0 } }
|
|
if is_id("alias") {
|
|
ns_parse_alias(nsname, is_exp)
|
|
skipnl()
|
|
continue
|
|
}
|
|
if not is_id("function") { perr("a namespace body holds functions or aliases: expected `function` or `alias`") }
|
|
let f = parse_fn()
|
|
push(fns, f); push(shorts, f.s); push(exps, is_exp)
|
|
skipnl()
|
|
}
|
|
eat_op("}")
|
|
let prefix = ns_lower(nsname) + ("_")
|
|
var i = 0 # prefix every function name
|
|
while i < len(fns) { fns[i].s = prefix + shorts[i]; i += 1 }
|
|
i = 0 # rewrite sibling calls to the prefixed name
|
|
while i < len(fns) { ns_rewrite_calls(fns[i].a, shorts, prefix); i += 1 }
|
|
register_namespace(nsname) # Name.method dispatch (generic path)
|
|
push(g_ns_blocks, nsname)
|
|
i = 0
|
|
while i < len(fns) {
|
|
if (exps[i] == 1) { push(g_ns_exports, fns[i].s) } # record the public surface
|
|
push(prog, fns[i])
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
# Is the compiler itself running on Windows? A property of the HOST, asked at run
|
|
# time - every Windows process inherits OS=Windows_NT - so one compiler source
|
|
# answers correctly whichever platform its binary was built for.
|
|
function host_is_windows() -> bool {
|
|
let os = getenv("OS")
|
|
if (os == null) { return false }
|
|
return os == "Windows_NT"
|
|
}
|
|
|
|
# a path separator: '/' everywhere, '\' too on Windows (argv[0] and %PATH% use it)
|
|
function is_sep(c: int) -> bool { return (c == '/') or (c == 92) }
|
|
|
|
# a copy of `s` with every '\' as '/', which Windows accepts wherever it takes a path
|
|
function fwd_slashes(s: pointer) -> pointer {
|
|
let n = len(s)
|
|
let out = bytes(n + 1)
|
|
var i = 0
|
|
while i < n { if s[i] == 92 { out[i] = '/' } else { out[i] = s[i] }; i += 1 }
|
|
out[n] = 0
|
|
return out
|
|
}
|
|
|
|
# directory part of a path, including the trailing separator, or "" if none
|
|
function dir_of(path: pointer) -> pointer {
|
|
var last = -1
|
|
var i = 0
|
|
while path[i] != 0 { if is_sep(path[i]) { last = i }; i += 1 }
|
|
if last < 0 { return "" }
|
|
return path[0..last + 1]
|
|
}
|
|
function join_path(dir: pointer, rel: pointer) -> pointer {
|
|
if is_sep(rel[0]) { return rel } # absolute
|
|
if rel[0] != 0 { if rel[1] == ':' { return rel } } # absolute: a Windows drive
|
|
return dir + rel
|
|
}
|
|
|
|
# The toolchain install root, with a trailing '/' ("" meaning the current
|
|
# directory). Everything the toolchain owns rather than the project — the engine
|
|
# runtime under runtime/native, the bundled ludic.* packages, the VERSION file,
|
|
# cocoa.ll — is resolved against it.
|
|
#
|
|
# $LUDIC_HOME wins when it is set. Otherwise it is derived from the compiler
|
|
# binary, whose directory is `bin` in both shapes that exist: an install
|
|
# (~/.ludic/bin/ludicc, root ~/.ludic) and a repo checkout (bin/ludicc, root the
|
|
# repo). So a directory named bin means the root is its parent; anything else is
|
|
# taken as the root itself, which is what a loose binary beside its runtime
|
|
# wants. Invoked through $PATH argv[0] carries no directory at all, so the PATH
|
|
# entries are searched for the binary first — an installed `ludicc mygame.ludic`
|
|
# has to find its own runtime with nothing in the environment.
|
|
#
|
|
# On Windows argv[0] and $PATH carry backslashes, $PATH is split on ';', and a
|
|
# binary found through it is `ludicc.exe` while argv[0] may say `ludicc`, so the
|
|
# directory is read with forward slashes and the lookup tries both names.
|
|
function ludic_home() -> pointer {
|
|
let env = getenv("LUDIC_HOME")
|
|
if (env != null) { return ensure_slash(fwd_slashes(env)) }
|
|
var d = fwd_slashes(dir_of(arg(0)))
|
|
if (len(d) == 0) { d = path_lookup_dir(base_name(arg(0))) }
|
|
if (len(d) == 0) { return "" }
|
|
if (len(d) >= 4) and (d[len(d) - 4..len(d)] == "bin/") { return d[0..len(d) - 4] }
|
|
return d
|
|
}
|
|
|
|
# the first $PATH entry holding an openable file called `name` (or `name`.exe on
|
|
# Windows), with a trailing '/' — "" when $PATH is unset or nothing matches.
|
|
function path_lookup_dir(name: pointer) -> pointer {
|
|
let p = getenv("PATH")
|
|
if (p == null) { return "" }
|
|
let win = host_is_windows()
|
|
var sep = ':'
|
|
if win { sep = ';' }
|
|
let n = len(p)
|
|
var start = 0
|
|
var i = 0
|
|
while i <= n {
|
|
if (i == n) or (p[i] == sep) {
|
|
if i > start {
|
|
let dir = ensure_slash(fwd_slashes(p[start..i]))
|
|
let f = file_open(dir + name, "rb")
|
|
if (f != null) { file_close(f); return dir }
|
|
if win {
|
|
let fx = file_open(dir + name + ".exe", "rb")
|
|
if (fx != null) { file_close(fx); return dir }
|
|
}
|
|
}
|
|
start = i + 1
|
|
}
|
|
i += 1
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var loaded_paths: []pointer
|
|
var cur_dir: pointer
|
|
var g_uses_regex: bool = false # a program mentioned Regex.* -> splice the regex runtime
|
|
var g_uses_bignum: bool = false # a program mentioned BigInt.*/Decimal.* -> splice the bignum runtime
|
|
var g_uses_dict: bool = false # a program mentioned Dict.*/Set.* -> splice the hash-table runtime
|
|
var g_uses_numeric: bool = false # a program mentioned Huge.*/Angle.*/Percent.* -> splice the numeric runtime
|
|
var g_uses_jobs: bool = false # a program mentioned Job.*/Promise.*/Sync.* -> splice the concurrency runtime
|
|
var g_uses_query: bool = false # a program mentioned Query.* -> splice the query runtime + reflection ABI
|
|
var g_uses_reflect: bool = false # a program mentioned Reflect.* -> force-emit the reflection ABI
|
|
var g_uses_light: bool = false # a program mentioned Light.* -> splice the 2D light pass
|
|
var g_uses_value: bool = false # Value.*/Json.*/Reflect.serialize -> splice the value tree + JSON (#44)
|
|
var g_uses_rng: bool = false # Random.* -> splice the seeded random numbers (runtime/native/rng.ludic)
|
|
var g_rt_wants: []pointer = new []pointer # bare names a spliced runtime answers (rng_*, seed, value_*, json_*)
|
|
var g_uses_xml: bool = false # Xml.* -> splice the XML reader (Tiled #67)
|
|
var g_uses_base64: bool = false # Base64.* -> splice the base64 codec + inflate (Tiled #67)
|
|
var g_uses_tiled: bool = false # Tiled.* -> splice the Tiled map runtime (#69)
|
|
var g_uses_atlas: bool = false # Sprite.sheet/cell/… or Assets.* -> splice the spritesheet/atlas runtime (#81)
|
|
var g_uses_reflect_io: bool = false # Reflect.serialize/apply -> splice the reflection serializer
|
|
var g_uses_esys: bool = false # an engine-owned system component (SpriteAnim/Motion/Light2D) is declared -> splice systems.ludic + force the reflection ABI
|
|
var g_uses_input: bool = false # a program used Input.bind/down/poll/… (action maps + record/replay) -> splice input.ludic
|
|
var g_has_clear_color: bool = false # @ClearColor(colour) declared -> the Render phase auto-clears + auto-presents (#86)
|
|
var g_clear_color: Node = null # the declared clear colour: a literal, a const name or a Color.Name
|
|
var g_warned_draw_sprite: bool = false # emit the bare-draw_sprite deprecation note once (#85)
|
|
var g_uses_world_despawn: bool = false # World.despawn / esys_bounds kill -> emit @fn_world_despawn (#84)
|
|
|
|
# #85 — steer a direct bare draw_sprite / draw_sprite_scaled call to the namespaced
|
|
# Screen.sprite (or the engine sprite-render system). Warns once, non-fatally — the
|
|
# bare form still compiles (Screen.sprite lowers to it), it is just deprecated.
|
|
function warn_draw_sprite(name: pointer) -> void {
|
|
if g_warned_draw_sprite { return }
|
|
g_warned_draw_sprite = true
|
|
let m = `ludicc: warning: bare {name}(...) is deprecated (#85) — use Screen.sprite / Screen.sprite_scaled, or declare a Sprite component for the engine sprite-render system\n`
|
|
file_write(file_stderr(), m, len(m))
|
|
}
|
|
var g_uses_anim_rt: bool = false # Anim.play/clip/on_frame/fired or Motion.to (#48) -> splice systems.ludic + force the reflection ABI
|
|
var g_uses_tween_rt: bool = false # Tween.to/chain/delay/… (#48) -> splice tween.ludic + run esys_tween each Update
|
|
var g_uses_fx: bool = false # Fx.sparks/number/clear -> splice fx.ludic; fx_tick each Update, fx_draw each Render
|
|
var g_uses_audio: bool = false # Audio.* (#22) -> splice audio.ludic; a windowed build also links audio.ll + AVFoundation
|
|
var g_uses_http: bool = false # Http.* (#6) -> splice http.ludic; links http.ll + Foundation (macOS)
|
|
var g_uses_udp: bool = false # Udp.* -> splice udp.ludic; links udp.ll (macOS) / udp_win.ll + ws2_32
|
|
var g_uses_process: bool = false # Process.* -> splice process.ludic; links process.ll (macOS) / process_win.ll
|
|
var g_uses_gl: bool = false # Gl.* -> splice gl.ludic (+ generated gl_api.ludic); links gl.ll + gl_thunks.ll + OpenGL
|
|
var g_uses_vk: bool = false # Vk.* -> splice vk.ludic (+ generated vk_api.ludic); links vk_thunks.ll + the platform loader
|
|
# issue #64: functions marked @System(Phase) in a prebuilt binary module — the
|
|
# compiler registers each with the host at load (it supplies the fn address,
|
|
# which Ludic source cannot take). Parallel arrays: fn name -> phase name.
|
|
var g_mod_sys_fn: []pointer
|
|
var g_mod_sys_phase: []pointer
|
|
# issue #62: package-declarable registries (data-driven, additive to the core
|
|
# hardcoded paths). Engine systems: (component, esys-fn, phase) — @EngineSystem.
|
|
# Namespaces: names a package provides Foo.* dispatch for — @Namespace.
|
|
var g_esys_comp: []pointer
|
|
var g_esys_fn: []pointer
|
|
var g_esys_phase: []pointer
|
|
var g_namespaces: []pointer
|
|
|
|
# issue #76: `namespace Name { export/internal function … }` block form. A block
|
|
# declares the namespace once and controls its public surface declaratively.
|
|
# g_ns_blocks names the namespaces declared this way; g_ns_exports holds the
|
|
# prefixed function names that are *exported* (e.g. "combat_amount"), so emit_ns_call
|
|
# can reject Name.method for an `internal` helper. (A namespace declared the old
|
|
# per-function @Namespace(Name) way has no block entry, so all its methods stay
|
|
# dispatchable — back-compatible.)
|
|
var g_ns_blocks: []pointer
|
|
var g_ns_exports: []pointer
|
|
function is_ns_block(name: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(g_ns_blocks) { if (g_ns_blocks[i] == name) { return true }; i += 1 }
|
|
return false
|
|
}
|
|
function ns_export_has(prefixed: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(g_ns_exports) { if (g_ns_exports[i] == prefixed) { return true }; i += 1 }
|
|
return false
|
|
}
|
|
|
|
# lever 5 of the controller extensibility contract (#57): `disable system <fn>`
|
|
# switches off exactly one engine-owned system (an esys_* registered via
|
|
# @EngineSystem or the core seed) at compile time, so a game can carry a
|
|
# well-known component but tick it with its own handler instead. Recorded at
|
|
# parse time and read by emit_one_engine_system, which then skips that call
|
|
# (byte-identical for any program that disables nothing).
|
|
var g_disabled_sys: []pointer
|
|
function is_system_disabled(fn: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(g_disabled_sys) { if (g_disabled_sys[i] == fn) { return true }; i += 1 }
|
|
return false
|
|
}
|
|
|
|
# #62: package namespace registry — a package marks a Foo.* provider with
|
|
# @Namespace(Foo); emit_ns_call aliases an otherwise-unknown Foo.method to the
|
|
# bare function foo_method (lowercased namespace + "_" + method).
|
|
function register_namespace(name: pointer) -> void {
|
|
var i = 0
|
|
while i < len(g_namespaces) { if (g_namespaces[i] == name) { return }; i += 1 }
|
|
push(g_namespaces, name)
|
|
}
|
|
function is_registered_namespace(name: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(g_namespaces) { if (g_namespaces[i] == name) { return true }; i += 1 }
|
|
return false
|
|
}
|
|
|
|
function already_loaded(full: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(loaded_paths) { if (loaded_paths[i] == full) { return true }; i += 1 }
|
|
return false
|
|
}
|
|
|
|
# a handler's hook, for the schema's code map: the attribute that made it one, and what it names
|
|
function decl_hook(ev: pointer, sp: pointer, dsp: pointer, att: pointer, det: pointer, en: pointer, dis: pointer, ph: pointer) -> pointer {
|
|
if ev != null { return "On" }
|
|
if sp != null { return "OnSpawn" }
|
|
if dsp != null { return "OnDespawn" }
|
|
if att != null { return "OnAttach" }
|
|
if det != null { return "OnDetach" }
|
|
if en != null { return "OnEnable" }
|
|
if dis != null { return "OnDisable" }
|
|
if ph != null {
|
|
if (ph == "Start") { return "OnStart" }
|
|
return "OnQuit"
|
|
}
|
|
return null
|
|
}
|
|
function decl_hook_target(ev: pointer, sp: pointer, dsp: pointer, att: pointer, det: pointer, en: pointer, dis: pointer) -> pointer {
|
|
if ev != null { return ev }
|
|
if sp != null { return sp }
|
|
if dsp != null { return dsp }
|
|
if att != null { return att }
|
|
if det != null { return det }
|
|
if en != null { return en }
|
|
return dis
|
|
}
|
|
# parse one top-level declaration (or resolve an import) into `prog`.
|
|
# Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`,
|
|
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
|
|
var g_decl_exported: bool = false # `export` read in front of the declaration being parsed
|
|
function parse_one_decl() -> void {
|
|
let p0 = len(prog)
|
|
let e0 = len(g_events)
|
|
let r0 = len(g_rg_var)
|
|
let outer = g_decl_exported
|
|
g_decl_exported = false
|
|
parse_one_decl_in()
|
|
if g_decl_exported { decl_mark_exported(p0, e0, r0) }
|
|
g_decl_exported = outer
|
|
}
|
|
# `export function f`, `export var v`, ...: what the declaration added is visible from other modules (L3)
|
|
function decl_mark_exported(p0: int, e0: int, r0: int) -> void {
|
|
var k = p0
|
|
while k < len(prog) {
|
|
prog[k].vis = 1
|
|
k += 1
|
|
}
|
|
k = r0
|
|
while k < len(g_rg_var) { # a @PerMap registry's own node is not in prog
|
|
g_rg_var[k].vis = 1
|
|
k += 1
|
|
}
|
|
k = e0
|
|
while k < len(g_events) {
|
|
g_events[k].vis = 1
|
|
k += 1
|
|
}
|
|
}
|
|
function parse_one_decl_in() -> void {
|
|
at_decl_reset() # attrs.ludic: @AppendOnly / @ByKey belong to one declaration
|
|
var is_export = false
|
|
var qspec: Node = null
|
|
var onspawn_model: pointer = null
|
|
var ondespawn_model: pointer = null
|
|
var ondespawn_reason: pointer = null # @OnDespawn(M, reason: r) — LC1 teardown reason binding
|
|
var onattach_prop: pointer = null
|
|
var ondetach_prop: pointer = null
|
|
var onenable_prop: pointer = null
|
|
var ondisable_prop: pointer = null
|
|
var on_event: pointer = null # @On(Event) — a compile-time event listener
|
|
var is_public = false # @Public — promote a lifecycle hook to an event
|
|
var hook_phase: pointer = null # @OnStart / @OnQuit override the phase
|
|
var is_sync_prop = false # @Sync property P — every field replicates (NETWORKING N2)
|
|
var is_owned = false # @Owned model M — entities carry a network owner (N3)
|
|
var role: pointer = null # @Server / @Predicted — a handler's network role (N5)
|
|
var remote_dir: pointer = null # @ToServer / @ToClients — a remote event's direction (N4)
|
|
var sys_phase: pointer = null # @System(Phase) — a binary module's runtime-registered system (#64)
|
|
var esys_comp: pointer = null # @EngineSystem(Comp, Phase) — a package compile-time engine system (#62)
|
|
var esys_phase: pointer = null
|
|
var ns_name: pointer = null # @Namespace(Name) — a package-provided Foo.* namespace (#62)
|
|
var is_det = false # @deterministic — no floating point inside (emit_float.ludic)
|
|
var is_rowverb = false # @RowVerb — a verb of a table's row (27.3, actions_rows.ludic)
|
|
# attributes and `export` in any order: `@ToClients export event E` keeps its @ToClients
|
|
while is_op("@") or (is_id("export") and (toks[pi + 1].kind == TK_ID or toks[pi + 1].text == "@")) {
|
|
if is_id("export") {
|
|
pi += 1
|
|
g_decl_exported = true
|
|
skipnl()
|
|
continue
|
|
}
|
|
pi += 1; let a = eat_id() # collect a leading @annotation
|
|
if a == "export" { is_export = true }
|
|
else if a == "Public" { is_public = true } # @Public hook promotion
|
|
else if a == "On" { eat_op("("); on_event = eat_id(); eat_op(")") } # @On(Event) listener
|
|
else if a == "Queries" { qspec = parse_queries_anno() } # @Queries(these: [...], on: ...)
|
|
else if a == "OnSpawn" { eat_op("("); onspawn_model = eat_id(); eat_op(")") }
|
|
else if a == "OnDespawn" {
|
|
eat_op("("); ondespawn_model = eat_id()
|
|
if is_op(",") { pi += 1; eat_id(); eat_op(":"); ondespawn_reason = eat_id() } # , reason: r
|
|
eat_op(")")
|
|
}
|
|
else if a == "OnAttach" { eat_op("("); onattach_prop = eat_id(); eat_op(")") }
|
|
else if a == "OnDetach" { eat_op("("); ondetach_prop = eat_id(); eat_op(")") }
|
|
else if a == "OnEnable" { eat_op("("); onenable_prop = eat_id(); eat_op(")") }
|
|
else if a == "OnDisable" { eat_op("("); ondisable_prop = eat_id(); eat_op(")") }
|
|
else if a == "OnStart" { hook_phase = "Start" } # boot
|
|
else if a == "OnQuit" { hook_phase = "OnQuit" } # shutdown
|
|
else if a == "Sync" { is_sync_prop = true } # @Sync property (N2)
|
|
else if a == "Owned" { is_owned = true } # @Owned model (N3)
|
|
else if a == "Server" { role = "server" } # @Server handler (N5)
|
|
else if a == "Predicted" { role = "predicted" } # @Predicted handler (N5)
|
|
else if a == "ToServer" { remote_dir = "toserver" } # @ToServer event (N4)
|
|
else if a == "ToClients" { remote_dir = "toclients" } # @ToClients event (N4)
|
|
else if a == "System" { sys_phase = "Update"; if is_op("(") { pi += 1; sys_phase = eat_id(); eat_op(")") } } # @System(Phase) binary-module system (#64)
|
|
else if a == "EngineSystem" { eat_op("("); esys_comp = eat_id(); eat_op(","); esys_phase = eat_id(); eat_op(")") } # @EngineSystem(Comp, Phase) package engine system (#62)
|
|
else if a == "Namespace" { eat_op("("); ns_name = eat_id(); eat_op(")") } # @Namespace(Name) package Foo.* namespace (#62)
|
|
else if a == "deterministic" { is_det = true }
|
|
else if a == "RowVerb" { is_rowverb = true }
|
|
else if a == "Machine" { mc_parse_attr() } # @Machine(Record.field) - 27.1, machines.ludic
|
|
else if (a == "AppendOnly") or (a == "ByKey") { at_decl(a) } # a registry's, for editors (attrs.ludic)
|
|
else if a == "PerMap" { # a map-scoped registry (permap.ludic)
|
|
at_decl(a)
|
|
g_pm_pending = true
|
|
}
|
|
else if a == "Chunked" { pm_parse_chunked() } # @Chunked(64): read a chunk at a time
|
|
else if a == "TextKey" { i18n_parse_textkey() } # @TextKey("steps"): the registry's text keys (i18n.ludic)
|
|
else if a == "alloc_ok" { # 25.2: @alloc_ok("why") - this allocates in a frame on purpose
|
|
eat_op("(")
|
|
let why = expr()
|
|
if why.kind != E_STR or len(why.s) == 0 { perr("@alloc_ok needs its reason: @alloc_ok(\"a memo miss, bounded by MM_CAP\")") }
|
|
eat_op(")")
|
|
g_aok_pending = true # a global: the declaration after the attributes reads it
|
|
}
|
|
else if a == "creates" or a == "releases" { # 25.5e: @creates(PhysShape) / @releases(PhysShape)
|
|
eat_op("(")
|
|
let kind = eat_id()
|
|
eat_op(")")
|
|
if a == "creates" { g_res_pending_make = kind } else { g_res_pending_free = kind }
|
|
}
|
|
else if a == "ClearColor" { # @ClearColor(colour) — Render auto-clear + auto-present (#86)
|
|
eat_op("(")
|
|
g_clear_color = expr() # a literal, a `const`, or a Color.Name — resolved when emitted
|
|
g_has_clear_color = true
|
|
eat_op(")")
|
|
}
|
|
else if is_op("(") { # any other @anno(args) — parsed and skipped
|
|
var d = 0
|
|
while true { if is_op("(") { d += 1 }; if is_op(")") { d -= 1 }; pi += 1; if d == 0 { break } }
|
|
}
|
|
skipnl()
|
|
}
|
|
mc_attr_placed() # @Machine is a registry's (machines.ludic)
|
|
if is_id("friend") and (toks[pi + 1].text == "module") {
|
|
mod_parse_friend() # frontend/modules.ludic
|
|
return
|
|
}
|
|
if is_id("module") and toks[pi + 1].kind == TK_ID {
|
|
mod_parse_line() # module NAME [uses A, B]
|
|
return
|
|
}
|
|
if is_id("numbers") and (toks[pi + 1].text == "float") {
|
|
pi += 2
|
|
if not is_float_file(g_parse_file) { float_file_add(g_parse_file) }
|
|
return
|
|
}
|
|
if is_id("import") { pi += 1
|
|
let t = toks[pi]
|
|
if t.kind != TK_STR { perr("expected \"path\" after import") }
|
|
let rel = t.text; pi += 1
|
|
do_import(rel)
|
|
return
|
|
}
|
|
if is_id("enum") { push(prog, parse_enum()); return }
|
|
if is_id("event") {
|
|
let ev = parse_event()
|
|
if (remote_dir != null) { ev.ty = remote_dir } # N4: a directional remote event (RPC)
|
|
register_event(ev); return
|
|
}
|
|
if is_id("property") {
|
|
let c = parse_component()
|
|
if is_sync_prop { var fi = 0; while fi < len(c.kids) { c.kids[fi].ival = 1; fi += 1 } } # N2: mark every field replicable
|
|
push(prog, c); return
|
|
}
|
|
if is_id("model") {
|
|
let m = parse_archetype()
|
|
if is_owned { m.ival = 1 } # N3: this model's entities carry a network owner
|
|
push(prog, m); return
|
|
}
|
|
if is_id("scene") { parse_scene(); return } # layers push handlers into prog; scene -> g_scenes
|
|
if is_id("prefab") { push(prog, parse_prefab()); return } # a model with preset component fields
|
|
if is_id("handler") {
|
|
let h = parse_system()
|
|
if is_det { push(g_det_names, h.s) }
|
|
if g_aok_pending { push(g_alloc_ok_names, h.s); g_aok_pending = false }
|
|
if (role != null) { if (role == "server") { h.ival = 1 } else { h.ival = 2 } } # N5: @Server=1 / @Predicted=2
|
|
cx_handler(h, h.s, decl_hook(on_event, onspawn_model, ondespawn_model, onattach_prop, ondetach_prop, onenable_prop, ondisable_prop, hook_phase),
|
|
decl_hook_target(on_event, onspawn_model, ondespawn_model, onattach_prop, ondetach_prop, onenable_prop, ondisable_prop), qspec, is_public, null, null) # the schema's code map
|
|
if (on_event != null) { register_onlisten(on_event, h.a); cx_listened(); return } # @On(Event) listener
|
|
if (onspawn_model != null) {
|
|
register_onspawn(onspawn_model, h.a) # spawn hook
|
|
if is_public { ensure_event(`model_{onspawn_model}_spawn`, false) } # @Public -> model_<M>_spawn
|
|
return
|
|
}
|
|
if (ondespawn_model != null) {
|
|
register_ondespawn(ondespawn_model, h.a, ondespawn_reason) # despawn hook
|
|
if is_public { ensure_event(`model_{ondespawn_model}_despawn`, true) } # @Public -> model_<M>_despawn (with reason)
|
|
return
|
|
}
|
|
if (onattach_prop != null) { register_onattach(onattach_prop, h.a); if is_public { ensure_event(`prop_{onattach_prop}_attach`, false) }; return } # -> prop_<P>_attach
|
|
if (ondetach_prop != null) { register_ondetach(ondetach_prop, h.a); if is_public { ensure_event(`prop_{ondetach_prop}_detach`, false) }; return } # -> prop_<P>_detach
|
|
if (onenable_prop != null) { register_onenable(onenable_prop, h.a); if is_public { ensure_event(`prop_{onenable_prop}_enable`, false) }; return } # -> prop_<P>_enable
|
|
if (ondisable_prop != null) { register_ondisable(ondisable_prop, h.a); if is_public { ensure_event(`prop_{ondisable_prop}_disable`, false) }; return } # -> prop_<P>_disable
|
|
if (hook_phase != null) { # @OnStart/@OnQuit
|
|
h.ty = hook_phase
|
|
if is_public { # -> program_start / program_quit
|
|
if (hook_phase == "Start") { ensure_event_empty("program_start") }
|
|
else { ensure_event_empty("program_quit") }
|
|
}
|
|
}
|
|
if (qspec != null) { # @Queries wraps the body in its S_QUERY
|
|
qspec.a = h.a
|
|
let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap
|
|
}
|
|
push(prog, h); return
|
|
}
|
|
if is_id("ui") { push(prog, parse_ui()); return }
|
|
if is_id("namespace") { parse_namespace(); return } # #76 namespace block
|
|
if is_id("port") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_port(); return } # L3 ports
|
|
if is_id("bind") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_bind(); return }
|
|
if is_id("open") and (toks[pi + 1].text == "registry") { # L8: open to other modules' defs
|
|
pi += 1
|
|
parse_registry()
|
|
g_rg_open[len(g_rg_open) - 1] = 1
|
|
return
|
|
}
|
|
if is_id("registry") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "of") { parse_registry(); return } # L8
|
|
if is_id("def") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].kind == TK_ID) { parse_def(); return } # L8
|
|
if is_id("view") and (toks[pi + 1].kind == TK_ID) and ((toks[pi + 2].text == "{") or (toks[pi + 2].text == "(")) { parse_view(); return } # L11
|
|
if is_id("component") and (toks[pi + 1].kind == TK_ID) and ((toks[pi + 2].text == "{") or (toks[pi + 2].text == "(")) { parse_ui_component(); return } # L11
|
|
if is_id("state") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_state(); return } # 0.S
|
|
if is_id("action") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_action(); return } # 0.R
|
|
if is_id("reducer") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "on") { parse_reducer(); return }
|
|
if is_id("reducer") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "in") { parse_row_reducer(); return } # 27.3
|
|
if is_id("var") {
|
|
if not g_allow_globals {
|
|
perr(`a module-level var is refused: a module's changing data is its state (state Name {{ ... }}), passed to the functions that use it - or, if it never changes, a let`)
|
|
}
|
|
push(prog, parse_var())
|
|
return
|
|
}
|
|
if is_id("let") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == ":") { # 0.S: a module-level let - immutable all the way down
|
|
let v = parse_var()
|
|
v.uns = 1
|
|
if v.a == null { perr(`a module-level let {v.s} needs its value`) }
|
|
push(prog, v)
|
|
return
|
|
}
|
|
if is_id("const") { push(prog, parse_const()); return }
|
|
var is_unsafe = false
|
|
if is_id("unsafe") and (toks[pi + 1].text == "function") { # L7: an unsafe function
|
|
pi += 1
|
|
is_unsafe = true
|
|
}
|
|
if is_id("function") {
|
|
let f = parse_fn()
|
|
if is_unsafe { f.uns = 1 }
|
|
if is_det { push(g_det_names, f.s) }
|
|
if is_rowverb { push(g_rv_nodes, f) }
|
|
if g_aok_pending { push(g_alloc_ok_names, f.s); g_aok_pending = false }
|
|
if g_res_pending_make != null { push(g_res_make_fn, f.s); push(g_res_make_kind, g_res_pending_make); g_res_pending_make = null }
|
|
if g_res_pending_free != null { push(g_res_free_fn, f.s); push(g_res_free_kind, g_res_pending_free); g_res_pending_free = null }
|
|
if is_export { f.ival = 1 }
|
|
if (sys_phase != null) { push(g_mod_sys_fn, f.s); push(g_mod_sys_phase, sys_phase) } # #64: register at load
|
|
if (esys_comp != null) { push(g_esys_comp, esys_comp); push(g_esys_fn, f.s); push(g_esys_phase, esys_phase) } # #62: package engine system
|
|
if (ns_name != null) { register_namespace(ns_name) } # #62: package Foo.* namespace
|
|
push(prog, f); return
|
|
}
|
|
if is_id("extern") { push(prog, parse_extern()); return }
|
|
if is_id("entry") { push(prog, parse_main()); return }
|
|
if is_id("test") { push(g_tests, parse_test()); return }
|
|
perr(`expected a declaration (property, model, handler, function, …), got {tok_desc()}`)
|
|
}
|
|
|
|
# test "name" { ... } — a named test block, collected into g_tests. The block
|
|
# runs under a synthetic runner main (see emit_test_runner); `expect` assertions
|
|
# inside it record failures. Kept out of `prog` so it never emits as a plain fn.
|
|
function parse_test() -> Node {
|
|
let ln = toks[pi].line
|
|
pi += 1 # past `test`
|
|
if toks[pi].kind != TK_STR { perr("expected a \"name\" string after test") }
|
|
let n = node(N_TEST)
|
|
n.s = toks[pi].text
|
|
n.line = ln
|
|
n.pos2 = toks[pi].end # 0.S: where state parameters would go
|
|
pi += 1 # past the name
|
|
let ps = parse_entry_params() # 0.S: test "name" (h: mut Hiker)
|
|
n.a = block()
|
|
entry_bind(n.a, ps)
|
|
return n
|
|
}
|
|
|
|
# lex and parse an imported fragment into `prog`, saving/restoring lexer state.
|
|
#
|
|
# An import is resolved first relative to the importing file (the historical
|
|
# behaviour). When that fails and the spec is not absolute, it is looked up
|
|
# under the package module root — $LUDIC_MODULES, default "ludic_modules" — so a
|
|
# fetched source package resolves by its import path, e.g.
|
|
# import "git.workshopsoft.io/user/pkg/foo.ludic"
|
|
# materialised by `ludic get` at ludic_modules/git.workshopsoft.io/user/pkg/foo.ludic.
|
|
# The fallback only fires when the local path does not exist, so every existing
|
|
# import resolves exactly as before (the emitted IR is byte-identical).
|
|
# true when `rel` is an engine-runtime splice path ("runtime/..."), as opposed to
|
|
# a user or package import. Those ship with the toolchain, not the project (#75).
|
|
function is_runtime_path(rel: pointer) -> bool { return str_starts(rel, "runtime/") }
|
|
|
|
# byte-wise `a < b`, for a stable import order of a directory's files
|
|
function str_less(a: pointer, b: pointer) -> bool {
|
|
var i = 0
|
|
while true {
|
|
let ca = a[i]; let cb = b[i]
|
|
if ca != cb { return ca < cb }
|
|
if ca == 0 { return false }
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
# `import "dir/*.ludic"` — every .ludic file of the directory, in name order, so
|
|
# a game lists its modules once instead of one import per file.
|
|
function do_import_dir(rel: pointer) -> void {
|
|
let dir = rel[0..len(rel) - 8] # strip "/*.ludic"
|
|
let names = Fs.list(join_path(cur_dir, dir))
|
|
if (names == null) { perr(`import: no directory {dir}`) }
|
|
var i = 0
|
|
while i < len(names) { # selection sort by name
|
|
var best = i
|
|
var j = i + 1
|
|
while j < len(names) { if str_less(names[j], names[best]) { best = j }; j += 1 }
|
|
let t = names[i]; names[i] = names[best]; names[best] = t
|
|
i += 1
|
|
}
|
|
i = 0
|
|
while i < len(names) {
|
|
let nm = names[i]
|
|
let n = len(nm)
|
|
if (n > 6) and (nm[n - 6..n] == ".ludic") { do_import(dir + ("/") + nm) }
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
# `import "camp"` names a directory: its index.ludic is the barrel that lists
|
|
# what the directory exports (its own imports, relative to itself).
|
|
function barrel_of(rel: pointer) -> pointer {
|
|
let n = len(rel)
|
|
if (n > 6) and (rel[n - 6..n] == ".ludic") { return "" }
|
|
if rel[n - 1] == '/' { return rel + "index.ludic" }
|
|
return rel + "/index.ludic"
|
|
}
|
|
|
|
function do_import(rel: pointer) -> void {
|
|
if (len(rel) > 8) and (rel[len(rel) - 8..len(rel)] == "/*.ludic") { do_import_dir(rel); return }
|
|
let barrel = barrel_of(rel)
|
|
if barrel != "" {
|
|
if (read_file(join_path(cur_dir, barrel)) == null) and Fs.is_dir(join_path(cur_dir, rel)) {
|
|
perr(`import: {rel} is a directory with no index.ludic (add {barrel} listing its imports)`)
|
|
}
|
|
do_import(barrel)
|
|
return
|
|
}
|
|
var full = join_path(cur_dir, rel)
|
|
var src = read_file(full)
|
|
# #75 — the engine runtime (runtime/native/*) ships with the toolchain, not the
|
|
# project. When an auto-spliced runtime import is not found relative to the build
|
|
# (an external game whose CWD is not the toolchain repo), resolve it from the
|
|
# install root $LUDIC_HOME (default: the compiler binary's directory) — the same
|
|
# place main.ludic finds cocoa.ll / audio.ll. This is tried before the package
|
|
# module root, so $LUDIC_MODULES / ludic_modules holds only third-party packages.
|
|
if (src == null) and (rel[0] != '/') and is_runtime_path(rel) {
|
|
let hc = ludic_home() + rel
|
|
let hs = read_file(hc)
|
|
if (hs != null) { full = hc; src = hs }
|
|
}
|
|
if (src == null) and (rel[0] != '/') {
|
|
let cand = ensure_slash(getenv_or("LUDIC_MODULES", "ludic_modules")) + rel
|
|
let s2 = read_file(cand)
|
|
if (s2 != null) { full = cand; src = s2 }
|
|
}
|
|
# The ludic.* packages ship with the toolchain too, so an installed compiler
|
|
# resolves `import "ludic.core/components.ludic"` from $LUDIC_HOME/packages
|
|
# with no ludic_modules/ to set up. It is the last candidate on purpose: a
|
|
# project that fetched its own copy of a package (ludic_modules, above) keeps
|
|
# the version it pinned.
|
|
if (src == null) and (rel[0] != '/') {
|
|
let pc = ludic_home() + "packages/" + rel
|
|
let ps = read_file(pc)
|
|
if (ps != null) { full = pc; src = ps }
|
|
}
|
|
if already_loaded(full) { return }
|
|
push(loaded_paths, full)
|
|
fm_put(g_fm_src, full, src)
|
|
if is_float_file(g_parse_file) and not is_runtime_path(rel) and not is_float_file(full) { float_file_add(full) }
|
|
# a module reaches as far as its own files: a package found through $LUDIC_HOME or
|
|
# ludic_modules is not beside its importer and keeps its own module (or none)
|
|
let beside = full == join_path(cur_dir, rel)
|
|
if is_runtime_path(rel) or not beside or (beside and unsafe_trusted(g_parse_file)) { trust_file(full) }
|
|
if beside and not is_runtime_path(rel) and not (module_of(g_parse_file) == "") { module_set(full, module_of(g_parse_file)) }
|
|
if not beside and not is_runtime_path(rel) { pkg_file_set(full, pkg_name_of(rel)) } # modules.ludic
|
|
if not beside and not is_runtime_path(rel) and (src != null) { native_note_pkg(full, rel) } # natives.ludic
|
|
if beside and not is_runtime_path(rel) and not (pkg_of_file(g_parse_file) == "") { pkg_file_set(full, pkg_of_file(g_parse_file)) }
|
|
if (src == null) { perr(`cannot open import {full}`) }
|
|
# the audio runtime can arrive through atlas.ludic's own import or the Assets
|
|
# splice, not only through an Audio.* call in the game; a windowed build must
|
|
# then link audio.ll + AVFoundation for its snd_* calls, so flag it here.
|
|
let atail = "runtime/native/audio.ludic"
|
|
let fl = len(full)
|
|
if (fl >= len(atail)) and (full[fl - len(atail)..fl] == atail) { g_uses_audio = true }
|
|
let saved_toks = toks; let saved_pi = pi; let saved_dir = cur_dir
|
|
let saved_file = g_parse_file; let saved_parsing = g_parsing
|
|
cur_dir = dir_of(full)
|
|
g_parse_file = full; g_parsing = true
|
|
lex(src) # resets the global token stream
|
|
pi = 0
|
|
skipnl()
|
|
while toks[pi].kind != TK_EOF { parse_one_decl(); skipnl() }
|
|
toks = saved_toks; pi = saved_pi; cur_dir = saved_dir
|
|
g_parse_file = saved_file; g_parsing = saved_parsing
|
|
}
|
|
|
|
# a game (has handlers/components) links the Ludic runtime; auto-splice it.
|
|
# Tools (an `entry` block, no ECS) get nothing.
|
|
# a bare name that only a spliced runtime answers: the runtime comes in when a program calls it
|
|
# directly (a package calling value_get, a helper calling rng_range), not only through Value.*
|
|
function rt_wanted_name(n: pointer) -> bool {
|
|
if (n == "seed") { return true }
|
|
return str_starts(n, "rng_") or str_starts(n, "value_") or str_starts(n, "json_")
|
|
}
|
|
# a wanted name with this prefix that nothing in the program defines
|
|
function rt_wants_unmet(prefix: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(g_rt_wants) {
|
|
let n = g_rt_wants[i]
|
|
if str_starts(n, prefix) and find_fn(n) == null and find_global(n) == null { return true }
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
function maybe_splice_runtime() -> void {
|
|
let saved = cur_dir
|
|
# L6: the engine's namespaces that are aliases of runtime functions, declared in Ludic
|
|
cur_dir = ""
|
|
do_import("runtime/native/namespaces.ludic")
|
|
# L7: the byte buffer's API, when a program reads or writes bytes (it opens files through
|
|
# the asset pack, and a program that does not should not carry the pack's machinery)
|
|
if g_uses_bytes { do_import("runtime/native/bytes.ludic") }
|
|
cur_dir = saved
|
|
# Random.* (or a bare rng_* / seed nobody else defines) in a program with no ECS: the seeded
|
|
# random numbers on their own; a game has them through core.ludic
|
|
if not has_ecs() and (g_uses_rng or rt_wants_unmet("rng_") or rt_wants_unmet("seed")) {
|
|
cur_dir = ""
|
|
do_import("runtime/native/rng.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# a game (has systems/components) links the Ludic runtime.
|
|
if has_ecs() {
|
|
cur_dir = ""
|
|
do_import("runtime/native/core.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# any program that uses Regex.* gets the regex engine spliced in (it is
|
|
# self-contained — only compiler intrinsics — so it works in a plain tool too).
|
|
if g_uses_regex {
|
|
cur_dir = ""
|
|
do_import("runtime/native/regex.ludic")
|
|
do_import("runtime/native/regex_vm.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# any program that uses BigInt.*/Decimal.* gets the bignum engine spliced in
|
|
# (self-contained — only compiler intrinsics — so a plain tool works too).
|
|
if g_uses_bignum {
|
|
cur_dir = ""
|
|
do_import("runtime/native/bignum.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# any program that uses Dict.*/Set.* gets the hash-table engine spliced in
|
|
# (self-contained — only compiler intrinsics — so a plain tool works too).
|
|
if g_uses_dict {
|
|
cur_dir = ""
|
|
do_import("runtime/native/dict.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# any program that uses Huge.*/Angle.*/Percent.* gets the numeric runtime
|
|
# spliced in (it builds on Math.*, which lowers inline, so a plain tool works).
|
|
if g_uses_numeric {
|
|
cur_dir = ""
|
|
do_import("runtime/native/numeric.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# any program that uses Job.*/Promise.*/Sync.* gets the concurrency runtime
|
|
# spliced in (it is self-contained — only compiler intrinsics — so a plain tool
|
|
# works too). A deterministic cooperative scheduler; see runtime/native/jobs.ludic.
|
|
if g_uses_jobs {
|
|
cur_dir = ""
|
|
do_import("runtime/native/jobs.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# any program that uses Query.* gets the ECS spatial-query helpers spliced in;
|
|
# they read entity state through the reflection ABI (emit_decl force-emits it
|
|
# for a Query program even when it declares no events).
|
|
if g_uses_query {
|
|
cur_dir = ""
|
|
do_import("runtime/native/query.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# any program that uses Light.* gets the 2D light-accumulation pass spliced in;
|
|
# it reads and writes the framebuffer (rt_fb), so it links with core.ludic.
|
|
if g_uses_light {
|
|
cur_dir = ""
|
|
do_import("runtime/native/light.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Value.*/Json.* (#44) get the generic value tree + JSON bridge spliced in; it
|
|
# is self-contained (only string/slice ops), so it works in a plain tool too.
|
|
if rt_wants_unmet("value_") or rt_wants_unmet("json_") { g_uses_value = true } # value_get called directly
|
|
if g_uses_value {
|
|
cur_dir = ""
|
|
do_import("runtime/native/value.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Xml.* (#67) — the minimal pure-Ludic XML reader for the TMX/TSX/TX subset.
|
|
# Self-contained (string/slice ops only), so it works in a plain tool too.
|
|
if g_uses_xml {
|
|
cur_dir = ""
|
|
do_import("runtime/native/xml.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Sprite.sheet/cell/… or Assets.* (#81) — the spritesheet / atlas runtime. It
|
|
# reads the framebuffer and the image loader (rt_image_load / img_px in
|
|
# image.ludic), so it links against core.ludic (do_import dedupes).
|
|
if g_uses_atlas {
|
|
cur_dir = ""
|
|
do_import("runtime/native/core.ludic")
|
|
do_import("runtime/native/audio.ludic") # Assets.enqueue loads .wav/.mp3 into the sound bank
|
|
do_import("runtime/native/atlas.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Fx.* — engine-owned sparks and floating numbers; drawn through the framebuffer,
|
|
# so it links against core.ludic (do_import dedupes).
|
|
if g_uses_fx {
|
|
cur_dir = ""
|
|
do_import("runtime/native/core.ludic")
|
|
do_import("runtime/native/fx.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Base64.* (#67) — base64 codec; pull in the DEFLATE inflater alongside it, so
|
|
# a plain tool can run the full base64 -> zlib/gzip decode chain (inflate.ludic
|
|
# is self-contained; do_import dedupes when a game already linked it via core).
|
|
if g_uses_base64 {
|
|
cur_dir = ""
|
|
do_import("runtime/native/inflate.ludic")
|
|
do_import("runtime/native/zstd.ludic")
|
|
do_import("runtime/native/base64.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Tiled.* (#69) — the Tiled map runtime: model, GID resolver, loader, draw. It
|
|
# reads/writes the tilemap + framebuffer, so it links against core.ludic, and
|
|
# it consumes the XML/base64/inflate primitives above.
|
|
if g_uses_tiled {
|
|
cur_dir = ""
|
|
do_import("runtime/native/core.ludic")
|
|
do_import("runtime/native/xml.ludic")
|
|
do_import("runtime/native/inflate.ludic")
|
|
do_import("runtime/native/zstd.ludic")
|
|
do_import("runtime/native/base64.ludic")
|
|
do_import("runtime/native/value.ludic")
|
|
do_import("runtime/native/tiled.ludic")
|
|
# #72 opt-in spawning stands on the reflection ABI, which the compiler emits
|
|
# only for a game (emit_decl: has_ecs -> emit_world_table). So splice the
|
|
# spawn helpers only for a Tiled game; a plain map-reading tool never links
|
|
# against the world table.
|
|
if has_ecs() { do_import("runtime/native/tiled_spawn.ludic") }
|
|
cur_dir = saved
|
|
}
|
|
# Reflect.serialize/apply add the reflection serializer, which stands on both
|
|
# the value tree (above) and the world table (force-emitted for Reflect.*).
|
|
if g_uses_reflect_io {
|
|
cur_dir = ""
|
|
do_import("runtime/native/reflect_io.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# engine-owned systems (#43/#47): a game that declares a well-known engine
|
|
# component (SpriteAnim / Motion / Light2D / Occluder) gets systems.ludic
|
|
# spliced, and the compiler inserts a call to each esys_* at its frame phase
|
|
# (emit_engine_systems_for_phase). The systems read/write components through the
|
|
# reflection ABI, so g_uses_esys also force-emits the world table (emit_decl).
|
|
# Light2D/Occluder additionally consume the 2D light pass, so pull it in too.
|
|
# Input.* action maps + record/replay (#7): splice input.ludic. It reads the
|
|
# live key through rt_poll (core.ludic), so pull the runtime in even for a
|
|
# program with no ECS (do_import dedupes when a game already linked core).
|
|
if g_uses_input {
|
|
cur_dir = ""
|
|
do_import("runtime/native/core.ludic")
|
|
do_import("runtime/native/input.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Tween.* fluent handles (#48): splice the stateful tween runtime; esys_tween is
|
|
# inserted into the Update phase (emit_game.ludic) to advance handles each tick.
|
|
# It reads the live frame clock via the standard game loop, so pull core in too.
|
|
if g_uses_tween_rt {
|
|
cur_dir = ""
|
|
do_import("runtime/native/core.ludic")
|
|
do_import("runtime/native/tween.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Audio.* (#22): splice the Audio runtime. It is self-contained — its native
|
|
# calls (snd_*) are is_windowed()-guarded, so a headless build carries the API
|
|
# as no-ops and needs no audio backend — so it works in a plain program too.
|
|
if g_uses_audio {
|
|
cur_dir = ""
|
|
do_import("runtime/native/audio.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Http.* (#6): splice the HTTP client. The transport (hs_* intrinsics) is native
|
|
# (linked from http.ll), but the response parser is pure Ludic; self-contained,
|
|
# so it works in a plain program too.
|
|
if g_uses_http {
|
|
cur_dir = ""
|
|
do_import("runtime/native/http.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Udp.*: splice the datagram library. The sockets are native (udp.ll / udp_win.ll, bound
|
|
# by `extern function` in udp.ludic); the address helpers are pure Ludic.
|
|
if g_uses_udp {
|
|
cur_dir = ""
|
|
do_import("runtime/native/udp.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# a @PerMap registry: the .lres reader its generated fill calls (permap.ludic)
|
|
if g_pm_any {
|
|
cur_dir = ""
|
|
do_import("runtime/native/lres.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Process.*: splice the child-process library. Spawning is native (process.ll /
|
|
# process_win.ll); the argv array and the Windows command line are built in Ludic.
|
|
if g_uses_process {
|
|
cur_dir = ""
|
|
do_import("runtime/native/process.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Vk.* draws into the runtime's window, whose size, drawable and clock (gl_width,
|
|
# gl_set_drawable, gl_now_us, ...) still live in the GL runtime: splice and link it too,
|
|
# until the window layer stands apart from OpenGL (maroon-lake docs/plan/22, 22.15)
|
|
if g_uses_vk { g_uses_gl = true }
|
|
# Gl.*: splice the OpenGL surface (gl.ludic + the generated gl_api.ludic). The
|
|
# native calls are the linked GL entry points themselves; the window attach is
|
|
# is_windowed()-guarded, so a headless build renders into an offscreen context.
|
|
if g_uses_gl {
|
|
cur_dir = ""
|
|
do_import("runtime/native/gl.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Vk.*: splice the Vulkan surface (vk.ludic + the generated vk_api.ludic). The
|
|
# loader is opened at run time, so a program that never calls Vk.open needs none.
|
|
if g_uses_vk {
|
|
cur_dir = ""
|
|
do_import("runtime/native/vk.ludic")
|
|
cur_dir = saved
|
|
}
|
|
# Anim.play/Motion.to sugar (#48): the writes live in systems.ludic and use the
|
|
# reflection ABI, so splice it and force the world table even when the game does
|
|
# not otherwise trip uses_engine_systems.
|
|
if g_uses_anim_rt {
|
|
g_uses_esys = true
|
|
cur_dir = ""
|
|
do_import("runtime/native/core.ludic")
|
|
do_import("runtime/native/systems.ludic")
|
|
cur_dir = saved
|
|
}
|
|
if uses_engine_systems() {
|
|
g_uses_esys = true
|
|
cur_dir = ""
|
|
# SpriteAnim / Motion -> the animation systems (self-contained, reflection only)
|
|
if (find_comp("SpriteAnim") != null) or (find_comp("Motion") != null) {
|
|
do_import("runtime/native/systems.ludic")
|
|
}
|
|
# Body -> the movement + collision system (#65). It reads the tilemap
|
|
# (rt_tile / rt_mapw / rt_maph) for the tile-grid broadphase, so it links
|
|
# against core.ludic; do_import dedupes when a game already pulled core in.
|
|
if find_comp("Body") != null {
|
|
do_import("runtime/native/core.ludic")
|
|
do_import("runtime/native/systems_move.ludic")
|
|
}
|
|
# Sprite -> the engine sprite-render system (#85). It draws through
|
|
# rt_draw_sprite_ex (image.ludic, part of core), so it links against core.ludic
|
|
# (do_import dedupes when a game already pulled core in).
|
|
if find_comp("Sprite") != null {
|
|
do_import("runtime/native/core.ludic")
|
|
do_import("runtime/native/systems_sprite.ludic")
|
|
}
|
|
# TileSkin -> the engine tilemap-render system: draws the Map.* grid from glyph -> sprite skins.
|
|
if find_comp("TileSkin") != null {
|
|
do_import("runtime/native/core.ludic")
|
|
do_import("runtime/native/systems_tileskin.ludic")
|
|
}
|
|
# Bounds -> the engine world-bounds system (#84). Self-contained (reflection
|
|
# ABI only); its kill policy calls world_despawn, so force the @fn_world_despawn
|
|
# helper to be emitted.
|
|
if find_comp("Bounds") != null {
|
|
do_import("runtime/native/systems_bounds.ludic")
|
|
}
|
|
# Light2D / Occluder -> the lighting render system, which links against the
|
|
# 2D light pass (light.ludic). do_import dedupes, so this is a no-op when the
|
|
# game also uses Light.* directly (g_uses_light already pulled it in).
|
|
if (find_comp("Light2D") != null) or (find_comp("Occluder") != null) {
|
|
do_import("runtime/native/light.ludic")
|
|
do_import("runtime/native/systems_light.ludic")
|
|
g_uses_light = true
|
|
}
|
|
cur_dir = saved
|
|
}
|
|
}
|
|
|
|
function parse_program() -> void {
|
|
prog = new []Node
|
|
g_computed = new []Node
|
|
g_onspawn = new []Node
|
|
g_ondespawn = new []Node
|
|
g_onattach = new []Node
|
|
g_ondetach = new []Node
|
|
g_onenable = new []Node
|
|
g_ondisable = new []Node
|
|
g_scenes = new []Node
|
|
g_scene_count = 0
|
|
g_start_scene = 0
|
|
g_events = new []Node
|
|
g_onlisten = new []Node
|
|
g_toggled_layers = new []pointer
|
|
g_uses_regex = false
|
|
g_uses_gl = false
|
|
g_uses_vk = false
|
|
g_uses_bignum = false
|
|
g_uses_dict = false
|
|
g_uses_numeric = false
|
|
g_uses_jobs = false
|
|
g_uses_query = false
|
|
g_uses_reflect = false
|
|
g_uses_esys = false
|
|
g_uses_input = false
|
|
g_has_clear_color = false
|
|
g_clear_color = null
|
|
g_warned_draw_sprite = false
|
|
g_uses_world_despawn = false
|
|
g_uses_light = false
|
|
g_uses_value = false
|
|
g_uses_rng = false
|
|
g_rt_wants = new []pointer
|
|
g_uses_xml = false
|
|
g_uses_base64 = false
|
|
g_uses_tiled = false
|
|
g_uses_atlas = false
|
|
g_uses_fx = false
|
|
g_uses_reflect_io = false
|
|
g_tests = new []Node
|
|
g_mod_sys_fn = new []pointer
|
|
g_mod_sys_phase = new []pointer
|
|
# #62: seed the engine-system registry with the core entries, in the historical
|
|
# emit order (SpriteAnim, Motion — Update; Light2D — Render), so a core game is
|
|
# byte-identical; packages append via @EngineSystem.
|
|
g_esys_comp = new []pointer; g_esys_fn = new []pointer; g_esys_phase = new []pointer
|
|
g_disabled_sys = new []pointer
|
|
push(g_esys_comp, "SpriteAnim"); push(g_esys_fn, "esys_spriteanim"); push(g_esys_phase, "Update")
|
|
push(g_esys_comp, "Motion"); push(g_esys_fn, "esys_motion"); push(g_esys_phase, "Update")
|
|
push(g_esys_comp, "Body"); push(g_esys_fn, "esys_move"); push(g_esys_phase, "Update")
|
|
push(g_esys_comp, "Bounds"); push(g_esys_fn, "esys_bounds"); push(g_esys_phase, "LateUpdate")
|
|
push(g_esys_comp, "TileSkin"); push(g_esys_fn, "esys_tileskin"); push(g_esys_phase, "Render")
|
|
push(g_esys_comp, "Sprite"); push(g_esys_fn, "esys_sprite"); push(g_esys_phase, "Render")
|
|
push(g_esys_comp, "Light2D"); push(g_esys_fn, "esys_light2d"); push(g_esys_phase, "Render")
|
|
g_namespaces = new []pointer
|
|
g_ns_blocks = new []pointer
|
|
g_ns_exports = new []pointer
|
|
loaded_paths = new []pointer
|
|
skipnl()
|
|
g_game_name = "Ludic"
|
|
# imports may precede the program block
|
|
while is_id("import") { pi += 1; let t = toks[pi]; let rel = t.text; pi += 1; do_import(rel); skipnl() }
|
|
# @annotations on the program itself (e.g. @Handles(Movement)) — parsed, skipped
|
|
while is_op("@") {
|
|
pi += 1; let a = eat_id()
|
|
if is_op("(") { var d = 0
|
|
while true { if is_op("(") { d += 1 }; if is_op(")") { d -= 1 }; pi += 1; if d == 0 { break } } }
|
|
skipnl()
|
|
}
|
|
if is_id("program") { pi += 1; g_game_name = eat_id(); skipnl(); eat_op("{") }
|
|
while true {
|
|
skipnl()
|
|
if toks[pi].kind == TK_EOF { break }
|
|
if is_op("}") { break }
|
|
parse_one_decl()
|
|
}
|
|
g_gen_nodes = true # 0.S: what the finishing passes make is generated
|
|
modules_finish() # L3: the declared uses have no cycle
|
|
registries_finish() # L8: each registry gets its defs
|
|
at_check_refs() # every @Ref names a registry (attrs.ludic)
|
|
ports_finish() # L3: each port gets its one bind
|
|
views_finish() # L11: each view gets its model and call
|
|
components_finish() # L11: each component gets its class
|
|
ui_blocks_states() # 0.S: a ui block reads a state as State.field
|
|
actions_finish() # 0.R: the reducers checked, the queue written
|
|
g_gen_nodes = false
|
|
}
|