feat(types): add Reflect.* — runtime reflection over the world schema (#20)
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:
parent
b25dc328a2
commit
12f2dbe958
20 changed files with 16376 additions and 14274 deletions
7
docs/language/reflect/_section.md
Normal file
7
docs/language/reflect/_section.md
Normal 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>.
|
||||
30
docs/language/reflect/reflect-field.md
Normal file
30
docs/language/reflect/reflect-field.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
27
docs/language/reflect/reflect-field_count.md
Normal file
27
docs/language/reflect/reflect-field_count.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
29
docs/language/reflect/reflect-field_name.md
Normal file
29
docs/language/reflect/reflect-field_name.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
29
docs/language/reflect/reflect-field_type.md
Normal file
29
docs/language/reflect/reflect-field_type.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
31
docs/language/reflect/reflect-get.md
Normal file
31
docs/language/reflect/reflect-get.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
29
docs/language/reflect/reflect-has.md
Normal file
29
docs/language/reflect/reflect-has.md
Normal 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) }
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/reflect/reflect-kind.md
Normal file
28
docs/language/reflect/reflect-kind.md
Normal 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) }
|
||||
}
|
||||
}
|
||||
```
|
||||
27
docs/language/reflect/reflect-model.md
Normal file
27
docs/language/reflect/reflect-model.md
Normal 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"))
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/reflect/reflect-prop.md
Normal file
28
docs/language/reflect/reflect-prop.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/reflect/reflect-prop_count.md
Normal file
24
docs/language/reflect/reflect-prop_count.md
Normal 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())
|
||||
}
|
||||
}
|
||||
```
|
||||
32
docs/language/reflect/reflect-prop_name.md
Normal file
32
docs/language/reflect/reflect-prop_name.md
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
33
docs/language/reflect/reflect-set.md
Normal file
33
docs/language/reflect/reflect-set.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue