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