Writing a Language
Table of Contents
- Directory Structure
- The Grammar File
- Hooks in procedures.zig
- Build and Run
- Parsing Verbose Output
- Conventions and Tips
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 inputGetting 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.
zig build
./zig-out/bin/galley path/to/language-dirTo build a runnable parser executable through this repository's build.zig, use the bundled language-directory convention:
Create a new directory under
languages/:shmkdir -p languages/mylangDefine your grammar in
languages/mylang/ll.grm(orlr.grm). See Grammar Guidelines for syntax and rules.Optionally write your
languages/mylang/config.zigandlanguages/mylang/procedures.zigfiles. If they are missing,galleycreates minimal versions:zigpub const indentation_syntax = false;zigpub const Payload = struct {};Create a
samples/directory and add acode-01file with an example input so the parser has something to parse on every build.Generate the parser:
shzig build ./zig-out/bin/galley --parser-type ll languages/mylangParser files start with
_because Galley overwrites them. Support files such asprocedures.zig,config.zig, andll_error_messages.zigdo 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 llfor top-down parsing or--parser-type lrfor 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 likedigit,letter,space,new_line(these can optionally receive exception suffix chains likecharacter^"\n"ordigit^"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 +4Hooks 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:
./zig-out/bin/galley --parser-type ll --fill-error-messages languages/mylangThe 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.
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
zig build
./zig-out/bin/galley --parser-type ll languages/mylangGenerate LR Parser
zig build
./zig-out/bin/galley --parser-type lr languages/mylangBuild and Run
zig build ll-mylang
./zig-out/bin/ll-mylang languages/mylang/samples/code-01zig build lr-mylang
./zig-out/bin/lr-mylang languages/mylang/samples/code-01The 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:
zig build test
zig build run-ll
zig build run-ll -- --iterations 100 --warmup-iterations 10Use 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:
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:
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:
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:
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:
zig build ll-json
./zig-out/bin/ll-json languages/json/samples/code-01.json --verbosity 1Conventions and Tips
- Keep your grammar in alphabetical rule order; the generator may depend on it.
- Use
_-prefixed CamelCase helper rules (like_WhiteSpaceor_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.grmandlr.grmwith equivalent grammars in each. - Remember that the first rule in the file is the entry point — no explicit
@startannotation is needed. - Check that all your terminal symbols match what the grammar generator expects (
digit,letter,space,new_line,operator, etc.).