1use crate::lexer::{Pos, Token, TokenKind};
9
10fn layout_keyword(tok: &TokenKind) -> Option<LayoutKeyword> {
14 match tok.keyword() {
15 Some("where") => Some(LayoutKeyword::Where),
16 Some("do") => Some(LayoutKeyword::Do),
17 Some("of") => Some(LayoutKeyword::Of),
18 Some("let") => Some(LayoutKeyword::Let),
19 Some("with") => Some(LayoutKeyword::With),
20 Some("catch") => Some(LayoutKeyword::Catch),
21 _ => None,
22 }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26enum LayoutKeyword {
27 Module,
28 Where,
29 Do,
30 Of,
31 Let,
32 With,
33 Catch,
34}
35
36#[derive(Debug)]
37struct Context {
38 col: usize,
40 line: usize,
42 opened_by: LayoutKeyword,
44 bracket_depth: usize,
47}
48
49#[must_use]
66pub fn resolve_layout(tokens: Vec<Token>) -> Vec<Token> {
67 let eof_pos = tokens
68 .last()
69 .map(|t| t.pos)
70 .unwrap_or(Pos { line: 1, column: 1 });
71 let mut out: Vec<Token> = Vec::with_capacity(tokens.len() + tokens.len() / 4);
72 let mut stack: Vec<Context> = Vec::new();
73 let mut bracket_depth = 0usize;
74 let mut expecting_open: Option<LayoutKeyword> = None;
76 let mut last_line = 0usize;
77
78 if let Some(first) = tokens.first() {
81 if !first.kind.is_keyword("module") {
82 stack.push(Context {
83 col: first.pos.column,
84 line: first.pos.line,
85 opened_by: LayoutKeyword::Module,
86 bracket_depth: 0,
87 });
88 out.push(virtual_tok(TokenKind::VLBrace, first.pos));
89 last_line = first.pos.line;
90 }
91 }
92
93 let close = |out: &mut Vec<Token>, pos: Pos| {
94 out.push(virtual_tok(TokenKind::VRBrace, pos));
95 };
96
97 for token in tokens {
98 let pos = token.pos;
99 let col = pos.column;
100
101 if let Some(kw) = expecting_open.take() {
102 if matches!(token.kind, TokenKind::LBrace) {
103 stack.push(Context {
105 col: 0,
106 line: pos.line,
107 opened_by: kw,
108 bracket_depth,
109 });
110 out.push(token);
111 last_line = pos.line;
112 continue;
113 }
114 let enclosing = stack.iter().rev().find(|c| c.col > 0).map_or(0, |c| c.col);
115 if col > enclosing {
116 stack.push(Context {
117 col,
118 line: pos.line,
119 opened_by: kw,
120 bracket_depth,
121 });
122 out.push(virtual_tok(TokenKind::VLBrace, pos));
123 last_line = pos.line;
124 } else {
126 out.push(virtual_tok(TokenKind::VLBrace, pos));
129 out.push(virtual_tok(TokenKind::VRBrace, pos));
130 }
131 }
132
133 if pos.line != last_line {
135 while let Some(top) = stack.last() {
136 if top.col > 0 && col < top.col {
137 if token.kind.is_keyword("in") && top.opened_by == LayoutKeyword::Let {
138 close(&mut out, pos);
139 stack.pop();
140 break;
141 }
142 close(&mut out, pos);
143 stack.pop();
144 } else {
145 break;
146 }
147 }
148 if let Some(top) = stack.last() {
151 if top.col > 0 && col == top.col && !token.kind.is_keyword("where") {
152 out.push(virtual_tok(TokenKind::VSemi, pos));
153 }
154 }
155 last_line = pos.line;
156 }
157
158 if token.kind.is_keyword("where") {
162 while let Some(top) = stack.last() {
163 if top.col > 0
167 && (top.col >= col
168 || (top.line == pos.line && top.opened_by == LayoutKeyword::With))
169 && top.opened_by != LayoutKeyword::Module
170 {
171 close(&mut out, pos);
172 stack.pop();
173 } else {
174 break;
175 }
176 }
177 }
178
179 if token.kind.is_keyword("in") {
184 if let Some(top) = stack.last() {
185 if top.col > 0
186 && top.opened_by == LayoutKeyword::Let
187 && (top.line == pos.line || col <= top.col)
188 {
189 close(&mut out, pos);
190 stack.pop();
191 }
192 }
193 }
194
195 match token.kind {
196 TokenKind::LParen | TokenKind::LBracket => bracket_depth += 1,
197 TokenKind::RParen | TokenKind::RBracket | TokenKind::Comma => {
198 let target = if matches!(token.kind, TokenKind::Comma) {
202 bracket_depth
203 } else {
204 bracket_depth.saturating_sub(1)
205 };
206 while let Some(top) = stack.last() {
207 if top.col > 0 && top.bracket_depth > target {
208 close(&mut out, pos);
209 stack.pop();
210 } else {
211 break;
212 }
213 }
214 if !matches!(token.kind, TokenKind::Comma) {
215 bracket_depth = bracket_depth.saturating_sub(1);
216 }
217 }
218 TokenKind::RBrace
219 if stack.last().is_some_and(|c| c.col == 0) => {
221 stack.pop();
222 }
223 _ => {}
224 }
225
226 let was_backslash = out
227 .last()
228 .is_some_and(|t| matches!(&t.kind, TokenKind::Op(o) if *o == "\\"));
229 let opens_layout = layout_keyword(&token.kind);
230 let case_after_backslash = token.kind.is_keyword("case") && was_backslash;
231 out.push(token);
232
233 if let Some(keyword) = opens_layout {
234 expecting_open = Some(keyword);
235 } else if case_after_backslash {
236 expecting_open = Some(LayoutKeyword::Of);
238 }
239 }
240
241 if expecting_open.is_some() {
243 out.push(virtual_tok(TokenKind::VLBrace, eof_pos));
244 out.push(virtual_tok(TokenKind::VRBrace, eof_pos));
245 }
246 for ctx in stack.iter().rev() {
247 if ctx.col > 0 {
248 out.push(virtual_tok(TokenKind::VRBrace, eof_pos));
249 }
250 }
251
252 out
253}
254
255const fn virtual_tok(tok: TokenKind, pos: Pos) -> Token {
256 Token {
259 kind: tok,
260 pos,
261 start: 0,
262 end: 0,
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use crate::lexer::lex;
270
271 fn layout_str(src: &str) -> String {
274 let (tokens, errors) = lex(src).into_parts();
275 assert!(errors.is_empty(), "lex errors: {errors:?}");
276 resolve_layout(tokens)
277 .iter()
278 .map(|t| match &t.kind {
279 TokenKind::VLBrace => "{".to_string(),
280 TokenKind::VRBrace => "}".to_string(),
281 TokenKind::VSemi => ";".to_string(),
282 TokenKind::LowerId { qualifier, name } | TokenKind::UpperId { qualifier, name } => {
283 qualifier
284 .as_ref()
285 .map_or_else(|| name.to_string(), |q| format!("{q}.{name}"))
286 }
287 TokenKind::Op(o) => o.to_string(),
288 TokenKind::IntLit(n) | TokenKind::DecimalLit(n) => n.clone(),
289 TokenKind::StringLit(s) => format!("{s:?}"),
290 TokenKind::CharLit(c) => format!("'{c}'"),
291 TokenKind::LParen => "(".into(),
292 TokenKind::RParen => ")".into(),
293 TokenKind::LBracket => "[".into(),
294 TokenKind::RBracket => "]".into(),
295 TokenKind::LBrace => "{{".into(),
296 TokenKind::RBrace => "}}".into(),
297 TokenKind::Comma => ",".into(),
298 TokenKind::Semi => ";;".into(),
299 TokenKind::Backtick => "`".into(),
300 })
301 .collect::<Vec<_>>()
302 .join(" ")
303 }
304
305 #[test]
306 fn module_where_opens_top_block() {
307 assert_eq!(
308 layout_str("module M where\n\nf = 1\ng = 2\n"),
309 "module M where { f = 1 ; g = 2 }"
310 );
311 }
312
313 #[test]
314 fn template_with_where_blocks() {
315 let src = "module M where\n\ntemplate Foo\n with\n x : Int\n y : Party\n where\n signatory y\n";
316 assert_eq!(
317 layout_str(src),
318 "module M where { template Foo with { x : Int ; y : Party } where { signatory y } }"
319 );
320 }
321
322 #[test]
323 fn do_block_and_dedent() {
324 let src = "module M where\nf = do\n a\n b\ng = 1\n";
325 assert_eq!(
326 layout_str(src),
327 "module M where { f = do { a ; b } ; g = 1 }"
328 );
329 }
330
331 #[test]
332 fn same_line_record_with() {
333 let src = "module M where\nf = do\n cid <- create this with owner = p\n pure cid\n";
334 assert_eq!(
335 layout_str(src),
336 "module M where { f = do { cid <- create this with { owner = p } ; pure cid } }"
337 );
338 }
339
340 #[test]
341 fn let_in_same_line() {
342 assert_eq!(
343 layout_str("module M where\nf = let x = 1 in x\n"),
344 "module M where { f = let { x = 1 } in x }"
345 );
346 }
347
348 #[test]
349 fn let_in_multiline() {
350 let src = "module M where\nf =\n let x = 1\n y = 2\n in x\n";
351 assert_eq!(
352 layout_str(src),
353 "module M where { f = let { x = 1 ; y = 2 } in x }"
354 );
355 }
356
357 #[test]
358 fn paren_closes_do_block() {
359 assert_eq!(
360 layout_str("module M where\nf = g (do\n a) b\n"),
361 "module M where { f = g ( do { a } ) b }"
362 );
363 }
364
365 #[test]
366 fn where_at_do_indent_closes_do() {
367 let src = "module M where\nf = do\n a\n where\n g = 1\n";
368 assert_eq!(
369 layout_str(src),
370 "module M where { f = do { a } where { g = 1 } }"
371 );
372 }
373
374 #[test]
375 fn in_closes_enclosing_let_after_inner_let_closes_offside() {
376 let src = "module M where\nf = let\n let x = 1\n y = 2\nin x\n";
377 assert_eq!(
378 layout_str(src),
379 "module M where { f = let { let { x = 1 ; y = 2 } } in x }"
380 );
381 }
382
383 #[test]
384 fn file_without_module_header() {
385 assert_eq!(layout_str("f = 1\ng = 2\n"), "{ f = 1 ; g = 2 }");
386 }
387
388 #[test]
392 fn corpus_lex_and_layout_survives() {
393 let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
394 .join("../../corpus/daml-finance/daml");
395 if !root.exists() {
396 eprintln!("corpus absent (published crate?), skipping");
399 return;
400 }
401 let mut files = Vec::new();
402 collect_daml(&root, &mut files).expect("collect corpus files");
403 assert!(
404 files.len() > 600,
405 "corpus incomplete: {} files",
406 files.len()
407 );
408 let mut lex_errors = 0usize;
409 for f in &files {
410 let src = std::fs::read_to_string(f)
411 .unwrap_or_else(|e| panic!("failed to read corpus file {}: {e}", f.display()));
412 let (tokens, errors) = lex(&src).into_parts();
413 lex_errors += errors.len();
414 let laid = resolve_layout(tokens);
415 let opens = laid.iter().filter(|t| t.kind == TokenKind::VLBrace).count();
416 let closes = laid.iter().filter(|t| t.kind == TokenKind::VRBrace).count();
417 assert_eq!(opens, closes, "unbalanced virtual braces in {f:?}");
418 }
419 assert_eq!(lex_errors, 0, "lex errors across corpus");
420 }
421
422 fn collect_daml(
423 dir: &std::path::Path,
424 out: &mut Vec<std::path::PathBuf>,
425 ) -> std::io::Result<()> {
426 for entry in std::fs::read_dir(dir)? {
427 let entry = entry?;
428 let p = entry.path();
429 if p.is_dir() {
430 collect_daml(&p, out)?;
431 } else if p.extension().is_some_and(|e| e == "daml") {
432 out.push(p);
433 }
434 }
435 Ok(())
436 }
437
438 #[test]
439 fn case_of_alternatives() {
440 let src = "module M where\nf x = case x of\n 1 -> a\n _ -> b\n";
441 assert_eq!(
442 layout_str(src),
443 "module M where { f x = case x of { 1 -> a ; _ -> b } }"
444 );
445 }
446}