Skip to content

Rust

Galley-generated parsers can be consumed from Rust through a safe wrapper crate: bindings/rust. It links against the same shared library as the C API and provides RAII session management, borrowed text slices, iterator-based tree traversal, and typed diagnostics.

A complete consumer lives in examples/rust; it is built and executed by CI on every push.

Procedures

Set pub const procedures = true; in your grammar's config.zig and implement the hooks in a procedures.rs file next to your grammar — the build helper compiles it with rustc into a static archive and links it into the shared library. Hooks are ordinary Rust: no C anywhere on the consumer side.

rust
/* procedures.rs */
mod procedure {
    include!(concat!(env!("OUT_DIR"), "/galley_procedure_types.rs"));
}
use procedure::ProcedureArguments;

#[no_mangle]
pub extern "C" fn reduction_Pair(arguments: &mut ProcedureArguments) {
    let Some(node) = arguments.current_node() else { return };
    let text = arguments.text(node).unwrap_or(b"");
    let (line, column) = arguments.line_column(node).unwrap_or((0, 0));
    eprintln!(
        "Pair {} ({} children) at {line}:{column}",
        String::from_utf8_lossy(text),
        arguments.child_count(node)
    );
}

#[no_mangle]
pub extern "C" fn reduction_KeyTail(arguments: &mut ProcedureArguments) {
    let _ = arguments.drop_if_empty();
}

#[no_mangle]
pub extern "C" fn hook_print(arguments: &mut ProcedureArguments) {
    let Some(node) = arguments.current_node() else { return };
    let text = arguments.text(node).unwrap_or(b"");
    let (line, column) = arguments.line_column(node).unwrap_or((0, 0));
    eprintln!("@print \"{}\" at {line}:{column}", String::from_utf8_lossy(text));
}

Reduction hooks keep their reduction_<VariableName> names (plus the general reduction); author-defined grammar hooks are declared as hook_<name>. Each hook fires after the corresponding variable is reduced. The helper compiles procedures.rs with panic=abort, so a panic inside a hook aborts rather than unwinding through generated parser code. Semantic payloads are unavailable through bindings.

Semantic Errors

A hook reports a semantic error when the input parses but its meaning is invalid. report_semantic_error records the diagnostic, marks the node, and returns the running total so hooks can limit themselves. Parsing continues; a syntax-clean parse with any semantic error fails with Error::Semantic:

rust
if value > 999 {
    let _ = arguments.report_semantic_error("value out of range");
}

Read them through Session::diagnostic / Session::diagnostics; the snapshot carries kind == DiagnosticKind::Semantic and semantic == Some((variable, message)).

Tree Walking

Session::walk returns a borrowing pre-order Walker over the last successful parse, yielding one WalkStep { node, depth, is_semantic_error } per node with the root at depth 0 — the shared runtime walker, so order and depths match every other binding. skip_children prunes the last yielded node's children; passing true prunes semantic-error subtrees:

rust
let root = session.root_node().expect("root");
for step in session.walk(root, false).expect("walker") {
    println!("{:width$}{:?}", "", step.node, width = step.depth as usize * 2);
}

Because the helper drives rustc directly, editors would see procedures.rs as outside any module tree. The example's Cargo.toml therefore declares it as a staticlib example target — mirroring exactly what the helper builds — so rust-analyzer links it as its own crate and cargo test keeps it compiling:

toml
[[example]]
name = "procedures"
path = "procedures.rs"
crate-type = ["staticlib"]

Error Messages

To replace messages with fixed strings — no Zig file at all — pass message_overrides in SessionOptions. Keys are structured identities: the innermost in-progress variable name (for example "Number"), or "*" for every syntax and indentation error. Variable keys win over "*", and overrides take priority over hooks. Placeholders expand against the failing diagnostic:

rust
let options = galley::SessionOptions {
    message_overrides: vec![(
        "Number".into(),
        "expected a number after ':' (digits only) at line {line}".into(),
    )],
    ..Default::default()
};

Override messages may contain {line}, {column}, {unexpected}, {expected}, and {context} placeholders, expanded against the failing diagnostic.

Build Model

Add the bindings crate to your Cargo.toml:

toml
[dependencies]
galley = { path = "../../bindings/rust" }

[build-dependencies]
galley = { path = "../../bindings/rust" }

Then call generate_and_link from your build.rs:

rust
fn main() {
    galley::build_helper::generate_and_link("language-dir");
}

No checkout is needed: the published crate carries the generator CLI for every platform and the compile inputs, so only the crate plus zig are required — for convenience, GALLEY_CHECKOUT=$(examples/scripts/fetch-galley.sh) fetches one into the system cache for contributors, but that cache is examples-only, not part of the bindings. It generates the parser from your grammar's ll.grm, compiles the C-API shared library directly next to the grammar, and emits the cargo directives that link your binary against it.

Generation-time options come from config.zig in the language directory — edit it and rebuild; the build script re-runs when either ll.grm or config.zig changes. Only the parser-type selection travels as a build-script option (it is not file-config): Options::new generates every parser type, .parser_type(ParserType::Ll) or .parser_type(ParserType::Lr) generates one.

Usage

rust
use galley::{Session, NodeHandle};

let mut session = Session::new().expect("session");

// Parse a string.
let bytes = session.parse_sentinel(r#"{"key": "value"}"#).expect("parse");

// Walk the AST.
if let Some(root) = session.root_node() {
    for child in session.children(root) {
        if let Some(name) = session.symbol_name(child) {
            println!("{}", String::from_utf8_lossy(name));
        }
        if let Some(text) = session.text(child) {
            println!("  → {}", String::from_utf8_lossy(text));
        }
    }
}

// Diagnostics on failure.
if session.parse_sentinel("bad input").is_err() {
    if let Some(d) = session.diagnostic() {
        eprintln!("{}:{} {}", d.line, d.column, d.message);
    }
}

Tree editing follows the same address-stable model as the C API: addresses never invalidate across edits or allocations.

rust
let head = session.tree_clean_children(root).unwrap();
session.tree_append_children(root, head.unwrap()).unwrap();

Session Options

rust
use galley::SessionOptions;

let opts = SessionOptions {
    max_errors: 20,
    recovery_window: 1000,
    stack_overflow_recovery: true,
    syntax_error_stack_depth: 3,
};
let mut session = Session::with_options(opts).expect("session");

Development builds

Every green CI run uploads the publish-equivalent .crate as a workflow artifact (Actions → the run → Artifacts → pkg-rust), carrying that commit's generator and compile kit. Download, extract, and depend by path — no Zig toolchain needed, same as a crates.io release:

toml
[dependencies]
galley = { path = "../galley-0.1.3-dev.42.gabc123456789" }

Contributors with a checkout can alternatively pin a git dependency to a rev, but that path builds the generator from source and needs Zig installed. Versioned releases go to crates.io as usual.