Skip to content

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_source with LARK_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.
  • %import of your own grammar files: it reaches only the files that ship with Hyperlark, such as common.

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.

Terminal window
cargo build -p hyperlark-c --release

This 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.

#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.

Terminal window
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 world

start 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_pretty string live inside the result. Copy what you keep before lark_result_free.
  • A result does not depend on its Lark. Free them in either order.
  • Free cursors with lark_cursor_free before the result.
  • Every function that can fail returns a LarkStatus; a Rust panic is LARK_INTERNAL_PANIC. lark_last_error() and lark_last_error_position() describe the last failure on the calling thread.
  • One Lark can 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.

Terminal window
make -C bindings/c install PREFIX=/usr/local

This 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.

Terminal window
cc -std=c11 $(pkg-config --cflags hyperlark) my_app.c \
$(pkg-config --libs hyperlark) -o my_app

This 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:

Terminal window
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_app
find_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/.dylib
target_link_libraries(my_app PRIVATE hyperlark::static) # the .a, with its system libraries

Point 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.