Proposal: reflection / runtime type info (Reflect.*) — powers auto-serialization & debug inspectors #20

Closed
opened 2026-08-29 20:22:01 +02:00 by orkun · 1 comment
Owner

Summary

A reflection capability: inspect properties, models, and fields at runtime
(and/or compile time) — names, types, and values. Primarily an engine/tooling
feature that powers things game devs consume without writing reflection code
themselves.

Why it matters (indirectly) for game devs

Reflection is advanced, so it's low priority as a user-facing feature — but
it unlocks conveniences non-experts love:

  • Automatic serialization: save/load any property without hand-writing it.
  • Auto-generated debug/inspector UI: tweak entity fields live in a dev overlay.
  • Data-driven tools: level editors, config binding, network sync of components.

Most developers should get these benefits via built-in features, not by calling
a reflection API directly.

Proposed API (illustrative)

# doc-check: skip — illustrative API sketch
for f in Reflect.fields_of(Pos) {
  Log.info("field", { name: f.name, type: f.type_name })
}
let v = Reflect.get(entity, "hp")
Reflect.set(entity, "hp", 10)
Reflect.serialize(entity)     # -> a generic value tree for save/JSON
  • Enumerate fields of a property/model (name, type, offset).
  • Get/set a field by name; iterate an entity's components.
  • A generic value tree that serialization/JSON/inspector UIs can walk.

Considerations

  • Prefer compile-time reflection where possible (generate the metadata tables
    at compile time; zero or near-zero runtime cost) — fits Ludic's AOT, native,
    deterministic model far better than heavy runtime introspection.
  • Emit an opt-in metadata table per type so binaries that don't use reflection
    don't pay for it.
  • Depends heavily on the type-system work in #1 (need a canonical type model first)
    — sequence this after that lands.
  • Keep the surface small; this is a foundation for tools, not an everyday API.

Scope / acceptance

  • Compile-time type/field metadata tables (opt-in per type).
  • Field enumeration + get/set by name; entity component iteration.
  • A generic value tree usable by a generic serialize.
  • Docs page (positioned as an advanced/tooling feature).
  • Tests + one real consumer (generic save or a debug inspector).

Related: #1 (type system — prerequisite), Filesystem (serialization), Logging.

## Summary A **reflection** capability: inspect properties, models, and fields at runtime (and/or compile time) — names, types, and values. Primarily an *engine/tooling* feature that powers things game devs consume without writing reflection code themselves. ## Why it matters (indirectly) for game devs Reflection is advanced, so it's **low priority** as a user-facing feature — but it unlocks conveniences non-experts love: - **Automatic serialization**: save/load any `property` without hand-writing it. - **Auto-generated debug/inspector UI**: tweak entity fields live in a dev overlay. - **Data-driven tools**: level editors, config binding, network sync of components. Most developers should get these *benefits* via built-in features, not by calling a reflection API directly. ## Proposed API (illustrative) ```ludic # doc-check: skip — illustrative API sketch for f in Reflect.fields_of(Pos) { Log.info("field", { name: f.name, type: f.type_name }) } let v = Reflect.get(entity, "hp") Reflect.set(entity, "hp", 10) Reflect.serialize(entity) # -> a generic value tree for save/JSON ``` - Enumerate fields of a `property`/`model` (name, type, offset). - Get/set a field by name; iterate an entity's components. - A generic value tree that serialization/JSON/inspector UIs can walk. ## Considerations - **Prefer compile-time reflection** where possible (generate the metadata tables at compile time; zero or near-zero runtime cost) — fits Ludic's AOT, native, deterministic model far better than heavy runtime introspection. - Emit an opt-in **metadata table** per type so binaries that don't use reflection don't pay for it. - Depends heavily on the type-system work in #1 (need a canonical type model first) — sequence this **after** that lands. - Keep the surface small; this is a foundation for tools, not an everyday API. ## Scope / acceptance - [ ] Compile-time type/field metadata tables (opt-in per type). - [ ] Field enumeration + get/set by name; entity component iteration. - [ ] A generic value tree usable by a generic `serialize`. - [ ] Docs page (positioned as an advanced/tooling feature). - [ ] Tests + one real consumer (generic save or a debug inspector). Related: #1 (type system — prerequisite), Filesystem (serialization), Logging.
orkun added the
proposal
priority:low
area:types
labels 2026-08-29 20:22:01 +02:00
Author
Owner

Shipped the reflection core in 12f2dbe (pushed to main).

Reflect.* — runtime reflection over the world schema:

  • Enumerate: prop_count / prop_name(i), field_count(prop) / field_name(prop, i) / field_type(prop, i) — walk every component and field by index; field_type reports the declared type ("int" / "fixed" / …).
  • Resolve: prop(name) / field(prop, name) — ids by name (-1 = none).
  • Read / write: get / set / has(entity, prop, …) — a field by (prop, field) id.
  • Identify: kind(entity) / model(name) — an entity's model.

Implementation: the enumeration adds an EV8 metadata layer to the generated reflection ABI (ludic_prop_count / prop_name / field_count / field_name / field_type, generated the same way as ludic_prop_id — a switch over compile-time property/field metadata, falling through to mod-registered components). It's all built at compile time — a table walk, not heavy runtime introspection — so, per the issue's "prefer compile-time reflection, opt-in per binary" guidance, a program that never reflects pays nothing, and one that uses Reflect.* force-emits the ABI without needing @events of its own.

Two of the acceptance items are covered directly: field enumeration + get/set by name + entity component iteration, and one real consumer — examples/library/reflect.ludic includes 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), 20 self-asserting cases wired into x test (now 64 passed). Docs: a Reflect section + 12 per-symbol pages, positioned as an advanced/tooling surface; x check-impl / x check-docs green.

Deferred: the generic value-tree serialize the proposal also lists. It needs an any/tagged-union value type — exactly the #1 type-system work this issue names as the prerequisite — so I've tracked it as #44 rather than forcing a value type in ahead of #1. Reflect.field_type already exposes the per-field type that a serializer will switch on, so #44 becomes a thin walk once #1 lands. Closing this as the reflection core + inspector consumer.

Shipped the reflection **core** in 12f2dbe (pushed to `main`). **`Reflect.*`** — runtime reflection over the world schema: - **Enumerate:** `prop_count` / `prop_name(i)`, `field_count(prop)` / `field_name(prop, i)` / `field_type(prop, i)` — walk every component and field by index; `field_type` reports the declared type (`"int"` / `"fixed"` / …). - **Resolve:** `prop(name)` / `field(prop, name)` — ids by name (`-1` = none). - **Read / write:** `get` / `set` / `has(entity, prop, …)` — a field by `(prop, field)` id. - **Identify:** `kind(entity)` / `model(name)` — an entity's model. Implementation: the enumeration adds an **EV8 metadata layer** to the generated reflection ABI (`ludic_prop_count` / `prop_name` / `field_count` / `field_name` / `field_type`, generated the same way as `ludic_prop_id` — a switch over compile-time property/field metadata, falling through to mod-registered components). It's all built at compile time — a table walk, not heavy runtime introspection — so, per the issue's "prefer compile-time reflection, opt-in per binary" guidance, a program that never reflects pays nothing, and one that uses `Reflect.*` force-emits the ABI without needing `@events` of its own. Two of the acceptance items are covered directly: **field enumeration + get/set by name + entity component iteration**, and **one real consumer** — `examples/library/reflect.ludic` includes 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), 20 self-asserting cases wired into `x test` (now 64 passed). Docs: a `Reflect` section + 12 per-symbol pages, positioned as an advanced/tooling surface; `x check-impl` / `x check-docs` green. Deferred: the **generic value-tree `serialize`** the proposal also lists. It needs an `any`/tagged-union value type — exactly the #1 type-system work this issue names as the prerequisite — so I've tracked it as #44 rather than forcing a value type in ahead of #1. `Reflect.field_type` already exposes the per-field type that a serializer will switch on, so #44 becomes a thin walk once #1 lands. Closing this as the reflection core + inspector consumer.
orkun closed this issue 2026-08-31 12:29:28 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#20
No description provided.