# Structure Introspection

`introspection.h`/`introspection.cpp` give any plain C++ struct - a device's model/instance parameter block, an analysis core's parameter block, `SimulatorOptions` - a name-and-index-based get/set interface driven by [`Value`](value.md), without hand-writing a get/set dispatch per struct. This is the reflection layer [Parameterized Objects](parameterized.md) and the [builtin device framework](devbuiltin.md) are built on.

## Registering a struct's members

`StructMember` records one field's name (`Id`), a small `Type` enum (`Int`, `Real`, `String`, `Id`, and vector/`Value` variants of each), and its byte `offset` within the struct - obtained via `offsetof`. `Introspection<T>` holds, as `T`-specific static state, the ordered list of `StructMember`s and a name-to-index map.

Registration happens once per struct type, via a required specialization of `Introspection<T>::setup()` (one per struct, written in that struct's own `.cpp` file - see `Introspection<SimulatorOptions>::setup()` in `options.cpp` for a concrete example) that calls `registerMember(fieldName)` for each reflected field. The `registerMember`/`registerNamedMember` macros expand to `registerMember_(Id::createStatic(#m), StructMember::typeCode<decltype(T::m)>(), offsetof(T, m))` - the field's C++ type is captured automatically via `decltype` and mapped to a `StructMember::Type` at compile time, so adding a field to the reflected set is a one-line macro call, not a hand-written case.

`instantiateIntrospection(S)` is what actually runs `setup()`: it explicitly instantiates `Introspection<S>` and initializes the static member `isRegistered` from `setup()`'s return value, so the registration runs exactly once, driven by ordinary static-initialization order, the first (and only) time that translation unit's static is initialized - no explicit "call setup once" bookkeeping is needed at any call site.

## Reading and writing by name or index

`get(st, ndx/name, v, s)` and `set(st, ndx/name, v, s)` are the actual accessors: given a struct instance `st` and either a field index or an `Id` name, they read or write that field through a `Value`. `set()` converts the incoming `Value` to the field's declared type when it doesn't already match (via [`Value::convertInPlace()`](value.md)), and reports whether the value actually *changed* (compared with the field's previous content) - the same "did this actually change" signal that recurs in [`Context`](context.md) and [`Parameterized`](parameterized.md), used here to decide whether a device/model/instance needs to redo setup work after a parameter write. `given()` always reports "yes" by default - plain struct fields have no notion of "was this explicitly provided," unlike a parsed netlist parameter, which does (see [`PTParameterValue`/`PTParameterExpression`](ptparametervalue.md)).

`memberRef<Tsub>(st, ndx)` is the mechanism underneath all of this: `reinterpret_cast`ing `(char*)&st + offset` to `Tsub*`. This is a raw memory-layout hack, not virtual dispatch - it works because `offsetof` and the registered `StructMember::Type` are guaranteed (by the `registerMember` macro reading the field's actual `decltype`) to agree with the struct's real layout, and it is exactly what makes introspected access essentially free of per-field overhead: no switch, no function pointer, just pointer arithmetic to the field and a typed read/write.

`memberEqual()` compares one field of two struct instances by dispatching on its registered type - used where two whole structs need a field-by-field "did anything change" comparison rather than a single field's.
