daml-parser 0.6.2

Lossless lexer, layout resolver, and parser for the Daml smart-contract language
Documentation

daml-parser

License: AGPL v3

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:

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

[dependencies]
daml-parser = "0.3"
use daml_parser::parse::parse_module;

let result = parse_module("module M where\nfoo : Int\nfoo = 1\n");
assert!(result.diagnostics.is_empty());
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:

  1. lexer::lex
  2. layout::resolve_layout
  3. 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 daml_parser::ast::{Decl, DiagnosticCategory};
use daml_parser::parse::parse_module;

let source = "\
module M where

template Account
  with
    owner : Party
  where
    signatory owner
";

let result = parse_module(source);
let module = result.module;
let diagnostics = result.diagnostics;

let has_lex_errors = diagnostics
    .iter()
    .any(|d| d.category == DiagnosticCategory::Lex);

let templates: Vec<_> = module
    .decls
    .iter()
    .filter_map(|decl| match decl {
        Decl::Template(template) => Some((&template.name, template.span)),
        _ => None,
    })
    .collect();

assert!(!has_lex_errors);
let (template_name, template_span) = templates
    .first()
    .expect("example source defines an Account template");

assert_eq!(template_name.as_str(), "Account");

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(template_span.start..template_span.end)
    .expect("parser spans are UTF-8 boundaries");

assert!(snippet.starts_with("template Account"));

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::Unknown preserves raw top-level declaration text.
  • Expr::Error preserves raw expression text.
  • Pat::Other preserves raw pattern text.
  • TemplateBodyDecl::Other preserves 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 daml_parser::lexer::{lex_with_trivia, render_lossless};

let lexed = lex_with_trivia(source);
let tokens = lexed.tokens;
let trivia = lexed.trivia;
let errors = lexed.errors;
assert!(errors.is_empty());
assert_eq!(render_lossless(source, &tokens, &trivia).unwrap(), source);

Use the AST-level oracle when you need to prove parsed node spans and trivia can still reconstruct the source:

use daml_parser::ast_span::render_from_ast;
use daml_parser::lexer::lex_with_trivia;
use daml_parser::parse::parse_module;

let lexed = lex_with_trivia(source);
let trivia = lexed.trivia;
let parsed = parse_module(source);
let module = parsed.module;
assert_eq!(render_from_ast(source, &module, &trivia).unwrap(), source);

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 daml_parser::layout::resolve_layout;
use daml_parser::lexer::{lex, TokenKind};

let lexed = lex(source);
assert!(lexed.errors.is_empty());

let laid_out = resolve_layout(lexed.tokens);
assert!(laid_out.iter().any(|t| matches!(t.kind(), &TokenKind::VLBrace)));

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.