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?​

CallAnswers
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.

jaiscript
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]);
}
output
[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:

jaiscript
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); }
output
Goblin: 2
string: 0
int: 0
null: 0
2 reflect::fields: unknown class 'hello'

Act: read, write, call, construct​

CallIs
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.

jaiscript
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);
output
42 37 30 42
Cannot access private member 'gold' of class 'Soldier'
through reflection: 7
value 30, shared 9

The 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.

CallAnswers
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.

jaiscript
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")); }
output
generation moved: true
live instance has b? true
live instance has b? true

The namespace is reserved​

Declaring into reflect:: is an error at declaration time on both backends. math:: is the opposite: scripts extend it freely.

jaiscript
namespace reflect { function evil() { return 1; } }
output
== error: Namespace 'reflect' is reserved for the engine (reflect:: is C++-registered only)