feat(types): add Reflect.* — runtime reflection over the world schema (#20)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m12s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 18s

Runtime type reflection: enumerate properties and fields by index, resolve ids
by name, read a field's type, and get/set/has an entity's fields generically —
the foundation the issue calls out for auto-serialization, data-driven tools,
and debug/inspector overlays. Built on the existing EV2 reflection ABI plus a
new EV8 metadata-enumeration layer, all generated at compile time (a table walk,
no heavy runtime introspection), so a binary that never reflects pays nothing.

Surface (Reflect.*, aliased in emit_call.ludic over the world_* reflection ABI):
  - Reflect.prop(name) / field(prop,name)        resolve ids by name (-1 = none)
  - Reflect.prop_count() / prop_name(i)          enumerate properties
  - Reflect.field_count(prop) / field_name(prop,i) / field_type(prop,i)
                                                 enumerate a component's fields
  - Reflect.get / set / has (entity, prop, ...)  read/write/test a field by id
  - Reflect.kind(entity) / model(name)           an entity's model, by id/name

New codegen (emit_world.ludic, EV8): ludic_prop_count / prop_name /
field_count / field_name / field_type, generated the same way as ludic_prop_id
— a switch over the compile-time property/field metadata, falling through to the
mod-registered (dynamic) registries. Field names/types come straight from the
AST, so field_type reports the declared type ("int"/"fixed"/…). A program that
uses Reflect.* force-emits the reflection ABI (g_uses_reflect) so it needs no
@events of its own, exactly like Query.* (#42).

examples/library/reflect.ludic asserts 20 cases including a generic inspector
that sums every field of every component an entity has while naming none of them
— the auto-save / debug-overlay pattern end to end. Wired into x test (now 64
passed). Docs: a Reflect section + 12 per-symbol pages (positioned as an
advanced/tooling surface), inventory/coverage green. Seed reseeded; the C-free
bootstrap fixpoint holds.

Scope: this lands the reflection core and a real consumer (the generic
inspector). The generic value-tree `serialize` the proposal also sketches wants
a tagged-union/any value type from the #1 type-system work, so it is tracked as
a follow-up rather than forced in here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 13:28:51 +03:00
parent b25dc328a2
commit 12f2dbe958
20 changed files with 16376 additions and 14274 deletions

View file

@ -0,0 +1,7 @@
---
id: reflect
title: Reflect
order: 31
---
Runtime reflection over the world schema — inspect properties, fields, and entity state by name and by index. It powers the conveniences game devs consume without writing reflection code: automatic save/load, data-driven tools, and debug/inspector overlays. Enumerate with <a href="reflect-prop_count"><code>Reflect.prop_count</code></a>/<a href="reflect-prop_name"><code>Reflect.prop_name</code></a> and <a href="reflect-field_count"><code>Reflect.field_count</code></a>/<a href="reflect-field_name"><code>Reflect.field_name</code></a>/<a href="reflect-field_type"><code>Reflect.field_type</code></a>; resolve ids with <a href="reflect-prop"><code>Reflect.prop</code></a>/<a href="reflect-field"><code>Reflect.field</code></a>; read and write an entity's fields with <a href="reflect-get"><code>Reflect.get</code></a>/<a href="reflect-set"><code>Reflect.set</code></a>/<a href="reflect-has"><code>Reflect.has</code></a>; and identify its model with <a href="reflect-kind"><code>Reflect.kind</code></a>/<a href="reflect-model"><code>Reflect.model</code></a>. The metadata tables are generated at compile time (the same reflection ABI a foreign mod binds), so introspection is a table walk, not heavy runtime machinery. This is an advanced/tooling surface — most developers get its benefits through built-in features. Related: <a href="world"><code>World</code></a>, <a href="query"><code>Query</code></a>.

View file

@ -0,0 +1,30 @@
---
id: reflect-field
name: Reflect.field
category: reflect
kind: namespace-method
tokens: Reflect.field
sig: Reflect.field(prop, name) -> int
tip: The field id of a named field within a property, or -1.
order: 2
ns: Reflect
member: field
---
Resolves a field name within property <code>prop</code> to its field id (index), or <code>-1</code> if the property has no such field. Pair it with <a href="reflect-get"><code>Reflect.get</code></a>/<a href="reflect-set"><code>Reflect.set</code></a> to read or write that field.
Parameters:
- `prop` — a property id (from <a href="reflect-prop"><code>Reflect.prop</code></a>)
- `name` — the field name
```ludic
program Demo {
property Health { hp: int = 0, max: int = 0 }
model Unit { Health }
entry {
let H = Reflect.prop("Health")
let mx = Reflect.field(H, "max")
print(mx)
}
}
```

View file

@ -0,0 +1,27 @@
---
id: reflect-field_count
name: Reflect.field_count
category: reflect
kind: namespace-method
tokens: Reflect.field_count
sig: Reflect.field_count(prop) -> int
tip: How many fields a property has.
order: 5
ns: Reflect
member: field_count
---
Returns the number of fields in property <code>prop</code>. Walk <code>0 .. Reflect.field_count(prop)</code> with <a href="reflect-field_name"><code>Reflect.field_name</code></a> and <a href="reflect-field_type"><code>Reflect.field_type</code></a> to enumerate a component's shape.
Parameters:
- `prop` — a property id (from <a href="reflect-prop"><code>Reflect.prop</code></a>)
```ludic
program Demo {
property Health { hp: int = 0, max: int = 0 }
model Unit { Health }
entry {
print(Reflect.field_count(Reflect.prop("Health"))) # 2
}
}
```

View file

@ -0,0 +1,29 @@
---
id: reflect-field_name
name: Reflect.field_name
category: reflect
kind: namespace-method
tokens: Reflect.field_name
sig: Reflect.field_name(prop, index) -> string
tip: The name of the field at an index within a property.
order: 6
ns: Reflect
member: field_name
---
Returns the name of the field at <code>index</code> within property <code>prop</code> (<code>0 .. Reflect.field_count(prop)</code>), or the empty string if out of range. The key half of a generic serializer or inspector row.
Parameters:
- `prop` — a property id (from <a href="reflect-prop"><code>Reflect.prop</code></a>)
- `index` — the field index
```ludic
program Demo {
property Health { hp: int = 0, max: int = 0 }
model Unit { Health }
entry {
let H = Reflect.prop("Health")
print(Reflect.field_name(H, 0)) # hp
}
}
```

View file

@ -0,0 +1,29 @@
---
id: reflect-field_type
name: Reflect.field_type
category: reflect
kind: namespace-method
tokens: Reflect.field_type
sig: Reflect.field_type(prop, index) -> string
tip: The type name of the field at an index within a property.
order: 7
ns: Reflect
member: field_type
---
Returns the declared type name of the field at <code>index</code> within property <code>prop</code> — <code>"int"</code>, <code>"fixed"</code>, <code>"bool"</code>, and so on. A serializer uses it to format each value correctly, and an inspector uses it to pick the right editor widget.
Parameters:
- `prop` — a property id (from <a href="reflect-prop"><code>Reflect.prop</code></a>)
- `index` — the field index
```ludic
program Demo {
property Velocity { dx: fixed = 0.0, dy: fixed = 0.0 }
model Mover { Velocity }
entry {
let V = Reflect.prop("Velocity")
print(Reflect.field_type(V, 0)) # fixed
}
}
```

View file

@ -0,0 +1,31 @@
---
id: reflect-get
name: Reflect.get
category: reflect
kind: namespace-method
tokens: Reflect.get
sig: Reflect.get(entity, prop, field) -> int
tip: Read one field of an entity by (prop, field) id.
order: 8
ns: Reflect
member: get
---
Reads the value of field <code>field</code> of property <code>prop</code> on <code>entity</code>, as an <code>int</code>. A <code>fixed</code> field comes back as its raw Q16.16 bits (the same integer <code>fixed</code> stores), so a generic reader can move it without interpreting it. Same as <a href="world-get"><code>World.get</code></a>, under the reflection namespace.
Parameters:
- `entity` — the entity to read
- `prop` — a property id (from <a href="reflect-prop"><code>Reflect.prop</code></a>)
- `field` — a field id (from <a href="reflect-field"><code>Reflect.field</code></a>)
```ludic
program Demo {
property Health { hp: int = 0 }
model Unit { Health }
entry {
spawn Unit { Health { hp: 7 } }
let H = Reflect.prop("Health")
print(Reflect.get(0, H, Reflect.field(H, "hp"))) # 7
}
}
```

View file

@ -0,0 +1,29 @@
---
id: reflect-has
name: Reflect.has
category: reflect
kind: namespace-method
tokens: Reflect.has
sig: Reflect.has(entity, prop) -> bool
tip: Does an entity carry a property?
order: 10
ns: Reflect
member: has
---
Returns whether <code>entity</code> carries property <code>prop</code>. A generic walker checks it before reading a component's fields, so it visits only the components an entity actually has.
Parameters:
- `entity` — the entity to test
- `prop` — a property id (from <a href="reflect-prop"><code>Reflect.prop</code></a>)
```ludic
program Demo {
property Health { hp: int = 0 }
model Unit { Health }
entry {
spawn Unit { Health { hp: 7 } }
if Reflect.has(0, Reflect.prop("Health")) { print(1) }
}
}
```

View file

@ -0,0 +1,28 @@
---
id: reflect-kind
name: Reflect.kind
category: reflect
kind: namespace-method
tokens: Reflect.kind
sig: Reflect.kind(entity) -> int
tip: The model id of an entity.
order: 11
ns: Reflect
member: kind
---
Returns the model (archetype) id of <code>entity</code> — which kind of thing it is. Compare it against <a href="reflect-model"><code>Reflect.model</code></a> to branch on an entity's type, or use it to label a save record. Same as <a href="world-kind"><code>World.kind</code></a>, under the reflection namespace.
Parameters:
- `entity` — the entity to identify
```ludic
program Demo {
property Health { hp: int = 0 }
model Unit { Health }
entry {
spawn Unit { Health { hp: 7 } }
if Reflect.kind(0) == Reflect.model("Unit") { print(1) }
}
}
```

View file

@ -0,0 +1,27 @@
---
id: reflect-model
name: Reflect.model
category: reflect
kind: namespace-method
tokens: Reflect.model
sig: Reflect.model(name) -> int
tip: The model id for a model name, or -1 if unknown.
order: 12
ns: Reflect
member: model
---
Resolves a model (archetype) name to its id, or <code>-1</code> if no such model exists. The counterpart to <a href="reflect-kind"><code>Reflect.kind</code></a>: resolve the name once, then compare entities' kinds against it.
Parameters:
- `name` — the model name
```ludic
program Demo {
property Health { hp: int = 0 }
model Unit { Health }
entry {
print(Reflect.model("Unit"))
}
}
```

View file

@ -0,0 +1,28 @@
---
id: reflect-prop
name: Reflect.prop
category: reflect
kind: namespace-method
tokens: Reflect.prop
sig: Reflect.prop(name) -> int
tip: The property id for a property name, or -1 if unknown.
order: 1
ns: Reflect
member: prop
---
Resolves a property name to its stable id, or <code>-1</code> if no such property exists. The id is what every other <code>Reflect.*</code> (and <a href="query"><code>Query</code></a>) call takes. Same as <a href="world-prop_id"><code>World.prop_id</code></a>, under the reflection namespace.
Parameters:
- `name` — the property name
```ludic
program Demo {
property Health { hp: int = 0 }
model Unit { Health }
entry {
let H = Reflect.prop("Health")
print(H)
}
}
```

View file

@ -0,0 +1,24 @@
---
id: reflect-prop_count
name: Reflect.prop_count
category: reflect
kind: namespace-method
tokens: Reflect.prop_count
sig: Reflect.prop_count() -> int
tip: How many properties the world schema defines.
order: 3
ns: Reflect
member: prop_count
---
Returns the number of properties in the world — every declared <code>property</code>, plus any the runtime or a mod registered. Walk <code>0 .. Reflect.prop_count()</code> with <a href="reflect-prop_name"><code>Reflect.prop_name</code></a> to enumerate the whole schema.
```ludic
program Demo {
property Health { hp: int = 0 }
model Unit { Health }
entry {
print(Reflect.prop_count())
}
}
```

View file

@ -0,0 +1,32 @@
---
id: reflect-prop_name
name: Reflect.prop_name
category: reflect
kind: namespace-method
tokens: Reflect.prop_name
sig: Reflect.prop_name(index) -> string
tip: The name of the property at an index (or "").
order: 4
ns: Reflect
member: prop_name
---
Returns the name of the property at <code>index</code> (<code>0 .. Reflect.prop_count()</code>), or the empty string if out of range. The reverse of <a href="reflect-prop"><code>Reflect.prop</code></a> — together they let a generic tool list every component by name.
Parameters:
- `index` — the property index
```ludic
program Demo {
property Health { hp: int = 0 }
model Unit { Health }
entry {
var p = 0
let n = Reflect.prop_count()
while p < n {
print(Reflect.prop_name(p))
p = p + 1
}
}
}
```

View file

@ -0,0 +1,33 @@
---
id: reflect-set
name: Reflect.set
category: reflect
kind: namespace-method
tokens: Reflect.set
sig: Reflect.set(entity, prop, field, value)
tip: Write one field of an entity by (prop, field) id.
order: 9
ns: Reflect
member: set
---
Writes <code>value</code> into field <code>field</code> of property <code>prop</code> on <code>entity</code>. For a <code>fixed</code> field, pass the raw Q16.16 bits. This is how a generic loader restores saved state and how a debug overlay applies an edit. Same as <a href="world-set"><code>World.set</code></a>, under the reflection namespace.
Parameters:
- `entity` — the entity to modify
- `prop` — a property id (from <a href="reflect-prop"><code>Reflect.prop</code></a>)
- `field` — a field id (from <a href="reflect-field"><code>Reflect.field</code></a>)
- `value` — the integer value to store
```ludic
program Demo {
property Health { hp: int = 0 }
model Unit { Health }
entry {
spawn Unit { Health { hp: 7 } }
let H = Reflect.prop("Health")
Reflect.set(0, H, Reflect.field(H, "hp"), 99)
print(Reflect.get(0, H, 0)) # 99
}
}
```