Skip to content

C — hyperlark.h

Rust, TypeScript and Python get their reference from a generator. C’s is the header itself — hand-written, and the actual contract the ABI promises. It is rendered here verbatim from the shipped file, so it cannot drift from what you compile against.

crates/hyperlark-c/include/hyperlark.h
/*
* hyperlark.h — C ABI for Hyperlark (the pure-Rust Lark port).
*
* HAND-WRITTEN and reviewed (Phase 12): cbindgen is used only as a CI drift
* check, because a generated header under-documents the three contracts below.
* This is the source of truth for the ABI.
*
* v1 SURFACE (deliberately minimal; user-approved). LALR parse + an opaque,
* zero-allocation tree cursor + the phase-3 error taxonomy + the incremental
* (interactive) parser + the name-keyed and reduce-time folds. Earley and
* postlex/indenter are DEFERRED; their status
* codes are RESERVED (LARK_UNEXPECTED_EOF / LARK_POSTLEX_ERROR) and never returned
* by an LALR call, but the numbering is stable so a later rev adds them without a
* renumber. Handles stay opaque for the same forward-compat reason.
*
* ============================ THE THREE CONTRACTS ============================
*
* 1. LIFETIME (result-scoped views).
* Two owning handles exist: Lark (a compiled grammar) and LarkParseResult (a
* finished parse). Free each with its lark_*_free. An LarkNode cursor and EVERY
* view it yields — child cursors, the (ptr,len) name/value byte slices, the
* position struct — BORROW the LarkParseResult and are invalid the instant it is
* freed. Copy anything you need to keep out before lark_result_free. The result
* bundles both the tree and the grammar's name table, so cursor name lookups
* stay valid even if you free the Lark FIRST.
*
* 2. ERRORS + PANICS (no unwind crosses C).
* Every fallible entry point returns an LarkStatus and runs inside a Rust
* catch_unwind: a bug that would panic becomes LARK_INTERNAL_PANIC, never an
* unwind across this boundary (which is undefined behavior). A parse failure
* (unexpected token/characters) is an ordinary non-OK status, not a panic.
* Detail lives on a thread-local last-error buffer: lark_last_error() returns the
* message; lark_last_error_expected_count()/lark_last_error_expected() return the
* accepts/allowed terminal list (populated for LARK_UNEXPECTED_TOKEN and
* LARK_UNEXPECTED_CHARACTERS). Those pointers are valid only until the next
* FAILING lark_* call on the same thread (errno-style); a successful call does
* not clear them. Do not free them.
* NOTE: catch_unwind requires the library be built with panic=unwind (the
* workspace profile forces it). Under panic=abort a bug aborts the process
* instead of returning LARK_INTERNAL_PANIC.
*
* WHAT THIS DOES AND DOES NOT PROMISE. It promises PANIC CONTAINMENT: no Rust
* panic becomes an unwind across the boundary. It does NOT promise the process
* survives everything, because two failures are not panics and catch_unwind
* cannot intercept either — both ABORT:
* - OUT OF MEMORY, generally. Rust aborts on allocation failure, so ANY call
* that allocates can abort if the allocator fails. That is most of this API:
* parsing builds arenas and tokens, the traversal cursors
* (lark_cursor_new_*) and accepts snapshots allocate, folds allocate.
* There is no subset of this API that is OOM-total. Two
* paths AMPLIFY the risk far beyond input size and are called out on their
* own functions: lark_result_pretty (output is O(nodes x depth)) and grammar
* compilation (a grammar can expand exponentially).
* - STACK OVERFLOW, which unlike OOM IS localized: grammar compilation
* (lark_from_source / lark_from_json) recurses over grammar structure and can
* overflow on a hostile grammar — treat a grammar as TRUSTED INPUT, see those
* functions. Parsing text with an already-built Lark*, the cursors, the
* folds, and the interactive parser are ITERATIVE and do not overflow:
* their stack use is independent of input depth, measured byte-identical
* from 10 to 500,000 levels (see lark_from_source on sizing a thread).
*
* 3. THREADING.
* Lark is Send + Sync: share one `const Lark*` across threads and parse
* concurrently. LarkParseResult is Send: you may move it to another thread, but
* treat a single result + its cursors as one-thread-at-a-time (a by-convention
* reservation for future lazy caches — today's accessors are const reads), and
* never race any use against lark_result_free. LarkInteractive is Send but NOT
* Sync: move it between threads, but drive ONE handle from one thread at a time —
* feed_token/feed_next advance parser state and accepts_count rewrites the
* handle's accepts snapshot, so none of its accessors are concurrent-safe reads
* (fork with lark_interactive_copy for parallel exploration). The last-error
* buffer is PER-THREAD: check it on the same thread that made the failing call.
* Full per-target contract + the pthread pin: dev/design/thread-safety.md
* and bindings/c/tests/threads.c.
*
* Positions are Unicode CODE-POINT offsets (matching Lark), not byte offsets.
* Token value byte slices are length-prefixed and NOT NUL-terminated; they are
* valid UTF-8 in string mode (the only interior NUL then being a literal U+0000).
*/
#ifndef HYPERLARK_H
#define HYPERLARK_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ------------------------------- version + ABI ----------------------------- *
* TWO INDEPENDENT identities, with different jobs. Do not conflate them:
*
* 1. RELEASE version — for logging, bug reports, and gating across RELEASES:
* HYPERLARK_VERSION_MAJOR/MINOR/PATCH, HYPERLARK_VERSION_STRING,
* HYPERLARK_ABI_VERSION (packed int), and runtime lark_version() /
* lark_abi_version(). These track the crate's marketing version. They do NOT
* change on a same-version ABI change, so on their own they CANNOT tell a
* header apart from a library built at a different commit that shares the
* version (routine during 0.x pre-release, where the ABI is unstable). Do not
* rely on them to catch a cross-commit header/library mismatch.
*
* 2. C ABI REVISION — for header<->library COMPATIBILITY:
* HYPERLARK_C_ABI (compile-time) and lark_c_abi_revision() (runtime) are a
* small integer bumped on EVERY breaking change to this C ABI, independent of
* the release version. Assert once at startup:
* lark_c_abi_revision() == HYPERLARK_C_ABI
* A mismatch means the header and the linked .a/.so are ABI-incompatible. (A
* library predating this symbol fails to LINK against a header that references
* it — itself a clear signal.) During pre-release the safe rule remains: build
* the header and the library from the SAME checkout.
*
* MAINTAINERS: bump HYPERLARK_C_ABI (and the matching Rust constant) whenever the
* C ABI changes — a signature, struct layout, enum value, or flag meaning. The C
* test suite asserts the macro and the linked library agree. */
#define HYPERLARK_VERSION_MAJOR 0
#define HYPERLARK_VERSION_MINOR 1
#define HYPERLARK_VERSION_PATCH 0
#define HYPERLARK_VERSION_STRING "0.1.0-beta.1"
/* Packed major*1000000 + minor*1000 + patch (SQLite/curl convention): each
* component gets its own decimal decade up to 999, so 0.1.0 -> 1000 and no two
* distinct versions collide (a *100 packing would alias 0.0.100 with 0.1.0). */
#define HYPERLARK_ABI_VERSION \
(HYPERLARK_VERSION_MAJOR * 1000000 + HYPERLARK_VERSION_MINOR * 1000 + \
HYPERLARK_VERSION_PATCH)
/* The C ABI revision — bumped on every breaking C-ABI change, independent of the
* release version above (see the maintainers note). What each bump covered:
* 1 -> 2: lark_node_token_double / lark_node_token_int64 REMOVED in favour of
* lark_node_token_value_copy. THAT is the break -- an old binary no longer
* resolves those symbols against a new library. LARK_BUFFER_TOO_SMALL
* joins LarkStatus in the same revision and is listed because the rule
* above names enum values, but on its own it breaks no link direction:
* only the new function returns 11, so no existing binary can meet it. */
#define HYPERLARK_C_ABI 2
/* ------------------------------- status codes ------------------------------ */
typedef enum LarkStatus {
LARK_OK = 0,
/* Construction (GrammarError-class): .lark compile, JSON-grammar load,
* parser/lexer configuration reject, lexer-build validation, or an
* unsupported build option. Distinct from an internal bug (LARK_INTERNAL). */
LARK_CONSTRUCTION_ERROR = 1,
/* A token (or synthetic $END) had no action in the current state
* (UnexpectedToken). Accepts set is on the last-error buffer. */
LARK_UNEXPECTED_TOKEN = 2,
/* No terminal matched the input at some position (UnexpectedCharacters).
* Allowed set is on the last-error buffer. */
LARK_UNEXPECTED_CHARACTERS = 3,
/* The start symbol passed to lark_parse is not declared by the grammar, or NULL
* was passed to a grammar with several declared starts (no default). */
LARK_UNKNOWN_START = 4,
/* A caller argument was NULL where required, not valid UTF-8, or a
* construction-options word set a bit outside LARK_OPT_ALL. */
LARK_INVALID_ARGUMENT = 5,
/* A Rust panic was caught at the boundary — an internal bug. */
LARK_INTERNAL_PANIC = 6,
/* A non-panic internal failure (catch-all). Not expected from v1. */
LARK_INTERNAL = 7,
/* --- RESERVED; never returned by an LALR call --- */
LARK_UNEXPECTED_EOF = 8, /* Earley UnexpectedEOF (Earley deferred) */
LARK_POSTLEX_ERROR = 9, /* indenter/postlex DedentError (postlex deferred) */
LARK_INTERACTIVE = 10, /* parse_interactive over a non-LALR / custom-lexer
* instance — unreachable on this LALR-only surface.
* (The interactive parser itself IS shipped; this
* code only guards the seat that can't occur here.) */
/* --- end of the reserved block; what follows IS returned --- */
/* A real output buffer was too short for the result -- today only
* lark_node_token_value_copy, whose *out_len then holds the VALUE LENGTH. Its own
* code, not LARK_INVALID_ARGUMENT, because it is the one buffer outcome you
* RETRY rather than fix -- realloc to *out_len + 1 (the value plus its NUL)
* and call again. Retrying with *out_len alone returns this same status
* forever. The NULL/0 size query is not this: it returns LARK_OK. Numbered
* after the reserved block above, whose codes were already frozen when this
* was added. */
LARK_BUFFER_TOO_SMALL = 11
} LarkStatus;
/* A stable, static, NUL-terminated name for a status (logging/tests). Takes a
* plain int32_t (not the enum) so any C-supplied integer is total and defined;
* an out-of-range code returns "unknown". */
const char *lark_status_name(int32_t status);
/* ------------------------------ opaque handles ----------------------------- */
typedef struct Lark Lark; /* compiled grammar (Send+Sync) */
typedef struct LarkParseResult LarkParseResult; /* finished parse (Send) */
typedef struct LarkInteractive LarkInteractive; /* incremental parser (Send; free
with lark_interactive_free) */
typedef struct LarkTreeCursor LarkTreeCursor; /* resumable traversal (owned; free
with lark_cursor_free BEFORE its
result; single-thread use) */
/* A borrowed tree cursor: two opaque pointers into the result's arenas. Pass and
* copy BY VALUE. Do NOT inspect the fields — they are result-lifetime-bound and
* meaningless once the result is freed. */
typedef struct LarkNode {
const void *_result;
const void *_node;
} LarkNode;
typedef enum LarkNodeKind {
LARK_NODE_TREE = 0, /* a rule node: has a data name + children */
LARK_NODE_TOKEN = 1, /* a leaf token: has a type name, value, positions */
LARK_NODE_NULL = 2, /* a maybe_placeholders hole (Lark's None child) */
LARK_NODE_FOREIGN = 3 /* transform-produced host value (reserved; no v1 path) */
} LarkNodeKind;
/* Six token positions (code-point offsets), each with a present flag. A position
* is absent (present == false) for a synthetic token. */
typedef struct LarkTokenPositions {
uint32_t start_pos; bool start_pos_present;
uint32_t end_pos; bool end_pos_present;
uint32_t line; bool line_present;
uint32_t end_line; bool end_line_present;
uint32_t column; bool column_present;
uint32_t end_column; bool end_column_present;
} LarkTokenPositions;
/* Construction option flags for lark_from_source / lark_from_json (a flat, LALR-only
* subset — parser/lexer/ambiguity are not exposed). OR the flags you want; pass
* LARK_OPT_NONE (0) for the defaults, which match Lark. The bit polarity is chosen
* so 0 == defaults: the single default-ON option is expressed as an opt-OUT, so a
* zeroed value is never wrong (no options struct, no abi_size, no init ceremony).
* A bit outside LARK_OPT_ALL is LARK_INVALID_ARGUMENT — a newer option requested of
* an older library that cannot honor it, rejected rather than silently dropped.
*
* Because the flags are OPT-OUT only, 0 leaves an option UNSET, which honors the
* default for .lark source AND the SERIALIZED value on lark_from_json. One
* consequence: a grammar serialized with maybe_placeholders=false loads with
* placeholders off, and C cannot force them back ON (there is no force-on bit) —
* honoring the saved value is the intended, safer behavior.
*
* lark_from_json — the flags are NOT all equal there. A serialized grammar baked
* some of these in, and core lets the SAVE win: LARK_OPT_KEEP_ALL_TOKENS has NO
* effect on lark_from_json (the serialized keep_all_tokens always wins; the flag is
* honored only by lark_from_source). maybe_placeholders is the exception core
* treats as caller-overridable — LARK_OPT_NO_PLACEHOLDERS DOES force it off on load
* (an absent bit honors the save, per above). Prefer lark_from_source when you need
* these options to take effect. */
#define LARK_OPT_NONE UINT64_C(0)
#define LARK_OPT_NO_PLACEHOLDERS (UINT64_C(1) << 0) /* disable maybe_placeholders (default ON) */
#define LARK_OPT_KEEP_ALL_TOKENS (UINT64_C(1) << 1) /* retain filtered terminals; from_source only
(serialized value wins on from_json) */
/* Stamp position meta on RULE (tree) nodes. Currently forward-compat only: the C
* surface has no rule-node position accessor, so this has NO observable effect
* here yet (token positions come from lark_node_token_positions on every parse,
* independent of this flag). Default off. */
#define LARK_OPT_PROPAGATE_POSITIONS (UINT64_C(1) << 2)
/* The mask of all flags THIS header knows; unknown bits are rejected. */
#define LARK_OPT_ALL \
(LARK_OPT_NO_PLACEHOLDERS | LARK_OPT_KEEP_ALL_TOKENS | LARK_OPT_PROPAGATE_POSITIONS)
/* -------------------------------- lifecycle -------------------------------- */
/* Compile a grammar from .lark source (%import resolves against the embedded
* stdlib only). On LARK_OK, *out owns an Lark to be freed with lark_free.
* `source` addresses `source_len` bytes (NULL allowed only when len == 0).
* `options` is a bitmask of LARK_OPT_* (LARK_OPT_NONE for defaults).
*
* NESTING LIMIT. Compiling a grammar descends once per nesting level, so source
* NESTING is bounded: past the limit the compile fails with LARK_CONSTRUCTION_ERROR
* instead of overflowing the stack. The exact depth accepted VARIES BY CONSTRUCT,
* because constructs cost different amounts of stack per level and the bound tracks
* that cost: 39 levels for plain groups `(...)`, optionals `[...]`, terminals, and
* template usage `t{t{...}}`, but fewer for compound shapes that descend further per
* level — e.g. `["a"]?` (an optional carrying a repeat operator) accepts 29. Assume
* ~29, not 39, if you need one number. Every accepted depth is stack-safe at the
* 512 KiB minimum below; deeper is a clean error, never a crash.
* Real grammars nest single digits deep. This is deliberately stricter than
* Python Lark (measured on 1.3.1 at the default recursion limit: ~500 levels for
* groups/templates, ~165 for optionals, then a CATCHABLE RecursionError — sometimes
* wrapped in a VisitError); hyperlark trades that range for not crashing a C host.
*
* GRAMMAR COMPILATION IS NOT HARDENED AGAINST HOSTILE GRAMMARS. The nesting bound
* above covers NESTING only — it is not a general safety guarantee, and contract
* #2's PANIC CONTAINMENT does not help here: these failures are not panics. Other
* shapes still recurse or expand without a bound and can ABORT the process — by
* overflowing the stack (not a panic; catch_unwind cannot intercept it) or by
* exhausting memory (Rust's alloc handler aborts). Known shapes include, but are NOT
* limited to: long terminal-reference chains (`A: B` `B: C` …, ~52 deep on a 512 KiB
* stack); very wide rule bodies (~600 group/alternation items in one rule there);
* deeply nested regex literals (~200 groups there); and nested `~N` repeats, which
* expand EXPONENTIALLY and can exhaust memory from a 50-byte grammar well INSIDE the
* nesting bound. These are pre-existing engine limits, not C-binding ones, and the
* list is illustrative — it has not been proven exhaustive. TREAT A GRAMMAR AS
* TRUSTED INPUT: bound its size and shape before compiling one you did not write.
* Parsing text with an already-built Lark* is a different matter — that path is
* iterative, so it does not overflow the stack on deep input; like every call in
* this API it can still abort if the allocator fails (see contract #2).
*
* STACK. Grammar compilation is recursive: give the calling thread a normal stack.
* A trivial grammar can already overflow a 256 KiB thread stack (independent of the
* nesting limit; the exact floor varies by target and build profile); 512 KiB
* suffices for the nesting shapes the limit accepts, but see the paragraph above for
* shapes it does not cover. Parsing, tree walking, and folding are iterative, and
* their stack use is INDEPENDENT of input depth — measured byte-identical from 10 to
* 500,000 levels. Note that the FIRST parse in a process does one-time lazy
* initialization costing tens of KiB, so size a worker thread for that, not for the
* few KiB a warm parse uses. */
LarkStatus lark_from_source(const char *source, size_t source_len,
uint64_t options, Lark **out);
/* Load a grammar from a serialized tools.serialize JSON string. On LARK_OK, *out
* owns an Lark to be freed with lark_free. `options` is a bitmask of LARK_OPT_*.
*
* The hostile-grammar caveat on lark_from_source applies HERE TOO, and MORE so: a
* serialized grammar is not re-parsed as .lark, so the NESTING BOUND DOES NOT APPLY
* TO IT AT ALL. JSON *structural* depth is bounded (the deserializer's own recursion
* limit yields LARK_CONSTRUCTION_ERROR), but the pattern STRINGS inside are not — a
* deeply nested regex literal in a serialized terminal overflows the stack (~140
* groups on a 512 KiB stack). Treat a serialized grammar as TRUSTED INPUT. */
LarkStatus lark_from_json(const char *json, size_t json_len,
uint64_t options, Lark **out);
/* Parse `text` under `start` (a NUL-terminated symbol name, or NULL for the
* grammar's sole declared start). On LARK_OK, *out owns an LarkParseResult to be
* freed with lark_result_free. A parse failure is a non-OK status (details on the
* last-error buffer); *out is set to NULL on any non-OK status. */
LarkStatus lark_parse(const Lark *lark, const char *text, size_t text_len,
const char *start, LarkParseResult **out);
/* Free a grammar handle (NULL is a no-op). */
void lark_free(Lark *lark);
/* Free a parse result (NULL is a no-op). Invalidates every cursor/view into it. */
void lark_result_free(LarkParseResult *result);
/* Render the parse tree as Lark's indented pretty() string. The returned
* NUL-terminated UTF-8 pointer is OWNED BY THE RESULT: valid until
* lark_result_free, the same on every call (rendered once, then cached), and
* never freed by you. NULL if result is NULL.
*
* A DEBUG/DUMP helper — do not call it on untrusted input unbounded. Indentation is
* emitted per nesting level, so the output is O(nodes x depth): a DEGENERATE deep
* tree (e.g. a left-recursive chain over a long input) amplifies enormously — a few
* KB of input can render to hundreds of MB, and an allocation failure ABORTS the
* process (Rust's alloc-error handler; catch_unwind cannot intercept it, so this is
* outside contract #2's status guarantee). Bound the input, or the tree's depth,
* before pretty-printing anything you did not produce. */
const char *lark_result_pretty(const LarkParseResult *result);
/* ---- interactive (incremental / STREAMING) parser ------------------------- *
* Lark's parse_interactive: drive a stateful LALR parse token-by-token — the pull
* interface for processing tokens as they are consumed and inspecting parser state
* between them. STREAM the built-in lexer one token at a time
* (lark_interactive_feed_next) and/or feed your own tokens
* (lark_interactive_feed_token); query the accepted next terminals at any point
* (lark_interactive_accepts_count/get); take the finished tree with
* lark_interactive_feed_eof / resume_parse. LALR + built-in lexer only (always true
* for the C surface).
* NOTE: feed_next streams TOKENS, but the parse still BUILDS its tree as it reduces
* — you receive that already-built tree from feed_eof/resume_parse; feed_next is
* not a way to avoid the tree's memory. For a genuinely TREE-FREE stream that folds
* to a value during the parse (no arena at all), register a reduce-time fold and
* call lark_parse_fold — callbacks fire per reduction. */
/* Seat an interactive parser over `text` (empty when text_len==0, for manual
* feeding). `start` is NULL (sole declared start) or a declared start name. On
* LARK_OK, *out owns a LarkInteractive to be freed with lark_interactive_free;
* *out is NULL on any non-OK status. */
LarkStatus lark_parse_interactive(const Lark *lark, const char *text, size_t text_len,
const char *start, LarkInteractive **out);
/* A streamed token from lark_interactive_feed_next. `value` is a borrowed (ptr,len)
* byte span, NOT NUL-terminated, valid until the NEXT feed_next on this handle or
* free. `type_id` is the id space lark_token_name resolves. */
typedef struct LarkTokenEvent {
uint32_t type_id;
const char *value;
size_t value_len;
LarkTokenPositions pos;
} LarkTokenEvent;
/* Pull ONE token: drive the built-in lexer to the next token, feed it to the
* parser, and fill *out — the streaming, pull counterpart to resume_parse (which
* drives to completion). On LARK_OK, *has_token is true and *out is filled when a
* token was consumed, or *has_token is false at end of input (then finish with
* feed_eof / resume_parse). A lexer/parser rejection is a non-OK status (accepts on
* the last-error buffer). `out` and `has_token` are both required (non-NULL).
*
* ON A NON-OK STATUS THE OFFENDING TOKEN HAS ALREADY BEEN CONSUMED, and the handle
* stays usable: the next feed_next continues at the FOLLOWING token. That is what
* makes skip-the-bad-token recovery a one-liner (just keep calling), but it also
* means IGNORING the status silently accepts a tree for input the grammar rejects —
* feeding "1 + + 2" to an arithmetic grammar and continuing yields the tree for
* "1 + 2", which lark_parse would have refused. So: STOP, RECOVER, OR RECORD on a
* non-OK status. Do not just loop until *has_token is false. */
LarkStatus lark_interactive_feed_next(LarkInteractive *ip, LarkTokenEvent *out,
bool *has_token);
/* Feed one token by terminal `name` (NUL-terminated) and value span. LARK_OK on a
* shift/reduce; LARK_UNEXPECTED_TOKEN (accepts set on the last-error buffer) if the
* state rejects it; LARK_INVALID_ARGUMENT if `name` is not a declared terminal. */
LarkStatus lark_interactive_feed_token(LarkInteractive *ip, const char *name,
const char *value, size_t value_len);
/* Snapshot the terminals accepted in the current state (sorted; "$END" appears when
* the state accepts end-of-input, not unconditionally) and return the count — the
* count+index form of the accepts set (no joined string to split; mirrors
* lark_last_error_expected_count). Read each with lark_interactive_accepts_get. 0 if
* ip is NULL. */
size_t lark_interactive_accepts_count(LarkInteractive *ip);
/* The index-th accepted terminal name (NUL-terminated) from the last
* lark_interactive_accepts_count snapshot, or NULL if out of range / no snapshot.
* Owned by the handle; valid until the next accepts_count on it or free. */
const char *lark_interactive_accepts_get(LarkInteractive *ip, size_t index);
/* Finish with a synthesized "$END"; on LARK_OK, *out owns a LarkParseResult (free
* with lark_result_free). The handle is exhausted afterward — do not feed it
* again. *out is NULL on any non-OK status. */
LarkStatus lark_interactive_feed_eof(LarkInteractive *ip, LarkParseResult **out);
/* Drive the remaining built-in-lexer input plus "$END" to a finished tree
* (resume_parse); on LARK_OK, *out owns a LarkParseResult. *out is NULL on any
* non-OK status. */
LarkStatus lark_interactive_resume_parse(LarkInteractive *ip, LarkParseResult **out);
/* Fork an independent interactive parser sharing the same grammar + input (copy),
* e.g. for speculative feeds. On LARK_OK, *out owns the fork. */
LarkStatus lark_interactive_copy(const LarkInteractive *ip, LarkInteractive **out);
/* Free an interactive-parser handle (NULL is a no-op). */
void lark_interactive_free(LarkInteractive *ip);
/* ------------------------------ cursor traversal --------------------------- */
/* A cursor at the tree root. */
LarkStatus lark_result_root(const LarkParseResult *result, LarkNode *out);
/* What this node points at (LARK_NODE_NULL for a NULL/invalid cursor). */
LarkNodeKind lark_node_kind(LarkNode node);
/* Number of children (0 for a non-tree node). */
size_t lark_node_child_count(LarkNode node);
/* The index-th child cursor. LARK_INVALID_ARGUMENT on a non-tree node or an
* out-of-range index. */
LarkStatus lark_node_child(LarkNode node, size_t index, LarkNode *out);
/* --- value-returning traversal (tree-sitter's null-node idiom) --------------
*
* The convenience forms of lark_result_root / lark_node_child: instead of a
* status + out-param, a miss returns the zeroed NULL NODE. Every accessor
* treats that node as "not a match" (kind LARK_NODE_NULL, child count 0, every
* status accessor LARK_INVALID_ARGUMENT), so lookups CHAIN safely:
*
* LarkNode b = lark_child(lark_child(lark_root(res), 0), 1);
*
* LOUD CAVEAT: under maybe_placeholders (the DEFAULT) a legitimate omitted
* optional is a real child whose kind is ALSO LARK_NODE_NULL, and
* lark_node_is_null is true for it too. When "index out of range" vs "the
* grammar has a hole here" matters, use lark_node_child_count or the
* status-returning forms (lark_node_child says LARK_INVALID_ARGUMENT for the
* former and LARK_OK for the latter). */
/* The root by value; the null node for a NULL result. */
LarkNode lark_root(const LarkParseResult *result);
/* The index-th child by value; the null node for a non-tree node (the null
* node included) or an out-of-range index. */
LarkNode lark_child(LarkNode node, size_t index);
/* True iff the node is "nothing" — exactly lark_node_kind(node) ==
* LARK_NODE_NULL: a traversal miss OR a maybe_placeholders hole (see the
* caveat above). */
bool lark_node_is_null(LarkNode node);
/* Tree data (rule) name as a (ptr,len) UTF-8 slice, valid until the result is
* freed. LARK_INVALID_ARGUMENT on a non-tree node. */
LarkStatus lark_node_data(LarkNode node, const char **out_ptr, size_t *out_len);
/* Token TYPE name as a (ptr,len) UTF-8 slice. LARK_INVALID_ARGUMENT on a non-token
* node. */
LarkStatus lark_node_token_type(LarkNode node, const char **out_ptr, size_t *out_len);
/* Token TYPE id — the id space lark_token_id / lark_token_name resolve into (the
* leaf mirror of lark_node_data_id). Dispatch on an integer token id instead of
* string-comparing the type name. LARK_INVALID_ARGUMENT on a non-token node. */
LarkStatus lark_node_token_type_id(LarkNode node, uint32_t *out_id);
/* Token VALUE as a (ptr,len) byte slice — length-prefixed, NOT NUL-terminated,
* UTF-8 in string mode. LARK_INVALID_ARGUMENT on a non-token node. */
LarkStatus lark_node_token_value(LarkNode node, const char **out_ptr, size_t *out_len);
/* Copy the token VALUE into `buf` and NUL-terminate it, so the C standard
* library applies: lark_node_token_value's slice is NOT NUL-terminated, so
* strtod / strtol / strtoull / sscanf cannot be pointed at it directly. After a
* copy they can, with the semantics YOU choose rather than a policy baked in
* here.
*
* char b[64];
* if (lark_node_token_value_copy(tok, b, sizeof b, NULL) == LARK_OK)
* d = strtod(b, NULL);
* else
* ...; // TOO LONG, or not a token -- do NOT just fall through
*
* "the semantics YOU choose" cuts both ways: strtod reads its decimal point
* from LC_NUMERIC, so after a setlocale(LC_ALL, "") in a comma-decimal locale
* the line above turns the token "3.5" into 3.0 -- silently, on ordinary input.
* Run the conversion under a "C" locale (POSIX 2008: uselocale() around strtod)
* if token text must parse the same everywhere. strtod also takes hex floats,
* leading whitespace and nan(...) payloads; an endptr check catches trailing
* junk but none of those. Pick the policy deliberately -- it is yours now.
*
* Note this is a convenience, not the only route: a parser that takes a pointer
* RANGE (C++17 std::from_chars, fast_float, Ryu's s2d_n) can read
* lark_node_token_value's (ptr,len) in place and copy nothing.
*
* SCOPE: this takes a LarkNode, so it serves tree cursors only. The other two
* places this ABI hands out token bytes give you a bare (text,len) with no node
* to pass -- the reduce-fold leaf callback (LarkReduceTokenFn) and the streaming
* LarkTokenEvent. Those callers do the same bounded copy by hand; the rules
* below (reject rather than truncate, room for the terminator) apply there too.
*
* `buf_len` must leave room for the terminator (value_len + 1); a buffer that
* is one byte short is REJECTED, never truncated. Two consequences worth
* spelling out, because both turn into silent wrong answers if the status is
* ignored:
* - a value longer than your buffer yields LARK_BUFFER_TOO_SMALL and writes
* NOTHING, so `if (... == LARK_OK)` with no else leaves your variable at
* whatever it held. There is no length limit on token values.
* - a token value may contain a literal NUL (the value is a byte slice, not a
* C string). It copies whole and *out_len counts it, but every C string
* function stops at it, so *out_len != strlen(buf). Compare the two when it
* matters.
*
* Whenever the node IS a token, *out_len (when non-NULL) receives the value
* length in bytes, NOT counting the terminator -- written even when the copy did
* not happen. So the two-step query works, and it is a plain success:
*
* size_t n;
* if (lark_node_token_value_copy(tok, NULL, 0, &n) != LARK_OK) ...; // not a token
* char *p = malloc(n + 1);
* if (p == NULL) ...; // NULL + capacity is not a query
* if (lark_node_token_value_copy(tok, p, n + 1, NULL) != LARK_OK) ...;
*
* Both steps are checked on purpose. A failed malloc makes the fill call
* NULL-with-capacity -- LARK_INVALID_ARGUMENT, safely, but nothing is written,
* and reading `p` afterwards is the same fall-through the paragraph above warns
* about.
*
* buf=NULL with buf_len=0 asks only for the size, gets it, returns LARK_OK, and
* does not disturb lark_last_error(). buf=NULL with a NONZERO buf_len is a
* different thing -- capacity claimed for a buffer that is not there -- and stays
* LARK_INVALID_ARGUMENT.
*
* So the failure codes split by what you do about them:
* - LARK_BUFFER_TOO_SMALL: a real buffer, short. Resize to *out_len + 1 (the
* value plus its NUL -- *out_len alone loops) and retry.
* - LARK_INVALID_ARGUMENT: a bug in the call -- an invalid node, a node that
* is not a token, or NULL buf with capacity. lark_last_error() names which,
* one message each ("... invalid cursor" / "... node is not a token" /
* "... buf is NULL"); LARK_BUFFER_TOO_SMALL has its own too ("... buffer too
* small for value + NUL").
* *out_len separates them a second way: it is written iff the call reached a
* token, so pre-seeding it with a value no real length can take (SIZE_MAX) tells
* the two node faults from everything else. Zero does NOT work as that sentinel,
* since a zero-length value would report 0. (The built-in lexer never produces
* one -- lexer-build validation rejects a terminal that can match the empty
* string, so construction fails with LARK_CONSTRUCTION_ERROR -- but a hand-fed
* interactive token can.)
*
* NEITHER `buf` NOR `out_len` may alias ANY memory owned by the parse result --
* not merely the token bytes being read, since the result is borrowed shared for
* the duration of the call. Reaching that requires casting away const on a
* pointer this API handed you: the token/rule byte slices (lark_node_token_value,
* lark_node_data), the name-table slices (lark_node_token_type, lark_rule_name,
* lark_token_name -- the result co-owns that table with the Lark), or a
* lark_result_pretty buffer. Ordinary use cannot reach it; the copy assumes the
* regions are disjoint. */
LarkStatus lark_node_token_value_copy(LarkNode node, char *buf, size_t buf_len, size_t *out_len);
/* Fill `out` with the token's six positions + present flags. LARK_INVALID_ARGUMENT
* on a non-token node. */
LarkStatus lark_node_token_positions(LarkNode node, LarkTokenPositions *out);
/* --------------------------- traversal cursors ----------------------------- */
/*
* Resumable whole-subtree walks in Lark's traversal orders, yielding LarkNode
* views one hit per call. `start` is any node of `result` (lark_result_root or a
* child cursor), but the walk descends through SUBTREES: a start that is a leaf
* (token / null / foreign — anything but a rule node) has no subtree, so the
* cursor constructs OK and is simply born exhausted (even lark_cursor_new_leaves
* does NOT emit the start token itself). Constructors write an OWNED
* LarkTreeCursor* to *out; free it with lark_cursor_free, and always before
* freeing the result (the nodes it yields — like every LarkNode — die with the
* result). A cursor may be moved to another thread but not shared between threads
* without external synchronization.
*/
/* Lark `iter_subtrees` order: leaves first, root last (snapshot taken at
* construction). */
LarkStatus lark_cursor_new_subtrees(const LarkParseResult *result, LarkNode start,
LarkTreeCursor **out);
/* Lark `iter_subtrees_topdown` order: live pre-order, root first. */
LarkStatus lark_cursor_new_topdown(const LarkParseResult *result, LarkNode start,
LarkTreeCursor **out);
/* Post-order DFS (Lark's Visitor_Recursive / Transformer.transform order). */
LarkStatus lark_cursor_new_postorder(const LarkParseResult *result, LarkNode start,
LarkTreeCursor **out);
/* Every non-tree leaf (token / null / foreign) in Lark `scan_values` order. */
LarkStatus lark_cursor_new_leaves(const LarkParseResult *result, LarkNode start,
LarkTreeCursor **out);
/* Lark `find_data`: iter_subtrees order restricted to rule nodes named
* `name` (a (ptr,len) UTF-8 span, resolved ONCE here; an unknown name yields
* nothing). */
LarkStatus lark_cursor_find_data(const LarkParseResult *result, LarkNode start,
const char *name, size_t name_len,
LarkTreeCursor **out);
/* Lark `find_token`: scan_values order restricted to tokens of type `name`
* (resolved ONCE; an unknown name yields nothing). */
LarkStatus lark_cursor_find_token(const LarkParseResult *result, LarkNode start,
const char *name, size_t name_len,
LarkTreeCursor **out);
/* Advance: write the next hit into *out and return true; false when exhausted
* (fused — keeps returning false; *out untouched then). */
bool lark_cursor_next(LarkTreeCursor *cursor, LarkNode *out);
/* Topdown cursors only: do NOT descend into the last-yielded node (prune).
* No-op on other orders. */
void lark_cursor_skip_children(LarkTreeCursor *cursor);
/* Free a traversal cursor (NULL is a no-op). */
void lark_cursor_free(LarkTreeCursor *cursor);
/* ------------------------------ name-keyed fold ----------------------------- */
/*
* Bottom-up evaluation without hand-built dispatch plumbing: register
* callbacks by RULE display name and by TOKEN TYPE name — each name resolves
* to its id ONCE, at registration — then lark_fold_apply runs one ITERATIVE
* bottom-up walk over a result, dispatching O(1) per node. No string compares
* in the loop, no lookup tables in your code, and no C stack growth on deep
* trees (the walk is heap-driven).
*
* VALUES. C has no generics, so folded values travel in LarkValue, an 8-byte
* union the library treats as opaque bits: it never reads a field, never
* frees a .p pointer, and the only value it fabricates is the ALL-ZERO one
* (see the dispatch defaults below). Anything you allocate behind .p is yours
* to manage — record it in userdata if you need to reclaim on error paths.
*
* DISPATCH, per node kind:
* rule node -> the lark_fold_rule callback for its name, else the
* lark_fold_default catch-all, else the built-in PASSTHROUGH:
* the first child's value (a childless rule folds to zero).
* Passthrough is the `?rule` spirit — single-child wrapper
* rules cost no registration.
* token leaf -> the lark_fold_token callback for its TYPE, else the
* lark_fold_leaf catch-all, else the zero value.
* hole -> the lark_fold_leaf catch-all (the node's kind is
* LARK_NODE_NULL, so holes are distinguishable), else zero.
*
* The `children` array a rule callback receives is valid ONLY for the
* duration of that call (it points into the walk's internal stack) — copy
* values out, never store the pointer. n_children may be 0; do not
* dereference `children` then. There is no per-callback error channel:
* record failures in userdata and check after lark_fold_apply returns.
*
* Lifetime + threading: the fold holds the grammar table, so it may outlive
* the Lark it was built from, but it only applies to results parsed by that
* SAME Lark handle (anything else is LARK_INVALID_ARGUMENT). Register from
* one thread; after registration, concurrent lark_fold_apply calls are const
* reads and safe IF your callbacks and userdata are. Do not mutate a fold or
* free the result from inside a callback.
*/
typedef struct LarkFold LarkFold; /* callback registry (owned; lark_fold_free) */
/* An 8-byte value cell for fold callbacks; the library never inspects it. */
typedef union LarkValue {
double d;
int64_t i;
uint64_t u;
void *p;
} LarkValue;
/* Leaf callback: the token (or hole) node in, one value out. */
typedef LarkValue (*LarkFoldLeafFn)(void *userdata, LarkNode leaf);
/* Rule callback: the rule node plus its children's already-folded values. */
typedef LarkValue (*LarkFoldRuleFn)(void *userdata, LarkNode node,
const LarkValue *children, size_t n_children);
/* New empty registry for this grammar. On LARK_OK, *out owns a LarkFold to be
* freed with lark_fold_free. */
LarkStatus lark_fold_new(const Lark *lark, LarkFold **out);
/* Register `fn` for the rule display name `name` (aliases included;
* NUL-terminated — registration names are startup-time literals, unlike the
* (ptr,len) spans on per-parse paths). Resolved to its id here, once. NULL
* `fn` clears the registration; re-registering replaces. LARK_INVALID_ARGUMENT
* on an unknown name. */
LarkStatus lark_fold_rule(LarkFold *fold, const char *name,
LarkFoldRuleFn fn, void *userdata);
/* Register `fn` for tokens of TYPE `name` — the leaf mirror of
* lark_fold_rule, same resolution and clearing rules. */
LarkStatus lark_fold_token(LarkFold *fold, const char *name,
LarkFoldLeafFn fn, void *userdata);
/* Catch-all leaf callback: unregistered token types and holes. NULL clears. */
LarkStatus lark_fold_leaf(LarkFold *fold, LarkFoldLeafFn fn, void *userdata);
/* Catch-all rule callback, replacing the built-in first-child passthrough for
* unregistered rules. NULL clears (passthrough returns). */
LarkStatus lark_fold_default(LarkFold *fold, LarkFoldRuleFn fn, void *userdata);
/* Fold `result` bottom-up through the registry; the root's value lands in
* *out. LARK_INVALID_ARGUMENT if `result` was parsed by a different Lark than
* the fold was built on. */
LarkStatus lark_fold_apply(const LarkFold *fold, const LarkParseResult *result,
LarkValue *out);
/* Free a fold registry (NULL is a no-op; never frees your userdata). */
void lark_fold_free(LarkFold *fold);
/* -------------------------- reduce-time value fold -------------------------- */
/*
* lark_parse_fold folds `text` straight to a LarkValue DURING the parse, so NO
* tree (no LarkParseResult, no arena) is ever built — the "no arena" peer of the
* post-parse lark_fold_apply above. Use it when you only want the folded value
* (evaluate an expression, sum a document, build one host object) and never the
* tree: it saves the whole arena allocation + walk. (When you DO want the tree
* too, parse then lark_fold_apply.)
*
* Same resolve-once design as LarkFold: register callbacks by RULE display name
* and TOKEN TYPE name (each resolved to its id ONCE here), then dispatch is O(1)
* per reduction — no string compares during the parse.
*
* DISTINCT CALLBACK SHAPE. A post-parse callback receives a LarkNode cursor into
* the live tree; at reduce time there is no tree and no node. So the reduce-time
* callbacks take the identity that node used to carry, and nothing else:
* - a rule callback gets the reduced rule's DISPLAY id (the lark_rule_id /
* lark_node_data_id space) plus its children's already-folded values;
* - a leaf callback gets the token's TYPE id plus the token's raw value bytes
* (a borrowed (ptr,len) span, NOT NUL-terminated, valid only for the call).
* This keeps the "no arena" promise honest — a faked node would have to point at
* arena memory that does not exist. To interpret a bare id, either pre-resolve the
* ids you care about (lark_rule_id / lark_token_id) and compare, or map an id back
* to its name inside the callback with lark_rule_name / lark_token_name (needed by
* the catch-alls, which receive ids you did not register by name).
*
* DISPATCH + VALUE MODEL mirror lark_fold_apply exactly (a fold written for one
* runs unchanged as the other):
* rule reduction -> the lark_reduce_fold_rule callback for its name, else the
* lark_reduce_fold_default catch-all, else the built-in
* PASSTHROUGH (first child; a childless rule folds to zero);
* token leaf -> the lark_reduce_fold_token callback for its TYPE, else the
* lark_reduce_fold_leaf catch-all, else the zero value;
* hole -> the lark_reduce_fold_leaf catch-all with
* token_id == LARK_REDUCE_FOLD_HOLE (a maybe_placeholders
* hole has no real token), else the zero value.
* `filter_out` punctuation, `?rule` single-child collapse, `[...]` holes, and
* `_`-transparent (`*`/`+`) flattening are all applied exactly as the tree does,
* so lark_parse_fold agrees value-for-value with lark_fold_apply on the same
* input for EVERY grammar.
*
* Lifetime + threading match LarkFold: the fold holds the grammar table (so it
* may outlive the Lark), but only folds parses of that SAME Lark handle
* (anything else is LARK_INVALID_ARGUMENT). Register from one thread; a built
* fold is then a `const` input and concurrent lark_parse_fold calls are safe IF
* your callbacks and userdata are. There is no per-callback error channel —
* record failures in userdata and check after lark_parse_fold returns.
*/
typedef struct LarkReduceFold LarkReduceFold; /* reduce-time registry (owned;
lark_reduce_fold_free) */
/* The token_id a leaf callback receives for a maybe_placeholders hole (no real
* token exists), with (text,text_len) == (NULL,0). */
#define LARK_REDUCE_FOLD_HOLE UINT32_MAX
/* Reduce-time rule callback: the reduced rule's DISPLAY id + its children's
* already-folded values (`children` is valid ONLY for the call; n_children may
* be 0, in which case do not dereference `children`). */
typedef LarkValue (*LarkReduceRuleFn)(void *userdata, uint32_t rule_id,
const LarkValue *children, size_t n_children);
/* Reduce-time leaf callback: the token's TYPE id (or LARK_REDUCE_FOLD_HOLE for a
* hole) + the token's raw value bytes (`text`/`text_len`; NOT NUL-terminated,
* valid only for the call; (NULL,0) for a hole). */
typedef LarkValue (*LarkReduceTokenFn)(void *userdata, uint32_t token_id,
const char *text, size_t text_len);
/* New empty reduce-time registry for this grammar. On LARK_OK, *out owns a
* LarkReduceFold to be freed with lark_reduce_fold_free. */
LarkStatus lark_reduce_fold_new(const Lark *lark, LarkReduceFold **out);
/* Register `fn` for the rule display name `name` (aliases included;
* NUL-terminated), resolved to its id here, once. NULL `fn` clears; re-registering
* replaces. LARK_INVALID_ARGUMENT on an unknown name. */
LarkStatus lark_reduce_fold_rule(LarkReduceFold *fold, const char *name,
LarkReduceRuleFn fn, void *userdata);
/* Register `fn` for tokens of TYPE `name` — the leaf mirror of
* lark_reduce_fold_rule, same resolution and clearing rules. */
LarkStatus lark_reduce_fold_token(LarkReduceFold *fold, const char *name,
LarkReduceTokenFn fn, void *userdata);
/* Catch-all leaf callback: unregistered token types and holes (a hole arrives
* with token_id == LARK_REDUCE_FOLD_HOLE). NULL clears. */
LarkStatus lark_reduce_fold_leaf(LarkReduceFold *fold, LarkReduceTokenFn fn,
void *userdata);
/* Catch-all rule callback, replacing the built-in first-child passthrough for
* unregistered rules. NULL clears (passthrough returns). */
LarkStatus lark_reduce_fold_default(LarkReduceFold *fold, LarkReduceRuleFn fn,
void *userdata);
/* Parse `text` under `start` (NUL-terminated symbol name, or NULL for the sole
* declared start) and FOLD it to a LarkValue reduce-time, writing the root value
* to *out. NO tree is materialized. *out is set to the zero value up front (so it
* is defined on every path); on LARK_OK it holds the root fold value. A parse
* failure is an ordinary non-OK status (details on the last-error buffer), never
* a panic. LARK_INVALID_ARGUMENT if `fold` was built from a different Lark than
* `lark`. */
LarkStatus lark_parse_fold(const Lark *lark, const char *text, size_t text_len,
const char *start, const LarkReduceFold *fold,
LarkValue *out);
/* Free a reduce-time fold registry (NULL is a no-op; never frees your userdata). */
void lark_reduce_fold_free(LarkReduceFold *fold);
/* ------------------------------ last error --------------------------------- */
/* The last error message on this thread (NUL-terminated UTF-8), or NULL. Valid
* until the next FAILING lark_* call on this thread. Do not free. */
const char *lark_last_error(void);
/* Count of expected/allowed terminals for the last error (0 unless the last error
* was LARK_UNEXPECTED_TOKEN / LARK_UNEXPECTED_CHARACTERS). */
size_t lark_last_error_expected_count(void);
/* The index-th expected/allowed terminal name (NUL-terminated), or NULL if out of
* range. Same lifetime rule as lark_last_error(). */
const char *lark_last_error_expected(size_t index);
/* The last error's structured 1-based line/column, written through the
* out-pointers (a NULL out-pointer is skipped) and returning true. Returns
* false — writing NOTHING — when the last error carried no position:
* construction/argument errors never do, and an LARK_UNEXPECTED_TOKEN on a
* position-less synthetic $END does not either. Errno-style thread-local
* lifetime, exactly as lark_last_error(): the position belongs to the most
* recent FAILING lark_* call on this thread, and a successful call does not
* clear it. */
bool lark_last_error_position(uint32_t *line, uint32_t *column);
/* The last parse error's caret context — Lark's get_context view: the source
* line the error sits on, a newline, then a '^' under the offending column (with
* a trailing newline). NUL-terminated UTF-8, or NULL when the last error carried
* no position — the same errors for which lark_last_error_position() returns
* false. Rendered at parse time, so no source text is re-supplied here. Same
* errno-style thread-local lifetime as lark_last_error(). Do not free. */
const char *lark_last_error_context(void);
/* ------------------------------ introspection ------------------------------ */
/* Number of rule display names in the grammar — the id space lark_rule_id and
* lark_node_data_id index. LALR parse results only carry ids below this count,
* so it is a safe lookup-table size. 0 for NULL. */
size_t lark_rule_count(const Lark *lark);
/* Resolve a rule display name (aliases included; (ptr,len), no NUL needed) to
* its numeric id. Resolve once at startup, then dispatch on integers via
* lark_node_data_id — C `case` labels need compile-time constants, so the
* idiom is a lookup table (sized by lark_rule_count) mapping ids into your own
* enum, then a real `switch` on that. LARK_INVALID_ARGUMENT on an unknown
* name.
*
* DENSE-ID CAVEAT: real rule ids are dense in [0, lark_rule_count), but the three
* reserved ambiguity names "_ambig"/"_iambig"/"_inter" resolve to SENTINEL ids at
* the top of the u32 range (UINT32_MAX, UINT32_MAX-1, UINT32_MAX-2), OUTSIDE that
* range. They never
* appear on the LALR C surface (no LALR node carries them), so a normal caller
* won't hit them — but if you resolve those names, do not index a
* lark_rule_count-sized table with the result. */
LarkStatus lark_rule_id(const Lark *lark, const char *name, size_t name_len,
uint32_t *out_id);
/* This tree node's rule id (the id space lark_rule_id resolves into).
* LARK_INVALID_ARGUMENT on a non-tree node. */
LarkStatus lark_node_data_id(LarkNode node, uint32_t *out_id);
/* Resolve a rule display id back to its name — the inverse of lark_rule_id for
* every id a parse can hand you, and the id->name lookup the reduce-time fold's rule
* callbacks need (they get a bare rule_id, not a node). Writes a (ptr,len) UTF-8
* slice valid until lark_free. LARK_INVALID_ARGUMENT on a NULL arg or an id that
* names no rule.
*
* The _ambig/_iambig/_inter sentinel ids also map back to those names — but that
* round-trip is NOT guaranteed in one pathological case: those names are not
* reserved, so a grammar may DEFINE a rule or alias called `_ambig`. Then
* lark_rule_id("_ambig") returns that real rule's dense id (a real rule outranks the
* sentinel, which is the useful behavior) while lark_rule_name(UINT32_MAX) still
* returns "_ambig" — two ids, one name, so no implementation could make it a
* bijection. Irrelevant on this LALR-only surface, where no parse ever produces a
* sentinel id; it only matters if you resolve those names by hand. */
LarkStatus lark_rule_name(const Lark *lark, uint32_t id,
const char **out_ptr, size_t *out_len);
/* Number of token (terminal) TYPE names — the [0, count) id space lark_token_id /
* lark_token_name index, and a safe size for a table indexed by a leaf callback's
* token_id. 0 for NULL. (Rules have lark_rule_count; this is the terminal mirror.)
* The count spans ALL declared terminals: your named ones, the grammar's anonymous
* terminals (keywords, __IGNORE_*), and the synthetic "$END" — so it can exceed the
* terminals you wrote, and a table sized by it may hold an unused $END slot. A
* maybe_placeholders hole is NOT in this range: it reaches a leaf callback as
* token_id == LARK_REDUCE_FOLD_HOLE (UINT32_MAX), so filter it before indexing. */
size_t lark_token_count(const Lark *lark);
/* Resolve a token TYPE name ((ptr,len), no NUL needed) to its numeric id — the
* terminal mirror of lark_rule_id, letting a reduce-time leaf callback pre-resolve
* the ids it branches on. LARK_INVALID_ARGUMENT on a NULL arg or an unknown name. */
LarkStatus lark_token_id(const Lark *lark, const char *name, size_t name_len,
uint32_t *out_id);
/* Resolve a token TYPE id back to its name — the INVERSE of lark_token_id, and
* the id->name lookup a reduce-time leaf callback needs (it gets a bare token_id).
* Writes a (ptr,len) UTF-8 slice valid until lark_free. LARK_INVALID_ARGUMENT on a
* NULL arg or an id that names no token (including LARK_REDUCE_FOLD_HOLE). */
LarkStatus lark_token_name(const Lark *lark, uint32_t id,
const char **out_ptr, size_t *out_len);
/* hyperlark's own release version, static NUL-terminated. Stamped from the
* crate version at build time; use it for logging and bug reports. Matches the
* HYPERLARK_VERSION_STRING macro for the paired header. */
const char *lark_version(void);
/* The linked library's RELEASE version, packed as major*1000000 + minor*1000 +
* patch — for logging and gating across RELEASES only. It does NOT change on a
* same-version ABI change, so it cannot detect a header/library mismatch between
* two builds that share a version; use lark_c_abi_revision() for that. */
int32_t lark_abi_version(void);
/* The linked library's C ABI revision (see HYPERLARK_C_ABI). Assert
* lark_c_abi_revision() == HYPERLARK_C_ABI at startup to catch a header/library ABI
* mismatch — this IS bumped on every ABI change, independent of the release version,
* so it catches a same-version cross-commit skew that lark_abi_version() cannot. */
uint32_t lark_c_abi_revision(void);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* HYPERLARK_H */