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.10.1"
use parse_module;
let result = parse_module;
assert!;
let module = result.module;
Choosing tolerant versus strict parsing
Use parse::parse_module when you need partial structure plus diagnostics —
formatters, editors, linters, and other tools that must keep working on broken
files. It always returns a [ParseModuleResult] with a [Module] and a
(possibly empty) diagnostics list.
Use parse::parse_module_strict or [ParseModuleResult::into_result] when
any diagnostic should stop the caller — CI gates, batch analysis, or other
fail-fast paths. Both are thin wrappers over tolerant parsing: they call
parse_module and return Err(ParseModuleError) when diagnostics are
non-empty. The error carries the same diagnostics and partial module tree for
inspection.
use ;
let tolerant = parse_module;
assert!;
let strict = parse_module_strict;
assert!;
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 |
parse::parse_module_strict |
the same tree as parse_module, but returns Result and fails on any diagnostic |
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 Some = templates.first else ;
assert_eq!;
Declarations, expressions, patterns, and most parser DTOs carry a 1-based
Pos for their first token and a byte Span for their full source extent.
Structured Type nodes carry Span only; map their spans back to line/column
coordinates when a type-fragment diagnostic needs a position. Use spans when
you need an exact source slice:
Diagnostics and partial structure
Use ParseDiagnostic::kind() for machine-readable handling and
ParseDiagnostic::message() for presentation. The stable
ParseDiagnostic::category()/DiagnosticCategory value remains the coarse
JSON/SARIF grouping:
| 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 |
Typed kinds preserve details that callers should not scrape from messages: for
example ParseDiagnosticKind::Lex(LexErrorKind::InvalidEscapeSequence('q')),
ParseDiagnosticKind::ExpectedToken(ExpectedToken::ElseKeyword),
ParseDiagnosticKind::MalformedTypeAnnotation(TypeAnnotationContext::Field),
ParseDiagnosticKind::UnsupportedSyntax(UnsupportedSyntaxKind::LegacyControllerCan),
and ParseDiagnosticKind::RecursionLimit { .. }.
Partial structure is explicit in the AST. For example:
Decl::Unknownpreserves raw top-level declaration text.Decl::UnsupportedSyntaxpreserves intentionally unmodeled top-level syntax (e.g. pattern synonyms) with a typed [UnsupportedSyntaxKind].Decl::Fixitymodels top-level fixity metadata (infix,infixl,infixr).- Parenthesized and infix operator definitions parse as
Decl::Functiongrouped by operator name. 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 typed byte offsets into the original source: [start, end).
Use Span::range(), start_usize(), or end_usize() only where raw byte
offsets are required for slicing or external interop.
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 ;
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;
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; parse_module_strict for fail-fast callers |
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.
Recent public AST changes (see CHANGELOG):
ChoiceDecl.authority_exprsplus braced/layoutwheremetadata blocks model sourcecontroller,observer, andauthorityclauses without LF choice metadata.InterfaceInstanceDecl.items(View/Method) replaces flatmethodssoview = ...is distinguishable from method implementations.Alt.branches, guard qualifiers, and alternative-localwhere_bindingspreserve guarded case alternatives instead of collapsing to the first body.- Module-level
Decl::Fixitydeclarations drive expression grouping for the whole module; imported fixity resolution is out of scope. Pat::Recordmodels brace andwithconstructor record pattern fields; positionalPat::Conpatterns are unchanged.ImportDecl.package_labelstores decoded package string literals fromimport "pkg" Modulewithout resolving to an LFPackageId.
Explicit non-goals: LF package metadata, NameMaps, qualified PackageIds,
Update/internal expression nodes, imported fixity resolution, and compiler
desugaring belong above daml-parser or in the Daml toolchain — not in this
crate.
License
AGPL-3.0-only. See LICENSE.