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