You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
From the root-io-spec reference now vendored at reference/root-io-spec (pin 9c3f776): ElementTypes.md §5 / §5.2, StreamerInfo.md §7 (§7.1, "The range fields moved out of the record"), and TLeaf.md §7.
Title grammar (TStreamerElement.cxx:118-185), applied only when kHasRange = BIT(6) of the element's own fBits is set:
the first [...] may be an array dimension — if it contains no comma, retry after it. Real cases: [fNvertex][0,0,6], [fNsp][0,3].
pi, 2pi, 2*pi, twopi, pi/2, pi/4 are literals, with a leading - negating.
nbits absent or outside [2,32] → 32.
bigint = nbits<32 ? 1<<nbits : 0xffffffff; if xmin<xmax: factor = bigint/(xmax-xmin); if xmin>=xmax and nbits<15: xmin = nbits+0.1.
Widths — never 8:
condition
bytes
decode
factor != 0
4
aint:u32 → aint/factor + xmin
factor == 0, (int)xmin != 0
3
exp:u8,man:u16, nbits = (int)xmin
factor == 0, xmin == 0, kDouble32
4
plain binary32
factor == 0, xmin == 0, kFloat16
3
exp/man with nbits = 12
Mantissa path: i = exp<<23 | (man & ((1<<(nbits+1))-1)) << (23-nbits), negate if man & (1<<(nbits+1)). Traps: [0,0,15] and above silently degrade to a plain 4-byte float and don't even set kHasRange — wider than [0,0,14]; and the range path costs 4 bytes whatever nbits says. Array form is +20, counted pointer +40.
Against bootstrap/double32.py today:
:22-24 sizes the read from nbits — and "BHIQ"[(nbytes-1)//2] mis-indexes too (nbits 32 → 2 bytes read).
:74 divides by 2**nbits - 1; ROOT's divisor is 1<<nbits. Only nbits=32 agrees; [-1,1,2] is off by 4/3.
:71-72"pi"→3.1415… turns 2pi into 23.14 and makes pi/2 raise.
no array-dim retry → [fNvertex][0,0,6] raises ValueError; mantissa path absent; kHasRange never consulted.
Status:uproot-Event.root / uproot-issue-308.root xfail on the array form (container.py:125-127); uproot-double32-float16.root xfails on float16 (structutil.py:17-19) — those are TLeafF16::fMinimum/fMaximum at 3 bytes each, vs TLeafD32's 4 (TLeaf.md §7). uproot-demo-double32.root passes but is silently mis-generated; it is the ideal vector (fF32 plain, fI32…fI2 range, fR14…fR2 mantissa). Byte-level assertions: gen/cases/serialization/double32/case.toml.
Addendum from the bootstrap review (#66), which looked at the same code independently. Points not already in the comment above:
Float16_t never reaches Double32Serde.TStreamerBasicType.member_definition special-cases only kDouble32 (src/rootfilespec/bootstrap/TStreamerInfo.py:351-358); kFloat16 maps to the sentinel format "float16" at :274, which raises in src/rootfilespec/structutil.py:17-19.
There is a third raise site:src/rootfilespec/container.py:63-65 (BasicArray.build_reader, the counted-pointer +40 form), besides container.py:125-127 (the fixed-array +20 form).
The spec fixture fails today:reference/root-io-spec/data/serialization/double32.root → NotImplementedError: Unimplemented format float16. Its case.toml has byte-level expected values for every form (see also Use the root-io-spec data/ fixtures as a parametrized test corpus #76).
The nbits clamp is missing: the rule "nbits outside [2, 32] becomes 32" is not applied in src/rootfilespec/bootstrap/double32.py:48-77.
Reproduced widths:[0,1] reads 2 bytes instead of 4; [0,0,14] reads 1 instead of 3; [-1,1,2] reads 1 instead of 4. [0,0,15] must be a plain f32 (the fixture's fBits15 member asserts the value 1.5).
Why the array-dimension failure takes down whole files: the title is parsed without first testing kHasRange (BIT(6) of the element's fBits), so an ordinary comment such as [3] a fixed array raises during class generation, and that kills every class in the file, not just the member. Related: TObjFlag.__repr__ raises on valid fBits, and several enum members mislabel class-specific bits as TObject bits #109 notes that TObjFlag labels that very bit (0x40) kCannotPick.
Design note: Double32_t/Float16_t decoding depends on streamer-element state (fTitle, fBits, and for old files fXmin/fXmax/fFactor, see #95), not on the member's Python type — one of the motivations listed in #67.
Activity
nsmith- commentedon Sep 18, 2026
From the root-io-spec reference now vendored at
reference/root-io-spec(pin9c3f776): ElementTypes.md §5 / §5.2, StreamerInfo.md §7 (§7.1, "The range fields moved out of the record"), and TLeaf.md §7.Title grammar (
TStreamerElement.cxx:118-185), applied only whenkHasRange=BIT(6)of the element's ownfBitsis set:[...]may be an array dimension — if it contains no comma, retry after it. Real cases:[fNvertex][0,0,6],[fNsp][0,3].pi,2pi,2*pi,twopi,pi/2,pi/4are literals, with a leading-negating.[2,32]→ 32.bigint = nbits<32 ? 1<<nbits : 0xffffffff;if xmin<xmax: factor = bigint/(xmax-xmin);if xmin>=xmax and nbits<15: xmin = nbits+0.1.Widths — never 8:
factor != 0aint:u32→aint/factor + xminfactor == 0,(int)xmin != 0exp:u8,man:u16,nbits = (int)xminfactor == 0,xmin == 0, kDouble32factor == 0,xmin == 0, kFloat16nbits = 12Mantissa path:
i = exp<<23 | (man & ((1<<(nbits+1))-1)) << (23-nbits), negate ifman & (1<<(nbits+1)). Traps:[0,0,15]and above silently degrade to a plain 4-byte float and don't even setkHasRange— wider than[0,0,14]; and the range path costs 4 bytes whatever nbits says. Array form is+20, counted pointer+40.Against
bootstrap/double32.pytoday::22-24sizes the read from nbits — and"BHIQ"[(nbytes-1)//2]mis-indexes too (nbits 32 → 2 bytes read).:74divides by2**nbits - 1; ROOT's divisor is1<<nbits. Only nbits=32 agrees;[-1,1,2]is off by 4/3.:71-72"pi"→3.1415…turns2piinto 23.14 and makespi/2raise.[fNvertex][0,0,6]raisesValueError; mantissa path absent;kHasRangenever consulted.Status:
uproot-Event.root/uproot-issue-308.rootxfail on the array form (container.py:125-127);uproot-double32-float16.rootxfails on float16 (structutil.py:17-19) — those areTLeafF16::fMinimum/fMaximumat 3 bytes each, vsTLeafD32's 4 (TLeaf.md §7).uproot-demo-double32.rootpasses but is silently mis-generated; it is the ideal vector (fF32plain,fI32…fI2range,fR14…fR2mantissa). Byte-level assertions:gen/cases/serialization/double32/case.toml.nsmith- commentedon Sep 18, 2026
Addendum from the bootstrap review (#66), which looked at the same code independently. Points not already in the comment above:
Float16_tnever reachesDouble32Serde.TStreamerBasicType.member_definitionspecial-cases onlykDouble32(src/rootfilespec/bootstrap/TStreamerInfo.py:351-358);kFloat16maps to the sentinel format"float16"at:274, which raises insrc/rootfilespec/structutil.py:17-19.src/rootfilespec/container.py:63-65(BasicArray.build_reader, the counted-pointer+40form), besidescontainer.py:125-127(the fixed-array+20form).reference/root-io-spec/data/serialization/double32.root→NotImplementedError: Unimplemented format float16. Itscase.tomlhas byte-level expected values for every form (see also Use the root-io-spec data/ fixtures as a parametrized test corpus #76).nbitsclamp is missing: the rule "nbitsoutside [2, 32] becomes 32" is not applied insrc/rootfilespec/bootstrap/double32.py:48-77.[0,1]reads 2 bytes instead of 4;[0,0,14]reads 1 instead of 3;[-1,1,2]reads 1 instead of 4.[0,0,15]must be a plainf32(the fixture'sfBits15member asserts the value 1.5).kHasRange(BIT(6) of the element'sfBits), so an ordinary comment such as[3] a fixed arrayraises during class generation, and that kills every class in the file, not just the member. Related: TObjFlag.__repr__ raises on valid fBits, and several enum members mislabel class-specific bits as TObject bits #109 notes thatTObjFlaglabels that very bit (0x40)kCannotPick.Design note:
Double32_t/Float16_tdecoding depends on streamer-element state (fTitle,fBits, and for old filesfXmin/fXmax/fFactor, see #95), not on the member's Python type — one of the motivations listed in #67.Finish the Locator migration: no library code fetches (#114), and kee…