Merge Phase 6d: unify struct into property
This commit is contained in:
commit
c5bd50d544
15 changed files with 4054 additions and 4378 deletions
34
LANGUAGE.md
34
LANGUAGE.md
|
|
@ -22,8 +22,8 @@ A program is one `program` block containing declarations:
|
|||
# doc-check: skip — illustrative: elided import list
|
||||
program Name {
|
||||
import ... # pull declarations in from another file
|
||||
property ... # data (per entity)
|
||||
struct ... # a plain record, not tied to an entity
|
||||
property ... # a record of typed fields — a per-entity component, or a
|
||||
# plain `new`-allocated record; its use decides which
|
||||
model ... # a named entity KIND (bundle of properties)
|
||||
const ... # compile-time constants
|
||||
fn ... # functions
|
||||
|
|
@ -366,13 +366,24 @@ whole timeline), plus [`examples/toggle.ludic`](examples/toggle.ludic)
|
|||
(enable/disable). Still to come: **scene** hooks (`@OnEnter`/`@OnExit`), which
|
||||
wait on `scene` support landing in the compiler.
|
||||
|
||||
## Structs, arrays and slices
|
||||
## Records (`property`), arrays and slices
|
||||
|
||||
`struct` is the aggregate that is *not* tied to an entity — a plain record, for
|
||||
the data a program keeps outside the ECS.
|
||||
There is one record keyword, `property` — a named set of typed fields with
|
||||
defaults. How a property is *stored* follows from how it is *used*, so the same
|
||||
declaration covers both ECS components and the plain records a program keeps
|
||||
outside the ECS:
|
||||
|
||||
- listed in a `model` (or attached by `spawn`) → a **component**, stored in the
|
||||
engine's per-entity arrays and bound in queries;
|
||||
- constructed with **`new`** → a **heap record**, addressed by a pointer.
|
||||
|
||||
A program that only declares `property` records and functions — never a `model`
|
||||
or `handler` — is not an ECS program at all: it gets no entity storage or
|
||||
runtime, just the record layouts and `new`. (This is exactly how the Ludic
|
||||
compiler is written in itself.)
|
||||
|
||||
```ludic
|
||||
struct Tok { kind: int = 0, line: int = 0, next: Tok }
|
||||
property Tok { kind: int = 0, line: int = 0, next: Tok }
|
||||
|
||||
handler Lex phase Update {
|
||||
let t = new Tok # allocates; every field seeded from its default
|
||||
|
|
@ -380,11 +391,11 @@ handler Lex phase Update {
|
|||
}
|
||||
```
|
||||
|
||||
Struct values have **reference semantics**: a struct value is a pointer to the
|
||||
A `new` record has **reference semantics**: the value is a pointer to the
|
||||
object, so assigning or passing one shares it rather than copying.
|
||||
|
||||
```ludic
|
||||
struct Tok { kind: int = 0, line: int = 0, next: Tok }
|
||||
property Tok { kind: int = 0, line: int = 0, next: Tok }
|
||||
|
||||
fn bump(t: Tok) -> void { t.kind = t.kind + 1 }
|
||||
|
||||
|
|
@ -398,7 +409,7 @@ handler Share phase Update {
|
|||
}
|
||||
```
|
||||
|
||||
Fields chain, so a struct can refer to its own type and be walked without
|
||||
Fields chain, so a record can refer to its own type and be walked without
|
||||
temporaries — which is what an AST or a linked list needs:
|
||||
|
||||
```ludic
|
||||
|
|
@ -648,8 +659,9 @@ are future work.
|
|||
formatting lives in `build/ludic-fmt` instead). Output-path and IR flags are in
|
||||
flux as the CLI front-end is rebuilt — check `ludicc` usage for the current set.
|
||||
|
||||
`struct` and array types, `break`/`continue`, and argv/stderr — once listed here
|
||||
as near-term — are now implemented and self-hosting; their lowerings are in
|
||||
Records (`property` used with `new`) and array types, `break`/`continue`, and
|
||||
argv/stderr — once listed here as near-term — are now implemented and
|
||||
self-hosting; their lowerings are in
|
||||
[BOOTSTRAP.md](BOOTSTRAP.md) §4.
|
||||
|
||||
## Scenes & layers
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const E_FLOAT: int = 40
|
|||
const E_REC: int = 41
|
||||
const E_FINIT: int = 42
|
||||
|
||||
struct Node {
|
||||
property Node {
|
||||
kind: int = 0
|
||||
s: ptr = ptr_null() # name / operator / string / type-of-new
|
||||
ival: int = 0 # int literal, bool, flags
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# module-level material (types, globals, string constants) into one buffer and
|
||||
# function bodies into another, then prints them in order.
|
||||
|
||||
struct Buf { data: ptr = ptr_null(), len: int = 0, cap: int = 0 }
|
||||
property Buf { data: ptr = ptr_null(), len: int = 0, cap: int = 0 }
|
||||
|
||||
fn buf_new() -> Buf {
|
||||
let b = new Buf
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# module header. Mirrors the pieces of compiler/back/ that this subset needs.
|
||||
# structs and slices are references, so every non-scalar type lowers to `ptr`.
|
||||
|
||||
struct Val { code: ptr = ptr_null(), ty: ptr = ptr_null() }
|
||||
property Val { code: ptr = ptr_null(), ty: ptr = ptr_null() }
|
||||
fn val(code: ptr, ty: ptr) -> Val { let v = new Val; v.code = code; v.ty = ty; return v }
|
||||
|
||||
var head: Buf # module-level: types, globals, string constants
|
||||
|
|
@ -66,17 +66,6 @@ fn llty(t: ptr) -> ptr {
|
|||
fn is_slice_ty(t: ptr) -> bool { return peek8(t, 0) == 91 and peek8(t, 1) == 93 } # "[]"
|
||||
fn slice_elem(t: ptr) -> ptr { return substr(t, 2, slen(t) - 2) }
|
||||
|
||||
fn find_struct(name: ptr) -> Node {
|
||||
let i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_STRUCT and streq(d.s, name) { return d }
|
||||
i = i + 1
|
||||
}
|
||||
return ptr_null()
|
||||
}
|
||||
fn is_struct_ty(t: ptr) -> bool { return not ptr_is_null(find_struct(t)) }
|
||||
|
||||
fn find_arch(name: ptr) -> Node {
|
||||
let i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and streq(d.s, name) { return d }; i = i + 1 }
|
||||
|
|
@ -87,15 +76,10 @@ fn find_comp(name: ptr) -> Node {
|
|||
while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and streq(d.s, name) { return d }; i = i + 1 }
|
||||
return ptr_null()
|
||||
}
|
||||
# a struct or a component — both have %Str_/%Cmp_ layouts with named fields
|
||||
fn layout_node(name: ptr) -> Node {
|
||||
let s = find_struct(name); if not ptr_is_null(s) { return s }
|
||||
return find_comp(name)
|
||||
}
|
||||
fn layout_ty(name: ptr) -> ptr {
|
||||
if not ptr_is_null(find_struct(name)) { return sconcat("%Str_", name) }
|
||||
return sconcat("%Cmp_", name)
|
||||
}
|
||||
# every record is a `property` with a %Cmp_ layout of named fields — whether it
|
||||
# is stored per-entity by the ECS or heap-allocated by `new` is a matter of use.
|
||||
fn layout_node(name: ptr) -> Node { return find_comp(name) }
|
||||
fn layout_ty(name: ptr) -> ptr { return sconcat("%Cmp_", name) }
|
||||
|
||||
fn field_index(s: Node, fname: ptr) -> int {
|
||||
let i = 0
|
||||
|
|
|
|||
|
|
@ -5,16 +5,22 @@
|
|||
|
||||
const MAX_ENT: int = 1024
|
||||
|
||||
fn has_ecs() -> bool {
|
||||
let i = 0
|
||||
while i < len(prog) { let k = prog[i].kind; if k == N_COMP or k == N_SYS { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
fn has_systems() -> bool {
|
||||
let i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_SYS { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
fn has_models() -> bool {
|
||||
let i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_ARCH { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
# Does this program run the ECS? A property alone no longer answers that — the
|
||||
# same `property` keyword also declares plain `new`-allocated records (the merged
|
||||
# `struct`). A program uses the ECS when it has a handler or a model; a tool that
|
||||
# only declares record types and functions does not, and gets no entity storage,
|
||||
# allocator, snapshot or runtime splice.
|
||||
fn has_ecs() -> bool { return has_systems() or has_models() }
|
||||
|
||||
fn emit_ecs_storage() -> void {
|
||||
emith("@L_running = internal global i32 1\n")
|
||||
|
|
@ -28,12 +34,10 @@ fn emit_ecs_storage() -> void {
|
|||
let i = 0
|
||||
while i < len(prog) {
|
||||
let c = prog[i]
|
||||
# per-entity storage for a property (its %Cmp_ layout is emitted in the
|
||||
# header). Every property in an ECS program is a component today; a property
|
||||
# used only via `new` would not need these, but no such program mixes the two.
|
||||
if c.kind == N_COMP {
|
||||
emith(sconcat("%Cmp_", sconcat(c.s, " = type { ")))
|
||||
if len(c.kids) == 0 { emith("i32") }
|
||||
let f = 0
|
||||
while f < len(c.kids) { if f > 0 { emith(", ") }; emith(llty(c.kids[f].ty)); f = f + 1 }
|
||||
emith(" }\n")
|
||||
emith(sconcat("@S_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, sconcat(" x %Cmp_", sconcat(c.s, "] zeroinitializer\n")))))))
|
||||
emith(sconcat("@H_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, " x i8] zeroinitializer\n")))))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,12 +76,14 @@ fn emit_header() -> void {
|
|||
emith("@.gametitle = private unnamed_addr constant [")
|
||||
emith(itoa(slen(g_game_name) + 1)); emith(" x i8] c\""); emith(g_game_name); emith("\\00\"\n")
|
||||
emith("%LSlice = type { ptr, i32, i32 }\n")
|
||||
# struct layouts
|
||||
# property layouts — a %Cmp_ record of named fields, emitted here so `new`
|
||||
# works whether or not the program runs the ECS. The per-entity @S_/@H_ arrays
|
||||
# are separate (emit_ecs_storage), emitted only for a program that runs the ECS.
|
||||
let i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_STRUCT {
|
||||
emith("%Str_"); emith(d.s); emith(" = type { ")
|
||||
if d.kind == N_COMP {
|
||||
emith(layout_ty(d.s)); emith(" = type { ")
|
||||
if len(d.kids) == 0 { emith("i32") }
|
||||
let f = 0
|
||||
while f < len(d.kids) {
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ fn emit_sizeof(llt: ptr) -> ptr {
|
|||
}
|
||||
|
||||
fn emit_new_struct(name: ptr) -> Val {
|
||||
let s = find_struct(name)
|
||||
if ptr_is_null(s) { perr("unknown struct in new") }
|
||||
let sz = emit_sizeof(sconcat("%Str_", name))
|
||||
let s = layout_node(name) # a struct or a property — same shape
|
||||
if ptr_is_null(s) { perr("unknown record type in new") }
|
||||
let lty = layout_ty(name)
|
||||
let sz = emit_sizeof(lty)
|
||||
let obj = emit_bind(sconcat("call ptr @malloc(i64 ", sconcat(sz, ")")))
|
||||
let f = 0
|
||||
while f < len(s.kids) {
|
||||
let fd = s.kids[f]
|
||||
let addr = nreg()
|
||||
emit(" "); emit(addr); emit(" = getelementptr inbounds %Str_"); emit(name)
|
||||
emit(" "); emit(addr); emit(" = getelementptr inbounds "); emit(lty)
|
||||
emit(", ptr "); emit(obj); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
|
||||
let lt = llty(fd.ty)
|
||||
let v = "0"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const TK_NL: int = 4
|
|||
const TK_EOF: int = 5
|
||||
const TK_FLOAT: int = 6
|
||||
|
||||
struct Tok { kind: int = 0, text: ptr = ptr_null(), ival: int = 0, line: int = 0 }
|
||||
property Tok { kind: int = 0, text: ptr = ptr_null(), ival: int = 0, line: int = 0 }
|
||||
|
||||
var toks: []Tok
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -224,14 +224,6 @@ fn stmt() -> Node {
|
|||
}
|
||||
|
||||
# ---- declarations ----------------------------------------------------------
|
||||
fn parse_struct() -> Node {
|
||||
pi = pi + 1; let n = node(N_STRUCT); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true { skipnl(); if is_op("}") { break }
|
||||
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
|
||||
if is_op("=") { pi = pi + 1; f.a = expr() }
|
||||
push(n.kids, f); if is_op(",") { pi = pi + 1 } }
|
||||
eat_op("}"); return n
|
||||
}
|
||||
fn parse_var() -> Node {
|
||||
pi = pi + 1; let n = node(N_VAR); n.s = eat_id(); eat_op(":"); n.ty = ptype()
|
||||
if is_op("=") { pi = pi + 1; n.a = expr() }
|
||||
|
|
@ -311,7 +303,6 @@ fn parse_one_decl() -> void {
|
|||
do_import(rel)
|
||||
return
|
||||
}
|
||||
if is_id("struct") { push(prog, parse_struct()); return }
|
||||
if is_id("enum") { push(prog, parse_enum()); return }
|
||||
if is_id("property") { push(prog, parse_component()); return }
|
||||
if is_id("model") { push(prog, parse_archetype()); return }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
program T {
|
||||
struct P { x: int = 0, y: int = 7, next: P }
|
||||
property P { x: int = 0, y: int = 7, next: P }
|
||||
fn bump(p: P) -> void { p.x = p.x + 100 }
|
||||
entry {
|
||||
let a = new P
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ object LudicTokens {
|
|||
*/
|
||||
object LudicVocabulary {
|
||||
val DECL = setOf(
|
||||
"program", "import", "property", "struct", "model", "enum", "ui",
|
||||
"program", "import", "property", "model", "enum", "ui",
|
||||
"const", "var", "fn", "extern", "handler", "entry"
|
||||
)
|
||||
val CLAUSE = setOf(
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@
|
|||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|struct|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
|
||||
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\b" },
|
||||
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false)\\b" },
|
||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@
|
|||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|struct|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
|
||||
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\b" },
|
||||
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false)\\b" },
|
||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ typedef struct {
|
|||
* 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","struct","model","enum","ui",
|
||||
"program","import","property","model","enum","ui",
|
||||
"const","var","fn","extern","handler","entry", 0
|
||||
};
|
||||
static const char* LUDIC_KW_CLAUSE[] = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue