nifcore
nimony/src/lib/nifcore.nim
nifcore — in-memory NIF representation, builder, and cursor.
Design summary ==============
A NIF token is a distinct uint32 (4 bytes) with the kind in the low 4 bits and a 28-bit kind-specific payload above.
There is no separate ParLe / ParRi kind — a node is just a TagLit that carries (tag, jump), where jump counts the body tokens that follow. The matching close is implicit; iterators stop after consuming jump body tokens.
TagLit payload (28 bits): [3..0] kind = TagLit [12..4] tag (9 bits, 0..511) [31..13] jump (19 bits, 0..524287 body tokens)
Atom kinds (StrLit, IntLit, FloatLit, …) put a 28-bit pool id (or a small inline value, e.g. CharLit) in the payload.
ExtendedSuffix is the universal extension knob — one kind handles both literal overflow and jump overflow uniformly:
Atoms with wide payload: [P] StrLit(low28) [P+1] ExtendedSuffix(high28) ⇒ 56-bit combined id
TagLits with overflowing jump: [P] TagLit(tag, jumplow19) [P+1] ExtendedSuffix(jumphigh28) ⇒ 47-bit combined jump [P+2 .. P+1+combined_jump] body
Putting the extension after the kinded token means a cursor always lands on the kinded token; kind(c) is one load + one mask (no branch). The suffix is only consulted by the helpers that actually need extended bits (combinedPayload, cursorJump, tokenWidth).
Pool ownership is per-TokenBuf by default; pass sharedPool to createTokenBuf to thread the same intern tables through many trees.
type NifKind = enum DotToken = (0, "DotToken") CharLit = (1, "CharLit") StrLit = (2, "StrLit") IntLit = (3, "IntLit") UIntLit = (4, "UIntLit") FloatLit = (5, "FloatLit") Symbol = (6, "Symbol") SymbolDef = (7, "SymbolDef") Ident = (8, "Ident") TagLit = (9, "TagLit") ExtendedSuffix = (10, "ExtendedSuffix") LineInfoLit = (11, "LineInfoLit") UnknownToken = (12, "UnknownToken") EofToken = (13, "EofToken") ParLe = (14, "ParLe") ParRi = (15, "ParRi")
func dollar`.NifKind(e: NifKind): stringtype NifToken = distinct uint32type TagId = distinct uint32type StrId = distinct uint32type SymId = distinct uint32func ==(a: NifToken; b: NifToken): boolfunc ==(a: TagId; b: TagId): boolfunc ==(a: StrId; b: StrId): boolfunc ==(a: SymId; b: SymId): boolfunc hash(x: TagId): uint64func hash(x: StrId): uint64func hash(x: SymId): uint64proc $(x: TagId): stringproc $(x: StrId): stringproc $(x: SymId): stringconst KindBits: uint32const KindMask: uint32const PayloadBits: uint32const PayloadMask: uint32const IdPayloadMax: uint32const TagBits: uint32const TagShift: uint32const TagMask: uint32const JumpShift: uint32const JumpBits: uint32const InlineJumpCap: uint32proc kind(n: NifToken): NifKindA
proc(not atemplate) so downstreamexport … except kindcan drop it — nimony'sexceptcannot filter a name that has a template overload.template uoperand(n: NifToken): uint32template soperand(n: NifToken): int32Sign-extended 28-bit payload (for inline signed values).
proc dotToken(): NifTokenproc tagLitToken(t: TagId; jump: uint32): NifTokenproc charToken(ch: char): NifTokenproc strLitToken(id: StrId): NifTokenproc symToken(id: SymId): NifTokenproc symdefToken(id: SymId): NifTokenproc identToken(id: StrId): NifTokenproc extendedSuffixToken(high28: uint32): NifTokenconst LiColBitsC: uint32const LiFileBitsC: uint32const LiLineBitsC: uint32const LiColMaxC: uint32const LiFileMaxC: uint32const LiLineMaxC: uint32type NifLineInfo = object file: FileId line: int32 col: int32 comment: StrId
const NoNifLineInfo: NifLineInfoproc isValid(x: NifLineInfo): boolfunc ==(a: NifLineInfo; b: NifLineInfo): boolExplicit because Nimony does not synthesize structural
==/hashfor objects (frontend code keeps infos in HashSets, e.g. error dedup).func hash(x: NifLineInfo): uint64proc setJump(n: var NifToken; j: uint32)proc setTag(n: var NifToken; t: TagId)type NifSymbol = object name: StrId disamb: int32 dedup: StrId module: StrId
const NoDisamb: int32proc ==(a: NifSymbol; b: NifSymbol): boolproc hash(s: NifSymbol): uint64Four ids mixed, not a string walked: this is what the pool hashes on every intern, and the reason interning a symbol stopped costing a string hash.
type Pool = ref Pool.Objtype Pool.Obj = object strings: BiTable symbols: BiTable filenames: BiTable
type TagPool = ref TagPool.Objtype TagPool.Obj = object tags: BiTable escapeTag: TagId
proc newPool(): ref Pool.Objproc newTagPool(): ref TagPool.ObjAll BiTable ids start at 1 (id 0 is the "not used" sentinel). Adapters whose enum has ordinal 0 as the first real value bridge the gap with a
+/- 1shim in theirtagId/myKindhelpers (see jsonnif/htmlnif).proc registerTag(tp: ref TagPool.Obj; tag: string): TagIdIntern a tag string. Adapters call this in enum-ordinal order at startup so the returned TagId equals the enum ordinal (1-based).
const FirstEscapeId: uint32The first tag id that no longer fits the 9-bit tag field, hence the first one an adapter must spell through its
escapeTag. Registering tags is otherwise unaffected:registerTagkeeps counting.template hasEscapes(tp: ref TagPool.Obj): boolproc createTags(): ref TagPool.ObjOne-shot tag-pool builder for an adapter's enum. Registers every value of
Ein ordinal order using its string form ($e), so the resulting TagIds are1, 2, …matching thetagId/myKind+/- 1shim. Replaces hand-rolledcreateJsonTagPool/createHtmlTagPoolboilerplate.The assertion guards against an enum with holes or out-of-order ordinals — those would silently break the
cast[E](tagId-1)path.template tagName(tp: ref TagPool.Obj; t: TagId): lent stringtemplate poolStr(p: ref Pool.Obj; s: StrId): lent stringproc sym(p: ref Pool.Obj; id: SymId): NifSymbolThe symbol
id, taken apart. A field read.proc symBasename(p: ref Pool.Obj; id: SymId): stringThe identifier of
id, without disambiguator, key or module suffix:abc.12.Ikey.modgivesabc. A name may contain dots itself (Pool.Obj.0givesPool.Obj), and an atom that is not a symbol at all gives""-- it has no identifier to name.proc symNameId(p: ref Pool.Obj; id: SymId): StrIdsymBasenameas an id -- the one to compare identifiers by.proc symVersionedBasename(p: ref Pool.Obj; id: SymId): stringThe identifier AND its disambiguator, without key or module suffix:
abc.12.Ikey.modgivesabc.12. That is a name for the ROUTINE, not for one instantiation of it --symWithoutModuleis the one that keeps the key. An atom that is not a symbol gives"".proc symIsInstantiation(p: ref Pool.Obj; id: SymId): boolWhether
idcarries a deduplication key -- theIkeyofabc.12.Ikey.mod, which every module needing that instantiation derives the same way. That is what makessymWithoutModulea cross-module identity for it and only for it.ONE key, spelled the way nimony spells one: a name with two of them (
foo.0.Ia.Ib.mod) is not an instantiation of anything this toolchain minted, and nifasm merges symbols by this answer -- it once hand-rolled its own and merged such a name wrongly.proc symWithoutModule(p: ref Pool.Obj; id: SymId): stringidminus its module suffix:abc.12.Ikey.modgivesabc.12.Ikey, and a local symbol gives itself. For an instantiation this is the name every module that needs it arrives at independently, so it is the key to merge the copies by -- DCE, the type-key builder and overload resolution all use it for exactly that.proc symSameEntity(p: ref Pool.Obj; a: SymId; b: SymId): boolWhether two DIFFERENT symbols name the same entity seen from two modules: both are instantiations and everything but the module suffix matches.
proc symModule(p: ref Pool.Obj; id: SymId): stringThe module suffix of
id,""when it is local. For the places that need the suffix as a string -- a table key, a file name, a(strlit)-- whilesym(p, id).moduleis what a COMPARISON should use.proc symIsLocal(p: ref Pool.Obj; id: SymId): boolWhether
idhas no module suffix. A question about the module, never about how many dots the spelling has: real names contain dots (Pool.Obj,dollar.CaseMode,..<`).proc symModuleIs(p: ref Pool.Obj; id: SymId; suffix: string): boolWhether
idcomes from the modulesuffix. Builds nothing.proc symSpellingLen(p: ref Pool.Obj; id: SymId): int64How long
symStringwould be, without building it. The token writers ask only to decide whether the name fits inside the token itself.proc symString(p: ref Pool.Obj; s: NifSymbol): stringThe spelling
swould be interned under.proc symString(p: ref Pool.Obj; id: SymId): stringThe whole spelling of
id. Builds a string, so it is for the places that genuinely need one -- the C mangler, error messages, serialization -- and never for asking a question the accessors above answer.const DisambMax: int64Largest disambiguator
NifSymbol.disambcan hold.proc parseDisamb(s: string; start: int64; len: int64): int64The value of the disambiguator spelled by
s[start ..< start+len], or -1 when that is not how NIF spells a number:- digits only --
p.0h107names nopwith a disambiguator; - no leading zero --
d.00is a NAME, and deliberately so: no user symbol
can collide with a field the compiler injects (
typenav.DataField);- small enough to fit the field.
A spelling that fails any of these keeps its tail inside the NAME, which is what lets every symbol round-trip through the pool unchanged. The reader's split-symbol path asks this too, so the two agree about the same bytes.
- digits only --
proc symRecord(p: ref Pool.Obj; s: string): NifSymbolTake a spelling apart into the record the pool stores, interning each component. Every spelling round-trips: one whose disambiguator is not a NUMBER keeps it inside the name (
NoDisamb), so the reserved forms older artifacts carry (_exit.sys.mod,p.0h107) come back out unchanged.proc symId(p: ref Pool.Obj; s: string): SymIdIntern a symbol given its whole spelling. This is what the reader and the deserializers have; code that MINTS a symbol should say what its parts are instead (the overload below).
proc symId(p: ref Pool.Obj; name: string; disamb: int64; module: string; dedup: string): SymIdIntern a symbol from its parts.
moduleempty means a local symbol.proc symId(p: ref Pool.Obj; s: NifSymbol): SymIdproc isLocal(s: NifSymbol): booltype SymPool = object pool: ref Pool.Obj
template syms(p: ref Pool.Obj): SymPooltemplate poolSym(p: ref Pool.Obj; s: SymId): stringThe classic name for
symString, and the fourth thing the Nim compiler's IC modules ask (ast2nif.indexFromBif). It used to yieldlent stringstraight out of the pool; the pool has no string to lend now, so this builds one -- the callers all wanted a copy anyway (a table key, a name to re-emit).proc getOrIncl(s: SymPool; name: string): SymIdproc [](s: SymPool; id: SymId): stringproc len(s: SymPool): int64proc getKeyId(s: SymPool; name: string): SymIdThe id
namealready has, orSymId(0)when the pool does not hold it. Looks up without interning ANYTHING: a component this pool never saw is proof the symbol is not in it, which is the answer the caller wanted.type Cursor = object owner: ptr CursorOwnerObj p: ptr NifToken rem: int64
type CursorScope = object savedP: ptr NifToken savedRem: int64 bodyLen: int64
proc pool(c: Cursor): ref Pool.ObjLiterals pool the cursor's underlying buffer was built against, or the plugin build's default (
nileverywhere else) when it carries none.proc escapeTagOf(c: Cursor): TagIdThe adapter's escape tag, or
TagId(0)when it declares none — which is also what a cursor with no tag pool at all answers, since "no escape space" is exactly whatTagId(0)means.Exists to be read WITHOUT
tags.tagsreturns the pool BY VALUE, and arefreturned by value is an owned temporary: ARC pairs every call with an incRef and a decRef, and the decRef has to carry the pool's whole destructor behind it — aBiTableof strings — so the one-line accessor lowers to several thousand tokens of Leng and stops being inlinable at all. On nifbenchtagsplus that destructor cost more thanskipandsymIdtogether, for a pool that outlives the program. Reading the field through the raw owner pointer takes no reference and reaches no hook.proc toUniqueId(c: Cursor): int64A stable identity for the cursor's position: two cursors over the same buffer at the same token share it, distinct positions differ. Suitable as a
HashSet[int]/IntSetkey (e.g. type-traversal dedup). Not stable across buffers or runs — it is the underlying token pointer reinterpreted.proc =destroy(c: var Cursor)proc =wasMoved(c: var Cursor)proc =copy(dest: var Cursor; src: Cursor)proc =dup(src: Cursor): Cursorconst nifcoreChecks: boolInternal cursor invariants (
load,kind) are checked only when this is explicitly requested, NOT under plain-d:release.loadis the single most executed routine in the whole toolchain — everykind,tokenWidth,combinedPayloadand literal decode bottoms out in it. Its two-condition assert was more than the check: it madeload's NIFC body 131 tokens, over the inliner's 100-token bound, soloadAND its callers stayed real calls — 28 instructions forkind, whose body is two. The invariant it guards (a cursor is non-nil and has tokens left) is an internal one: a violation is a nifcore bug, and every walk re-establishes it structurally. Build with-d:nifcoreChecksto get it back.proc load(c: Cursor): NifTokenproc cursorIsNil(c: Cursor): boolproc kind(c: Cursor): NifKindEffective kind of the value at the cursor. Always one load + one mask — the cursor lands on the kinded token; any
ExtendedSuffixsits after it and is only consulted when extended bits are needed.proc tokenWidth(c: Cursor): int64Tokens occupied by the head of the current value — the kinded token plus any consecutive
ExtendedSuffixtokens chained behind it. NOT including a TagLit's body.Chains are unbounded in principle; in practice the writers in nifcore emit at most 2 suffixes (enough to cover int64/float64 / 47-bit jumps / 55-bit pool ids). The hot path — no suffix at all — is one peek + one branch.
Written as NESTED
ifs ending inbreaks rather than awhileover a conjunction, and deliberately WITHOUT anoinlineslow-path helper.Do NOT give this one the hot/cold treatment
combinedPayloadgets, even though the two look alike. Measured on nifbench: splittingcombinedPayloadis −0.68 % instructions, splitting THIS is +6.5 M (a loss), and doing both is only −0.46 % — the loss here eats a third of the win there. The hot path returns a constant, so the call it saves is already the cheapest kind while the wrapper still costs a branch at all 10 M call sites. Reasons: a conjunction whose second operand contains a call cannot be handed to the backend as branches, so it materialises a bool in an if/else diamond — 9 x86 instructions and ~90 NIFC tokens for two compares; factoring the chain walk into a helper made this routine a NON-LEAF, and arkham then homes its locals in callee-saved registers and pushes them in the prologue on EVERY call, including the overwhelmingly common no-suffix path. That cost more than the loop it removed (parse+8%).{.alwaysInline.}is load-bearing for the native backend. Hexer's inliner ignores.inline(it only auto-splices bodies ≤ 100 tokens, and this one is larger oncepeekAheadexpands) while GCC-O3inlines thestatic inlineC anyway. Without the force-splice,skip/inccall this and copy the 24-byte Cursor onto the stack for the by-value argument — the dominant arkham-vs-GCC gap on nifbench's walk/parse.Measured, not assumed: at 262 asm-NIF tokens this sits above
InlineTinyBound, so the size heuristic never took it, and it is the one shape that pays — scalar in, scalar out, few live values, so the callee's push/pop and call/ret vanish and the caller adds no spill traffic. Measured on nimsem (128-invocationcheck,-d:danger): 55.320 G → 55.035 G Ir, −0.51 %, for +4 KB of image.The two neighbours that looked like the same shape on nifbench do NOT survive this measurement — do not annotate them:
incon top of this one is −0.36 % (i.e. it gives back a third of the win, while nifbench said it added to it), andrawLineInfois +0.22 %, a loss — it returns an aggregate and holds ~20 live values, so the push/pop it removes comes straight back as frame traffic in every caller.proc combinedPayload(c: Cursor): uint64Combine the kinded token's 28-bit payload with any chained ExtendedSuffix tokens. Each suffix supplies the next 28 high bits.
HOT PATH INLINE, COLD PATH OUT OF LINE. 7.3 M calls per nifbench run at 29 instructions each, for a common case that is one load and one branch — 12 of those 29 were callee-saved pushes and pops for registers only the chain walk uses.
{.alwaysInline.}because the wrapper is 269 NIFC tokens once hexer has flattened it, far pastInlineTinyBound;combinedPayloadSlowis{.noinline.}because otherwise it gets spliced back in here and the wrapper stops being worth inlining. Both pragmas are load-bearing. nifbench −0.68 % instructions, self-hosted compiler unchanged.tokenWidthlooks like the same shape but must NOT get the same treatment — see the measurement in its doc comment.const StrInlineFlag: uint32bit 0 = inline mode
const StrLengthShift: uint32const StrLengthMask: uint32bits 1..2
const StrDataShift: uint32bits 3..26 hold up to 3 bytes
const StrInlineMaxLen: int64bytes that fit inline
proc isInlineLit(c: Cursor): boolTrue if a StrLit/Ident/Symbol/SymbolDef stores its bytes inline.
proc strVal(c: Cursor; pool: ref Pool.Obj): stringDecode a StrLit/Ident at the cursor into a
string. Handles the inline-short path (no pool touch) and the pool-ref path (with or without an ExtendedSuffix extending the pool id).proc strVal(c: Cursor): stringproc strId(c: Cursor; pool: ref Pool.Obj): StrIdStable pool id of the StrLit/Ident at
c— the inverse ofstrVal. A pool-ref token already carries its id, so use it directly; only an inline short string (stored in the token itself) has to be interned. MirrorssymIdand avoids the decode-then-reintern round trip ofstrings[strVal].proc strId(c: Cursor): StrIdproc symName(c: Cursor; pool: ref Pool.Obj): stringproc symName(c: Cursor): stringproc symId(c: Cursor; pool: ref Pool.Obj): SymIdStable pool id of the Symbol/SymbolDef at
c— the inverse ofsymName. A pool-ref token already carries its id, so use it directly; only an inline short name (stored in the token itself) has to be interned.proc symId(c: Cursor): SymIdproc valueWidth(c: Cursor): int64Tokens carrying value bits — the kinded token plus consecutive
ExtendedSuffixtokens. Excludes trailing LineInfoLit (whichtokenWidthdoes include for advance-past purposes). This is the width sign-extension wants for IntLit; it lines up exactly with whatcombinedPayloadchooses to OR together.proc intVal(c: Cursor): int64proc uintVal(c: Cursor): uint64proc floatVal(c: Cursor): float64proc charLit(c: Cursor): charReturns the character stored in the
CharLitatc.proc cursorTagId(c: Cursor): TagIdproc cursorJump(c: Cursor): uint64Body tokens following this TagLit. 19 bits if unsuffixed; with one ExtendedSuffix the field widens to 47 bits (further chaining would extend it further, but no plausible jump needs that). The unified
combinedPayloadhandles the suffix walk for us; we just discard the low 9 tag bits to expose the jump.proc rawLineInfo(c: Cursor): NifLineInfoDecode the
LineInfoLittrailing the value atc, if present, returning aFileId(resolve againstpool.filenames) plus line/col, and an optionalcommentStrId(a NIF#…#decoration;StrId(0)= none). ReturnsNoNifLineInfowhen the head carries no line info. TheLineInfoLitsits after the value'sExtendedSuffixchain (valueWidth); file and comment are interned, so cross-pool readers must map them via the right pool. Layout is selected structurally by the countkofExtendedSuffixwords trailing theLineInfoLit(see the section header):k==0common/no-comment,k==1wide/no-comment,k>=2wide + comment.proc lineInfoFile(c: Cursor): stringThe filename for the value's line info, resolved against
c.pool.filenames. Empty string when there is no line info.proc inc(c: var Cursor)Advance past the head of the current value (kinded token plus its suffix, if any). For a TagLit this lands at the first body token; use
skipto jump past the whole subtree.proc skip(c: var Cursor)Advance past the current value, including all descendants of a TagLit.
template hasMore(c: Cursor): boolTrue while there are more tokens in the current bounded scope. No sentinel:
remdoes all the work.proc enterScope(c: var Cursor): CursorScopeEnters the current
TagLitbody and returns its saved outer bounds. Pair withleaveScopeafter consuming every child.proc leaveScope(c: var Cursor; scope: CursorScope)Leaves a scope opened by
enterScope. The cursor must have consumed the complete bounded body.proc sub(c: Cursor): CursorRead-only descent: returns a bounded cursor over the children of the
TagLitatc, leavingcitself untouched. Use it for a throwaway walk of a node's body (while result.hasMore: …) where there is no dest to preserve into and hence no scope toleaveScope. Replaces the oldvar t = c; discard enterScope(t)idiom.proc isEscapedTag(c: Cursor): boolIs the node at
cspelled through its adapter's escape tag — i.e. does its real id sit in a leadingIntLitchild, one token in front of the operands?{.alwaysInline.}rather than{.inline.}, whichcomputeInlineInfodoes not consult: what is left here isescapeTagOfplus two comparisons, and it has five call sites. SeeisEscapedBodyfor what was taken out to make that true.proc resolvedTagId(c: Cursor): TagIdThe tag id of the node at
c, undoing the escape. Equal tocursorTagIdfor everything that fits the 9-bit field, which is every tag of an adapter that declares no escape space — so this is the accessor to reach for, andcursorTagIdthe one for when you truly mean the token.template into(c: var Cursor; body: untyped)template loopInto(c: var Cursor; body: untyped)proc leaveScopePartial(c: var Cursor; scope: CursorScope)Leaves a scope opened by
enterScopewithout requiring the body to have been fully consumed: rewinds to the scope head and skips the whole subtree. The early-out counterpart toleaveScope.template peekInto(c: var Cursor; body: untyped)proc rootOf(c: Cursor): SymIdThe access root of an lvalue: the first
Symbolin the subtree atc—xinx.f[i]— orSymId(0)if there is none.type TokenBuf = object data: ptr UncheckedArray[NifToken] len: int64 cap: int64 owner: ptr CursorOwnerObj openTags: seq pool: ref Pool.Obj tags: ref TagPool.Obj
template requirePools(b: var TokenBuf)A buffer the interning builders and the cross-pool copy are about to write through must carry both of its pools. They are THREADED in at construction (
createTokenBuf/initTokenBuf) and never invented here: a literals pool decides what a token's payload id means and a tag pool is the buffer's whole kind space, so substituting a fresh one for either silently reinterprets everything already in the buffer.default(TokenBuf)is not a usable buffer;initTokenBufis the zero-allocation way to spell one.The plugin build is the exception, and only there: it binds its process-wide pools rather than reject the buffer, because
plugins.NifBuilderis a public alias whosedefaultshape must keep working. See thenimonyPluginblock above.proc createTokenBuf(cap: int64; sharedPool: ref Pool.Obj; sharedTags: ref TagPool.Obj): TokenBufMint a new buffer. Pass
sharedPoolto thread a single literals pool through multiple buffers (cross-format dedup). PasssharedTagsonly if you want multiple buffers to use the same tag namespace — adapters typically create their own freshTagPoolper buffer socast[Enum](c.cursorTagId.uint32)lines up with the adapter's enum ordinals.proc initTokenBuf(sharedPool: ref Pool.Obj; sharedTags: ref TagPool.Obj): TokenBufA buffer bound to
sharedPool/sharedTagsthat owns NO storage yet — the firstaddallocates it. This is what an object field, aseqslot or any other buffer that used to be spelleddefault(TokenBuf)must be initialized with: the tag pool is a buffer's whole kind space, so it is THREADED in at construction and never fallen back to.proc adoptForeignTokens(data: pointer; count: int64; sharedPool: ref Pool.Obj; sharedTags: ref TagPool.Obj): TokenBufBuild a buffer whose
counttokens are BORROWED fromdata(e.g. an mmap'd.bifregion) rather than copied into a heap allocation — a zero-copy load. The buffer reads like any other. It NEVER freesdata: the block is handed to an eagerly-created cursor owner seeded with an EXTRA, permanent reference (rc = 2= the buffer's own ref plus one keep-alive), sodecRcAndFreenever reaches 0 and neverdeallocs the borrowed storage. A mutation still works unchanged: because the owner is shared (rc > 1),prepareMutationforks a private heap copy and abandons the borrowed block — no special-casing anywhere else. The buffer does NOT own the mapping's lifetime:datamust stay valid (and aligned forNifToken, 4 bytes) for as long as the buffer or any cursor over it lives. The caller keeps the backing store resident — typically for the whole process; an mmap left mapped costs only address space, reclaimed at exit.proc len(b: TokenBuf): int64proc [](b: TokenBuf; i: int64): NifTokenproc []=(b: var TokenBuf; i: int64; v: NifToken)proc prepareMutation(b: var TokenBuf)proc expectUnique(b: var TokenBuf)proc add(b: var TokenBuf; t: NifToken)Plain append. Use
openTag/closeTagfor nested TagLits.proc shrink(b: var TokenBuf; newLen: int64)Truncate to
newLentokens. Any still-open tags at or past the cut are discarded from the build-time stack so latercloseTagcalls keep sealing the right opens (rollback-past-an-open-tag pattern).proc rawTokenPtr(b: TokenBuf): pointerPointer to the first token word;
len(b) * sizeof(NifToken)bytes follow.proc growRawUninit(b: var TokenBuf; count: int64): pointerGrow storage to hold exactly
counttokens, setlen = count, and return the storage pointer so the caller can fill it directly (e.g.readBuffer). The contents are left uninitialized. Binary loaders only.proc subtreeWidth(c: Cursor): int64Total tokens occupied by the value at
c(head + body).proc replace(dest: var TokenBuf; by: Cursor; pos: int64)Replace the sealed subtree at
poswith the sealed subtreeby.proc insert(dest: var TokenBuf; src: TokenBuf; pos: int64)Splice
src's tokens intodestat indexpos(raw). Enclosing sealed scopes' jumps are NOT adjusted — the caller mustwidenSealedthem.proc appendLineInfo(b: var TokenBuf; file: FileId; line: int32; col: int32; comment: StrId)Append a
LineInfoLitsuffix (plus oneExtendedSuffixon overflow) to the head token /openTagjust emitted — call it immediately after, so it lands as that head's trailing suffix. Implements no policy: the caller decides when the position actually changed (emit-on-change). No-op whenfileis invalid. The filename must already be interned inb.pool.filenames. Optionally attachcomment(aStrIdintob.pool.strings,StrId(0)= none) — a NIF#…#decoration on this head. A non-zero comment forces the overflow position layout and rides as one furtherExtendedSuffix(a second only for ids past 2^28), sorawLineInforecovers it unambiguously.proc appendLineInfo(b: var TokenBuf; info: NifLineInfo)proc addDotToken(b: var TokenBuf)Appends an empty dot placeholder.
proc addCharLit(b: var TokenBuf; c: char)Appends a character literal.
proc addStrLit(b: var TokenBuf; s: string)Appends a string literal, using inline storage when possible.
proc addStrLit(b: var TokenBuf; s: openArray)Appends a string literal taken straight from a byte view. For a parser reading someone else's buffer this saves cutting a slice out of it first: a short value costs nothing at all, and a long one costs the single copy the pool was going to make anyway.
proc addIdent(b: var TokenBuf; s: string)Appends an identifier, using inline storage when possible.
proc addSymUse(b: var TokenBuf; s: string)Appends a symbol use, interning
swhen it does not fit inline.proc addSymDef(b: var TokenBuf; s: string)Appends a symbol definition, interning
swhen it does not fit inline.proc internedSymToken(p: ref Pool.Obj; kind: NifKind; id: SymId): NifTokenThe single token
addSymUse/addSymDefwould emit forid: inline when the name fits inStrInlineMaxLenbytes, a pool ref otherwise. Patching a Symbol/SymbolDef token IN PLACE must go through this — a rawsymTokenfor a short name gives one symbol two different payloads within a tree, which breaks the same-string ⇒ same-payload invariant the writer rule establishes (and hence every payload-level equality test).proc addSymDef(b: var TokenBuf; id: SymId)Emits a symbol definition already interned in
b.pool.proc addSymUse(b: var TokenBuf; id: SymId)Emit a symbol already interned in
b.pool. Short symbols remain inline; longer symbols reuseidwithout a second hash-table lookup.proc addIntLit(b: var TokenBuf; v: int64)Pure inline. Writer picks the shortest carrier whose SIGNED width holds
v: 28-bit (one token) for v in [-2^27, 2^27), 56-bit (two tokens) for v in [-2^55, 2^55), 84-bit (three tokens) otherwise. The token COUNT must follow the chosen signed width, notv's unsigned magnitude: a positivevwhose top carrier bit is set (e.g. 2^27 ≤ v < 2^28) still needs the wider carrier, or the reader's sign-extend-from-width would read it back negative. (Hence this does NOT go throughemitChained, which trims by magnitude — correct for unsigned/float, wrong for signed.)proc addUIntLit(b: var TokenBuf; v: uint64)Appends an unsigned integer literal using the shortest token chain.
proc addFloatLit(b: var TokenBuf; v: float64)Appends a floating-point literal using its exact bit representation.
proc openTag(b: var TokenBuf; t: TagId)Begin a new tagged subtree. The matching
closeTagpatches the emitted TagLit's jump in place (or splices in anExtendedSuffixright after the TagLit if the body overflows the 19-bit jump field).An id past
TagMaskdoes not fit the token and goes through the adapter's escape tag instead; callers never have to sort the two apart.proc reopenLastTree(b: var TokenBuf; pos: int64)Reopen the finished tree headed by the TagLit at
posso more children can be appended; close it again withcloseTag(which recomputes the jump over the old + new children). The tree's subtree must be the last content ofb.proc closeTag(b: var TokenBuf)Seal the most recently opened tag.
template buildTree(b: var TokenBuf; tag: TagId; body: untyped)proc unclosedTagPositions(b: TokenBuf): seqDebug helper: build-time stack of still-open TagLit positions.
proc beginRead(b: var TokenBuf): Cursorproc childCursor(c: Cursor): CursorReturns a bounded cursor over the children of the
TagLitatc. The input cursor is not advanced.proc endRead(c: var Cursor)proc cursorAt(b: var TokenBuf; i: int64): Cursorproc cursorTailAt(b: var TokenBuf; i: int64): CursorLike
cursorAtbut acceptsi == b.len(a sealed tree's implicit close can leave the next position at end-of-buffer). Thenrem == 0and the cursor reports a virtual close — onlykind-style probes are valid.proc cursorToPosition(b: TokenBuf; c: Cursor): int64Token index of
cwithinb(inverse ofcursorAt). Stable key for per-expression tables (e.g. a register allocator's location map).proc readonlyCursorAt(b: TokenBuf; i: int64): CursorLike
cursorAtbut does not requirevar b.The
CursorOwnerheader is minted on demand — through a cast, sincebis only borrowed here. It has to be: the owner is where a cursor's pools live, so an ownerless cursor answersnilfor bothpoolandtagsand every pool-backed payload read and everyaddSubtreeoff it would have to guess which pool world it came from. That guess is what the compiler's formerfallbackPool/fallbackTagsglobals used to make. Minting the header also makes the cursor keep the tokens alive like any other cursor instead of dangling the momentbdies.The header costs one small allocation, and only for a buffer that has none yet; a following mutation reclaims it on the cheap
rc == 1path.proc +!(c: Cursor; diff: int64): CursorAdvance
cbydifftokens, keeping the boundedremcorrect (used by CF goto navigation).diffmust not exceedrem.proc hasCurrentToken(c: Cursor): boolTrue if
cpoints at a readable token (pointer + bound check). PreferhasMorein regular traversal; this survives malformed input.proc unsafeDec(c: var Cursor)Move one token backwards (no bounds check). Only valid on a cursor known to sit past a real token; used by backward scans over a raw buffer.
proc peekPastEnd(n: Cursor): CursorA loosely-bounded cursor at the token following the fully-consumed bounded scope
n(rem == 0; the close is implicit). Read-only peek at a sibling the caller KNOWS exists.proc addSubtree(dest: var TokenBuf; c: Cursor)Copy the subtree rooted at
cintodest. When both pools AND both tag pools match, this is a single bulkcopyMem; otherwise the source's literals and tag names are re-interned intodest's pools token-by-token. Callers don't need to know which case applies.proc addBufferSamePool(dest: var TokenBuf; src: TokenBuf)Append a closed buffer that shares
dest's literal and tag pools.The source is borrowed and remains usable. Matching pools make the append one bulk copy without constructing a read cursor.
proc addBuffer(dest: var TokenBuf; src: var TokenBuf)Append all complete top-level values from
srctodest.Matching pools permit one bulk copy. Otherwise values are re-interned through
addSubtree.destandsrcmust be distinct buffers.