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