Skip to content

Writing a Language

Table of Contents


This guide covers how to scaffold a new grammar directory for a custom language.


Directory Structure

Each language lives under languages/<name>/. The build system automatically discovers languages by scanning for a _ll-parser.zig or _lr-parser.zig file.

languages/
└── mylang/
    ├── ll.grm          # LL grammar rules (for top-down parsing)
    ├── lr.grm          # LR grammar rules (for bottom-up parsing)
    ├── config.zig      # Options, configuration, and indentation syntax
    ├── _ll-parser.zig  # Auto-generated by the grammar generator
    ├── _lr-parser.zig  # Auto-generated by the grammar generator
    ├── ll_error_messages.zig # Optional LL syntax error message hooks
    ├── lr_error_messages.zig # Optional LR syntax error message hooks
    ├── procedures.zig  # Required: hook functions and runtime payload definition
    └── samples/        # Test inputs — will be parsed each build
        └── code-01     # Example sample code input

Getting Started

Run all commands from the root directory of the repository. A Galley language is a directory containing ll.grm, lr.grm, or both. When you run galley, it generates parser files and creates missing support files without overwriting your existing procedures.zig, config.zig, error-message files, or build files.

sh
zig build
./zig-out/bin/galley path/to/language-dir

To build a runnable parser executable through this repository's build.zig, use the bundled language-directory convention:

  1. Create a new directory under languages/:

    sh
    mkdir -p languages/mylang
  2. Define your grammar in languages/mylang/ll.grm (or lr.grm). See Grammar Guidelines for syntax and rules.

  3. Optionally write your languages/mylang/config.zig and languages/mylang/procedures.zig files. If they are missing, galley creates minimal versions:

    zig
    pub const indentation_syntax = false;
    zig
    pub const Payload = struct {};
  4. Create a samples/ directory and add a code-01 file with an example input so the parser has something to parse on every build.

  5. Generate the parser:

    sh
    zig build
    ./zig-out/bin/galley --parser-type ll languages/mylang

    Parser files start with _ because Galley overwrites them. Support files such as procedures.zig, config.zig, and ll_error_messages.zig do not start with _ because they are user-owned and preserved.


The Grammar File

The grammar file format is documented in detail in Grammar Guidelines. Key points:

  • Entry point: The first rule defined in the file becomes the parser's start symbol.
  • Parser type: Choose --parser-type ll for top-down parsing or --parser-type lr for bottom-up parsing.
  • AST allocation: Variables starting with a capital letter allocate AST nodes; variables starting with _ (CamelCase, e.g. _WhiteSpace) are skipped entirely.
  • Terminals: Exact string literals must be in double quotes (e.g. "let"). Unquoted lowercase identifiers match named character classes or generative terminals like digit, letter, space, new_line (these can optionally receive exception suffix chains like character^"\n" or digit^"1"^"3" to exclude specific characters, see Grammar Guidelines for details).

Example: Simple Arithmetic

languages/mylang/ll.grm:

Definition
| "let" _WhiteSpace Expr _WhiteSpace

Expr
| Number
| "+" Number
| "-" Number

Number
| "4"
| "2"

_WhiteSpace
| space _WhiteSpace
| new_line _WhiteSpace
|

languages/mylang/samples/code-01:

let +4

Hooks in procedures.zig

During parsing, you can execute custom Zig logic (known as reduction procedures or hooks) when grammar rules are matched and reduced. These hooks are exported from your language's procedures.zig file.

To learn how to register explicit/implicit hooks, how to write custom procedures in Zig, and how they map to AST generation options, see the dedicated Reduction Procedures User Guide.

Syntax Error Messages

Generated parsers record structured syntax diagnostics and print Galley's default message unless a matching public hook exists in ll_error_messages.zig or lr_error_messages.zig.

Generate default hook bodies for every current syntax-error site with:

sh
./zig-out/bin/galley --parser-type ll --fill-error-messages languages/mylang

The fill mode appends missing pub fn syntax_error_* hooks, preserves existing hooks, and reports obsolete public hooks that no longer match the generated parser. Non-public helper functions are ignored.

LL hook names are semantic: syntax_error_ll_<parser-symbol>__expected_<semantic-alternatives>. The LL parser resolves hooks at comptime from most specific to broadest: exact semantic hook, syntax_error_ll_<parser-symbol>, syntax_error_ll, syntax_error, then the default renderer.

LR hook names identify exact generated sites, for example syntax_error_lr_state_12_action_19. LR resolution checks that exact hook first, followed by the language-wide syntax_error_lr, the cross-parser syntax_error, and finally the default renderer.

zig
const root = @import("galley");

pub fn syntax_error_ll_Value__expected_String_or_Number(args: root.SyntaxErrorMessageArgs) ![]const u8 {
    return try root.renderParseDiagnostic(args.allocator, args.diagnostic, args.style);
}

Build and Run

Generate LL Parser

sh
zig build
./zig-out/bin/galley --parser-type ll languages/mylang

Generate LR Parser

sh
zig build
./zig-out/bin/galley --parser-type lr languages/mylang

Build and Run

sh
zig build ll-mylang
./zig-out/bin/ll-mylang languages/mylang/samples/code-01
sh
zig build lr-mylang
./zig-out/bin/lr-mylang languages/mylang/samples/code-01

The executable will be built automatically — build.zig scans the languages/ directory for any subdirectory containing a _ll-parser.zig or _lr-parser.zig file.

Standalone Directories

When you run galley on a language directory outside this repository's languages/ directory, Galley also creates a local build.zig, main.zig, tests/parser_test.zig, and samples/code-01 if they are missing. Replace the placeholder sample with valid source for your language, then run:

sh
zig build test
zig build run-ll
zig build run-ll -- --iterations 100 --warmup-iterations 10

Use run-lr instead when the directory only has an LR grammar.


Generate from Zig Code

Projects that depend on Galley can call the generator directly:

zig
const galley = @import("galley");

try galley.generator.emitParserFromSource(
    allocator,
    grammar_source,
    writer,
    .ll,
    .{},
);

Use .lr for LR generation. The options struct matches the CLI flags.


Use a Generated Parser from Zig Code

Generated parsers are also importable Zig modules. The repository build exposes included languages by parser target name, such as ll-json and lr-json; standalone generated build.zig files expose ll-parser and lr-parser.

For one-shot parsing, call parseBytes:

zig
const json_parser = @import("ll-json");

var parsed = try json_parser.parseBytes(io, allocator, "{\"ok\": true}", .{});
defer parsed.deinit();

std.debug.assert(parsed.result.parsed_bytes == 12);

ParsedInput owns the parser session, buffers, arena, and AST memory. Keep it alive while inspecting AST data, and call deinit when finished.

For repeated parses, reuse a Session:

zig
const json_parser = @import("ll-json");

var session = try json_parser.Session.init(io, allocator, .{});
defer session.deinit();

_ = try session.parseBytes("{}", null);
_ = try session.parseBytes("[]", null);

Use session.parseFile(file, input_path) when parsing from a std.Io.File.

For callers that already own sentinel-terminated input, use parseSentinelBytes or session.parseSentinelBytes:

zig
const input: [:0]const u8 = "{\"ok\": true}";
const result = try session.parseSentinelBytes(input, "inline-json");

The sentinel byte must remain valid for the duration of the parse. This path avoids the defensive copy that parseBytes performs to add Galley's required trailing zero byte.


Parsing Verbose Output

Pass --verbosity 1 (or 2) to the generated executable to print the resulting AST alongside benchmark metrics:

sh
zig build ll-json
./zig-out/bin/ll-json languages/json/samples/code-01.json --verbosity 1

Conventions and Tips

  • Keep your grammar in alphabetical rule order; the generator may depend on it.
  • Use _-prefixed CamelCase helper rules (like _WhiteSpace or _Tail) for whitespace, trailing symbols, and optional parts — they incur zero overhead.
  • If your language needs to handle both LL and LR, write both ll.grm and lr.grm with equivalent grammars in each.
  • Remember that the first rule in the file is the entry point — no explicit @start annotation is needed.
  • Check that all your terminal symbols match what the grammar generator expects (digit, letter, space, new_line, operator, etc.).