# Instance and Model Base Classes (Builtin Devices)

`devbuiltin.h` is the generic template framework hand-coded (non-OSDI) device types are built from: `BuiltinDevice<ModelParams, InstanceParams, InstanceData>`, `BuiltinModel<...>`, and `BuiltinInstance<...>`, each a template implementation of [`Device`/`Model`/`Instance`](devbase.md). Writing a new builtin device means writing three plain structs and specializing a handful of `*Core()` methods for that specific instantiation - not writing a hand-rolled `Device`/`Model`/`Instance` subclass.

## The three type parameters

- **`ModelParams`** - the device type's model-level parameters, reflected via [`Introspection`](introspection.md)/[`IStruct`](parameterized.md) (`BuiltinModel` owns an `IStruct<ModelParams> params`).
- **`InstanceParams`** - the instance-level (settable) parameters, similarly reflected (`BuiltinInstance` owns `IStruct<InstanceParams> params`).
- **`InstanceData`** - an instance's internal working state and output variables, reflected the same way (`IStruct<InstanceData> data`) so its fields double as `save`-able output variables (`outvarCount()`/`getOutvar()` on `BuiltinInstance` simply forward to `data`'s introspection).

Because all three are ordinary structs registered through the same reflection mechanism, `BuiltinDevice::dump()` can print a device's node names, model parameters, instance parameters, and output variables generically, without any device-specific code.

## Wrapper/Core split

Every setup-related pipeline stage from [`Device`](devbase.md) has a `*Wrapper()` and a `*Core()` version, and the wrapper always calls the corresponding `*Core()` for the actual per-device-type logic. The two wrappers are not symmetric: `BuiltinModel::setupWrapper()` checks `force || checkFlags(Flags::NeedsSetup)` and skips `setupCore()` (and leaves the model's own `NeedsSetup` flag untouched) when neither holds; when it does run `setupCore()`, it forces every one of the model's instances to re-run their own setup afterwards. `BuiltinInstance::setupWrapper()`, by contrast, unconditionally calls `setupCore()` on every invocation and unconditionally clears the instance's `NeedsSetup` flag on success - it does not itself gate on `force`/`NeedsSetup` the way [`OsdiInstance::setupWrapper()`](osdiinstance.md) does. `setupCore()`, `preAnalysisCore()`, and `collapseNodesCore()` have harmless do-nothing defaults (most devices don't need them); `setStaticTolerancesCore()`, `populateStructuresCore()`, `bindCore()`, `evalCore()`, and `loadCore()` have no default and must be specialized by every concrete builtin device.

## Specialization instead of virtual dispatch

Per-device-type behavior is provided by explicit template specialization of these `Core()` methods for one specific `BuiltinInstance<...>`/`BuiltinModel<...>`/`BuiltinDevice<...>` instantiation (declared in that device's header, defined in its `.cpp` - see [Independent Sources](devvisrc.md), [Controlled Sources](devctlsrc.md)), not by overriding a virtual function in a subclass. The header's own comment states the reason directly: the API is designed to avoid virtual method calls during the mapping/setup/eval/load hot path. Since a `BuiltinInstance<ModelParams, InstanceParams, InstanceData>` for a given device is a single concrete type once its three type parameters are fixed, the compiler can inline straight through to the specialized `Core()` body instead of going through a vtable.

## Construction and terminal/node setup

`defineInternals()` (device-specific, empty by default) and `buildInternals()` (generic) run once when a `BuiltinDevice` is constructed: `defineInternals()` is where a device type populates `terminalCount`/`nodeIds` (its fixed terminal and internal-node name list), and `buildInternals()` then builds `nodeNameMap` from it - the name-to-index lookup `BuiltinInstance::nodeIndex()` uses.

`BuiltinInstance`'s dangling-terminal policy (noted directly in the header) is to *require* every terminal to be explicitly connected - `verifyTerminalsConnected()` fails if `connectedTerminalCount` falls short of the device's `terminalCount`. Contrast this with [`OsdiInstance`](osdiinstance.md), which instead auto-connects any unconnected terminal to an internal node.

## Instance creation

`BuiltinModel::createInstance()` (called from `Circuit`'s hierarchy-building code, ultimately from the [parser](dflparser.md)-produced `PTInstance`) binds each parsed terminal connection to a node (translating names through the parent instance first), sets the instance's parameters via [`Parameterized::setParameters()`](parameterized.md) (wiring a fresh `RpnEvaluationNetlistContext` with `MCData::CtxType::Instance` for Monte Carlo generator identity, see [the netlist context](rpnevalctx.md)), and then calls `buildHierarchy()` - a no-op for an ordinary builtin device, since none of them are hierarchical.
