Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

BIR Value Model — Argument, AccessPattern, Immediate, Register

All symbols, addresses, struct offsets, and vtable VAs on this page apply to neuronx_cc 2.24.5133.0+58f8de22 (cp310), library neuronxcc/starfish/lib/libBIR.so (md5 12bb979f7ca41248252abb0f16b2da98, ELF64, stripped .symtab/rich .dynsym). VA == file offset for .text (base 0x1820c0) and .rodata (base 0x708000); .data.rel.ro is VMA − 0x1000. The cp311/cp312 wheels share the ABI but drift the VAs. Provenance: reports D-E14, D-E12, D-D12, D-R10.

Abstract

Every operand a BIR Instruction references — a tensor access, a scalar literal, a register lane — is a heap-allocated bir::Argument subclass object, not an inline field and not an index into a flat array. An instruction owns three intrusive circular doubly-linked lists keyed by role (inputs / indirection / outputs), and each node on those lists is a polymorphic Argument whose C++ class encodes what it binds to. The class is selected on the wire by a "kind" string (eight live spellings) and recorded in-memory as an ArgumentKind integer at Argument+0x18. This page is the in-memory value model: the Argument base header, the three concrete families that derive it (AccessPattern, the ArgValue/immediate family, and RegisterArgument), their byte layouts, and the single createFromJson dispatch that builds them.

The shape will be familiar to anyone who has read an LLVM Value/Use graph, with two deliberate departures. First, the discriminator is doubled: the wire carries a "kind" string, but the in-memory object carries an ArgumentKind ordinal set by the leaf constructor — there is no string2ArgumentKind, so the two are bridged only inside createFromJson. Second, the use-def edge is not a separate Use list: an Argument registers itself directly as a reader or writer on its bound StorageBase (a MemoryLocation, MemoryLocationSet, or Register) via setLocation, and the output-vs-input distinction that drives reader-vs-writer comes from the operand's role list, not from the operand class. Immediates bind to nothing and contribute no edge.

Three things a reimplementer must get right and that the naive reading gets wrong: (1) AccessPattern is multiple-inheritancebir::Argument at offset 0 plus a logging::SrcHandle vptr at +0x48 — so the AP's own data resumes at +0x50, not +0x48; (2) ImmediateArray derives Argument directly, skipping the ArgValue shim that its siblings use; (3) RegisterSet (kind 12) is a declared, RTTI-present class whose constructor is a hard assert(false) — the enumerator exists but no live RegisterSet operand can be built, and there is no "register_set" wire spelling. This page documents the AP family at the value-object level only; the on-wire silicon descriptors (ADDR4 / TENSOR4D / MXMEM_PATTERN1D) are Part 2 and the codegen-side abstract AP builders are 7.22 — both cross-referenced, not duplicated.

For reimplementation, the contract is:

  • The Argument base header (8 fields, 0x48 bytes) and the ArgumentKind discriminator {1,2,3,6,7,8,11,12} with its five reserved gaps.
  • The createFromJson 8-way string dispatch, the per-leaf allocation sizes, and how role (InstructionArgumentType) threads the node onto one of three lists.
  • The three value families' byte layouts: AccessPattern (MI base + two SmallVector<APPair,4>), the immediate family (ImmediateValue/SymbolicImmediateValue/ImmediateArray), and the register family (RegisterAccess, plus the RegisterAccessPattern dynamic AP).
  • The use-def model: setLocationregisterReader/registerWriter on the bound StorageBase, and why immediates skip it.
Polymorphic rootbir::Argument (introduces the vptr; RTTI _ZTI @ 0x8fbc88, vtable 0x8fbcf0)
Base ctor0x2320c0Argument(this, ArgumentKind, Instruction*); throws on NULL parent
DiscriminatorArgumentKind @ Argument+0x18 (int); live {1,2,3,6,7,8,11,12}; gaps {0,4,5,9,10}
Dispatchbir::Argument::createFromJson 0x235500 (thunk 0x17e760) — 8 "kind" spellings
Role enumbir::InstructionArgumentType {0 Argument, 1 IndirectionArgument, 2 Output}
Role listsInstruction+0xA0 / +0xB0 / +0xC0 (intrusive circular, insertion-ordered)
Concrete leaves8: {Physical,Symbolic,Register}AccessPattern, Immediate{Value,Array}, SymbolicImmediateValue, RegisterAccess (+ RegisterSet stub)

The Argument Hierarchy

Purpose

bir::Argument is the abstract base of the entire operand/value family — one of the four independent polymorphic roots of the non-Instruction BIR types (the others being StorageBase, pelican::Expr, and sync::SyncRef; D-R10 §0). It contributes the vtable (its sole RTTI base, llvm::ilist_node<Argument>, has no vtable of its own, so Argument introduces the polymorphism), the intrusive list links that thread an operand onto its instruction, the ArgumentKind discriminator, the back-pointer to the parent instruction, and the StorageBase* location that carries the use-def binding for the kinds that have one.

Class Tree

Three intermediate bases sit under Argument, each opening a family; eight concrete leaves hang off them. Every edge below is a confirmed _ZTI base relocation (D-R10 §1a):

bir::Argument                       (ABSTRACT root — introduces vptr; vtable 0x8fbcf0)
 ├─ bir::AccessPattern              (ABSTRACT — 2 __cxa_pure_virtual; __vmi MI:
 │     │                             Argument@0 + logging::SrcHandle@0x48)
 │     ├─ PhysicalAccessPattern      kind 1   "physical_ap"           sizeof 0x1E0
 │     ├─ SymbolicAccessPattern      kind 2   "symbolic_ap"/"symbolic_pwap"  0x1B8
 │     └─ RegisterAccessPattern      kind 3   "register_ap"           sizeof 0x118
 ├─ bir::ArgValue                   (ABSTRACT shim — adds no field/virtual)
 │     └─ ImmediateValue             kind 6   "imm_value"             sizeof 0x50
 │          └─ SymbolicImmediateValue kind 7  "symbolic_imm_value"    sizeof 0xD0
 ├─ bir::ImmediateArray             kind 8   "imm_array"   sizeof 0x98  (⚠ derives Argument DIRECT)
 └─ bir::RegisterArgument           (ABSTRACT shim)
       ├─ RegisterAccess             kind 11  "register_access"       (no own field)
       └─ RegisterSet                kind 12  (STUB — ctor assert(false); no wire spelling)

QUIRK — createFromJson/toJson are vtable slots in the Argument family (slots 4/5 on the concrete leaves; D-R10 §1b), unlike the Instruction family where createFromJson is a static factory and not virtual. A reimplementer who models all BIR deserialisers uniformly as static factories will mis-shape the operand layer: an Argument round-trips through its own vtable, dispatched by the live object's class, which is exactly why the base Argument::toJson (0x2352f0) is a thin virtual thunk that lands in the leaf.

The ArgumentKind Discriminator

ArgumentKind is an int at Argument+0x18, written by the base constructor from the ordinal the leaf passes up the chain. The ArgumentKind2string body (0x233ae0, Argument.cpp:303) handles exactly eight ordinals and asserts on the rest:

OrdClassHow the ordinal is setConfidence
0(reserved)hits assert in 2stringCERTAIN (gap)
1PhysicalAccessPatternctor @0x3ae620AccessPattern(this, 1, inst)CERTAIN
2SymbolicAccessPatternctor @0x3d4460AccessPattern(this, 2, inst)CERTAIN
3RegisterAccessPatternctor @0x3c4fd0AccessPattern(this, 3, inst)CERTAIN
4(reserved)hits assertCERTAIN (gap)
5(reserved)hits assertCERTAIN (gap)
6ImmediateValuector @0x29ed00ArgValue(this, 6, inst)CERTAIN
7SymbolicImmediateValuector @0x29f010ImmediateValue(this, inst, 7)CERTAIN
8ImmediateArrayctor @0x29f890Argument(this, 8, inst)CERTAIN
9(reserved)hits assertCERTAIN (gap)
10(reserved)hits assertCERTAIN (gap)
11RegisterAccessctor @0x3c7de0RegisterArgument(this, 11, inst)CERTAIN
12RegisterSetctor @0x3c7f80RegisterArgument(this, 12, inst) (STUB)CERTAIN

NOTE — there is no string2ArgumentKind. The ordinal is never serialised as an enum name; the wire discriminator is the "kind" string (§dispatch below). The ordinal exists only in-memory, set by the leaf ctor, read by the abstract-AP guards (e.g. tryGetLocation only calls the location vtable slot when kind-1 <= 2, i.e. for AP kinds 1/2/3; 0x232210). The five gap ordinals {0,4,5,9,10} are reserved enumerators with no class and no string — historical or forward-reserved, unrecoverable from this build.

Base Struct — bir::Argument (0x48 bytes)

Byte-exact from the constructor at 0x2320c0 (every store witnessed in the decompiled body; CONFIRMED). Subclass fields begin at +0x48.

FieldOffTypeMeaningConf
vtable+0x00qwordoff_8FBD00 (base) — overwritten per leafCERTAIN
list.next+0x08ptrintrusive role-list next (=0 at ctor, live once threaded)CERTAIN
list.prev+0x10ptrintrusive role-list prev (=0 at ctor)CERTAIN
kind+0x18intArgumentKind discriminator {1,2,3,6,7,8,11,12}CERTAIN
parent+0x20Instruction*owning instruction — ctor throws runtime_error("NULL Parent Instruction for Argument!") if NULLCERTAIN
uniqueId+0x28intNamedObject<Instruction,BasicBlock>::getUniqueId(parent)CERTAIN
isOutput/isWriter+0x2Cbytea3 of setLocation — 1 ⇒ writes its storageCERTAIN
dtype+0x30intper-operand Dtype (set by setType, vtable slot +16)CERTAIN
location+0x38StorageBase*bound MemoryLocation/Set/Register (use-def); =0 at ctorCERTAIN
isActive/registered+0x40bytea4 of setLocation — 1 ⇒ registered on storageCERTAIN
// bir::Argument::Argument(this, kind, parent)            // 0x2320c0 — CONFIRMED byte-exact
this[+0x08] = 0;  this[+0x10] = 0;        // list links empty
this[+0x00] = off_8FBD00;                 // base vtable
this[+0x18] = kind;                       // ArgumentKind ordinal (leaf-supplied)
this[+0x20] = parent;                     // owning Instruction
this[+0x2C] = 0; this[+0x38] = 0; this[+0x40] = 0;   // isOutput / location / active
if (parent == NULL)                       // NO operand may be parentless
    throw runtime_error("NULL Parent Instruction for Argument!");
this[+0x28] = parent->NamedObject::getUniqueId();      // stable per-instruction id

NOTE — dtype at +0x30 sits inside the base footprint (between uniqueId and location) but is not written by the base ctor — it is set later by Argument::setType (0x20fde0, *(int*)(this+0x30) = dtype), the vtable-slot-16 virtual. Value, register, and AP operands all carry a per-operand element Dtype here on the wire "dtype" key; the immediate family additionally uses it to width-select its payload (§immediates).


The createFromJson Dispatch

Purpose

bir::Argument::createFromJson(Instruction*, InstructionArgumentType, const json&) (0x235500, thunk 0x17e760) is the single deserialiser entry point: it reads the "kind" string, builds the matching leaf, fills its fields, and threads it onto the role list selected by the InstructionArgumentType argument. The per-instruction readFieldsFromJson calls it once per operand record, passing Argument for inputs, Output for results, IndirectionArgument for index operands — this is the bridge from the JSON args/outputs/indirection arrays to the three in-memory lists.

The Role Enum and the Three Lists

bir::InstructionArgumentType (0x2e4a80) has exactly three values; an Instruction owns one intrusive circular doubly-linked list head per value (initialised empty, next==prev==&head; D-D12 §5.2):

RoleOrdList headqword idxJSON section
Argument0Instruction+0xA0+20args (inputs)
IndirectionArgument1Instruction+0xB0+22indirection / gather-scatter index
Output2Instruction+0xC0+24outputs (results)

The attach routine addArgumentIndirectionArgumentOrOutput<T>(inst, role) (template; T=PhysicalAccessPattern body @0x237780) is new T(inst) followed by a tail-append onto the head picked by role — so operand order within a list is insertion order, and inputs / outputs / indirection-args are physically separate lists, never one interleaved array.

Algorithm

function Argument::createFromJson(inst, role, j):          // 0x235500
    kind = j["kind"];                                       // wire discriminator (string)
    assert(kind in {                                        // Argument.cpp:101 — 8 spellings
        "physical_ap","symbolic_ap","symbolic_pwap","register_ap",
        "imm_value","symbolic_imm_value","imm_array","register_access"
    }) || fail("Unsupported argument kind");

    switch (kind):
      // ---- AccessPattern family: honour role, set use-def edge ----
      case "physical_ap":                                   // kind 1
          ap = add<PhysicalAccessPattern>(inst, role);
          loc = Function/Module.getMemoryLocationByName(j["memref"]);
          ap.setLocation(loc, /*isOutput=*/ role==Output, /*active=*/1);
      case "symbolic_ap": case "symbolic_pwap":             // kind 2 (two spellings, one class)
          ap = add<SymbolicAccessPattern>(inst, role);      // new(0x1B8)
          ap.setLocation(getMemoryLocationSetByName(j["memsetref"]), role==Output, 1);
      case "register_ap":                                   // kind 3
          ap = add<RegisterAccessPattern>(inst, role);      // new(0x118)
          ap.setLocation(getMemoryLocationSetByName(j["memsetref"]));
          ap.setRegLocation(getRegisterByName(j["regref"]));         //  +0xE8
          if (j has "reg_ap_offset")   ap.setRegAPOffset(...);       //  +0xF0 (Argument.cpp:165)
          if (j has "reg_sub_tensor_id") ap[+0xF8] = getRegisterByName(...);  // Argument.cpp:171

      // ---- Immediate family: IGNORE role, always plain input (+0xA0) ----
      case "imm_value":                                     // kind 6
          v = operator new(0x50);  ImmediateValue::createFromJson(v, inst, j);
          thread_onto(inst+0xA0, v);                        // inline, never Output/Indirection
      case "symbolic_imm_value":                            // kind 7
          v = operator new(0xD0);  SymbolicImmediateValue::createFromJson(v, inst, j);
          thread_onto(inst+0xA0, v);
      case "imm_array":                                     // kind 8
          v = operator new(0x98);  ImmediateArray::createFromJson(v, inst, j);
          thread_onto(inst+0xA0, v);

      // ---- Register family ----
      case "register_access":                               // kind 11
          ra = add<RegisterAccess>(inst, role);
          ra.from_json(j);                                  // reads "dtype" only
          ra.setLocation(getRegisterByName(j["regref"]), role==Output, 1);   // isa<Register>
    return ref;

GOTCHA — the three immediate kinds (6/7/8) ignore the role argument entirely. They thread directly onto the input list at Instruction+0xA0 regardless of the JSON section they appeared in — an immediate is always a plain Argument, never an Output or IndirectionArgument. Only the AP and register kinds forward role==Output into setLocation to register as a writer. A reimplementer who routes every operand through the role-selected list will silently mis-place an immediate that the producer emitted in an outputs array.

The dispatch-string assert and the four operator new sizes are byte-witnessed in the decompiled body: line 235 carries the verbatim 8-spelling assert string, and the new(0x50) / new(0xD0) / new(0x98) / new(0x1B8) allocations appear at lines 748 / 758 / 768 / 881 (CONFIRMED).


The AccessPattern Family

Purpose

An AccessPattern is the operand that binds an instruction to a tensor access — a (step, num)-pair stride descriptor over a MemoryLocation (physical) or MemoryLocationSet (symbolic / register-offset), plus a logical shape. It is the abstract base of the three AP leaves; it is hard-abstract, carrying two __cxa_pure_virtual slots (getLocation, getMemoryLocationSet) that each concrete leaf fills (D-R10 §1b).

Multiple Inheritance and the +0x50 Data Start

AccessPattern is a __vmi (multiple-inheritance) class with two public bases (__vmi_class_type_info @ 0x8fb348, base_count 2; CONFIRMED):

bir::AccessPattern : public bir::Argument        (primary base, offset 0)
                     public logging::SrcHandle    (secondary base, offset +0x48 → public)

The logging::SrcHandle subobject is an 8-byte vptr at +0x48 (a source-location/diagnostic handle, no further data mapped here). Consequently the AP's own data resumes at +0x50, not +0x48. The _ZThn72_ (thn72 = 0x48) non-virtual dtor thunk confirms the 0x48 subobject offset.

CORRECTION (D-E14→D-E12/D-R10) — an early read of the operand model (D-E14 §1) described Argument as a flat 0x48-byte header with subclass fields beginning immediately at +0x48. For the immediate and register families that is correct (they are single-inheritance, no SrcHandle). For the AP family it is not: the +0x48 slot is the logging::SrcHandle secondary-base vptr, and the AP's first own field (Pattern.data) is at +0x50. D-E12/D-R10 pin the MI via the typeinfo relocs; treat +0x50 as the AP data start.

Abstract Base Struct — bir::AccessPattern (0xD0 bytes)

Constructor 0x2019b0 (AccessPattern(this, ArgumentKind, Instruction*)). Two llvm::SmallVector<…,4> with inline storage, both initialised {size=0, cap=4} (the ctor stores 0x400000000 = cap 4 << 32 | size 0):

FieldOffTypeMeaningConf
(Argument base)+0x000x48§base — kind/parent/dtype@+0x30/location@+0x38CERTAIN
SrcHandle.vptr+0x48qwordlogging::SrcHandle secondary-base vptrCERTAIN
Pattern.data+0x50APPair*the (step,num) stride vector data ptr (inline → +0x60)CERTAIN
Pattern.size+0x58uint32num_dims (1..4)CERTAIN
Pattern.cap+0x5Cuint32SmallVector cap (init 4)CERTAIN
Pattern.inline[4]+0x60APPair[4] (0x40)inline (step,num) buffer, canonical [W,Z,Y,X]CERTAIN
access_shape.data+0xA0uint64*logical per-dim shape vector (inline → +0xB0)CERTAIN
access_shape.size/cap+0xA8uint32×2shape length / cap 4CERTAIN
access_shape.inline[4]+0xB0uint64[4] (0x20)inline shape bufferCERTAIN
(base end)+0xD0leaves resume hereCERTAIN

APPair is a 16-byte POD (no RTTI): step @ +0x00 (signed int64), num @ +0x08 (int64). The canonical axis order is [W,Z,Y,X] = Pattern[0..3], with Pattern[0] the outermost/partition dim — getStepBytesPerHighestDim (0x203170) reads Pattern[0].step and asserts >= 0. setPattern (0x211d40) copies an external SmallVector<APPair> in, striding 16 bytes per entry.

The Three Leaves

PhysicalAccessPattern  kind 1  "physical_ap"   0x1E0  → MemoryLocation (compile-time AP)
SymbolicAccessPattern  kind 2  "symbolic_ap"   0x1B8  → MemoryLocationSet (pelican::Expr step/num)
RegisterAccessPattern  kind 3  "register_ap"   0x118  → MemoryLocationSet + Register (dynamic AP)

PhysicalAccessPattern (0x1E0, ctor 0x3ae620) appends to the 0xD0 base: offset @ +0xD0 (uint64, the start byte-offset → wire "offset"; setter 0x3a2aa0), a +0xD8..+0x1D0 cached-address-interval region (derived state, memset 0 in ctor, guarded by the +0x1D0 spinlock), and a DynamicAPINFO* @ +0x1D8 (=0 unless the AP is an indirect/gather access → wire "dynamic_ap_info"). Its to_json (0x4869b0) emits kind/ap/offset/dtype/memref/memsetref/access_shape/dynamic_ap_info/memref_owner_func.

SymbolicAccessPattern (0x1B8, ctor 0x3d4460) carries the symbolic step/num as pelican::QuasiAffineExpr trees (32-byte wrappers: RefPtr<pelican::Expr> @ +0x00, SmallVector<LoopAxis*> @ +0x08) rather than integers. Its persistent fields: tensor_indirect_arg_id @ +0xD0 (int, init 0xFFFFFFFF), num_tensor_indirect_indices @ +0xD4, and the addrs vector of QuasiAffineExpr @ +0xE0 (appended by addAffineExpr 0x3d6640). Fields +0x100/+0x130/+0x178 are evaluation caches (predicates / shard-in APs / evaluated concrete APs — derived, not serialised). Two wire spellings ("symbolic_ap", "symbolic_pwap") map to this one class.

RegisterAccessPattern (0x118, ctor 0x3c4fd0) is a dynamic AP: the start address or a per-dim offset is supplied at runtime by a Register, not a compile-time constant. Concrete fields and wire keys (from to_json 0x487090; CONFIRMED field→key):

FieldOffTypeWire keyConf
regLoc+0xE8Register*"regref" (setter setRegLocation 0x3c5040)CERTAIN
RegAPOffset_+0xF0Register*"reg_ap_offset" (if non-0; setter 0x3c6520, asserts numPhysicalRegs match)CERTAIN
reg_sub_tensor_id+0xF8Register*"reg_sub_tensor_id" (if non-0)CERTAIN
const_ap_offset+0x100qword"const_ap_offset" (static fall-through; init 0)CERTAIN
const_sub_tensor_id+0x108int"const_sub_tensor_id" (init -1)CERTAIN
is_regloc_offset+0x114byte"is_regloc_offset" — register holds offset directlyCERTAIN

NOTE — this page maps the AP value-object only. The abstract (stride,size) AP that the Penguin→BIR codegen builds before it becomes one of these C++ objects is BirCodeGenLoop Access-Pattern Builders (7.22); the on-wire silicon descriptors the encoder lowers these to are ADDR4, Tensor4D / MemPattern4D, and Indirect Descriptors (Part 2). The pelican::QuasiAffineExpr/Expr internals held by symbolic APs are Affine Expression Algebra.


The Immediate / Value Family

Purpose

The immediate family carries inline literals and symbolic expressions — operands that bind to nothing external (location @ +0x38 stays null, no use-def edge). ArgValue is an abstract shim (D-R10 §1b: 4-slot vtable, adds no field or virtual) whose only purpose is to group ImmediateValue and its symbolic subclass; ImmediateArray deliberately bypasses it.

ImmediateValue (kind 6, 0x50, ctor 0x29ed00)

A scalar literal: dtype @ +0x30 (Argument base slot), an 8-byte value union @ +0x48. Both fields are pinned by the symmetric createFromJson (0x2a7000, read) / dataToJson (0x2a2c10, write), which share one Dtype switch. The wire "value" width is keyed by the Dtype ordinal (D-D04 table):

Dtype ordinal → storage width @ +0x48
  0  uint8  / 1  int8                    → byte
  0xA uint16 / 0xC bf16 / 0xD fp16       → ushort      (0xB int16 → short)
  0xE uint32 / 0xF int32                 → dword
  0x10 float32                           → dword via SerializableFloat
  0x12 uint64 / 0x13 int64               → qword
  DEFAULT (fp8/fp4 variants 3–9, 0x11 float32r) → ASSERT "unsupported dtype"
                                          (ImmediateValue.cpp:188)

GOTCHA — immediates carry only standard int/float scalars on the wire. The fp8/fp4 variants (Dtype 3–9) and float32r (0x11) hit a hard assert in both createFromJson and dataToJson (ImmediateValue.cpp:188/:226). ImmediateValue::castTo (0x2a0ac0) can internally convert to fp8 dtypes, but the result is never wire-deserialised — a reimplementer must not emit an imm_value with one of those dtypes.

SymbolicImmediateValue (kind 7, 0xD0, ctor 0x29f010)

A 3-level chain (→ ImmediateValue → ArgValue → Argument). It carries a pelican::QuasiAffineExpr (a symbolic affine expression) at +0x68, built from Instruction::getPelicanContext, alongside the folded scalar inherited from ImmediateValue. The expr's coefficient set heads are self-referential (+0xB8/+0xC0 = this+168, an empty intrusive AffineIdx→coeff map at ctor). The kind-7 ordinal is byte-witnessed: the ctor calls ImmediateValue::ImmediateValue(this, inst, 7) (CONFIRMED — closes the prior HIGH-only inference on ordinal 7).

ImmediateArray (kind 8, 0x98, ctor 0x29f890)

QUIRK — ImmediateArray derives bir::Argument directly, not ArgValue — confirmed both by the ctor (Argument::Argument(this, 8, inst)) and by the _ZTI reloc (__si → Argument 0x8fbc88, not ArgValue; D-R10 §1a). Its siblings ImmediateValue/SymbolicImmediateValue go through the ArgValue shim; ImmediateArray skips it. A reimplementer modeling the family as a clean ArgValue ⊃ {scalar, array, symbolic} will get the vtable shape wrong.

It is a SmallVector<int64,8> where every element is widened to 8 bytes regardless of dtype:

FieldOffTypeWire keyConf
(Argument base)+0x000x48kind="imm_array"CERTAIN
dtype+0x30int"dtype"CERTAIN
vec.data+0x48int64*"value_array" elems (inline → +0x58)CERTAIN
vec.size+0x50uint32element countCERTAIN
vec.cap+0x54uint32inline cap = 8CERTAIN
vec.inline[8]+0x58int64[8] (0x40)inline bufferCERTAIN

createFromJson (0x2a6080) reads "dtype" then a required "value_array" (missing → ImmediateArray.cpp error), typing each element by dtype and storing it widened to a qword; growth via sub_67B350(…, 8) (elem size 8). It shares the same int/float-only dtype gate as ImmediateValue.


The Register Family

Purpose

The register family references a bir::Register — a StorageBase subclass (a register definition, scoped to the Function and referenced by name, not itself an Argument). RegisterArgument is an abstract shim; RegisterAccess is the only live leaf.

RegisterAccess (kind 11, ctor 0x3c7de0)

A scalar register reference with no new fields beyond the Argument base: dtype @ +0x30, the bound Register* @ +0x38 (the base location). Its own createFromJson (0x3c96f0) reads only "dtype"; the outer Argument::createFromJson "register_access" branch does the name resolution — getRegisterByName(j["regref"]) then setLocation(reg, isOut=role==Output, 1). setLocation (0x3c78d0) asserts isa<Register> (StorageBase class-tag @ +0x110 == 2); getLocation (0x3c7e10) re-asserts it.

FieldOffTypeMeaningConf
(Argument base)+0x000x48kind=11, vtable off_8FFFC8CERTAIN
dtype+0x30intwire "dtype"CERTAIN
regref+0x38Register*bound Register (the base location)CERTAIN

RegisterSet (kind 12, ctor 0x3c7f80) — STUB

GOTCHA — RegisterSet is a declared, RTTI-present class (_ZTI 0x8fff70, vtable 0x900000) whose constructor is a hard __assert_fail("false", RegisterArg.cpp:0x4D). No live RegisterSet operand can be built, its vtable has no createFromJson/toJson slot, and createFromJson has no "register_set" wire spelling among its eight. The ArgumentKind 12 enumerator exists, but the kind is dormant — treat it as RESERVED, like the gap ordinals {0,4,5,9,10}. A reimplementer should declare the class for ABI completeness but never instantiate it.

The Register Definition (binding target)

bir::Register is a StorageBase ⊃ Storage leaf, created at function level (Register::createFromJson 0x3c4860, keyed by name into the Function) and referenced by operands via getRegisterByName. Two fields matter to the value model: the StorageBase class-tag @ +0x110 (== 2 for Register, vs 1 for MemoryLocationSet) is the isa<> discriminator every register-operand guard checks; numPhysicalRegs @ +0x18C (setter 0x287010) is read by RegisterAccessPattern::setRegAPOffset's physical-reg-count match assert. The full StorageBase/Register layout is the storage strand, out of scope here.


The Operand-Binding Model

The five rules that connect an instruction's operands to the values they reference:

  1. Storage / ownership. Operand objects are heap-allocated Argument subclasses owned by the Instruction via three role-keyed intrusive circular lists (+0xA0 input · +0xB0 indirection · +0xC0 output). list.next/prev live at Argument+0x08/+0x10; order within a list is insertion order.

  2. Value-class by ArgumentKind. The class says what it binds to. AP leaves → a MemoryLocation (physical) or MemoryLocationSet (symbolic/register) via location @ +0x38 plus a stride Pattern vector. RegisterAccess/RegisterAccessPattern → a Register (+0x38, or +0xE8 for the reg-AP). The immediate kinds → an inline literal/expr, with location null.

  3. Use-def edge. Argument::setLocation (0x2318f0) wires the operand into the storage's reader/writer set: if isOutput(+0x2C) → StorageBase::registerWriter, else registerReader (only when active, +0x40). This is the SSA-like def-use graph between operands and MemoryLocations/Registers; rebind deregisters first. The output role (role==2 → list +0xC0) drives isOutput=true → writer. Immediates never call setLocation → no def-use edge.

  4. Dtype. Per-operand element type @ Argument+0x30, set via setType (vtable slot 16), carried on the wire "dtype" key by value, register, and AP operands.

  5. Wire ↔ C++. The per-instruction readFieldsFromJson iterates the JSON args/indirection/outputs arrays, calling Argument::createFromJson(inst, role, record) once per operand; the "kind" string selects the leaf, the leaf fills its fields, the role threads it onto the matching list and (for AP/register) sets the use-def edge. Emit is the symmetric vtable-dispatched toJson per leaf (8 spellings).


Verification Ceiling

What is CONFIRMED (byte-witnessed in the decompiled/disasm sidecars of libBIR.so):

  • The Argument base ctor's eight field stores at 0x2320c0 (offsets +0x08/+0x10/+0x18/+0x20/+0x28/+0x2C/+0x38/+0x40) and the NULL-parent throw.
  • The eight-spelling "kind" assert string verbatim and the four operator new sizes (0x50/0xD0/0x98/0x1B8) inside createFromJson (0x235500).
  • The ArgumentKind discriminator ordinals and their leaf-ctor witnesses (kind 7 now byte-exact via ImmediateValue::ImmediateValue(this, inst, 7)).
  • The full RTTI inheritance graph (12 typeinfos, the AccessPattern __vmi MI, ImmediateArray-direct-Argument, RegisterSet no-factory) from D-R10 base relocs.

What is STRONG but frame-limited: the RegisterAccessPattern/PhysicalAccessPattern to_json wire-key→offset mappings are pinned in D-E12/D-E14 from the real serialiser body, but in this corpus the serialiser sidecars resolve to their thunk frame (0x181570) which tail-calls the un-named real body at 0x487090/0x4869b0 — the two-VA-frame artifact. The keys are cross-checked via ctor⇄setter⇄serialiser agreement, not re-read line-by-line here.

What is INFERRED / not pinned: the exact APPair internal field names (mapped as {step:i64, num:i64} by stride and the getStepBytesPerHighestDim read, but not name-witnessed); the SymbolicImmediateValue QuasiAffineExpr coefficient encoding (structural, not element-by-element); the SymbolicAccessPattern cache regions +0x100/+0x130/+0x178 (role-inferred, element types MEDIUM); the logging::SrcHandle subobject's own data beyond its vptr (none mapped). No NEFF/BIR-JSON fixture byte-diff was available to round-trip any of this against a real serialised operand.


NameRelationship
bir::InstructionOwns the three role-keyed operand lists; createFromJson is the caller of Argument::createFromJson
bir::StorageBase / MemoryLocation / RegisterThe binding targets; setLocation registers the use-def edge on them
pelican::QuasiAffineExpr / ExprHeld by SymbolicAccessPattern.addrs and SymbolicImmediateValue; the symbolic step/num/value
DynamicAPINFOPhysicalAccessPattern+0x1D8 indirection/gather companion ("dynamic_ap_info")

Cross-References