# Identifiers

An `Id` is an interned name: a name string is stored once, and every `Id` referring to it is a cheap 32-bit index into a table rather than a copy of the string. This makes comparing and hashing identifiers (node names, parameter names, master names, and the like, which are compared constantly during elaboration and simulation) an integer operation instead of a string operation. `Id` is declared and fully implemented inline in `identifier.h`, with the out-of-line pieces in `identifier.cpp`.

## Layout

An `Id` wraps a single `IdentifierIndex` (`uint32_t`, from `common.h`). The 32-bit index space is split into two ranges that grow toward each other:

- **Ordinary identifiers** occupy `[1, nextOrdinary)`. `nextOrdinary` starts at 1 and is incremented each time a new ordinary name is interned. Ordinary identifiers are created by the public `Id` constructors (`Id(const std::string&)`, `Id(std::string&&)`, `Id(const char*)`) and are the ones produced while parsing a netlist.
- **Static identifiers** occupy `(nextStatic, maxValidIdentifierIndex]`, where `maxValidIdentifierIndex` is `UINT32_MAX`. `nextStatic` starts at `maxValidIdentifierIndex` and is decremented each time a new static name is interned. Static identifiers are created with `Id::createStatic()` and are meant for names fixed at compile time, such as builtin constant names (`M_PI` in `context.cpp`) or homotopy option names (`Homotopy::gdev` and friends in `homotopy.cpp`).
- Index `0` (`bad`) is reserved and never assigned to a real name; it is the value behind the default-constructed `Id` and `Id::none`.

Interning either kind of identifier past the point where the two ranges would meet throws `std::length_error("Too many identifiers.")`.

Because the ranges never overlap and `nextOrdinary` only grows while `nextStatic` only shrinks, a bare index comparison `id_ < nextStatic` (using the *current* `nextStatic`) is enough for `c_str()` to tell which range an id belongs to and thus which table to look it up in - no extra tag bit is needed.

## Interning

All construction funnels through `Id::constructorHelper(s, n, makeStatic)`:

1. Look up `s` in `nameToIndex()`, a single `unordered_map<const char*, IdentifierIndex>` shared by both ordinary and static names, hashed and compared by string content (`CstrHash`/`CstrEqual`), not by pointer.
2. If found, reuse the existing index. Requesting `makeStatic` for a name that already exists as an ordinary identifier (`id_ < nextOrdinary`) throws `std::logic_error`, since a name cannot retroactively become static once it has been interned as ordinary.
3. If not found, claim the next index from the appropriate end of the range, copy the string (plus its length) into the matching pool - `Id::stringPool()` for ordinary names, `Id::staticStringPool()` for static names, both [`CStringPool`](pool.md)s - and record the pool-owned `const char*` in `indexToName()`/`staticIndexToName()` (a `std::deque<const char*>`, indexed by ordinary index or by `maxValidIdentifierIndex - index` for static) and in `nameToIndex()`.

The `CStringPool` gives each unique name a single, stable, permanently-owned copy: `c_str()` can hand out that pointer directly without any lifetime concerns, and the pool never needs to free individual strings because identifiers live for the duration of the program (static ones) or of the run (ordinary ones).

`nameToIndex()`, `indexToName()`, `staticIndexToName()`, `stringPool()`, and `staticStringPool()` are function-local statics rather than plain static members. This guarantees they exist before any `Id` (including other file-scope static `Id`s such as `Homotopy::gdev`) is constructed, regardless of static-initialization order across translation units.

## Static identifiers and load order

`Id::createStatic()` should only be called from static-linkage global variables in the simulator itself, never from a dynamically loaded module (an OSDI device or plugin). Static identifiers are meant to be registered before any ordinary identifier exists, claiming the low static-index end of the range while it is still empty. A module loaded at runtime is initialized after the netlist has already started producing ordinary identifiers, so a name it tries to register as static may already exist as an ordinary identifier - triggering the conflict check in step 2 above.

## Other operations

- `id()` returns the raw `IdentifierIndex`.
- `operator std::string()` and `c_str()` recover the name.
- `operator bool()` is `false` only for `Id::none` (index `0`).
- `operator==`/`operator!=` compare the raw index.
- `std::hash<Id>` (specialized in namespace `std`) hashes the raw index, so `Id` can be used directly as an unordered-map/-set key without a custom hasher at each use site.
- `std::hash<std::pair<Id, Id>>` is likewise specialized, combining both ids' raw indices via [`hash_val()`](hash.md) - used for maps keyed on an id pair.
- `dump(os, details=false)` writes the ordinary [`CStringPool`](pool.md)'s contents to `os`, for debugging what has been interned.
- In `SIMDEBUG` builds, `Id` carries an extra `const char* str` member set to the interned string on construction, purely so a debugger can show the name next to an otherwise-opaque index; it does not exist in release builds.
