diff --git a/source/compiler/qsc_eval/src/tests.rs b/source/compiler/qsc_eval/src/tests.rs index 7a76b0414c3..2058cd71646 100644 --- a/source/compiler/qsc_eval/src/tests.rs +++ b/source/compiler/qsc_eval/src/tests.rs @@ -3357,6 +3357,34 @@ fn nested_interpolated_string_with_exprs() { ); } +#[test] +fn interpolated_string_block() { + check_expr("", r#"$"{ { 2 + 3 } }""#, &expect!["5"]); +} + +#[test] +fn interpolated_string_for_loop() { + check_expr("", r#"$"{for _ in 1..3 {}}""#, &expect!["()"]); +} + +#[test] +fn interpolated_string_block_with_loop() { + check_expr( + "", + r#"$"result is { { mutable r = 0; for _ in 1..3 { set r += 1; } r } }""#, + &expect!["result is 3"], + ); +} + +#[test] +fn nested_interpolated_string_with_block() { + check_expr( + "", + r#"$"A={ { let b = $"B={ {2 + 3} }"; b } }""#, + &expect!["A=B=5"], + ); +} + #[test] fn udt_unwrap() { check_expr( diff --git a/source/compiler/qsc_parse/src/expr/tests.rs b/source/compiler/qsc_parse/src/expr/tests.rs index 7786259ad3e..59b4b51fd0f 100644 --- a/source/compiler/qsc_parse/src/expr/tests.rs +++ b/source/compiler/qsc_parse/src/expr/tests.rs @@ -2366,6 +2366,36 @@ fn interpolated_string_braced() { ); } +#[test] +fn interpolated_string_block() { + check( + expr, + r#"$"{ {x} }""#, + &expect![[r#" + Expr _id_ [0-10]: Interpolate: + Expr: Expr _id_ [4-7]: Expr Block: Block _id_ [4-7]: + Stmt _id_ [5-6]: Expr: Expr _id_ [5-6]: Path: Path _id_ [5-6] (Ident _id_ [5-6] "x")"#]], + ); +} + +#[test] +fn interpolated_string_for_loop() { + check( + expr, + r#"$"{for i in 1..3 {}}""#, + &expect![[r#" + Expr _id_ [0-21]: Interpolate: + Expr: Expr _id_ [3-19]: For: + Pat _id_ [7-8]: Bind: + Ident _id_ [7-8] "i" + Expr _id_ [12-16]: Range: + Expr _id_ [12-13]: Lit: Int(1) + + Expr _id_ [15-16]: Lit: Int(3) + Block _id_ [17-19]: "#]], + ); +} + #[test] fn interpolated_string_escape_brace() { check( diff --git a/source/compiler/qsc_parse/src/lex/cooked/tests.rs b/source/compiler/qsc_parse/src/lex/cooked/tests.rs index 57e7e0a645a..c682c3a0eb7 100644 --- a/source/compiler/qsc_parse/src/lex/cooked/tests.rs +++ b/source/compiler/qsc_parse/src/lex/cooked/tests.rs @@ -1157,6 +1157,200 @@ fn interpolated_string_braced() { ); } +#[test] +fn interpolated_string_block() { + check( + r#"$"{ {x} }""#, + &expect![[r#" + [ + Ok( + Token { + kind: String( + Interpolated( + DollarQuote, + LBrace, + ), + ), + span: Span { + lo: 0, + hi: 3, + }, + }, + ), + Ok( + Token { + kind: Open( + Brace, + ), + span: Span { + lo: 4, + hi: 5, + }, + }, + ), + Ok( + Token { + kind: Ident, + span: Span { + lo: 5, + hi: 6, + }, + }, + ), + Ok( + Token { + kind: Close( + Brace, + ), + span: Span { + lo: 6, + hi: 7, + }, + }, + ), + Ok( + Token { + kind: String( + Interpolated( + RBrace, + Quote, + ), + ), + span: Span { + lo: 8, + hi: 10, + }, + }, + ), + ] + "#]], + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn interpolated_string_for_loop() { + check( + r#"$"{for i in 1..3 {}}""#, + &expect![[r#" + [ + Ok( + Token { + kind: String( + Interpolated( + DollarQuote, + LBrace, + ), + ), + span: Span { + lo: 0, + hi: 3, + }, + }, + ), + Ok( + Token { + kind: Keyword( + For, + ), + span: Span { + lo: 3, + hi: 6, + }, + }, + ), + Ok( + Token { + kind: Ident, + span: Span { + lo: 7, + hi: 8, + }, + }, + ), + Ok( + Token { + kind: Keyword( + In, + ), + span: Span { + lo: 9, + hi: 11, + }, + }, + ), + Ok( + Token { + kind: Int( + Decimal, + ), + span: Span { + lo: 12, + hi: 13, + }, + }, + ), + Ok( + Token { + kind: DotDot, + span: Span { + lo: 13, + hi: 15, + }, + }, + ), + Ok( + Token { + kind: Int( + Decimal, + ), + span: Span { + lo: 15, + hi: 16, + }, + }, + ), + Ok( + Token { + kind: Open( + Brace, + ), + span: Span { + lo: 17, + hi: 18, + }, + }, + ), + Ok( + Token { + kind: Close( + Brace, + ), + span: Span { + lo: 18, + hi: 19, + }, + }, + ), + Ok( + Token { + kind: String( + Interpolated( + RBrace, + Quote, + ), + ), + span: Span { + lo: 19, + hi: 21, + }, + }, + ), + ] + "#]], + ); +} + #[test] fn interpolated_string_escape_brace() { check( diff --git a/source/compiler/qsc_parse/src/lex/raw.rs b/source/compiler/qsc_parse/src/lex/raw.rs index 2812cdffe9f..3b0f2f334e1 100644 --- a/source/compiler/qsc_parse/src/lex/raw.rs +++ b/source/compiler/qsc_parse/src/lex/raw.rs @@ -172,7 +172,7 @@ pub enum CommentKind { #[derive(Clone)] pub struct Lexer<'a> { chars: Peekable>, - interpolation: u8, + interp_brace_depths: Vec, starting_offset: u32, } @@ -181,7 +181,7 @@ impl<'a> Lexer<'a> { pub fn new(input: &'a str) -> Self { Self { chars: input.char_indices().peekable(), - interpolation: 0, + interp_brace_depths: Vec::new(), starting_offset: 0, } } @@ -190,7 +190,7 @@ impl<'a> Lexer<'a> { pub fn new_with_starting_offset(input: &'a str, starting_offset: u32) -> Self { Self { chars: input.char_indices().peekable(), - interpolation: 0, + interp_brace_depths: Vec::new(), starting_offset, } } @@ -348,12 +348,22 @@ impl<'a> Lexer<'a> { } } else if c == '"' { Some(StringKind::Normal) - } else if self.interpolation > 0 && c == '}' { - self.interpolation = self - .interpolation - .checked_sub(1) - .expect("interpolation level should have been incremented at left brace"); - Some(StringKind::Interpolated) + } else if let Some(depth) = self.interp_brace_depths.last_mut() { + match c { + '{' => { + *depth += 1; + None + } + '}' if *depth > 0 => { + *depth -= 1; + None + } + '}' => { + self.interp_brace_depths.pop(); + Some(StringKind::Interpolated) + } + _ => None, + } } else { None } @@ -372,10 +382,7 @@ impl<'a> Lexer<'a> { }; let end = if self.next_if_eq('{') { - self.interpolation = self - .interpolation - .checked_add(1) - .expect("interpolation should not exceed maximum depth"); + self.interp_brace_depths.push(0); Some(InterpolatedEnding::LBrace) } else if self.next_if_eq('"') { Some(InterpolatedEnding::Quote) diff --git a/source/compiler/qsc_parse/src/lex/raw/tests.rs b/source/compiler/qsc_parse/src/lex/raw/tests.rs index 5e3311cacf2..d5d67089943 100644 --- a/source/compiler/qsc_parse/src/lex/raw/tests.rs +++ b/source/compiler/qsc_parse/src/lex/raw/tests.rs @@ -303,6 +303,316 @@ fn interpolated_string_braced() { ); } +#[test] +fn interpolated_string_block() { + check( + r#"$"{ {x} }""#, + &expect![[r#" + [ + Token { + kind: String( + Interpolated( + DollarQuote, + Some( + LBrace, + ), + ), + ), + offset: 0, + }, + Token { + kind: Whitespace, + offset: 3, + }, + Token { + kind: Single( + Open( + Brace, + ), + ), + offset: 4, + }, + Token { + kind: Ident, + offset: 5, + }, + Token { + kind: Single( + Close( + Brace, + ), + ), + offset: 6, + }, + Token { + kind: Whitespace, + offset: 7, + }, + Token { + kind: String( + Interpolated( + RBrace, + Some( + Quote, + ), + ), + ), + offset: 8, + }, + ] + "#]], + ); +} + +#[test] +fn interpolated_string_nested_block() { + check( + r#"$"{ { {x} } }""#, + &expect![[r#" + [ + Token { + kind: String( + Interpolated( + DollarQuote, + Some( + LBrace, + ), + ), + ), + offset: 0, + }, + Token { + kind: Whitespace, + offset: 3, + }, + Token { + kind: Single( + Open( + Brace, + ), + ), + offset: 4, + }, + Token { + kind: Whitespace, + offset: 5, + }, + Token { + kind: Single( + Open( + Brace, + ), + ), + offset: 6, + }, + Token { + kind: Ident, + offset: 7, + }, + Token { + kind: Single( + Close( + Brace, + ), + ), + offset: 8, + }, + Token { + kind: Whitespace, + offset: 9, + }, + Token { + kind: Single( + Close( + Brace, + ), + ), + offset: 10, + }, + Token { + kind: Whitespace, + offset: 11, + }, + Token { + kind: String( + Interpolated( + RBrace, + Some( + Quote, + ), + ), + ), + offset: 12, + }, + ] + "#]], + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn interpolated_string_for_loop() { + check( + r#"$"{for i in 1..3 {}}""#, + &expect![[r#" + [ + Token { + kind: String( + Interpolated( + DollarQuote, + Some( + LBrace, + ), + ), + ), + offset: 0, + }, + Token { + kind: Ident, + offset: 3, + }, + Token { + kind: Whitespace, + offset: 6, + }, + Token { + kind: Ident, + offset: 7, + }, + Token { + kind: Whitespace, + offset: 8, + }, + Token { + kind: Ident, + offset: 9, + }, + Token { + kind: Whitespace, + offset: 11, + }, + Token { + kind: Number( + Int( + Decimal, + ), + ), + offset: 12, + }, + Token { + kind: Single( + Dot, + ), + offset: 13, + }, + Token { + kind: Single( + Dot, + ), + offset: 14, + }, + Token { + kind: Number( + Int( + Decimal, + ), + ), + offset: 15, + }, + Token { + kind: Whitespace, + offset: 16, + }, + Token { + kind: Single( + Open( + Brace, + ), + ), + offset: 17, + }, + Token { + kind: Single( + Close( + Brace, + ), + ), + offset: 18, + }, + Token { + kind: String( + Interpolated( + RBrace, + Some( + Quote, + ), + ), + ), + offset: 19, + }, + ] + "#]], + ); +} + +#[test] +fn interpolated_string_unclosed_block() { + check( + r#"$"{ {x }""#, + &expect![[r#" + [ + Token { + kind: String( + Interpolated( + DollarQuote, + Some( + LBrace, + ), + ), + ), + offset: 0, + }, + Token { + kind: Whitespace, + offset: 3, + }, + Token { + kind: Single( + Open( + Brace, + ), + ), + offset: 4, + }, + Token { + kind: Ident, + offset: 5, + }, + Token { + kind: Whitespace, + offset: 6, + }, + Token { + kind: Single( + Close( + Brace, + ), + ), + offset: 7, + }, + Token { + kind: String( + Normal { + terminated: false, + }, + ), + offset: 8, + }, + ] + "#]], + ); +} + #[test] fn interpolated_string_escape_brace() { check( @@ -792,6 +1102,97 @@ fn nested_interpolated_string_followed_by_braces() { ); } +#[test] +fn nested_interpolated_string_with_block() { + check( + r#"$"{ $"{ {x} }" }""#, + &expect![[r#" + [ + Token { + kind: String( + Interpolated( + DollarQuote, + Some( + LBrace, + ), + ), + ), + offset: 0, + }, + Token { + kind: Whitespace, + offset: 3, + }, + Token { + kind: String( + Interpolated( + DollarQuote, + Some( + LBrace, + ), + ), + ), + offset: 4, + }, + Token { + kind: Whitespace, + offset: 7, + }, + Token { + kind: Single( + Open( + Brace, + ), + ), + offset: 8, + }, + Token { + kind: Ident, + offset: 9, + }, + Token { + kind: Single( + Close( + Brace, + ), + ), + offset: 10, + }, + Token { + kind: Whitespace, + offset: 11, + }, + Token { + kind: String( + Interpolated( + RBrace, + Some( + Quote, + ), + ), + ), + offset: 12, + }, + Token { + kind: Whitespace, + offset: 14, + }, + Token { + kind: String( + Interpolated( + RBrace, + Some( + Quote, + ), + ), + ), + offset: 15, + }, + ] + "#]], + ); +} + #[test] fn binary() { check(