(parser is a piece of the loft-in-loft front end that lives in loft's own lib/, @F121; it is not a registry library yet, so a program outside this tree cannot use it.) The parser library lets a Loft program read and validate other Loft source code at runtime. This is useful when you want to:
- validate configuration files written in the Loft syntax
- build tools that inspect or transform Loft source
- write a test that checks whether a generated code snippet is syntactically correct
Together the lexer and parser libraries are designed as a reusable foundation. You can use them to build entirely new languages: feed the lexer your own token rules, then layer a custom parser on top. This keeps the tokeniser and grammar logic separate from the language runtime so you only bring in what you need.
parse(name, source) is the single entry point. It takes a display name (used in error messages) and a Loft source string. The name does not need to correspond to a real file — it is only used when reporting parse errors.
parse() answers how many errors the source contained — 0 for a snippet that parsed cleanly. The diagnostics themselves go to the log as it reads; the count is what a caller can branch on.
What the parser understands
The parser handles most of Loft's syntax:
struct Name { field: type [= default] }— data containers with named fieldsenum Name { Variant [{ field: type }] }— named choices, each with optional datafn name(params) [-> type] { body }— functions with a block bodyfn name(params) [-> type]; #rust "template"— operator templates backed by Rustuse module;— module imports- Statements:
forandwhileloops,if/else if/else,return, typed locals (c: (text?, integer) = ("c0", 3);) and expressions; a statement that ends in a block needs no; - Expressions: binary operators with precedence, function calls, field access (
p.x,pair.0), attributes (f#size),matchwith guards and patterns, both lambda forms (|x| { x * 2 }andfn(x: integer) -> integer { x + 1 }), tuples, vectors, struct literals and blocks — each also as a value (y = match x { … }) - Type expressions: plain and qualified names (
lexer::Lexer), generic types likevector<T>, keyed collections (sorted/hash/index), tuples,fn(…) -> Tand integer ranges withinteger limit(min, max)— in parameter, field, and return position
It reads an index expression (v[0]), the null-coalescing operator (a ?? 0) and a formatted string literal ("v={n}") as the compiler does.
Outside its grammar, so a snippet using one answers a non-zero count although the compiler accepts it: const fields and top-level const, **, /=, hex and f-suffixed literals, generic functions (fn f<T>), filtered loops (for e in v if e > 1), is patterns, slices (v[1..]) and backtick strings.
use parser;
fn main() {
Parsing a minimal function
The double braces {{ and }} produce literal { and } inside a Loft format string — needed here because the source code itself contains braces.
assert(parser::parse("hello", "fn main() {{ println(\"Hello World\"); }}") == 0,
"a minimal function parses cleanly");
Parsing a struct and function together
The parser validates the entire source snippet as one unit. Both the struct definition and the function body are checked.
assert(parser::parse("point", "struct Point {{ x: integer, y: integer }} fn origin() -> Point {{ Point {{ x: 0, y: 0 }} }}") == 0,
"a struct and a function in one unit parse cleanly");
Parsing an enum
Both a plain variant and a struct variant (with fields) are recognised.
assert(parser::parse("shape", "enum Shape {{ Circle {{ radius: float }}, Rectangle {{ w: float, h: float }} }}") == 0,
"both a plain and a struct variant parse cleanly");
Parsing several statements in a body
A body is a sequence of statements; each call below is one.
assert(parser::parse("body", "fn m() {{ println(\"a\"); println(\"b\"); }}") == 0,
"a body of several statements parses cleanly");
Parsing assignments and operators
Binary operators parse with their normal precedence, and so does assignment.
assert(parser::parse("assign", "fn f() {{ t = 0; t += 1; }}") == 0,
"assignment and compound assignment parse cleanly");
Parsing a for loop and arithmetic
This exercises the expression parser, range syntax, and a tail expression after a loop body — the shape where a speculative struct-literal parse has to give its tokens back so the loop can claim them.
assert(parser::parse("loop", "fn sum(n: integer) -> integer {{ t = 0; for i in 1..=n {{ t += i; }} t }}") == 0,
"a for loop over a range parses cleanly");
Parsing while, match and lambdas
Each of these is a value as well as a statement: a match or a lambda can stand on the right of =.
assert(parser::parse("while", "fn f() {{ n = 0; while n < 3 {{ n += 1; }} }}") == 0,
"a while loop parses cleanly");
assert(parser::parse("match", "fn f(x: integer) -> integer {{ match x {{ 0 => 1, n if n > 9 => 2, 2 | 3 => 3, _ => 0 }} }}") == 0,
"a match with a guard and alternatives parses cleanly");
assert(parser::parse("lambdas", "fn f() {{ g = |x| {{ x * 2 }}; h = fn(x: integer) -> integer {{ x + 1 }}; }}") == 0,
"both lambda forms parse cleanly");
Practical use: validating user-supplied code
If your application lets users write Loft snippets (for scripting or configuration), you can parse them before executing:
snippet = read_user_input();
if parser::parse("user_code", snippet) > 0 {
// The snippet was invalid — the count is how many errors it had.
}
Combine this with the lexer (see the Lexer page) when you need to extract individual tokens from the source rather than validate all of the syntax.
What a failed parse looks like
The count is the whole point: a snippet that does not parse answers a NON-zero number, so a caller can tell the two apart. Watching the call not crash cannot.
assert(parser::parse("broken", "fn main() {{ println(\"hi\");") > 0,
"an unterminated body reports at least one error");
A missing operand is an error too, and so is text after the last definition.
assert(parser::parse("no_value", "fn f() {{ x = ; }}") > 0, "a missing value is reported");
assert(parser::parse("stray", "fn f() {{ }} }}") > 0, "a stray brace is reported");
What it does not parse
Each construct in the list at the top of this page answers a non-zero count here.
outside = [
"const K = 3;",
"struct S {{ const k: integer }}",
"fn f() {{ x = 2 ** 3; }}",
"fn f() {{ x = 6; x /= 2; }}",
"fn f() {{ x = 0xff; }}",
"fn f() {{ x = 1.5f; }}",
"fn f<T>(x: T) -> T {{ x }}",
"fn f(v: vector<integer>) {{ for e in v if e > 1 {{ }} }}",
"enum Dir {{ North, South }} fn f(e: Dir) {{ if e is North {{ }} }}",
"fn f(v: vector<integer>) {{ w = v[1..]; }}",
"fn f() {{ x = `a`; }}"
];
for snippet in outside {
assert(parser::parse("outside", snippet) > 0, "{snippet} is outside the grammar");
}
println("parser test passed");
}
Run it yourself
This page is a loft program. It runs here, in your browser — press Run, then ask it something.
Stop on a line
// Copyright (c) 2024-2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// @NAME: Parser
// @TITLE: Parsing Loft source code programmatically
// (`parser` is a piece of the loft-in-loft front end that lives in loft's own `lib/`, @F121;
// it is not a registry library yet, so a program outside this tree cannot `use` it.)
// The 'parser' library lets a Loft program read and validate other Loft source
// code at runtime. This is useful when you want to:
// * validate configuration files written in the Loft syntax
// * build tools that inspect or transform Loft source
// * write a test that checks whether a generated code snippet is syntactically correct
//
// Together the 'lexer' and 'parser' libraries are designed as a reusable foundation.
// You can use them to build entirely new languages: feed the lexer your own token rules,
// then layer a custom parser on top. This keeps the tokeniser and grammar logic separate
// from the language runtime so you only bring in what you need.
//
// 'parse(name, source)' is the single entry point. It takes a display name
// (used in error messages) and a Loft source string. The name does not need to
// correspond to a real file — it is only used when reporting parse errors.
//
// parse() answers how many errors the source contained — '0' for a snippet that
// parsed cleanly. The diagnostics themselves go to the log as it reads; the count
// is what a caller can branch on.
// ## What the parser understands
//
// The parser handles most of Loft's syntax:
// * 'struct Name { field: type [= default] }' — data containers with named fields
// * 'enum Name { Variant [{ field: type }] }' — named choices, each with optional data
// * 'fn name(params) [-> type] { body }' — functions with a block body
// * 'fn name(params) [-> type]; #rust "template"' — operator templates backed by Rust
// * 'use module;' — module imports
// * Statements: 'for' and 'while' loops, 'if' / 'else if' / 'else', 'return', typed
// locals ('c: (text?, integer) = ("c0", 3);') and expressions; a statement that ends
// in a block needs no ';'
// * Expressions: binary operators with precedence, function calls, field access
// ('p.x', 'pair.0'), attributes ('f#size'), 'match' with guards and patterns, both
// lambda forms ('|x| { x * 2 }' and 'fn(x: integer) -> integer { x + 1 }'), tuples,
// vectors, struct literals and blocks — each also as a value ('y = match x { … }')
// * Type expressions: plain and qualified names ('lexer::Lexer'), generic types like
// 'vector<T>', keyed collections (sorted/hash/index), tuples, 'fn(…) -> T' and
// integer ranges with 'integer limit(min, max)' — in parameter, field, and return
// position
//
// It reads an index expression ('v[0]'), the null-coalescing operator ('a ?? 0')
// and a formatted string literal ('"v={n}"') as the compiler does.
//
// Outside its grammar, so a snippet using one answers a non-zero count although the
// compiler accepts it: 'const' fields and top-level 'const', '**', '/=', hex and
// 'f'-suffixed literals, generic functions ('fn f<T>'), filtered loops
// ('for e in v if e > 1'), 'is' patterns, slices ('v[1..]') and backtick strings.
use parser;
// @PAR-001 — validate a source string before acting on it: `== 0` is good, `> 0` is not
fn main() {
// ## Parsing a minimal function
// The double braces '{{' and '}}' produce literal '{' and '}' inside a
// Loft format string — needed here because the source code itself contains braces.
assert(parser::parse("hello", "fn main() {{ println(\"Hello World\"); }}") == 0,
"a minimal function parses cleanly");
// ## Parsing a struct and function together
// The parser validates the entire source snippet as one unit.
// Both the struct definition and the function body are checked.
assert(parser::parse("point", "struct Point {{ x: integer, y: integer }} fn origin() -> Point {{ Point {{ x: 0, y: 0 }} }}") == 0,
"a struct and a function in one unit parse cleanly");
// ## Parsing an enum
// Both a plain variant and a struct variant (with fields) are recognised.
assert(parser::parse("shape", "enum Shape {{ Circle {{ radius: float }}, Rectangle {{ w: float, h: float }} }}") == 0,
"both a plain and a struct variant parse cleanly");
// ## Parsing several statements in a body
// A body is a sequence of statements; each call below is one.
assert(parser::parse("body", "fn m() {{ println(\"a\"); println(\"b\"); }}") == 0,
"a body of several statements parses cleanly");
// ## Parsing assignments and operators
// Binary operators parse with their normal precedence, and so does assignment.
assert(parser::parse("assign", "fn f() {{ t = 0; t += 1; }}") == 0,
"assignment and compound assignment parse cleanly");
// ## Parsing a for loop and arithmetic
// This exercises the expression parser, range syntax, and a tail expression after a
// loop body — the shape where a speculative struct-literal parse has to give its
// tokens back so the loop can claim them.
assert(parser::parse("loop", "fn sum(n: integer) -> integer {{ t = 0; for i in 1..=n {{ t += i; }} t }}") == 0,
"a for loop over a range parses cleanly");
// ## Parsing while, match and lambdas
// Each of these is a value as well as a statement: a 'match' or a lambda can stand on
// the right of '='.
assert(parser::parse("while", "fn f() {{ n = 0; while n < 3 {{ n += 1; }} }}") == 0,
"a while loop parses cleanly");
assert(parser::parse("match", "fn f(x: integer) -> integer {{ match x {{ 0 => 1, n if n > 9 => 2, 2 | 3 => 3, _ => 0 }} }}") == 0,
"a match with a guard and alternatives parses cleanly");
assert(parser::parse("lambdas", "fn f() {{ g = |x| {{ x * 2 }}; h = fn(x: integer) -> integer {{ x + 1 }}; }}") == 0,
"both lambda forms parse cleanly");
// ## Practical use: validating user-supplied code
//
// If your application lets users write Loft snippets (for scripting or
// configuration), you can parse them before executing:
//
// snippet = read_user_input();
// if parser::parse("user_code", snippet) > 0 {
// // The snippet was invalid — the count is how many errors it had.
// }
//
// Combine this with the lexer (see the Lexer page) when you need to extract
// individual tokens from the source rather than validate all of the syntax.
// ## What a failed parse looks like
// The count is the whole point: a snippet that does not parse answers a NON-zero
// number, so a caller can tell the two apart. Watching the call not crash cannot.
assert(parser::parse("broken", "fn main() {{ println(\"hi\");") > 0,
"an unterminated body reports at least one error");
// A missing operand is an error too, and so is text after the last definition.
assert(parser::parse("no_value", "fn f() {{ x = ; }}") > 0, "a missing value is reported");
assert(parser::parse("stray", "fn f() {{ }} }}") > 0, "a stray brace is reported");
// ## What it does not parse
// Each construct in the list at the top of this page answers a non-zero count here.
outside = [
"const K = 3;",
"struct S {{ const k: integer }}",
"fn f() {{ x = 2 ** 3; }}",
"fn f() {{ x = 6; x /= 2; }}",
"fn f() {{ x = 0xff; }}",
"fn f() {{ x = 1.5f; }}",
"fn f<T>(x: T) -> T {{ x }}",
"fn f(v: vector<integer>) {{ for e in v if e > 1 {{ }} }}",
"enum Dir {{ North, South }} fn f(e: Dir) {{ if e is North {{ }} }}",
"fn f(v: vector<integer>) {{ w = v[1..]; }}",
"fn f() {{ x = `a`; }}"
];
for snippet in outside {
assert(parser::parse("outside", snippet) > 0, "{snippet} is outside the grammar");
}
println("parser test passed");
}