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