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.
/* * 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 __cplusplusextern "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 */