use crate::lexer::{Pos, Token, TokenKind};
fn layout_keyword(tok: &TokenKind) -> Option<LayoutKeyword> {
match tok.keyword() {
Some("where") => Some(LayoutKeyword::Where),
Some("do") => Some(LayoutKeyword::Do),
Some("of") => Some(LayoutKeyword::Of),
Some("let") => Some(LayoutKeyword::Let),
Some("with") => Some(LayoutKeyword::With),
Some("catch") => Some(LayoutKeyword::Catch),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LayoutKeyword {
Module,
Where,
Do,
Of,
Let,
With,
Catch,
}
#[derive(Debug)]
struct Context {
col: usize,
line: usize,
opened_by: LayoutKeyword,
bracket_depth: usize,
}
#[must_use]
pub fn resolve_layout(tokens: Vec<Token>) -> Vec<Token> {
let eof_pos = tokens
.last()
.map(|t| t.pos)
.unwrap_or(Pos { line: 1, column: 1 });
let mut out: Vec<Token> = Vec::with_capacity(tokens.len() + tokens.len() / 4);
let mut stack: Vec<Context> = Vec::new();
let mut bracket_depth = 0usize;
let mut expecting_open: Option<LayoutKeyword> = None;
let mut last_line = 0usize;
if let Some(first) = tokens.first() {
if !first.kind.is_keyword("module") {
stack.push(Context {
col: first.pos.column,
line: first.pos.line,
opened_by: LayoutKeyword::Module,
bracket_depth: 0,
});
out.push(virtual_tok(TokenKind::VLBrace, first.pos));
last_line = first.pos.line;
}
}
let close = |out: &mut Vec<Token>, pos: Pos| {
out.push(virtual_tok(TokenKind::VRBrace, pos));
};
for token in tokens {
let pos = token.pos;
let col = pos.column;
if let Some(kw) = expecting_open.take() {
if matches!(token.kind, TokenKind::LBrace) {
stack.push(Context {
col: 0,
line: pos.line,
opened_by: kw,
bracket_depth,
});
out.push(token);
last_line = pos.line;
continue;
}
let enclosing = stack.iter().rev().find(|c| c.col > 0).map_or(0, |c| c.col);
if col > enclosing {
stack.push(Context {
col,
line: pos.line,
opened_by: kw,
bracket_depth,
});
out.push(virtual_tok(TokenKind::VLBrace, pos));
last_line = pos.line;
} else {
out.push(virtual_tok(TokenKind::VLBrace, pos));
out.push(virtual_tok(TokenKind::VRBrace, pos));
}
}
if pos.line != last_line {
while let Some(top) = stack.last() {
if top.col > 0 && col < top.col {
if token.kind.is_keyword("in") && top.opened_by == LayoutKeyword::Let {
close(&mut out, pos);
stack.pop();
break;
}
close(&mut out, pos);
stack.pop();
} else {
break;
}
}
if let Some(top) = stack.last() {
if top.col > 0 && col == top.col && !token.kind.is_keyword("where") {
out.push(virtual_tok(TokenKind::VSemi, pos));
}
}
last_line = pos.line;
}
if token.kind.is_keyword("where") {
while let Some(top) = stack.last() {
if top.col > 0
&& (top.col >= col
|| (top.line == pos.line && top.opened_by == LayoutKeyword::With))
&& top.opened_by != LayoutKeyword::Module
{
close(&mut out, pos);
stack.pop();
} else {
break;
}
}
}
if token.kind.is_keyword("in") {
if let Some(top) = stack.last() {
if top.col > 0
&& top.opened_by == LayoutKeyword::Let
&& (top.line == pos.line || col <= top.col)
{
close(&mut out, pos);
stack.pop();
}
}
}
match token.kind {
TokenKind::LParen | TokenKind::LBracket => bracket_depth += 1,
TokenKind::RParen | TokenKind::RBracket | TokenKind::Comma => {
let target = if matches!(token.kind, TokenKind::Comma) {
bracket_depth
} else {
bracket_depth.saturating_sub(1)
};
while let Some(top) = stack.last() {
if top.col > 0 && top.bracket_depth > target {
close(&mut out, pos);
stack.pop();
} else {
break;
}
}
if !matches!(token.kind, TokenKind::Comma) {
bracket_depth = bracket_depth.saturating_sub(1);
}
}
TokenKind::RBrace
if stack.last().is_some_and(|c| c.col == 0) => {
stack.pop();
}
_ => {}
}
let was_backslash = out
.last()
.is_some_and(|t| matches!(&t.kind, TokenKind::Op(o) if *o == "\\"));
let opens_layout = layout_keyword(&token.kind);
let case_after_backslash = token.kind.is_keyword("case") && was_backslash;
out.push(token);
if let Some(keyword) = opens_layout {
expecting_open = Some(keyword);
} else if case_after_backslash {
expecting_open = Some(LayoutKeyword::Of);
}
}
if expecting_open.is_some() {
out.push(virtual_tok(TokenKind::VLBrace, eof_pos));
out.push(virtual_tok(TokenKind::VRBrace, eof_pos));
}
for ctx in stack.iter().rev() {
if ctx.col > 0 {
out.push(virtual_tok(TokenKind::VRBrace, eof_pos));
}
}
out
}
const fn virtual_tok(tok: TokenKind, pos: Pos) -> Token {
Token {
kind: tok,
pos,
start: 0,
end: 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::lex;
fn layout_str(src: &str) -> String {
let (tokens, errors) = lex(src).into_parts();
assert!(errors.is_empty(), "lex errors: {errors:?}");
resolve_layout(tokens)
.iter()
.map(|t| match &t.kind {
TokenKind::VLBrace => "{".to_string(),
TokenKind::VRBrace => "}".to_string(),
TokenKind::VSemi => ";".to_string(),
TokenKind::LowerId { qualifier, name } | TokenKind::UpperId { qualifier, name } => {
qualifier
.as_ref()
.map_or_else(|| name.to_string(), |q| format!("{q}.{name}"))
}
TokenKind::Op(o) => o.to_string(),
TokenKind::IntLit(n) | TokenKind::DecimalLit(n) => n.clone(),
TokenKind::StringLit(s) => format!("{s:?}"),
TokenKind::CharLit(c) => format!("'{c}'"),
TokenKind::LParen => "(".into(),
TokenKind::RParen => ")".into(),
TokenKind::LBracket => "[".into(),
TokenKind::RBracket => "]".into(),
TokenKind::LBrace => "{{".into(),
TokenKind::RBrace => "}}".into(),
TokenKind::Comma => ",".into(),
TokenKind::Semi => ";;".into(),
TokenKind::Backtick => "`".into(),
})
.collect::<Vec<_>>()
.join(" ")
}
#[test]
fn module_where_opens_top_block() {
assert_eq!(
layout_str("module M where\n\nf = 1\ng = 2\n"),
"module M where { f = 1 ; g = 2 }"
);
}
#[test]
fn template_with_where_blocks() {
let src = "module M where\n\ntemplate Foo\n with\n x : Int\n y : Party\n where\n signatory y\n";
assert_eq!(
layout_str(src),
"module M where { template Foo with { x : Int ; y : Party } where { signatory y } }"
);
}
#[test]
fn do_block_and_dedent() {
let src = "module M where\nf = do\n a\n b\ng = 1\n";
assert_eq!(
layout_str(src),
"module M where { f = do { a ; b } ; g = 1 }"
);
}
#[test]
fn same_line_record_with() {
let src = "module M where\nf = do\n cid <- create this with owner = p\n pure cid\n";
assert_eq!(
layout_str(src),
"module M where { f = do { cid <- create this with { owner = p } ; pure cid } }"
);
}
#[test]
fn let_in_same_line() {
assert_eq!(
layout_str("module M where\nf = let x = 1 in x\n"),
"module M where { f = let { x = 1 } in x }"
);
}
#[test]
fn let_in_multiline() {
let src = "module M where\nf =\n let x = 1\n y = 2\n in x\n";
assert_eq!(
layout_str(src),
"module M where { f = let { x = 1 ; y = 2 } in x }"
);
}
#[test]
fn paren_closes_do_block() {
assert_eq!(
layout_str("module M where\nf = g (do\n a) b\n"),
"module M where { f = g ( do { a } ) b }"
);
}
#[test]
fn where_at_do_indent_closes_do() {
let src = "module M where\nf = do\n a\n where\n g = 1\n";
assert_eq!(
layout_str(src),
"module M where { f = do { a } where { g = 1 } }"
);
}
#[test]
fn in_closes_enclosing_let_after_inner_let_closes_offside() {
let src = "module M where\nf = let\n let x = 1\n y = 2\nin x\n";
assert_eq!(
layout_str(src),
"module M where { f = let { let { x = 1 ; y = 2 } } in x }"
);
}
#[test]
fn file_without_module_header() {
assert_eq!(layout_str("f = 1\ng = 2\n"), "{ f = 1 ; g = 2 }");
}
#[test]
fn corpus_lex_and_layout_survives() {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../corpus/daml-finance/daml");
if !root.exists() {
assert!(
std::env::var_os("CI").is_none(),
"vendored corpus missing under CI (was it committed?): {}",
root.display()
);
eprintln!("corpus absent (published crate?), skipping");
return;
}
let mut files = Vec::new();
collect_daml(&root, &mut files).expect("collect corpus files");
assert!(
files.len() > 600,
"corpus incomplete: {} files",
files.len()
);
let mut lex_errors = 0usize;
for f in &files {
let src = std::fs::read_to_string(f)
.unwrap_or_else(|e| panic!("failed to read corpus file {}: {e}", f.display()));
let (tokens, errors) = lex(&src).into_parts();
lex_errors += errors.len();
let laid = resolve_layout(tokens);
let opens = laid.iter().filter(|t| t.kind == TokenKind::VLBrace).count();
let closes = laid.iter().filter(|t| t.kind == TokenKind::VRBrace).count();
assert_eq!(opens, closes, "unbalanced virtual braces in {f:?}");
}
assert_eq!(lex_errors, 0, "lex errors across corpus");
}
fn collect_daml(
dir: &std::path::Path,
out: &mut Vec<std::path::PathBuf>,
) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let p = entry.path();
if p.is_dir() {
collect_daml(&p, out)?;
} else if p.extension().is_some_and(|e| e == "daml") {
out.push(p);
}
}
Ok(())
}
#[test]
fn case_of_alternatives() {
let src = "module M where\nf x = case x of\n 1 -> a\n _ -> b\n";
assert_eq!(
layout_str(src),
"module M where { f x = case x of { 1 -> a ; _ -> b } }"
);
}
}