Reflection: reflect::
The reflect:: namespace answers questions about the program at run time: what classes exist, what fields and methods they carry, and how to read, write, call and construct by name (signatures).
- Engine truth. Each engine reflects only what it registered, and the namespace is reserved.
- Live queries, plain results. Every call reads the current class shape, hot reloads included, and answers in ordinary arrays and maps, safe to hold and serialize.
- A string subject is a class name, never a string value to inspect (below).
- Script access control is ignored; the host boundary still holds (below).
Ask: what shape is this?
| Call | Answers |
|---|---|
reflect::type_name(v) | type_of, except that a typed container gives its canonical tag ("array<int>", not "array") |
reflect::fields(v) | one map per field: name, type, kind, access, static, from (the declaring class) |
reflect::methods(v) | one map per overload: name, arity, static, access, from, params |
reflect::has_field(v, "name"), reflect::has_method(v, "name") | bool; both walk the inheritance chain |
Every shape call, with its ordering rules: reflection calls.
A C++-bound class lists the host's property schema, where kind tells a plain field from a "signal" or "observable" member.
class Creature { int hp = 10; private: string name = "grim"; }
class Goblin : Creature {
float speed = 1.5;
private: int wound(int amount, bool crit = false) { hp = hp - amount; return hp; }
}
for (auto f : reflect::fields(Goblin())) {
print("[{}] {} {} (from {})", f["access"], f["type"], f["name"], f["from"]);
}
for (auto m : reflect::methods("Goblin")) {
print("{} arity {} ({}) {}", m["name"], m["arity"], m["access"], m["params"][1]);
}[public] int hp (from Creature)
[private] string name (from Creature)
[public] float speed (from Goblin)
wound arity 2 (private) {"default": true, "name": "crit", "type": "bool"}Strings are class names
Every function that takes an instance also takes a class name, so a string there is looked up as a class and an unknown name raises (reflect::fields: unknown class 'hello', each function under its own name). Given a class name, get, set and invoke reach its static members (reflect::get: class 'C' has no static field 'a' otherwise). Only reflect::type_name reads a string as a value.
Another non-class value (5, an array, null) gives fields an empty array and has_field false, and raises from methods, get, set and invoke. Each function's answer: a non-class argument.
So a property grid over arbitrary values tests for a string before it calls reflect::fields:
class Goblin { int hp = 10; float speed = 1.5; }
function field_count(auto v) -> int {
if (type_of(v) == "string") { return 0; } // a string would be read as a class name
return reflect::fields(v).size();
}
array<var> things = [Goblin(), "hello", 5, null];
for (auto t : things) { print("{}: {}", reflect::type_name(t), field_count(t)); }
try { reflect::fields("hello"); } catch (e) { print(reflect::fields("Goblin").size(), " ", e); }Goblin: 2
string: 0
int: 0
null: 0
2 reflect::fields: unknown class 'hello'Act: read, write, call, construct
| Call | Is |
|---|---|
reflect::get(s, "hp") | s.hp |
reflect::set(s, "hp", 42) | s.hp = 42 |
reflect::invoke(s, "wound", 5) | s.wound(5) |
reflect::construct("Class", args...) | Class(args): a value |
reflect::construct_shared("Class", args...) | new Class(args): a shared handle (Value Semantics) |
reflect::bind(s, "wound") gives the method as a callable that holds s (Classes), and reflect::call("ns::name", args...) calls a function by name. Every rule: reflection calls.
Reflection ignores script access control, by design. get, set, invoke, bind and construct reach private members and private constructors, while direct syntax keeps enforcing private. from_json and from_binary likewise restore private state.
class Soldier {
int hp = 10;
Soldier(int start) { hp = start; }
int wound(int amount) { hp = hp - amount; return hp; }
private: int gold = 100;
}
namespace tools { function twice(auto x) -> auto { return x * 2; } }
var s = reflect::construct("Soldier", 10); // == Soldier(10)
reflect::set(s, "hp", 42);
var wound = reflect::bind(s, "wound");
print(reflect::get(s, "hp"), " ", reflect::invoke(s, "wound", 5), " ", wound(7), " ", reflect::call("tools::twice", 21));
try { s.gold = 0; } catch (e) { print(e); }
reflect::set(s, "gold", 7);
print("through reflection: {}", reflect::get(s, "gold"));
var shared = reflect::construct_shared("Soldier", 1); // == new Soldier(1)
var alias = shared; alias.hp = 9; // a handle: shared sees the write
var copy = s; copy.hp = 0; // a value: s is untouched
print("value {}, shared {}", s.hp, shared.hp);42 37 30 42
Cannot access private member 'gold' of class 'Soldier'
through reflection: 7
value 30, shared 9The security boundary is the host, and reflection never crosses it: a C++-bound class shows only what its binding registered (a read-only property stays read-only), builtin values expose no internal state, and engine capabilities such as file access are never reachable.
Navigate: the wider world
| Call | Answers |
|---|---|
reflect::classes() | every class, script-declared and host-bound, sorted |
reflect::bases(v) | the base classes in declaration order, empty at a root |
reflect::instances("Class") | the live instances of a class, in creation order |
reflect::generation("Class") | a counter that moves whenever the class is redefined |
Functions, globals, host-bound classes and privileges are asked the same way: program-wide queries.
Hot reload: ask again
reflect::generation is the cheap poll: a property grid re-reads fields() only when the number moved. The sample declares Evolver twice, which is legal because a class declared again at program scope replaces the class in place for every live instance (Program-scope redeclarations); a reloaded file does that.
class Evolver { int a = 1; }
var first = new Evolver(); var second = new Evolver();
auto g0 = reflect::generation("Evolver");
class Evolver { int a = 1; int b = 2; } // what a reload does: same engine, same class, new shape
print("generation moved: {}", reflect::generation("Evolver") > g0);
for (auto e : reflect::instances("Evolver")) { print("live instance has b? {}", reflect::has_field(e, "b")); }generation moved: true
live instance has b? true
live instance has b? trueThe namespace is reserved
Declaring into reflect:: is an error at declaration time on both backends. math:: is the opposite: scripts extend it freely.
namespace reflect { function evil() { return 1; } }== error: Namespace 'reflect' is reserved for the engine (reflect:: is C++-registered only)