daml-parser
A lossless lexer, layout resolver, and parser for the Daml smart-contract language, in pure Rust with zero external dependencies.
Part of the daml-tools workspace.
This is the shared foundation under both
daml-lint (a static analyzer) and
daml-fmt (a code formatter). The pipeline is:
lexer → layout → parse
(tokens (Haskell (typed AST with a
+ trivia) offside byte Span on every node)
rule)
Documentation
The workspace documentation is organized under
docs:
- Crate reference for workspace package facts
- Workspace architecture for
how
daml-parser,daml-lint, anddaml-fmtrelate - CLI reference for the tools built on top of this parser
Lossless by design
The lexer records every comment and whitespace run as trivia
(lexer::lex_with_trivia), so the original bytes can be reconstructed exactly
(lexer::render_lossless, ast_span::render_from_ast). One tree serves two
very different readers:
- the linter ignores trivia and reads meaning;
- the formatter keeps trivia and re-prints layout — if it dropped a comment it would eat your code, so losslessness is not optional.
This is the same shape the grown-up tools use (rust-analyzer, ruff, biome): one parser, one lossless tree, many consumers.
Usage
[]
= "0.3"
use parse_module;
let result = parse_module;
assert!;
let module = result.module;
Choosing a parser layer
parse::parse_module is the normal entry point when you want a typed syntax
tree. It runs the full pipeline:
lexer::lexlayout::resolve_layout- recursive-descent parsing into
ast::Module
Lower layers are public when a consumer needs more control:
| Layer | Use when you need |
|---|---|
lexer::lex_with_trivia |
source tokens plus comments, CPP directives, blank-line trivia, and lexical diagnostics |
layout::resolve_layout |
the token stream after Daml's offside rule has inserted virtual layout tokens |
parse::parse_module |
a typed ast::Module with imports, declarations, expressions, types, byte spans, and parse diagnostics |
ast_span::render_from_ast |
an oracle that checks AST spans plus trivia can reconstruct the source bytes |
The parser does not prescribe what downstream crates do with the tree. A consumer can inspect the AST directly, lower it into its own representation, or combine the AST with tokens and trivia for source-preserving work.
Reading parser output
parse_module returns ParseModuleResult { module, diagnostics }. Diagnostics are not
fatal; the parser records what it could understand and keeps going.
use ;
use parse_module;
let source = "\
module M where
template Account
with
owner : Party
where
signatory owner
";
let result = parse_module;
let module = result.module;
let diagnostics = result.diagnostics;
let has_lex_errors = diagnostics
.iter
.any;
let templates: = module
.decls
.iter
.filter_map
.collect;
assert!;
let = templates
.first
.expect;
assert_eq!;
Every AST node that represents source text carries a 1-based Pos and a byte
Span. Use spans when you need an exact source slice:
let snippet = source
.get
.expect;
assert!;
Diagnostics and partial structure
ParseDiagnostic::category separates different kinds of recovery:
| Category | Meaning |
|---|---|
Lex |
the lexer found malformed source such as an unterminated string or block comment |
Malformed |
an expression, pattern, or expected token was malformed inside an otherwise recognized construct |
SkippedDecl |
a top-level declaration could not be parsed and was skipped to the next item |
UnsupportedSyntax |
the source used syntax this parser intentionally does not model yet |
RecursionLimit |
deeply nested input exceeded the parser recursion bound and was degraded to raw text |
Partial structure is explicit in the AST. For example:
Decl::Unknownpreserves raw top-level declaration text.Expr::Errorpreserves raw expression text.Pat::Otherpreserves raw pattern text.TemplateBodyDecl::Otherpreserves template body items that are not modeled.
Consumers can decide how much diagnostic tolerance is acceptable for their own use. The parser itself does not treat diagnostics as a process-level failure.
Source positions, spans, and trivia
Span values are byte offsets into the original source: [start, end).
Virtual layout tokens have zero-width spans and are skipped when AST node spans
are computed. Comments and whitespace live in lexer trivia rather than AST
nodes.
Use the lexer-level oracle when you need to prove tokens and trivia still cover the source:
use ;
let lexed = lex_with_trivia;
let tokens = lexed.tokens;
let trivia = lexed.trivia;
let errors = lexed.errors;
assert!;
assert_eq!;
Use the AST-level oracle when you need to prove parsed node spans and trivia can still reconstruct the source:
use render_from_ast;
use lex_with_trivia;
use parse_module;
let lexed = lex_with_trivia;
let trivia = lexed.trivia;
let parsed = parse_module;
let module = parsed.module;
assert_eq!;
Layout-aware tokens
Daml uses indentation-sensitive layout. layout::resolve_layout converts the
raw token stream into a laid-out token stream by inserting zero-width virtual
tokens:
| Token | Meaning |
|---|---|
TokenKind::VLBrace |
start of an implicit layout block |
TokenKind::VSemi |
separator between items in an implicit layout block |
TokenKind::VRBrace |
end of an implicit layout block |
This is useful when a consumer wants parser-equivalent token boundaries without building the full AST.
use resolve_layout;
use ;
let lexed = lex;
assert!;
let laid_out = resolve_layout;
assert!;
Public modules
| Module | What it gives you |
|---|---|
lexer |
tokenizer, trivia, TokenKind/Token, Pos, lossless reconstruction |
layout |
offside-rule resolution into virtual { ; } tokens |
parse |
parse_module → typed AST + diagnostics |
ast |
the syntax tree types, byte Span on every node |
ast_span |
render_from_ast, the byte-span losslessness oracle |
Stability
daml-parser is pre-1.0. The supported public entry points are the modules
listed above, with parse::parse_module as the normal start. The AST is public
so tools can inspect parser output; downstream code should prefer parser-created
trees over manual construction. Breaking public API changes use 0.x minor bumps,
and patch releases should stay compatible. cargo-semver-checks is a blocking
CI signal for public API compatibility.
daml-parser is syntax-only. It does not perform name resolution, package
resolution, type checking, scenario/script execution, or authorization analysis.
Consumers that need those concepts should derive them above the syntax layer or
delegate them to the Daml toolchain.
License
AGPL-3.0-only. See LICENSE.