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