parser-core API
@lanexio/parser-core is the foundation every grammar builds on: the zero-copy LexTree and its views, the writers grammar packs use to emit trees, the streaming and incremental helpers, and the frozen shared-buffer protocol constants.
Layer: 1 (Core). No dependencies. Runtime: Universal.
LexTree
Section titled “LexTree”An immutable, zero-copy parse tree over one ArrayBuffer. Constructing one from an untrusted buffer runs full validation (header + every node record) and throws LexTreeValidationError with a stable code on any violation; trees produced by the writers below skip the per-node pass safely.
import { LexTree } from '@lanexio/parser-core';
const tree = new LexTree(buffer, sourceBytes, { metadata: { fieldNamesById } });| Member | Type | Description |
|---|---|---|
buffer | ArrayBuffer | The raw flat-AST buffer (header + node records). |
source | Uint8Array | The original parsed bytes. Never copied. |
nodeCount | number | Node records in the tree. Cached; O(1). |
root | LexNode | Root node view. |
cursor() | LexCursor | New cursor positioned at the root. |
fieldNameForId(id) | string | null | Grammar field-role name for a field id (null for 0/unknown). |
fieldIdForName(name) | number | null | Reverse lookup; resolve once for hot loops. |
readNodeUint8/16/32(index, fieldOffset) | number | Raw little-endian record reads. fieldOffset must be a protocol layout constant; only index is bounds-checked. |
parentIndex(index) | number | null | Parent’s node index (null at root). Builds a parent side-table lazily on first use, then O(1). |
nextSiblingIndex(index) / previousSiblingIndex(index) | number | null | Sibling index math over subtreeSize. |
getCachedSerialization(key) / setCachedSerialization(key, value) | string | undefined / void | Per-tree serialization memo used by grammar serializers (keys like "html:outer"). Trees are immutable, so cached output is always valid. |
LexTreeValidationError
Section titled “LexTreeValidationError”Thrown only by the constructor on malformed buffers. err.code is one of:
buffer_too_small · invalid_magic · invalid_version · invalid_record_size · empty_tree · invalid_root_index · node_section_out_of_bounds · invalid_node_index · invalid_subtree_size · invalid_node_range
(invalid_node_index is also thrown by the raw readNode* methods for an out-of-range index.)
LexNode
Section titled “LexNode”An immutable 16-byte-record view. Cheap to create; hold or discard freely.
| Member | Type | Notes |
|---|---|---|
index | number | Preorder index in the flat AST. |
kind / flags / fieldId | number | Raw record fields. |
hasError | boolean | (flags & LEX_NODE_HAS_ERROR) !== 0. |
range | LexRange = readonly [start, end] | Half-open byte range. A tuple — destructure it. |
text | string | UTF-8 decode of the range. Allocates; avoid in hot loops. |
subtreeSize | number | Nodes in this subtree including itself. |
fieldName() | string | null | Field-role name via tree metadata. |
children() | IterableIterator<LexNode> | Direct children; O(k) total. |
childCount() | number | O(k). |
child(i) | LexNode | null | O(i) per call — prefer children() over indexed loops. |
childByField(name) | LexNode | null | First direct child with the named field role. |
firstChildForRange([s, e]) | LexNode | null | First direct child whose range contains [s, e). |
descendantForRange([s, e]) | LexNode | null | Deepest containing descendant. |
nextSibling() / previousSibling() | LexNode | null | Sibling navigation. |
followingSiblings() | IterableIterator<LexNode> | Later siblings. |
parent() | LexNode | null | Via the lazy parent table. |
Traversal is hang-proof by construction: child/sibling stride arithmetic carries forward-progress guards, so even a corrupt-but-header-valid buffer cannot loop traversal forever.
LexCursor
Section titled “LexCursor”Stateful, allocation-light traversal. All moves return boolean (did the cursor move?).
| Method | Description |
|---|---|
current | LexNode at the position. |
gotoFirstChild() / gotoNextSibling() / gotoPreviousSibling() / gotoParent() | Structural moves. |
gotoFirstChildForRange([s, e]) / gotoDescendantForRange([s, e]) | Byte-range descent for editor tooling. |
See Tree Traversal for the canonical DFS pattern.
Tree writers (grammar-author surface)
Section titled “Tree writers (grammar-author surface)”Grammar packs emit trees through one of two writers. Both attach optional field-name metadata and return a ready LexTree.
Object form — createTree
Section titled “Object form — createTree”import { createTree, createPendingNode } from '@lanexio/parser-core';
const nodes = [ createPendingNode(kind, flags, fieldId, rangeStart, rangeEnd), // subtree_size defaults to 1 // …preorder DFS; patch node.subtree_size as containers close…];const tree = createTree(sourceBytes, nodes, { fieldNamesById: MY_FIELD_NAMES });PendingNode is the mutable record shape (kind, flags, field_id, range_start, range_end, subtree_size).
Typed-array form — createTreeFromUint32Array
Section titled “Typed-array form — createTreeFromUint32Array”The allocation-light path used by the JSON/CSS/toy grammars: nodes live in a Uint32Array with 6 slots per node.
import { NODE_STRIDE, // 6 SLOT_KIND, SLOT_FLAGS, SLOT_FIELD, // 0, 1, 2 SLOT_START, SLOT_END, SLOT_SIZE, // 3, 4, 5 growNodeData, createTreeFromUint32Array,} from '@lanexio/parser-core';
let nd = new Uint32Array(64 * NODE_STRIDE);let nc = 0;
nd = growNodeData(nd, nc + 1); // doubling growth; returns (possibly new) bufferconst o = nc * NODE_STRIDE;nd[o + SLOT_KIND] = kind;nd[o + SLOT_FLAGS] = 0;nd[o + SLOT_FIELD] = 0;nd[o + SLOT_START] = start;nd[o + SLOT_END] = end;nd[o + SLOT_SIZE] = 1;nc += 1;
const tree = createTreeFromUint32Array(nd, nc, sourceBytes, { fieldNamesById });Streaming — createParseStream
Section titled “Streaming — createParseStream”createParseStream(grammarName: string, parseFn: (bytes: Uint8Array) => LexTree): LexParseStreamPush-based sessions with per-push trees and a never-rejecting async iterator. Full contract in the Streaming guide.
Incremental — applyEdit and reparse
Section titled “Incremental — applyEdit and reparse”applyEdit(source: Uint8Array, edit: LexEdit): Uint8Array // pure; throws LexEditError on bad rangesreparse(previousTree: LexTree, edit: LexEdit): LexTree // never throws; toy-grammar demo bindingLexEditErrorCode: range_out_of_bounds · range_inverted. Note that core’s standalone reparse is bound to the toy demo grammar (it exists for the never-throw harness); real applications use parser.reparse from a createParser handle, which binds the loaded grammar. Semantics in the Incremental guide.
Protocol constants
Section titled “Protocol constants”| Constant | Value | Meaning |
|---|---|---|
PROTOCOL_VERSION | 1 | Shared TypeScript/WASM buffer-contract version. |
LEX_TREE_MAGIC | 0x4c584943 | Header magic for tree buffers. |
LEX_TREE_VERSION | 1 | Tree buffer format version. |
TREE_HEADER_SIZE | 16 | Bytes before the node section. |
NODE_RECORD_SIZE | 16 | Bytes per node record. |
LEX_NODE_HAS_ERROR | 1 | Error flag bit. |
Header/record byte offsets are documented in Flat AST.
Demo grammar exports
Section titled “Demo grammar exports”parse (the toy expression parser), LexToyKind, LexToyField, and LEX_TOY_FIELD_NAMES_BY_ID implement the (ident + ident) demo grammar used by the test harnesses and the @lanexio/parser quickstart (re-exported there as toyParse). They are not general-purpose parsers.