Merge pull request #7497 from smores56/new-interpolation-syntax

Move to new interpolation syntax
This commit is contained in:
Sam Mohr 2025-01-10 15:25:12 -08:00 committed by GitHub
commit 528d1d2b69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 630 additions and 596 deletions

View file

@ -887,7 +887,7 @@ increase_size = \@Dict({ data, max_bucket_capacity, max_load_factor, shifts }) -
},
)
else
crash("Dict hit limit of $(Num.to_str(max_bucket_count)) elements. Unable to grow more.")
crash("Dict hit limit of ${Num.to_str(max_bucket_count)} elements. Unable to grow more.")
alloc_buckets_from_shift : U8, F32 -> (List Bucket, U64)
alloc_buckets_from_shift = \shifts, max_load_factor ->

View file

@ -1485,7 +1485,7 @@ for_each! = \list, func! ->
## List.for_each_try!(files_to_delete, \path ->
## File.delete!(path)?
##
## Stdout.line!("$(path) deleted")
## Stdout.line!("${path} deleted")
## )
## ```
for_each_try! : List a, (a => Result {} err) => Result {} err
@ -1527,14 +1527,15 @@ walk! = \list, state, func! ->
## If the function returns `Err`, the iteration stops and the error is returned.
##
## ```
## names = try List.walk_try!(
## names =
## List.walk_try!(
## ["First", "Middle", "Last"],
## [],
## \accumulator, which ->
## try Stdout.write! ("$(which) name: ")
## name = try Stdin.line! ({})
## Ok (List.append accumulator name),
## )
## Stdout.write!("${which} name: ")?
## name = Stdin.line!({})?
## Ok(List.append(accumulator, name)),
## )?
## ```
##
## This is the same as [walk_try], except that the step function can have effects.

View file

@ -124,11 +124,12 @@ on_err = \result, transform ->
## Like [on_err], but it allows the transformation function to produce effects.
##
## ```roc
## Result.on_err(Err("missing user"), (\msg ->
## Stdout.line!("ERROR: $(msg)")?
##
## Err(msg)
## ))
## Result.on_err(
## Err("missing user"),
## \msg ->
## Stdout.line!("ERROR: ${msg}")?
## Err(msg),
## )
## ```
on_err! : Result a err, (err => Result a other_err) => Result a other_err
on_err! = \result, transform! ->

View file

@ -34,7 +34,7 @@
## ```
## name = "Sam"
##
## "Hi, my name is $(name)!"
## "Hi, my name is ${name}!"
## ```
##
## This will evaluate to the string `"Hi, my name is Sam!"`
@ -44,7 +44,7 @@
## ```
## colors = ["red", "green", "blue"]
##
## "The colors are $(colors |> Str.join_with(", "))!"
## "The colors are ${colors |> Str.join_with(", ")}!"
## ```
##
## Interpolation can be used in multiline strings, but the part inside the parentheses must still be on one line.
@ -800,7 +800,7 @@ replace_first : Str, Str, Str -> Str
replace_first = \haystack, needle, flower ->
when split_first(haystack, needle) is
Ok({ before, after }) ->
"$(before)$(flower)$(after)"
"${before}${flower}${after}"
Err(NotFound) -> haystack
@ -818,7 +818,7 @@ replace_last : Str, Str, Str -> Str
replace_last = \haystack, needle, flower ->
when split_last(haystack, needle) is
Ok({ before, after }) ->
"$(before)$(flower)$(after)"
"${before}${flower}${after}"
Err(NotFound) -> haystack

View file

@ -2157,10 +2157,10 @@ mod test_can {
// // This should NOT be string interpolation, because of the \\
// indoc!(
// r#"
// "abcd\$(efg)hij"
// "abcd\${efg}hij"
// "#
// ),
// Str(r"abcd$(efg)hij".into()),
// Str(r"abcd${efg}hij".into()),
// );
// }

View file

@ -911,8 +911,8 @@ fn format_str_segment(seg: &StrSegment, buf: &mut Buf) {
buf.push(escaped.to_parsed_char());
}
Interpolated(loc_expr) => {
buf.push_str("$(");
// e.g. (name) in "Hi, $(name)!"
buf.push_str("${");
// e.g. {name} in "Hi, ${name}!"
let min_indent = buf.cur_line_indent() + INDENT;
loc_expr.value.format_with_options(
buf,
@ -921,7 +921,7 @@ fn format_str_segment(seg: &StrSegment, buf: &mut Buf) {
min_indent,
);
buf.indent(min_indent);
buf.push(')');
buf.push('}');
}
}
}

View file

@ -5864,7 +5864,7 @@ mod test_reporting {
r#"
greeting = "Privet"
if Bool.true then 1 else "$(greeting), World!"
if Bool.true then 1 else "${greeting}, World!"
"#,
),
@r#"
@ -5872,7 +5872,7 @@ mod test_reporting {
This `if` has an `else` branch with a different type from its `then` branch:
6 if Bool.true then 1 else "$(greeting), World!"
6 if Bool.true then 1 else "${greeting}, World!"
^^^^^^^^^^^^^^^^^^^^^
The `else` branch is a string of type:
@ -15052,7 +15052,7 @@ All branches in an `if` must have the same type!
u64_nums = parse_items_with Str.to_u64
u8_nums = parse_items_with Str.to_u8
"$(Inspect.to_str u64_nums) $(Inspect.to_str u8_nums)"
"${Inspect.to_str(u64_nums)} ${Inspect.to_str(u8_nums)}"
"#
),
@"" // no errors
@ -15304,7 +15304,7 @@ All branches in an `if` must have the same type!
get_cheer = \msg ->
name = Effect.get_line! {}
"$(msg), $(name)!"
"${msg}, ${name}!"
"#
),
@r"
@ -15340,7 +15340,7 @@ All branches in an `if` must have the same type!
trim : Str -> Str
trim = \msg ->
Effect.put_line! "Trimming $(msg)"
Effect.put_line!("Trimming ${msg}")
Str.trim msg
"#
),
@ -15349,7 +15349,7 @@ All branches in an `if` must have the same type!
This call to `Effect.put_line!` might produce an effect:
10 Effect.put_line! "Trimming $(msg)"
10 Effect.put_line!("Trimming ${msg}")
^^^^^^^^^^^^^^^^
However, the type of the enclosing function requires that it's pure:
@ -15736,7 +15736,7 @@ All branches in an `if` must have the same type!
(get, put) = (Effect.get_line!, Effect.put_line!)
name = get {}
put "Hi, $(name)"
put "Hi, ${name}"
"#
),
@r###"
@ -15808,7 +15808,7 @@ All branches in an `if` must have the same type!
Tag get put = Tag Effect.get_line! Effect.put_line!
name = get {}
put "Hi, $(name)"
put "Hi, ${name}"
"#
),
@r###"

View file

@ -1574,7 +1574,7 @@ fn module_params_checks() {
r#"
module { key } -> [url]
url = "example.com/$(key)"
url = "example.com/${key}"
"#
),
),
@ -1605,7 +1605,7 @@ fn module_params_optional() {
r#"
module { key, exp ? "default" } -> [url]
url = "example.com/$(key)?exp=$(exp)"
url = "example.com/${key}?exp=${exp}"
"#
),
),
@ -1636,7 +1636,7 @@ fn module_params_typecheck_fail() {
r#"
module { key } -> [url]
url = "example.com/$(key)"
url = "example.com/${key}"
"#
),
),
@ -1687,7 +1687,7 @@ fn module_params_missing_fields() {
r#"
module { key } -> [url]
url = "example.com/$(key)"
url = "example.com/${key}"
"#
),
),
@ -1740,7 +1740,7 @@ fn module_params_extra_fields() {
r#"
module { key } -> [url]
url = "example.com/$(key)"
url = "example.com/${key}"
"#
),
),
@ -1839,7 +1839,7 @@ fn module_params_missing() {
r#"
module { key, exp } -> [url]
url = "example.com/$(key)?exp=$(Num.to_str exp)"
url = "example.com/${key}?exp=${Num.to_str(exp)}"
"#
),
),
@ -2169,7 +2169,7 @@ fn roc_package_depends_on_other_package() {
r#"
module [say]
say = \msg -> "$(msg), world!"
say = \msg -> "${msg}, world!"
"#
),
),

View file

@ -75,7 +75,7 @@ pub enum CalledVia {
UnaryOp(UnaryOp),
/// This call is the result of desugaring string interpolation,
/// e.g. "$(first) $(last)" is transformed into Str.concat (Str.concat first " ") last.
/// e.g. "${first} ${last}" is transformed into `Str.concat(Str.concat(first, " "))` last.
StringInterpolation,
/// This call is the result of desugaring a map2-based Record Builder field. e.g.

View file

@ -425,16 +425,18 @@ pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EStri
}
}
}
b'(' if preceded_by_dollar && !is_single_quote => {
b'(' | b'{' if preceded_by_dollar && !is_single_quote => {
let old_style_interpolation_block = one_byte == b'(';
// We're about to begin string interpolation!
//
// End the previous segment so we can begin a new one.
// Retroactively end it right before the `$` char we parsed.
// (We can't use end_segment! here because it ends it right after
// the just-parsed character, which here would be '(' rather than '$')
// the just-parsed character, which here would be '{' rather than '$')
// Don't push anything if the string would be empty.
if segment_parsed_bytes > 2 {
// exclude the 2 chars we just parsed, namely '$' and '('
// exclude the 2 chars we just parsed, namely '$' and '{'
let string_bytes = &state.bytes()[0..(segment_parsed_bytes - 2)];
match std::str::from_utf8(string_bytes) {
@ -452,19 +454,27 @@ pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EStri
}
}
// Advance past the `$(`
// Advance past the `${`
state.advance_mut(2);
let original_byte_count = state.bytes().len();
// Parse an arbitrary expression, followed by ')'
// Parse an arbitrary expression, followed by '}' or ')'
let terminating_char = if old_style_interpolation_block {
b')'
} else {
b'}'
};
let (_progress, (mut loc_expr, sp), new_state) = and(
specialize_err_ref(
EString::Format,
loc(allocated(reset_min_indent(expr::expr_help())))
.trace("str_interpolation"),
),
skip_second(space0_e(EString::FormatEnd), byte(b')', EString::FormatEnd)),
skip_second(
space0_e(EString::FormatEnd),
byte(terminating_char, EString::FormatEnd),
),
)
.parse(arena, state, min_indent)?;
@ -488,8 +498,8 @@ pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EStri
}
}
// iff the '$' is followed by '(', this is string interpolation.
// We'll check for the '(' on the next iteration of the loop.
// iff the '$' is followed by '{', this is string interpolation.
// We'll check for the '{' on the next iteration of the loop.
preceded_by_dollar = one_byte == b'$';
}

View file

@ -160,17 +160,17 @@ mod test_parse {
#[test]
fn escaped_interpolation() {
assert_segments(r#""Hi, \$(name)!""#, |arena| {
assert_segments(r#""Hi, \${name}!""#, |arena| {
bumpalo::vec![in arena;
Plaintext("Hi, "),
EscapedChar(EscapedChar::Dollar),
Plaintext("(name)!"),
Plaintext("{name}!"),
]
});
}
#[test]
fn string_with_interpolation_in_middle() {
fn string_with_old_interpolation_still_works_for_now() {
assert_segments(r#""Hi, $(name)!""#, |arena| {
let expr = arena.alloc(Var {
module_name: "",
@ -185,9 +185,31 @@ mod test_parse {
});
}
#[test]
fn string_with_mixed_new_and_old_interpolation_braces_fails() {
assert_parsing_fails(r#""${foo)""#, SyntaxError::Unexpected(Region::zero()));
assert_parsing_fails(r#""$(foo}""#, SyntaxError::Unexpected(Region::zero()));
}
#[test]
fn string_with_interpolation_in_middle() {
assert_segments(r#""Hi, ${name}!""#, |arena| {
let expr = arena.alloc(Var {
module_name: "",
ident: "name",
});
bumpalo::vec![in arena;
Plaintext("Hi, "),
Interpolated(Loc::new(7, 11, expr)),
Plaintext("!")
]
});
}
#[test]
fn string_with_interpolation_in_front() {
assert_segments(r#""$(name), hi!""#, |arena| {
assert_segments(r#""${name}, hi!""#, |arena| {
let expr = arena.alloc(Var {
module_name: "",
ident: "name",
@ -232,7 +254,7 @@ mod test_parse {
#[test]
fn string_with_interpolation_in_back() {
assert_segments(r#""Hello $(name)""#, |arena| {
assert_segments(r#""Hello ${name}""#, |arena| {
let expr = arena.alloc(Var {
module_name: "",
ident: "name",
@ -247,7 +269,7 @@ mod test_parse {
#[test]
fn string_with_multiple_interpolations() {
assert_segments(r#""Hi, $(name)! How is $(project) going?""#, |arena| {
assert_segments(r#""Hi, ${name}! How is ${project} going?""#, |arena| {
let expr1 = arena.alloc(Var {
module_name: "",
ident: "name",
@ -271,7 +293,7 @@ mod test_parse {
#[test]
fn string_with_non_interpolation_dollar_signs() {
assert_segments(
r#""$a Hi, $(name)! $b How is $(project) going? $c""#,
r#""$a Hi, ${name}! $b How is ${project} going? $c""#,
|arena| {
let expr1 = arena.alloc(Var {
module_name: "",

View file

@ -324,7 +324,7 @@ mod solve_expr {
r#"
what_it_is = "great"
"type inference is $(what_it_is)!"
"type inference is ${what_it_is}!"
"#
),
"Str",
@ -338,7 +338,7 @@ mod solve_expr {
r#"
what_it_is = "great"
str = "type inference is $(what_it_is)!"
str = "type inference is ${what_it_is}!"
what_it_is
"#
@ -354,7 +354,7 @@ mod solve_expr {
r#"
rec = { what_it_is: "great" }
str = "type inference is $(rec.what_it_is)!"
str = "type inference is ${rec.what_it_is}!"
rec
"#
@ -4751,7 +4751,7 @@ mod solve_expr {
r#"
set_roc_email : _ -> { name: Str, email: Str }_
set_roc_email = \person ->
{ person & email: "$(person.name)@roclang.com" }
{ person & email: "${person.name}@roclang.com" }
set_roc_email
"#
),

View file

@ -330,7 +330,7 @@ fn list_map_try_ok() {
List.map_try [1, 2, 3] \num ->
str = Num.to_str (num * 2)
Ok "$(str)!"
Ok "${str}!"
"#,
// Result Str [] is unwrapped to just Str
RocList::<RocStr>::from_slice(&[
@ -3870,10 +3870,10 @@ fn issue_3571_lowlevel_call_function_with_bool_lambda_set() {
List.concat state mapped_vals
add2 : Str -> Str
add2 = \x -> "added $(x)"
add2 = \x -> "added ${x}"
mul2 : Str -> Str
mul2 = \x -> "multiplied $(x)"
mul2 = \x -> "multiplied ${x}"
foo = [add2, mul2]
bar = ["1", "2", "3", "4"]

View file

@ -3120,7 +3120,7 @@ fn recursively_build_effect() {
hi = "Hello"
name = "World"
"$(hi), $(name)!"
"${hi}, ${name}!"
main =
when nest_help 4 is
@ -3876,8 +3876,8 @@ fn compose_recursive_lambda_set_productive_toplevel() {
compose = \f, g -> \x -> g (f x)
identity = \x -> x
exclaim = \s -> "$(s)!"
whisper = \s -> "($(s))"
exclaim = \s -> "${s}!"
whisper = \s -> "(${s})"
main =
res: Str -> Str
@ -3899,8 +3899,8 @@ fn compose_recursive_lambda_set_productive_nested() {
compose = \f, g -> \x -> g (f x)
identity = \x -> x
exclaim = \s -> "$(s)!"
whisper = \s -> "($(s))"
exclaim = \s -> "${s}!"
whisper = \s -> "(${s})"
res: Str -> Str
res = List.walk [ exclaim, whisper ] identity compose
@ -3921,8 +3921,8 @@ fn compose_recursive_lambda_set_productive_inferred() {
compose = \f, g -> \x -> g (f x)
identity = \x -> x
exclaim = \s -> "$(s)!"
whisper = \s -> "($(s))"
exclaim = \s -> "${s}!"
whisper = \s -> "(${s})"
res = List.walk [ exclaim, whisper ] identity compose
res "hello"
@ -3947,8 +3947,8 @@ fn compose_recursive_lambda_set_productive_nullable_wrapped() {
else \x -> f (g x)
identity = \x -> x
exclame = \s -> "$(s)!"
whisper = \s -> "($(s))"
exclame = \s -> "${s}!"
whisper = \s -> "(${s})"
main =
res: Str -> Str
@ -4475,7 +4475,7 @@ fn reset_recursive_type_wraps_in_named_type() {
Cons x xs ->
str_x = f x
str_xs = print_linked_list xs f
"Cons $(str_x) ($(str_xs))"
"Cons ${str_x} (${str_xs})"
"#
),
RocStr::from("Cons 2 (Cons 3 (Cons 4 (Nil)))"),

View file

@ -37,7 +37,7 @@ fn early_return_nested_ifs() {
else
third
"$(first), $(second)"
"${first}, ${second}"
main : List Str
main = List.map [1, 2, 3] display_n
@ -76,7 +76,7 @@ fn early_return_nested_whens() {
_ ->
third
"$(first), $(second)"
"${first}, ${second}"
main : List Str
main = List.map [1, 2, 3] display_n

View file

@ -1759,7 +1759,7 @@ fn lambda_capture_niches_with_other_lambda_capture() {
when val is
_ -> ""
capture2 = \val -> \{} -> "$(val)"
capture2 = \val -> \{} -> "${val}"
x : [A, B, C]
x = A
@ -2072,7 +2072,7 @@ fn polymorphic_expression_unification() {
]
parse_function : Str -> RenderTree
parse_function = \name ->
last = Indent [Text ".trace(\"$(name)\")" ]
last = Indent [Text ".trace(\"${name}\")" ]
Indent [last]
values = parse_function "interface_header"
@ -2636,7 +2636,7 @@ fn recursively_build_effect() {
hi = "Hello"
name = "World"
"$(hi), $(name)!"
"${hi}, ${name}!"
main =
when nest_help 4 is
@ -2956,8 +2956,8 @@ fn compose_recursive_lambda_set_productive_nullable_wrapped() {
else \x -> f (g x)
identity = \x -> x
exclaim = \s -> "$(s)!"
whisper = \s -> "($(s))"
exclaim = \s -> "${s}!"
whisper = \s -> "(${s})"
main =
res: Str -> Str
@ -3291,7 +3291,7 @@ fn dbg_nested_expr() {
fn dbg_inside_string() {
indoc!(
r#"
"Hello $(dbg "world")!"
"Hello ${dbg "world"}!"
"#
)
}
@ -3690,7 +3690,7 @@ fn dec_refcount_for_usage_after_early_return_in_if() {
else
third
"$(first), $(second)"
"${first}, ${second}"
display_n 3
"#

View file

@ -1,2 +1,2 @@
"""$(g)""":q
"""${g}""":q
f

View file

@ -1,5 +1,5 @@
"""
"""
"$(i
"${i
"""
""")"
"""}"

View file

@ -1 +1 @@
"""""""$(i"""""")"
"""""""${i""""""}"

View file

@ -1,8 +1,8 @@
"""
$({
${{
}
i)
$({
i}
${{
}
i)
i}
"""

View file

@ -1,4 +1,4 @@
"""$({
}i)
$({
}i)"""
"""${{
}i}
${{
}i}"""

View file

@ -3,5 +3,5 @@ main =
|> List.dropFirst 1
|> List.mapTry? Str.toU8
|> List.sum
|> \total -> "Sum of numbers: $(Num.to_str total)"
|> \total -> "Sum of numbers: ${Num.to_str total}"
|> Str.toUpper

View file

@ -6575,13 +6575,13 @@ mod test_fmt {
expr_formats_to(
indoc!(
"
x = \"foo:\u{200B} $(bar).\"
x = \"foo:\u{200B} ${bar}.\"
x
"
),
indoc!(
r#"
x = "foo:\u(200b) $(bar)."
x = "foo:\u(200b) ${bar}."
x
"#
),
@ -6595,7 +6595,7 @@ mod test_fmt {
"
x =
\"\"\"
foo:\u{200B} $(bar).
foo:\u{200B} ${bar}.
\"\"\"
x
"
@ -6604,7 +6604,7 @@ mod test_fmt {
r#"
x =
"""
foo:\u(200b) $(bar).
foo:\u(200b) ${bar}.
"""
x
"#

View file

@ -1036,7 +1036,7 @@ mod test_snapshots {
#[test]
fn string_with_interpolation_in_middle() {
assert_segments(r#""Hi, $(name)!""#, |arena| {
assert_segments(r#""Hi, ${name}!""#, |arena| {
let expr = arena.alloc(Var {
module_name: "",
ident: "name",
@ -1052,7 +1052,7 @@ mod test_snapshots {
#[test]
fn string_with_interpolation_in_front() {
assert_segments(r#""$(name), hi!""#, |arena| {
assert_segments(r#""${name}, hi!""#, |arena| {
let expr = arena.alloc(Var {
module_name: "",
ident: "name",
@ -1067,7 +1067,7 @@ mod test_snapshots {
#[test]
fn string_with_interpolation_in_back() {
assert_segments(r#""Hello $(name)""#, |arena| {
assert_segments(r#""Hello ${name}""#, |arena| {
let expr = arena.alloc(Var {
module_name: "",
ident: "name",
@ -1082,7 +1082,7 @@ mod test_snapshots {
#[test]
fn string_with_multiple_interpolations() {
assert_segments(r#""Hi, $(name)! How is $(project) going?""#, |arena| {
assert_segments(r#""Hi, ${name}! How is ${project} going?""#, |arena| {
let expr1 = arena.alloc(Var {
module_name: "",
ident: "name",