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

pelican::Expr Core: Affine / Binary Nodes & Member Byte-Offsets

All addresses and offsets on this page apply to neuronx_cc 2.24.5133.0+58f8de22, the cp310 build of libBIR.so (md5 12bb979f7ca41248252abb0f16b2da98, sha256 2387d0d4…dc5a4cfa, 22151 functions). The cp311/cp312 builds carry the identical ABI but their virtual addresses drift; member offsets, object sizes, and kind tags are version-stable.

Abstract

pelican::Expr is the polymorphic root of Penguin's quasi-affine expression algebra — every tensor index, address term, stride, division, and predicate atom is an Expr subtree. The shape of that algebra (c + Σ cᵢ·idxᵢ with floordiv/mod locals, plus the collective-cyclic family) is documented from the Penguin side in Penguin AffineExpr Algebra, and the LLVM-RTTI'd class tree — 29 typeinfo records, 39 vtable slots — is the remit of the pelican::Expr hierarchy page. This page is the third leg: the byte-level struct layout of the affine/binary core nodes — the universal object header that every node shares, the BinaryExpr two-field payload, the AffineExpr/SumExpr compound bodies, and the integer kind tags that discriminate them.

The central reverse-engineering finding is that the whole hierarchy is built on two shared constructors and one fixed header. pelican::Expr is a RefCountedObject; its base constructor sub_5FE310 lays down a four-field header — vptr@0x00, refcount@0x08, kind@0x10, ctx@0x18 — identical for every node. Binary nodes (MultExpr, FloorDivExpr, ModuloExpr, CCDivExpr, CCModExpr) chain through a second base constructor sub_60F4B0 that appends operand@0x20 and intval@0x28. Compound nodes (AffineExpr, SumExpr) skip the binary base and grow a variable-length operand body at +0x20 — but the two use different containers: AffineExpr holds a boost::container::vector<pair<RefPtr<AffineIdx>, long>> (heap begin/end/cap triple, no inline buffer), while SumExpr holds an llvm::SmallVector<RefPtr<Expr>> with 8 inline slots (see the per-node sections below). No node moves a base field; subclasses only append.

CORRECTION — an earlier draft described the AffineExpr term container as an llvm::SmallVector<pair<RefPtr<AffineIdx>, long>, 16>. That is wrong — only SumExpr (kind 18) uses a SmallVector. AffineExpr (kind 17) uses a boost::container::vector. CONFIRMED from libBIR.so: factory sub_62BCF0 @ 0x62bcf0 zeroes a begin/end/cap triple ([e+0x20]=0; movups [e+0x28],xmm0) with no self-pointer to an inline buffer and no capacity literal — the boost empty-init, not the SmallVector begin = e+0x30; cap = 8 pattern that SumExpr's sub_62C8D0 does show. The populate-from-terms factory sub_635860 @ 0x635860 carries verbatim boost assert strings naming both the element type std::pair<pelican::RefPtr<pelican::AffineIdx>, long int> and the source path boost/container/vector.hpp, and frees the backing store unconditionally (no begin != inline-buffer guard).

A reimplementer who reproduces the header, the two payload shapes, and the kind-tag literals can rebuild any core node.

Every offset below is anchored to a real mov [reg+N] store in a constructor (write side) and, where possible, cross-checked against an accessor or dispatch read (cmp dword [reg+10h], …). There is no pelican::* struct in IDA's type database — the binary carries only ELF/libc types — so this table is the definition.

Reimplementation contract

To rebuild a core node a reimplementer must reproduce:

  • The universal 4-field header {vptr@0, refcount@8, kind@0x10 (int32), ctx@0x18} written by the Expr base ctor, with refcount initialised to 1 and the vptr set to Expr-vtable + 0x10 (later overwritten by the derived vtable).
  • The BinaryExpr payload {operand@0x20, intval@0x28} written by the binary base ctor; the second field doubles as a RefPtr<Expr> operand or a raw long (denominator / modulus), per node.
  • The kind tags 17 (AffineExpr) · 18 (SumExpr) · 23 (MultExpr) · 25 (FloorDivExpr) · 26 (ModuloExpr) · 27 (CCDivExpr) — plus 28 (CCModExpr), 2 (CCGetRankExpr), 3 (IndirectArgExpr) — read as a 4-byte field at +0x10 by every dispatcher.
  • The object sizes: 0x30 for the plain binary nodes, 0x38 for the CC binary nodes (one extra word), 0x40 for AffineExpr, 0x70 for SumExpr.
  • The denom > 0 invariant the div-like factories enforce before publishing the node.
Defining binarylibBIR.so (cp310), 22151 functions
Expr base ctorsub_5FE310 @ 0x5fe310 (30 bytes) — sets header
BinaryExpr base ctorsub_60F4B0 @ 0x60f4b0 (82 bytes) — sets operand/intval
Expr vtable_ZTVN7pelican4ExprE @ 0x90a920; vptr = symbol +0x10 = 0x90a930
BinaryExpr vtable_ZTVN7pelican10BinaryExprE @ 0x90be00; vptr = 0x90be10
Kind tags (core)17 AffineExpr · 18 SumExpr · 23 MultExpr · 25 FloorDivExpr · 26 ModuloExpr · 27 CCDivExpr
Source path (asserts)neuronxcc/pelican/include/pelican/IR/AffineExpr.h

NOTE — the base-class chain (which node derives from BinaryExpr vs CompoundExpr vs Expr) is owned by the hierarchy page. Here it is inferred from construction evidence only: a node that chains through sub_60F4B0 is a BinaryExpr; a node built directly on sub_5FE310 with a vector body is a CompoundExpr. That inference agrees with the typeinfo tree but is restated, not re-derived.

The page proceeds: the universal header (§1), the BinaryExpr payload and the binary nodes (§2), the compound nodes AffineExpr/SumExpr (§3), the consolidated member-offset map (§4), the kind-tag census (§5), and the adversarial self-verification (§6).


1. The universal header

The base constructor

Every pelican::Expr node, regardless of subclass, begins life in sub_5FE310 — the pelican::Expr (≡ RefCountedObject) base constructor. It is 30 bytes and stores exactly four fields. CONFIRMED from the raw disassembly:

; sub_5FE310 @ 0x5fe310  —  pelican::Expr::Expr(this, int kind, PelicanContext* ctx)
0x5fe310: lea  rax, _ZTVN7pelican4ExprE     ; Expr vtable @ 0x90a920
0x5fe317: mov  qword [rdi+8], 1             ; refcount@0x08 = 1
0x5fe31f: mov  [rdi+10h], esi               ; kind@0x10 = <int kind>   (32-bit!)
0x5fe322: add  rax, 10h                     ; vptr = vtable + 0x10  (past RTTI header)
0x5fe326: mov  [rdi+18h], rdx               ; ctx@0x18 = PelicanContext*
0x5fe32a: mov  [rdi], rax                   ; vptr@0x00
0x5fe32d: retn

Reconstructed as pseudocode:

// sub_5FE310 @ 0x5fe310 — pelican::Expr::Expr
void Expr_ctor(Expr *this, int kind, PelicanContext *ctx) {
    this->refcount = 1;                 // [this+0x08]
    this->kind     = kind;              // [this+0x10]  (int32, NOT a full qword)
    this->ctx      = ctx;               // [this+0x18]
    this->vptr     = &Expr_vtable + 2;  // [this+0x00] = 0x90a930
}

Header field map

OffsetWidthFieldEvidence
0x008vptrmov [rdi], rax where rax = _ZTVN7pelican4ExprE + 0x10
0x088refcountmov qword [rdi+8], 1 — initialised to 1
0x104kind (ExprKind int)mov [rdi+10h], esi — 32-bit store from esi
0x144(padding / unused)no store; 0x18 is 8-aligned
0x188ctx (PelicanContext*)mov [rdi+18h], rdx

GOTCHA — kind is a 32-bit field (mov [rdi+10h], esi, and every reader uses cmp dword [reg+10h], …). The high 4 bytes at +0x14 are never written by the base ctor and act as padding before the 8-aligned ctx. Treat the discriminator as int32, not int64.

QUIRK — the vptr stored by the base ctor is the Expr base vtable (0x90a930), not the concrete node's. Every concrete factory overwrites [obj+0] with the derived vtable immediately after the base ctor returns (e.g. MultExpr writes off_90B4E8). A node briefly carries the base vtable mid-construction — relevant only if a constructor throws (the unwind path resets the vptr to the base before operator delete).

The refcount@0x08 confirms pelican::Expr : pelican::RefCountedObject: the typeinfo _ZTIN7pelican16RefCountedObjectE exists at 0x90a8f0, the increment/decrement helpers are sub_638F80 (retain) and sub_638FB0 (release, returns "was-last"), and every factory's tail calls sub_638F80(obj + 8) — i.e. it bumps the refcount field at +0x08, the second proof of the offset.


2. BinaryExpr & the binary nodes

The binary base constructor

MultExpr, FloorDivExpr, ModuloExpr, CCDivExpr, and CCModExpr are BinaryExpr nodes. They chain through sub_60F4B0, which calls the Expr base ctor and then appends two fields. CONFIRMED from disassembly:

; sub_60F4B0 @ 0x60f4b0  —  pelican::BinaryExpr::BinaryExpr(this, int kind, ctx, RefPtr lhs, long rhs)
0x60f4bd: call sub_5FE310                    ; header: vptr/refcount/kind/ctx
0x60f4c2: lea  rax, _ZTVN7pelican10BinaryExprE   ; BinaryExpr vtable @ 0x90be00
0x60f4c9: mov  [rbx+20h], rbp                ; operand@0x20 = lhs (4th arg, rcx)
0x60f4cd: add  rax, 10h
0x60f4d1: mov  [rbx], rax                    ; vptr := BinaryExpr vtable + 0x10
        ; … if (lhs != 0) retain(lhs+8) …
0x60f4df: mov  [rbx+28h], r12               ; intval@0x28 = rhs (5th arg, r8)
// sub_60F4B0 @ 0x60f4b0 — pelican::BinaryExpr::BinaryExpr
void BinaryExpr_ctor(BinaryExpr *this, int kind, PelicanContext *ctx,
                     RefPtr<Expr> lhs, long rhs) {
    Expr_ctor(this, kind, ctx);          // header @ 0x00..0x18
    this->operand = lhs;                 // [this+0x20]
    this->vptr    = &BinaryExpr_vtable + 2;
    if (lhs) retain(&lhs->refcount);     // sub_638F80(lhs + 8)
    this->intval  = rhs;                 // [this+0x28]
}

NOTE — the field at +0x28 is named intval here but is polymorphic in role: for MultExpr it is the integer coefficient; for FloorDivExpr/ModuloExpr it is the denominator/modulus (a raw long); for CCDivExpr/CCModExpr it is the group-size denominator. The first operand +0x20 is always a RefPtr<Expr> (retained). This single {RefPtr, long} pair is the entire BinaryExpr payload — there is no second RefPtr slot; binary nodes are expr ⊕ constant, not expr ⊕ expr.

The five binary factories

Each concrete binary node has a factory that operator news the object, calls sub_60F4B0 with its kind tag, then publishes the derived vtable. CONFIRMED — kind literals and operator new sizes read directly from each factory's disassembly (mov edi, <size> before __Znwm, mov esi, <kind> before the base-ctor call):

Factorynew sizekind tagFinal vtableNode
sub_62BAA0 @ 0x62baa00x300x17 = 23off_90B4E8 (MultExpr vt 0x90b4d8)MultExpr
sub_62B7A0 @ 0x62b7a00x300x19 = 25off_90B8D0 (FloorDivExpr vt 0x90b8c0)FloorDivExpr
sub_62B830 @ 0x62b8300x300x1A = 26off_90BA20 (ModuloExpr vt 0x90ba10)ModuloExpr
sub_62B8C0 @ 0x62b8c00x380x1B = 27off_90BB70 (CCDivExpr vt 0x90bb60)CCDivExpr
sub_62B960 @ 0x62b9600x380x1C = 28off_90BCC0 (CCModExpr vt 0x90bcb0)CCModExpr
// sub_62BAA0 @ 0x62baa0 — pelican::MultExpr factory  (kind 23)
MultExpr *make_MultExpr(PelicanContext *ctx, RefPtr<Expr> sub, long coef) {
    MultExpr *e = operator new(0x30);
    BinaryExpr_ctor(e, 23, ctx, sub, coef);   // sub_60F4B0
    e->vptr = &MultExpr_vtable + 2;           // off_90B4E8
    if (e) retain(&e->refcount);              // sub_638F80(e + 8)
    return e;
}

GOTCHA — the div-like factories (FloorDivExpr, ModuloExpr, CCDivExpr, CCModExpr) transiently store unk_90B638 — the DivLikeExpr vtable (0x90b628 + 0x10) — before overwriting it with the concrete vtable. DivLikeExpr is the abstract intermediate base for the div family; its vtable appears in the byte stream but no object is ever published with it. All four div factories share the assert string AffineExpr.h:711: denom > 0 and throw "Unsupported denom for DivLikeExpr!" if the denominator is ≤ 0 — the construction-time invariant on intval@0x28.

NOTE — CCModExpr factory anchor. The sub_62B960 row is byte-pinned in libBIR.so (cp310): 62b974: bf 38 00 00 00 mov $0x38,%edi (size 0x38) → 62b982: call _Znwm@plt (operator new) → 62b990: be 1c 00 00 00 mov $0x1c,%esi (kind 0x1C = 28) → 62b9b8: lea 0x2e02f1(%rip),%rax # 90bcb0 then 62b9cb: mov %rax,0x0(%rbp) (publishes vtable 0x90bcb0) → 62b9bf: mov %r13,0x30(%rbp) (replica_groups_id@+0x30). This is a distinct factory from sub_62B830 (ModuloExpr, 0x30 bytes / kind 0x1A=26), so kind-28 is not shared with ModuloExpr. The same node has a CERTAIN analogue in the libwalrus frame — createCCModExpr (the libwalrus VA for the same 0x38/kind-28/0x90bcb0-class constructor) — so the two frames cross-confirm the kind-tag.

CORRECTION — the Penguin-algebra page sketches Sum/Mult/Modulo/FloorDiv as direct children of AffineExpr. The binary disagrees: MultExpr/FloorDivExpr/ModuloExpr/CCDivExpr/CCModExpr chain through the BinaryExpr base ctor (sub_60F4B0), not the AffineExpr body. The clean affine-combination node (AffineExpr, kind 17) and the operand-list SumExpr (kind 18) are the compound nodes; the multiplicative/division nodes are binary. The vtable/typeinfo evidence (MultExpr typeinfo at 0x90b448, distinct from AffineExpr at 0x90aa80) is on the hierarchy page.

The CC binary nodes' extra word

CCDivExpr (27) and CCModExpr (28) are 0x38-byte objects — one word longer than the plain binary nodes — because they carry a replica_groups_id at +0x30. CONFIRMED: both factories write v10[6] = a5 ([obj+0x30]) after the base ctor.

OffsetField (CC binary)Evidence
0x000x18universal headersub_5FE310
0x20operand (RefPtr<Expr>, the rank/index sub-expr)sub_60F4B0
0x28intval (group size / denominator, > 0)sub_60F4B0
0x30replica_groups_id (long)mov [obj+0x30], a5 in factory

3. Compound nodes: AffineExpr & SumExpr

The two compound core nodes skip the BinaryExpr base and build a variable-length operand body directly on the Expr header.

AffineExpr (kind 17) — object size 0x40

AffineExpr is the canonical affine combination c + Σ cᵢ·idxᵢ. Its body is a boost::container::vector<pair<RefPtr<AffineIdx>, long>> (the term list) plus a constant. CONFIRMED from sub_62BCF0 @ 0x62bcf0 (the empty-with-const factory) and sub_635860 @ 0x635860 (the populate-from-terms factory):

// sub_62BCF0 @ 0x62bcf0 — pelican::AffineExpr factory (empty body, kind 17)
AffineExpr *make_AffineExpr(PelicanContext *ctx, long c) {
    AffineExpr *e = operator new(0x40);
    Expr_ctor(e, 17, ctx);               // sub_5FE310 — header
    *(qword*)(e + 0x20) = 0;             // terms.begin  = null
    *(qword*)(e + 0x38) = c;             // const        = c
    e->vptr = &AffineExpr_vtable + 2;    // off_90ABF0  (0x90abe0 + 0x10)
    *(oword*)(e + 0x28) = 0;             // terms.end / terms.cap = 0
    if (e) retain(&e->refcount);
    return e;
}
OffsetWidthField (AffineExpr)Evidence
0x000x18universal headersub_5FE310
0x208terms.begin (vector data ptr)[e+0x20] = 0 then filled by sub_62B490
0x288terms.end / sizeoword [e+0x28] = 0; reader at +0x28 in sub_635860
0x308terms.capacitysecond half of the oword zeroing
0x388const (long)[e+0x38] = c (sub_62BCF0) / = a4 (sub_635860)

NOTE — the term list is a boost::container::vector of pair<RefPtr<AffineIdx>, long> (16-byte elements: the index ref + its integer coefficient). sub_635860's asserts name the element type verbatim — std::pair<pelican::RefPtr<pelican::AffineIdx>, long int> — and the boost source path boost/container/vector.hpp. The factory copies an input term vector, then erases zero-coefficient terms in place before publishing.

SumExpr (kind 18) — object size 0x70

SumExpr is the operand-list compound — a sum of arbitrary sub-Exprs — backed by an llvm::SmallVector<RefPtr<Expr>> with 8 inline slots. CONFIRMED from sub_62C8D0 @ 0x62c8d0:

// sub_62C8D0 @ 0x62c8d0 — pelican::SumExpr factory (kind 18)
SumExpr *make_SumExpr(PelicanContext *ctx, SmallVector<RefPtr<Expr>> *src) {
    SumExpr *e = operator new(0x70);
    Expr_ctor(e, 18, ctx);               // sub_5FE310 — header
    *(qword*)(e + 0x20) = e + 0x30;      // ops.begin = inline buffer
    e->vptr = &SumExpr_vtable + 2;       // off_90B788  (0x90b778 + 0x10)
    *(qword*)(e + 0x28) = 0x800000000;   // size=0 (lo32), capacity=8 (hi32)
    // … copy src elements into the SmallVector, retaining each …
}
OffsetWidthField (SumExpr)Evidence
0x000x18universal headersub_5FE310
0x208ops.BeginX (SmallVector data ptr)[e+0x20] = e+0x30 (points at inline buffer)
0x284ops.Sizelow dword of 0x8000000000
0x2C4ops.Capacityhigh dword → 8
0x300x6F64inline element buffer (8 × RefPtr<Expr>)ops.begin aimed here; operator delete(e, 0x70) in dtor

QUIRK — SumExpr's SmallVector is inline-first: ops.begin@0x20 initially points into the object itself (e + 0x30). The destructor (sub_616FB0/sub_6169A0) only free()s the buffer when ops.begin != e + 0x30 — i.e. when the sum spilled past 8 terms and heap-promoted. The 0x70 object size is exactly header(0x20) + SmallVector control(0x10) + 8 inline RefPtr slots(0x40).


4. The consolidated member-offset map

Every cell is a field written by a constructor at the stated offset; blank = field does not exist in that node. Header rows (0x000x18) are universal — written by sub_5FE310 for all nodes.

OffsetExpr baseBinaryExpr (Mult/Floor/Modulo)CC binary (CCDiv/CCMod)AffineExpr (k17)SumExpr (k18)
0x00vptrvptrvptrvptrvptr
0x08refcount (=1)refcountrefcountrefcountrefcount
0x10kind (int32)kindkindkindkind
0x14(pad)(pad)(pad)(pad)(pad)
0x18ctxctxctxctxctx
0x20operand (RefPtr<Expr>)operandterms.beginops.begin
0x28intval (long)intval (group size)terms.end/sizeops.size(lo32) cap(hi32)
0x30replica_groups_idterms.capacityinline buf[0]
0x38const (long)inline buf[1]
0x40+(end, 0x40)inline buf[2…7]
size0x300x380x400x70

The two-VA-frame artifact applies: the nm-style symbol VAs above (e.g. 0x5fe310) are from the per-binary frame; internal per-symbol sidecar offsets in IDA's split exports may use a second frame. All addresses on this page are the binary-VA frame, cross-checked against the disasm sidecars whose filenames embed the same VA.


5. The kind-tag census

The ExprKind discriminator at +0x10 is a dense small-integer enum. The core affine/binary band is 1728; the collective leaves CCGetRankExpr/IndirectArgExpr sit low at 2/3. CONFIRMED kinds are pinned to a mov esi, <lit> immediately preceding a base-ctor call in the named factory:

KindHexNodePinned atBase ctor
20x02CCGetRankExprCCGetRankExpr::create @ 0x3bebf0sub_5FE310
30x03IndirectArgExprIndirectArgExpr::create @ 0x3becf0sub_5FE310
170x11AffineExprsub_62BCF0, sub_635860sub_5FE310
180x12SumExprsub_62C8D0sub_5FE310
230x17MultExprsub_62BAA0sub_60F4B0
250x19FloorDivExprsub_62B7A0sub_60F4B0
260x1AModuloExprsub_62B830sub_60F4B0
270x1BCCDivExprsub_62B8C0sub_60F4B0
280x1CCCModExprsub_62B960sub_60F4B0

NOTE — the six tags the title calls out — 17, 18, 23, 25, 26, 27 — are exactly {AffineExpr, SumExpr, MultExpr, FloorDivExpr, ModuloExpr, CCDivExpr}. Tag 28 (CCModExpr) completes the binary band; 24 is not observed among the affine/binary factories (a gap in the captured set — possibly DivLikeExpr as an abstract tag, never constructed). The low tags 2/3 belong to the collective/indirect leaves built directly on the simple base.

The read side independently confirms both the field offset and the tag values. isV1 (0x3b4110) dispatches on the discriminator:

// isV1 @ 0x3b4110 — version-1 affine-form predicate (reads kind@0x10)
v3 = *(int32*)( *(qword*)expr_ref + 0x10 );   // kind@0x10
if (v3 == 17) return true;                     // AffineExpr
if (v3 <= 0x11) return (v3 - 2 <= 1);          // CCGetRank(2)/IndirectArg(3) band
if (v3 - 25 > 2) return false;                 // outside FloorDiv/Modulo/CCDiv(25..27)
return *(int32*)( operand_at_0x20->kind ) == 17; // recurse into operand@0x20

This single function pins three claims at once: kind is a 4-byte field at +0x10; the div-like band is the contiguous range 25..27; and the binary operand lives at +0x20 (v2[4]). bir::QuasiAffineExpr::isSimpleAffineExpr (0x3b5a20) reduces to *(int32*)(expr->kind) == 17, and QuasiAffineExprIterator::traverseChildren (0x3b07d0) switches on +0x10 against 0x11/0x12 — all reading the same field.


6. Adversarial self-verification

The five strongest claims, each re-checked against the binary from an independent site:

  1. Universal header {vptr@0, refcount@8, kind@0x10, ctx@0x18}.CONFIRMED. Write side: sub_5FE310 disasm stores [rdi+8]=1, [rdi+10h]=esi, [rdi+18h]=rdx, [rdi]=Expr_vtable+0x10. Read side: isV1/isSimpleAffineExpr/traverseChildren all read [expr+0x10] as the kind; every factory's tail retain(obj+8) confirms refcount@0x08. No ambiguity.

  2. refcount@0x08 initialised to 1 (≡ RefCountedObject).CONFIRMED. mov qword [rdi+8], 1 in the base ctor; _ZTIN7pelican16RefCountedObjectE @ 0x90a8f0 exists; retain sub_638F80/release sub_638FB0 operate on obj + 8.

  3. BinaryExpr payload {operand@0x20, intval@0x28}.CONFIRMED. sub_60F4B0 disasm: mov [rbx+20h], rbp (lhs), mov [rbx+28h], r12 (rhs). Read side: isV1 dereferences v2[4] (= +0x20) as the operand and recurses on its kind.

  4. Kind tags 17/18/23/25/26/27 (+28/2/3).CONFIRMED. Each tag read as the mov esi, <imm> immediately before the base-ctor call in the named factory (0x11,0x12,0x17,0x19,0x1A,0x1B,0x1C), and as cmp dword [reg+10h], <imm> in the readers. Both sides agree.

  5. Object sizes 0x30 / 0x38 / 0x40 / 0x70.CONFIRMED. Each mov edi, <size> precedes the operator new call in the factory (0x30 Mult/Floor/Modulo, 0x38 CCDiv/CCMod, 0x40 AffineExpr, 0x70 SumExpr); SumExpr's dtor operator delete(e, 0x70) cross-checks.

Re-verify ceiling / what is INFERRED:

  • The base-class chain (BinaryExpr ⊃ DivLikeExpr ⊃ {FloorDiv, CCDiv}, etc.) is STRONG-but-inferred here: argued from which base ctor each factory chains through and from the transient DivLikeExpr vtable, not from reading the __si_class_type_info base pointers. The authoritative tree is the hierarchy page.
  • The field semantics of intval@0x28 (coefficient vs denominator vs modulus per node) are INFERRED from the assert strings (denom > 0) and the node names — the binary stores one long; the role label is contextual.
  • AffineExpr's terms triple at 0x20/0x28/0x30 is identified as a boost::container::vector from sub_635860's asserts; the exact begin/end/cap field assignment is STRONG (matches the boost layout and the zeroing pattern) but the precise end-vs-size encoding at +0x28 was not separately disassembled field-by-field.
  • Kind 24 is unobserved — no captured factory emits it; whether it is a reserved/abstract DivLikeExpr tag or a node outside this page's scope is SPECULATIVE.

Cross-references