C
Hyperlark is a parsing library for C, written in Rust. You write a grammar in
Lark’s .lark language and get a
parse tree, or a value computed during the parse. Link it as a static or
shared library and include one header, hyperlark.h.
Grammars compile when your program runs: no generated code. On the JSON
benchmark, bison and lemon, which generate C ahead of time, are both faster
(Alternatives has the numbers).
From C you can:
- Parse and walk the tree in place with cursors.
- Compute a value with callbacks keyed by rule name and token type, during the parse with no tree, or over a finished tree.
- Feed tokens one at a time and ask which terminals may come next.
- Get a status code, message, position and the expected terminals from a failed parse.
Some features of the other targets are missing in C, including:
- Earley, for now: C has only the
LALR(1) parser, so a grammar with a conflict that LALR cannot resolve
fails in
lark_from_sourcewithLARK_CONSTRUCTION_ERROR. - A postlex (lexer post-processing), so there is no built-in indentation-based syntax.
- A custom lexer option. To tokenize the input yourself, feed tokens by terminal name through the interactive parser; they carry no line or column.
%importof your own grammar files: it reaches only the files that ship with Hyperlark, such ascommon.
The feature matrix has the full per-language table.
Source available with license. Contact us. Needs Rust 1.96 or later. These steps are for Linux and macOS.
cargo build -p hyperlark-c --releaseThis writes libhyperlark_c.a and libhyperlark_c.so (.dylib on macOS)
to target/release/; on Windows, the static hyperlark_c.lib,
hyperlark_c.dll and an import library. The header is
crates/hyperlark-c/include/hyperlark.h.
The ABI can still change between betas, so build the header and the library
from the same source tree. Comparing lark_c_abi_revision() with the
header’s HYPERLARK_C_ABI catches a mismatch at startup.
Your first parser
Section titled “Your first parser”#include <stdio.h>#include <string.h>#include "hyperlark.h"
int main(void) { const char *grammar = "start: \"hello\" NAME\nNAME: /\\w+/\n%ignore \" \"\n";
Lark *lark = NULL; if (lark_from_source(grammar, strlen(grammar), LARK_OPT_NONE, &lark) != LARK_OK) { fprintf(stderr, "grammar: %s\n", lark_last_error()); return 1; }
const char *text = "hello world"; LarkParseResult *res = NULL; LarkStatus st = lark_parse(lark, text, strlen(text), NULL, &res); /* NULL: the grammar's start rule */ if (st != LARK_OK) { uint32_t line = 0, col = 0; lark_last_error_position(&line, &col); fprintf(stderr, "parse (%s) at %u:%u: %s\n", lark_status_name(st), line, col, lark_last_error()); lark_free(lark); return 1; }
printf("%s", lark_result_pretty(res)); /* the string belongs to res */
lark_result_free(res); lark_free(lark); return 0;}Compile it against the static archive, from the root of the source
distribution. On macOS leave out -lpthread -ldl -lm.
cc -std=c11 -I crates/hyperlark-c/include \ my_app.c target/release/libhyperlark_c.a -lpthread -ldl -lm -o my_app./my_app prints:
start worldstart is a rule, NAME a terminal. Terminals define the alphabet of your
language, while rules define its structure; terminals are written in
UPPER-CASE, rules in lower-case. %ignore " " tells the parser to ignore
spaces. The literal "hello" is matched, but filtered out of the tree as
punctuation.
LARK_OPT_NONE means the default options. Grammar and input must be UTF-8,
or you get LARK_INVALID_ARGUMENT.
Memory, errors and threads:
- Nodes, token slices and the
lark_result_prettystring live inside the result. Copy what you keep beforelark_result_free. - A result does not depend on its
Lark. Free them in either order. - Free cursors with
lark_cursor_freebefore the result. - Every function that can fail returns a
LarkStatus; a Rust panic isLARK_INTERNAL_PANIC.lark_last_error()andlark_last_error_position()describe the last failure on the calling thread. - One
Larkcan be shared across threads. Use each result or interactive parser from one thread at a time.
Give lark_from_source only grammars you trust: a hostile grammar can
overflow the stack or exhaust memory, and either aborts the process. Parsing
is iterative, so deeply nested input cannot overflow the stack.
Install for pkg-config and CMake
Section titled “Install for pkg-config and CMake”make -C bindings/c install PREFIX=/usr/localThis builds in release mode and installs the header, both libraries,
hyperlark.pc and a CMake package config under PREFIX. DESTDIR, LIBDIR
and INCLUDEDIR work as usual.
pkg-config
Section titled “pkg-config”cc -std=c11 $(pkg-config --cflags hyperlark) my_app.c \ $(pkg-config --libs hyperlark) -o my_appThis links the shared library. On Linux, run sudo ldconfig after
installing, or the program fails at startup; if /usr/local/lib is not on
the loader’s path, add it under /etc/ld.so.conf.d/ first.
To link the static archive instead, name it by path, since -lhyperlark_c
picks the shared library. --static adds the system libraries
hyperlark.pc lists, and the sed drops the -lhyperlark_c it also prints:
cc -std=c11 $(pkg-config --cflags hyperlark) my_app.c \ $(pkg-config --variable=libdir hyperlark)/libhyperlark_c.a \ $(pkg-config --libs-only-l --static hyperlark | sed 's/-lhyperlark_c//') \ -o my_appfind_package(Hyperlark 0.1 REQUIRED)target_link_libraries(my_app PRIVATE hyperlark::hyperlark) # shared if installed, else static# or pick one:target_link_libraries(my_app PRIVATE hyperlark::shared) # the .so/.dylibtarget_link_libraries(my_app PRIVATE hyperlark::static) # the .a, with its system librariesPoint CMake at the install with -DCMAKE_PREFIX_PATH=<prefix>.
hyperlark::shared and hyperlark::static exist only when that library was
installed.
To use the output of cargo build without installing, pass
-DHyperlark_DIR=<dist>/bindings/c/cmake -DHYPERLARK_ROOT=<dist> (<dist>
is the root of the source distribution) and call
find_package(Hyperlark REQUIRED) with no version, since the version file is
generated at install.
Next steps
Section titled “Next steps”- Examples: two C programs, a calculator and an interactive parser. The build line is in the comment at the top of each
.cfile. - Grammar reference: the
.larklanguage. - Working with trees: cursors, token values, positions.
- Transformers: a value from the tree, or during the parse with no tree.
- Errors: status codes, the last-error functions, the caret view.
- Interactive parsing: token-by-token parsing and the expected terminals.
- Feature matrix: what each target supports.
- Alternatives: compared with bison + flex, lemon and tree-sitter.
hyperlark.h: every function and option flag.