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)]
268mod tests {
269 use super::*;
270 use crate::lexer::lex;
271
272 fn layout_str(src: &str) -> String {
275 let (tokens, errors) = lex(src).into_parts();
276 assert!(errors.is_empty(), "lex errors: {errors:?}");
277 resolve_layout(tokens)
278 .iter()
279 .map(|t| match &t.kind {
280 TokenKind::VLBrace => "{".to_string(),
281 TokenKind::VRBrace => "}".to_string(),
282 TokenKind::VSemi => ";".to_string(),
283 TokenKind::LowerId { qualifier, name } | TokenKind::UpperId { qualifier, name } => {
284 qualifier
285 .as_ref()
286 .map_or_else(|| name.to_string(), |q| format!("{q}.{name}"))
287 }
288 TokenKind::Op(o) => o.to_string(),
289 TokenKind::IntLit(n) | TokenKind::DecimalLit(n) => n.clone(),
290 TokenKind::StringLit(s) => format!("{s:?}"),
291 TokenKind::CharLit(c) => format!("'{c}'"),
292 TokenKind::LParen => "(".into(),
293 TokenKind::RParen => ")".into(),
294 TokenKind::LBracket => "[".into(),
295 TokenKind::RBracket => "]".into(),
296 TokenKind::LBrace => "{{".into(),
297 TokenKind::RBrace => "}}".into(),
298 TokenKind::Comma => ",".into(),
299 TokenKind::Semi => ";;".into(),
300 TokenKind::Backtick => "`".into(),
301 })
302 .collect::<Vec<_>>()
303 .join(" ")
304 }
305
306 #[test]
307 fn module_where_opens_top_block() {
308 assert_eq!(
309 layout_str("module M where\n\nf = 1\ng = 2\n"),
310 "module M where { f = 1 ; g = 2 }"
311 );
312 }
313
314 #[test]
315 fn template_with_where_blocks() {
316 let src = "module M where\n\ntemplate Foo\n with\n x : Int\n y : Party\n where\n signatory y\n";
317 assert_eq!(
318 layout_str(src),
319 "module M where { template Foo with { x : Int ; y : Party } where { signatory y } }"
320 );
321 }
322
323 #[test]
324 fn do_block_and_dedent() {
325 let src = "module M where\nf = do\n a\n b\ng = 1\n";
326 assert_eq!(
327 layout_str(src),
328 "module M where { f = do { a ; b } ; g = 1 }"
329 );
330 }
331
332 #[test]
333 fn same_line_record_with() {
334 let src = "module M where\nf = do\n cid <- create this with owner = p\n pure cid\n";
335 assert_eq!(
336 layout_str(src),
337 "module M where { f = do { cid <- create this with { owner = p } ; pure cid } }"
338 );
339 }
340
341 #[test]
342 fn let_in_same_line() {
343 assert_eq!(
344 layout_str("module M where\nf = let x = 1 in x\n"),
345 "module M where { f = let { x = 1 } in x }"
346 );
347 }
348
349 #[test]
350 fn let_in_multiline() {
351 let src = "module M where\nf =\n let x = 1\n y = 2\n in x\n";
352 assert_eq!(
353 layout_str(src),
354 "module M where { f = let { x = 1 ; y = 2 } in x }"
355 );
356 }
357
358 #[test]
359 fn paren_closes_do_block() {
360 assert_eq!(
361 layout_str("module M where\nf = g (do\n a) b\n"),
362 "module M where { f = g ( do { a } ) b }"
363 );
364 }
365
366 #[test]
367 fn where_at_do_indent_closes_do() {
368 let src = "module M where\nf = do\n a\n where\n g = 1\n";
369 assert_eq!(
370 layout_str(src),
371 "module M where { f = do { a } where { g = 1 } }"
372 );
373 }
374
375 #[test]
376 fn in_closes_enclosing_let_after_inner_let_closes_offside() {
377 let src = "module M where\nf = let\n let x = 1\n y = 2\nin x\n";
378 assert_eq!(
379 layout_str(src),
380 "module M where { f = let { let { x = 1 ; y = 2 } } in x }"
381 );
382 }
383
384 #[test]
385 fn file_without_module_header() {
386 assert_eq!(layout_str("f = 1\ng = 2\n"), "{ f = 1 ; g = 2 }");
387 }
388
389 #[test]
393 fn corpus_lex_and_layout_survives() {
394 let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
395 .join("../../corpus/daml-finance/daml");
396 if !root.exists() {
397 assert!(
398 std::env::var_os("CI").is_none(),
399 "vendored corpus missing under CI (was it committed?): {}",
400 root.display()
401 );
402 eprintln!("corpus absent (published crate?), skipping");
403 return;
404 }
405 let mut files = Vec::new();
406 collect_daml(&root, &mut files).expect("collect corpus files");
407 assert!(
408 files.len() > 600,
409 "corpus incomplete: {} files",
410 files.len()
411 );
412 let mut lex_errors = 0usize;
413 for f in &files {
414 let src = std::fs::read_to_string(f)
415 .unwrap_or_else(|e| panic!("failed to read corpus file {}: {e}", f.display()));
416 let (tokens, errors) = lex(&src).into_parts();
417 lex_errors += errors.len();
418 let laid = resolve_layout(tokens);
419 let opens = laid.iter().filter(|t| t.kind == TokenKind::VLBrace).count();
420 let closes = laid.iter().filter(|t| t.kind == TokenKind::VRBrace).count();
421 assert_eq!(opens, closes, "unbalanced virtual braces in {f:?}");
422 }
423 assert_eq!(lex_errors, 0, "lex errors across corpus");
424 }
425
426 fn collect_daml(
427 dir: &std::path::Path,
428 out: &mut Vec<std::path::PathBuf>,
429 ) -> std::io::Result<()> {
430 for entry in std::fs::read_dir(dir)? {
431 let entry = entry?;
432 let p = entry.path();
433 if p.is_dir() {
434 collect_daml(&p, out)?;
435 } else if p.extension().is_some_and(|e| e == "daml") {
436 out.push(p);
437 }
438 }
439 Ok(())
440 }
441
442 #[test]
443 fn case_of_alternatives() {
444 let src = "module M where\nf x = case x of\n 1 -> a\n _ -> b\n";
445 assert_eq!(
446 layout_str(src),
447 "module M where { f x = case x of { 1 -> a ; _ -> b } }"
448 );
449 }
450}