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

@ -278,14 +278,14 @@ import pf.Stdin
main =
Stdout.line! "What's your name?"
name = Stdin.line!
Stdout.line! "Hi $(name)!""#;
Stdout.line! "Hi ${name}!""#;
const UNFORMATTED_ROC: &str = r#"app [main] { pf: platform "platform/main.roc" }
main =
Stdout.line! "What's your name?"
name = Stdin.line!
Stdout.line! "Hi $(name)!"
Stdout.line! "Hi ${name}!"
"#;
fn setup_test_file(dir: &Path, file_name: &str, contents: &str) -> PathBuf {

View file

@ -10,7 +10,7 @@ show = \list ->
|> List.map(Num.to_str)
|> Str.join_with(", ")
"[$(content)]"
"[${content}]"
sort_by : List a, (a -> Num *) -> List a
sort_by = \list, to_comparable ->

View file

@ -25,7 +25,7 @@ show_rb_tree = \tree, show_key, show_value ->
s_l = node_in_parens(left, show_key, show_value)
s_r = node_in_parens(right, show_key, show_value)
"Node $(s_color) $(s_key) $(s_value) $(s_l) $(s_r)"
"Node ${s_color} ${s_key} ${s_value} ${s_l} ${s_r}"
node_in_parens : RedBlackTree k v, (k -> Str), (v -> Str) -> Str
node_in_parens = \tree, show_key, show_value ->
@ -36,7 +36,7 @@ node_in_parens = \tree, show_key, show_value ->
Node(_, _, _, _, _) ->
inner = show_rb_tree(tree, show_key, show_value)
"($(inner))"
"(${inner})"
show_color : NodeColor -> Str
show_color = \color ->

View file

@ -8,7 +8,7 @@ snapshot_kind: text
The get_user function expects 1 argument, but it got 2 instead:
12│ $(Api.get_user(1, 2))
12│ ${Api.get_user(1, 2)}
^^^^^^^^^^^^
Are there any missing commas? Or missing parentheses?
@ -18,7 +18,7 @@ Are there any missing commas? Or missing parentheses?
This value is not a function, but it was given 1 argument:
13│ $(Api.base_url(1))
13│ ${Api.base_url(1)}
^^^^^^^^^^^^
Are there any missing commas? Or missing parentheses?
@ -28,7 +28,7 @@ Are there any missing commas? Or missing parentheses?
The get_post_comment function expects 2 arguments, but it got only 1:
16│ $(Api.get_post_comment(1))
16│ ${Api.get_post_comment(1)}
^^^^^^^^^^^^^^^^^^^^
Roc does not allow functions to be partially applied. Use a closure to

View file

@ -11,7 +11,7 @@ fn_annotated_as_value definition:
3│ fn_annotated_as_value : Str
4│> fn_annotated_as_value = \post_id, comment_id ->
5│> "/posts/$(post_id)/comments/$(Num.to_str(comment_id))"
5│> "/posts/${post_id}/comments/${Num.to_str(comment_id)}"
The body is an anonymous function of type:
@ -28,7 +28,7 @@ Something is off with the body of the missing_arg definition:
7│ missing_arg : Str -> Str
8│> missing_arg = \post_id, _ ->
9│> "/posts/$(post_id)/comments"
9│> "/posts/${post_id}/comments"
The body is an anonymous function of type:

View file

@ -8,7 +8,7 @@ snapshot_kind: text
This argument to this string interpolation has an unexpected type:
10│ "$(Api.get_post)"
10│ "${Api.get_post}"
^^^^^^^^^^^^
The argument is an anonymous function of type:

View file

@ -12,7 +12,7 @@ main =
_: Task.ok(Dict.single("a", "b")),
}!
Stdout.line!("For multiple tasks: $(Inspect.to_str(multiple_in))")
Stdout.line!("For multiple tasks: ${Inspect.to_str(multiple_in)}")
sequential : Task a err, Task b err, (a, b -> c) -> Task c err
sequential = \first_task, second_task, mapper ->

View file

@ -15,8 +15,8 @@ main! = \{} ->
validate! : U32 => Result {} U32
validate! = \x ->
if Num.is_even(x) then
Effect.put_line!("✅ $(Num.to_str(x))")
Effect.put_line!("✅ ${Num.to_str(x)}")
Ok({})
else
Effect.put_line!("$(Num.to_str(x)) is not even! ABORT!")
Effect.put_line!("${Num.to_str(x)} is not even! ABORT!")
Err(x)

View file

@ -7,7 +7,7 @@ main! = \{} ->
first = ask!("What's your first name?")
last = ask!("What's your last name?")
Effect.put_line!("\nHi, $(first) $(last)!\n")
Effect.put_line!("\nHi, ${first} ${last}!\n")
when Str.to_u8(ask!("How old are you?")) is
Err(InvalidNumStr) ->
@ -17,7 +17,7 @@ main! = \{} ->
Effect.put_line!("\nNice! You can vote!")
Ok(age) ->
Effect.put_line!("\nYou'll be able to vote in $(Num.to_str((18 - age))) years")
Effect.put_line!("\nYou'll be able to vote in ${Num.to_str(18 - age)} years")
Effect.put_line!("\nBye! 👋")

View file

@ -15,5 +15,5 @@ main! = \{} ->
else
{}
Effect.put_line!("You entered: $(line)")
Effect.put_line!("You entered: ${line}")
Effect.put_line!("It is known")

View file

@ -18,5 +18,5 @@ main! = \{} ->
get_line!: Effect.get_line!,
}
Effect.put_line!("not_effectful: $(not_effectful.get_line!({}))")
Effect.put_line!("effectful: $(effectful.get_line!({}))")
Effect.put_line!("not_effectful: ${not_effectful.get_line!({})}")
Effect.put_line!("effectful: ${effectful.get_line!({})}")

View file

@ -7,6 +7,6 @@ main! = \{} ->
logged!("hello", \{} -> Effect.put_line!("Hello, World!"))
logged! = \name, fx! ->
Effect.put_line!("Before $(name)")
Effect.put_line!("Before ${name}")
fx!({})
Effect.put_line!("After $(name)")
Effect.put_line!("After ${name}")

View file

@ -56,7 +56,7 @@ to_str = \{ scopes, stack, state, vars } ->
stack_str = Str.join_with(List.map(stack, to_str_data), " ")
vars_str = Str.join_with(List.map(vars, to_str_data), " ")
"\n============\nDepth: $(depth)\nState: $(state_str)\nStack: [$(stack_str)]\nVars: [$(vars_str)]\n============\n"
"\n============\nDepth: ${depth}\nState: ${state_str}\nStack: [${stack_str}]\nVars: [${vars_str}]\n============\n"
with! : Str, (Context => a) => a
with! = \path, callback! ->

View file

@ -21,7 +21,7 @@ main! = \filename ->
{}
Err(StringErr(e)) ->
Stdout.line!("Ran into problem:\n$(e)\n")
Stdout.line!("Ran into problem:\n${e}\n")
interpret_file! : Str => Result {} [StringErr Str]
interpret_file! = \filename ->
@ -44,7 +44,7 @@ interpret_file! = \filename ->
Err(StringErr("Ran into an invalid boolean that was neither false (0) or true (-1)"))
Err(InvalidChar(char)) ->
Err(StringErr("Ran into an invalid character with ascii code: $(char)"))
Err(StringErr("Ran into an invalid character with ascii code: ${char}"))
Err(MaxInputNumber) ->
Err(StringErr("Like the original false compiler, the max input number is 320,000"))

View file

@ -7,4 +7,4 @@ app [main] {
import json.JsonParser
import csv.Csv
main = "Hello, World! $(JsonParser.example) $(Csv.example)"
main = "Hello, World! ${JsonParser.example} ${Csv.example}"

View file

@ -7,4 +7,4 @@ app [main] {
import one.One
import two.Two
main = "$(One.example) | $(Two.example)"
main = "${One.example} | ${Two.example}"

View file

@ -2,4 +2,4 @@ module [example]
import two.Two
example = "[One imports Two: $(Two.example)]"
example = "[One imports Two: ${Two.example}]"

View file

@ -2,4 +2,4 @@ module [example]
import one.One
example = "[Zero imports One: $(One.example)]"
example = "[Zero imports One: ${One.example}]"

View file

@ -14,18 +14,18 @@ module { app_id, protocol } -> [
## value def referencing params
base_url : Str
base_url =
protocol("api.example.com/$(app_id)")
protocol("api.example.com/${app_id}")
## function def referencing params
get_user : U32 -> Str
get_user = \user_id ->
# purposefully not using baseUrl to test top-level fn referencing param
protocol("api.example.com/$(app_id)/users/$(Num.to_str(user_id))")
protocol("api.example.com/${app_id}/users/${Num.to_str(user_id)}")
## function def referencing top-level value
get_post : U32 -> Str
get_post = \post_id ->
"$(base_url)/posts/$(Num.to_str(post_id))"
"${base_url}/posts/${Num.to_str(post_id)}"
## function def passing top-level function
get_posts : List U32 -> List Str
@ -35,13 +35,13 @@ get_posts = \ids ->
## function def calling top-level function
get_post_comments : U32 -> Str
get_post_comments = \post_id ->
"$(get_post(post_id))/comments"
"${get_post(post_id)}/comments"
## function def passing nested function
get_companies : List U32 -> List Str
get_companies = \ids ->
get_company = \id ->
protocol("api.example.com/$(app_id)/companies/$(Num.to_str(id))")
protocol("api.example.com/${app_id}/companies/${Num.to_str(id)}")
List.map(ids, get_company)
@ -59,11 +59,11 @@ get_post_aliased =
get_user_safe : U32 -> Str
get_user_safe =
if Str.starts_with(app_id, "prod_") then
\id -> "$(get_user(id))?safe=true"
\id -> "${get_user(id)}?safe=true"
else
get_user
## two-argument function
get_post_comment : U32, U32 -> Str
get_post_comment = \post_id, comment_id ->
"$(get_post(post_id))/comments/$(Num.to_str(comment_id))"
"${get_post(post_id)}/comments/${Num.to_str(comment_id)}"

View file

@ -2,8 +2,8 @@ module { app_id } -> [fn_annotated_as_value, missing_arg]
fn_annotated_as_value : Str
fn_annotated_as_value = \post_id, comment_id ->
"/posts/$(post_id)/comments/$(Num.to_str(comment_id))"
"/posts/${post_id}/comments/${Num.to_str(comment_id)}"
missing_arg : Str -> Str
missing_arg = \post_id, _ ->
"/posts/$(post_id)/comments"
"/posts/${post_id}/comments"

View file

@ -1,6 +1,6 @@
module []
https = \url -> "https://$(url)"
https = \url -> "https://${url}"
expect
import Api { app_id: "one", protocol: https }

View file

@ -4,4 +4,4 @@ menu = \name ->
indirect(name)
indirect = \name ->
echo("Hi, $(name)!")
echo("Hi, ${name}!")

View file

@ -6,8 +6,8 @@ import Api { app_id: "one", protocol: https } as App1
import Api { app_id: "two", protocol: http } as App2
import Api { app_id: "prod_1", protocol: http } as Prod
https = \url -> "https://$(url)"
http = \url -> "http://$(url)"
https = \url -> "https://${url}"
http = \url -> "http://${url}"
users_app1 =
# pass top-level fn in a module with params
@ -27,33 +27,33 @@ main =
List.map([1, 2, 3], App3.get_user)
"""
App1.baseUrl: $(App1.base_url)
App2.baseUrl: $(App2.base_url)
App3.baseUrl: $(App3.base_url)
App1.getUser 1: $(App1.get_user(1))
App2.getUser 2: $(App2.get_user(2))
App3.getUser 3: $(App3.get_user(3))
App1.getPost 1: $(App1.get_post(1))
App2.getPost 2: $(App2.get_post(2))
App3.getPost 3: $(App3.get_post(3))
App1.getPosts [1, 2]: $(Inspect.to_str(App1.get_posts([1, 2])))
App2.getPosts [3, 4]: $(Inspect.to_str(App2.get_posts([3, 4])))
App2.getPosts [5, 6]: $(Inspect.to_str(App2.get_posts([5, 6])))
App1.getPostComments 1: $(App1.get_post_comments(1))
App2.getPostComments 2: $(App2.get_post_comments(2))
App2.getPostComments 3: $(App2.get_post_comments(3))
App1.getCompanies [1, 2]: $(Inspect.to_str(App1.get_companies([1, 2])))
App2.getCompanies [3, 4]: $(Inspect.to_str(App2.get_companies([3, 4])))
App2.getCompanies [5, 6]: $(Inspect.to_str(App2.get_companies([5, 6])))
App1.getPostAliased 1: $(App1.get_post_aliased(1))
App2.getPostAliased 2: $(App2.get_post_aliased(2))
App3.getPostAliased 3: $(App3.get_post_aliased(3))
App1.baseUrlAliased: $(App1.base_url_aliased)
App2.baseUrlAliased: $(App2.base_url_aliased)
App3.baseUrlAliased: $(App3.base_url_aliased)
App1.getUserSafe 1: $(App1.get_user_safe(1))
Prod.getUserSafe 2: $(Prod.get_user_safe(2))
usersApp1: $(Inspect.to_str(users_app1))
getUserApp3Nested 3: $(get_user_app3_nested(3))
usersApp3Passed: $(Inspect.to_str(users_app3_passed))
App1.baseUrl: ${App1.base_url}
App2.baseUrl: ${App2.base_url}
App3.baseUrl: ${App3.base_url}
App1.getUser 1: ${App1.get_user(1)}
App2.getUser 2: ${App2.get_user(2)}
App3.getUser 3: ${App3.get_user(3)}
App1.getPost 1: ${App1.get_post(1)}
App2.getPost 2: ${App2.get_post(2)}
App3.getPost 3: ${App3.get_post(3)}
App1.getPosts [1, 2]: ${Inspect.to_str(App1.get_posts([1, 2]))}
App2.getPosts [3, 4]: ${Inspect.to_str(App2.get_posts([3, 4]))}
App2.getPosts [5, 6]: ${Inspect.to_str(App2.get_posts([5, 6]))}
App1.getPostComments 1: ${App1.get_post_comments(1)}
App2.getPostComments 2: ${App2.get_post_comments(2)}
App2.getPostComments 3: ${App2.get_post_comments(3)}
App1.getCompanies [1, 2]: ${Inspect.to_str(App1.get_companies([1, 2]))}
App2.getCompanies [3, 4]: ${Inspect.to_str(App2.get_companies([3, 4]))}
App2.getCompanies [5, 6]: ${Inspect.to_str(App2.get_companies([5, 6]))}
App1.getPostAliased 1: ${App1.get_post_aliased(1)}
App2.getPostAliased 2: ${App2.get_post_aliased(2)}
App3.getPostAliased 3: ${App3.get_post_aliased(3)}
App1.baseUrlAliased: ${App1.base_url_aliased}
App2.baseUrlAliased: ${App2.base_url_aliased}
App3.baseUrlAliased: ${App3.base_url_aliased}
App1.getUserSafe 1: ${App1.get_user_safe(1)}
Prod.getUserSafe 2: ${Prod.get_user_safe(2)}
usersApp1: ${Inspect.to_str(users_app1)}
getUserApp3Nested 3: ${get_user_app3_nested(3)}
usersApp3Passed: ${Inspect.to_str(users_app3_passed)}
"""

View file

@ -4,14 +4,14 @@ app [main] {
import Api { app_id: "one", protocol: https }
https = \url -> "https://$(url)"
https = \url -> "https://${url}"
main =
"""
# too many args
$(Api.get_user(1, 2))
$(Api.base_url(1))
${Api.get_user(1, 2)}
${Api.base_url(1)}
# too few args
$(Api.get_post_comment(1))
${Api.get_post_comment(1)}
"""

View file

@ -1,3 +1,3 @@
module { stdout! } -> [log!]
log! = \msg, level -> stdout!("$(level):$(msg)")
log! = \msg, level -> stdout!("${level}:${msg}")

View file

@ -4,7 +4,7 @@ app [main] {
import Api { app_id: "one", protocol: https }
https = \url -> "https://$(url)"
https = \url -> "https://${url}"
main =
"$(Api.get_post)"
"${Api.get_post}"

View file

@ -11,4 +11,4 @@ import foo.Foo
main_for_host : Str
main_for_host =
"$(main) $(Foo.foo)"
"${main} ${Foo.foo}"

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",

View file

@ -48,7 +48,7 @@ shape = \@Types(types), id ->
Err(OutOfBounds) ->
id_str = Num.to_str(type_id_to_u64(id))
crash("TypeId #$(id_str) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>")
crash("TypeId #${id_str} was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>")
alignment : Types, TypeId -> U32
alignment = \@Types(types), id ->
@ -57,7 +57,7 @@ alignment = \@Types(types), id ->
Err(OutOfBounds) ->
id_str = Num.to_str(type_id_to_u64(id))
crash("TypeId #$(id_str) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>")
crash("TypeId #${id_str} was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>")
size : Types, TypeId -> U32
size = \@Types(types), id ->
@ -66,4 +66,4 @@ size = \@Types(types), id ->
Err(OutOfBounds) ->
id_str = Num.to_str(type_id_to_u64(id))
crash("TypeId #$(id_str) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>")
crash("TypeId #${id_str} was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>")

File diff suppressed because it is too large Load diff

View file

@ -755,14 +755,14 @@ fn type_problem_unary_operator() {
#[test]
fn type_problem_string_interpolation() {
expect_failure(
"\"This is not a string -> $(1)\"",
"\"This is not a string -> ${1}\"",
indoc!(
r#"
TYPE MISMATCH
This argument to this string interpolation has an unexpected type:
4 "This is not a string -> $(1)"
4 "This is not a string -> ${1}"
^
The argument is a number of type:
@ -833,14 +833,14 @@ fn list_get_negative_index() {
#[test]
fn invalid_string_interpolation() {
expect_failure(
"\"$(123)\"",
"\"${123}\"",
indoc!(
r#"
TYPE MISMATCH
This argument to this string interpolation has an unexpected type:
4 "$(123)"
4 "${123}"
^^^
The argument is a number of type:
@ -1537,7 +1537,7 @@ fn interpolation_with_nested_strings() {
expect_success(
indoc!(
r#"
"foo $(Str.join_with ["a", "b", "c"] ", ") bar"
"foo ${Str.join_with ["a", "b", "c"] ", "} bar"
"#
),
r#""foo a, b, c bar" : Str"#,
@ -1549,7 +1549,7 @@ fn interpolation_with_num_to_str() {
expect_success(
indoc!(
r#"
"foo $(Num.to_str Num.max_i8) bar"
"foo ${Num.to_str Num.max_i8} bar"
"#
),
r#""foo 127 bar" : Str"#,
@ -1561,7 +1561,7 @@ fn interpolation_with_operator_desugaring() {
expect_success(
indoc!(
r#"
"foo $(Num.to_str (1 + 2)) bar"
"foo ${Num.to_str (1 + 2)} bar"
"#
),
r#""foo 3 bar" : Str"#,
@ -1576,7 +1576,7 @@ fn interpolation_with_nested_interpolation() {
expect_failure(
indoc!(
r#"
"foo $(Str.join_with ["a$(Num.to_str 5)", "b"] "c")"
"foo ${Str.join_with ["a${Num.to_str 5}", "b"] "c"}"
"#
),
indoc!(
@ -1585,7 +1585,7 @@ fn interpolation_with_nested_interpolation() {
This string interpolation is invalid:
4 "foo $(Str.join_with ["a$(Num.to_str 5)", "b"] "c")"
4 "foo ${Str.join_with ["a${Num.to_str 5}", "b"] "c"}"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
String interpolations cannot contain newlines or other interpolations.

View file

@ -1098,7 +1098,7 @@ fn to_str_report<'a>(
alloc.region_with_subregion(lines.convert_region(surroundings), region, severity),
alloc.concat([
alloc.reflow(r"You could change it to something like "),
alloc.parser_suggestion("\"The count is $(count)\""),
alloc.parser_suggestion("\"The count is ${count}\""),
alloc.reflow("."),
]),
]);
@ -1176,7 +1176,7 @@ fn to_str_report<'a>(
alloc.stack([
alloc.concat([
alloc.reflow("I am part way through parsing this single-quote literal, "),
alloc.reflow("but I encountered a string interpolation like \"$(this)\","),
alloc.reflow("but I encountered a string interpolation like \"${this}\","),
alloc.reflow("which is not allowed in single-quote literals."),
]),
alloc.region_with_subregion(lines.convert_region(surroundings), region, severity),

View file

@ -341,7 +341,7 @@ fn joinpoint_with_closure() {
cat_sound = make_sound Cat
dog_sound = make_sound Dog
goose_sound = make_sound Goose
"Cat: $(cat_sound), Dog: $(dog_sound), Goose: $(goose_sound)"
"Cat: ${cat_sound}, Dog: ${dog_sound}, Goose: ${goose_sound}"
test
)
@ -370,7 +370,7 @@ fn joinpoint_with_reuse() {
Cons x xs ->
str_x = f x
str_xs = print_linked_list xs f
"Cons $(str_x) ($(str_xs))"
"Cons ${str_x} (${str_xs})"
test =
new_list = map_linked_list (Cons 1 (Cons 2 (Cons 3 Nil))) (\x -> x + 1)
@ -457,7 +457,7 @@ fn tree_rebalance() {
s_l = node_in_parens left show_key show_value
s_r = node_in_parens right show_key show_value
"Node $(s_color) $(s_key) $(s_value) $(s_l) $(s_r)"
"Node ${s_color} ${s_key} ${s_value} ${s_l} ${s_r}"
node_in_parens : RedBlackTree k v, (k -> Str), (v -> Str) -> Str
node_in_parens = \tree, show_key, show_value ->
@ -468,7 +468,7 @@ fn tree_rebalance() {
Node _ _ _ _ _ ->
inner = show_rb_tree tree show_key show_value
"($(inner))"
"(${inner})"
show_color : NodeColor -> Str
show_color = \color ->
@ -516,7 +516,7 @@ fn joinpoint_nullpointer() {
Nil -> "Nil"
Cons x xs ->
str_xs = print_linked_list xs
"Cons $(x) ($(str_xs))"
"Cons ${x} (${str_xs})"
linked_list_head : LinkedList Str -> LinkedList Str
linked_list_head = \linked_list ->
@ -528,7 +528,7 @@ fn joinpoint_nullpointer() {
test =
cons = print_linked_list (linked_list_head (Cons "foo" Nil))
nil = print_linked_list (linked_list_head (Nil))
"$(cons) - $(nil)"
"${cons} - ${nil}"
test
)

View file

@ -37,7 +37,7 @@ view =
Newline,
Desc([Ident("user"), Kw("="), Ident("Http.get!"), Ident("url"), Ident("Json.utf8")], "<p>This fetches the contents of the URL and decodes them as <a href=\"https://www.json.org\">JSON</a>.</p><p>If the shape of the JSON isn't compatible with the type of <code>user</code> (based on type inference), this will give a decoding error immediately.</p><p>As with all the other function calls involving the <code>!</code> operator, if there's an error, nothing else in <code>storeEmail</code> will be run, and <code>handleErr</code> will run.</p>"),
Newline,
Desc([Ident("dest"), Kw("="), Ident("Path.fromStr"), StrInterpolation("\"", "user.name", ".txt\"")], "<p>The <code>\$(user.name)</code> in this string literal will be replaced with the value stored in the <code>user</code> record's <code>name</code> field. This is <a href=\"/tutorial#string-interpolation\">string interpolation</a>.</p><p>Note that this function call doesn't involve the <code>!</code> operator. That's because <code>Path.fromStr</code> doesn't involve any Tasks, so there's no need to use <code>!</code> to wait for it to finish.</p>"),
Desc([Ident("dest"), Kw("="), Ident("Path.fromStr"), StrInterpolation("\"", "user.name", ".txt\"")], "<p>The <code>\${user.name}</code> in this string literal will be replaced with the value stored in the <code>user</code> record's <code>name</code> field. This is <a href=\"/tutorial#string-interpolation\">string interpolation</a>.</p><p>Note that this function call doesn't involve the <code>!</code> operator. That's because <code>Path.fromStr</code> doesn't involve any Tasks, so there's no need to use <code>!</code> to wait for it to finish.</p>"),
Newline,
Desc([Ident("File.writeUtf8!"), Ident("dest"), Ident("user.email")], "<p>This writes <code>user.email</code> to the file, encoded as <a href=\"https://en.wikipedia.org/wiki/UTF-8\">UTF-8</a>.</p><p>Since <code>File.writeUtf8</code> doesn't produce any information on success, we don't bother using <code>=</code> like we did on the other lines.</p>"),
Newline,
@ -90,7 +90,7 @@ tokens_to_str = \tokens ->
# Don't put spaces after opening parens or before closing parens
args_with_commas =
args
|> List.map(\ident -> "<span class=\"ident\">$(ident)</span>")
|> List.map(\ident -> "<span class=\"ident\">${ident}</span>")
|> Str.join_with("<span class=\"literal\">,</span> ")
buf_with_space
@ -99,22 +99,22 @@ tokens_to_str = \tokens ->
|> Str.concat("<span class=\"kw\"> -></span>")
Kw(str) ->
Str.concat(buf_with_space, "<span class=\"kw\">$(str)</span>")
Str.concat(buf_with_space, "<span class=\"kw\">${str}</span>")
Num(str) | Str(str) | Literal(str) -> # We may render these differently in the future
Str.concat(buf_with_space, "<span class=\"literal\">$(str)</span>")
Str.concat(buf_with_space, "<span class=\"literal\">${str}</span>")
Comment(str) ->
Str.concat(buf_with_space, "<span class=\"comment\"># $(str)</span>")
Str.concat(buf_with_space, "<span class=\"comment\"># ${str}</span>")
Ident(str) ->
Str.concat(buf_with_space, ident_to_html(str))
StrInterpolation(before, interp, after) ->
buf_with_space
|> Str.concat((if Str.is_empty(before) then "" else "<span class=\"literal\">$(before)</span>"))
|> Str.concat("<span class=\"kw\">\$(</span>$(ident_to_html(interp))<span class=\"kw\">)</span>")
|> Str.concat((if Str.is_empty(after) then "" else "<span class=\"literal\">$(after)</span>")))
|> Str.concat((if Str.is_empty(before) then "" else "<span class=\"literal\">${before}</span>"))
|> Str.concat("<span class=\"kw\">\${</span>${ident_to_html(interp)}<span class=\"kw\">}</span>")
|> Str.concat((if Str.is_empty(after) then "" else "<span class=\"literal\">${after}</span>")))
ident_to_html : Str -> Str
ident_to_html = \str ->
@ -122,18 +122,18 @@ ident_to_html = \str ->
len = Str.count_utf8_bytes(ident)
without_suffix = ident |> Str.replace_last("!", "")
ident_html = "<span class=\"ident\">$(without_suffix)</span>"
ident_html = "<span class=\"ident\">${without_suffix}</span>"
html =
# If removing a trailing "!" changed the length, then there must have been a trailing "!"
if len > Str.count_utf8_bytes(without_suffix) then
"$(ident_html)<span class=\"kw\">!</span>"
"${ident_html}<span class=\"kw\">!</span>"
else
ident_html
if Str.is_empty(accum) then
html
else
"$(accum)<span class=\"kw\">.</span>$(html)")
"${accum}<span class=\"kw\">.</span>${html}")
sections_to_str : List Section -> Str
sections_to_str = \sections ->
@ -178,5 +178,5 @@ radio = \index, label_html, desc_html ->
checked_html = if index == 0 then " checked" else ""
"""
<input class="interactive-radio" type="radio" name="r" id="r$(Num.to_str(index))" $(checked_html)><label for="r$(Num.to_str(index))" title="Tap to learn about this syntax">$(label_html)</label><span class="interactive-desc" role="presentation"><button class="close-desc">X</button>$(desc_html)</span>
<input class="interactive-radio" type="radio" name="r" id="r${Num.to_str(index)}" ${checked_html}><label for="r${Num.to_str(index)}" title="Tap to learn about this syntax">${label_html}</label><span class="interactive-desc" role="presentation"><button class="close-desc">X</button>${desc_html}</span>
"""

View file

@ -9,7 +9,7 @@ Roc's syntax isn't trivial, but there also isn't much of it to learn. It's desig
- `user.email` always accesses the `email` field of a record named `user`. <span class="nowrap">(Roc has</span> no inheritance, subclassing, or proxying.)
- `Email.isValid` always refers to something named `isValid` exported by a module named `Email`. (Module names are always capitalized, and variables/constants never are.) Modules are always defined statically and can't be modified at runtime; there's no [monkey patching](https://en.wikipedia.org/wiki/Monkey_patch) to consider either.
- `x = doSomething y z` always declares a new constant `x` (Roc has [no mutable variables, reassignment, or shadowing](/functional)) to be whatever the `doSomething` function returns when passed the arguments `y` and `z`. (Function calls in Roc don't need parentheses or commas.)
- `"Name: $(Str.trim name)"` uses *string interpolation* syntax: a dollar sign inside a string literal, followed by an expression in parentheses.
- `"Name: ${Str.trim(name)}"` uses *string interpolation* syntax: a dollar sign inside a string literal, followed by an expression in parentheses.
Roc also ships with a source code formatter that helps you maintain a consistent style with little effort. The `roc format` command neatly formats your source code according to a common style, and it's designed with the time-saving feature of having no configuration options. This feature saves teams all the time they would otherwise spend debating which stylistic tweaks to settle on!

View file

@ -65,7 +65,7 @@ A benefit of this design is that it makes Roc code easier to rearrange without c
<pre><samp class="code-snippet">func <span class="kw">=</span> <span class="kw">\</span>arg <span class="kw">-&gt;</span>
greeting <span class="kw">=</span> <span class="string">"Hello"</span>
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-&gt;</span> <span class="string">"</span><span class="kw">$(</span>greeting<span class="kw">)</span><span class="string">, </span><span class="kw">$(</span>name<span class="kw">)</span><span class="string">!"</span>
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-&gt;</span> <span class="string">"</span><span class="kw">${</span>greeting<span class="kw">}</span><span class="string">, </span><span class="kw">${</span>name<span class="kw">}</span><span class="string">!"</span>
<span class="comment"># …</span>
@ -82,7 +82,7 @@ Suppose I decide to extract the `welcome` function to the top level, so I can re
<span class="comment"># …</span>
welcome <span class="kw">=</span> <span class="kw">\</span>prefix<span class="punctuation section">,</span> name <span class="kw">-&gt;</span> <span class="string">"</span><span class="kw">$(</span>prefix<span class="kw">)</span><span class="string">, </span><span class="kw">$(</span>name<span class="kw">)</span><span class="string">!"</span></samp></pre>
welcome <span class="kw">=</span> <span class="kw">\</span>prefix<span class="punctuation section">,</span> name <span class="kw">-&gt;</span> <span class="string">"</span><span class="kw">${</span>prefix<span class="kw">}</span><span class="string">, </span><span class="kw">${</span>name<span class="kw">}</span><span class="string">!"</span></samp></pre>
Even without knowing the rest of `func`, we can be confident this change will not alter the code's behavior.
@ -90,7 +90,7 @@ In contrast, suppose Roc allowed reassignment. Then it's possible something in t
<pre><samp class="code-snippet">func <span class="kw">=</span> <span class="kw">\</span>arg <span class="kw">-&gt;</span>
greeting <span class="kw">=</span> <span class="string">"Hello"</span>
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-&gt;</span> <span class="string">"</span><span class="kw">$(</span>greeting<span class="kw">)</span><span class="string">, </span><span class="kw">$(</span>name<span class="kw">)</span><span class="string">!"</span>
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-&gt;</span> <span class="string">"</span><span class="kw">${</span>greeting<span class="kw">}</span><span class="string">, </span><span class="kw">${</span>name<span class="kw">}</span><span class="string">!"</span>
<span class="comment"># …</span>

View file

@ -6,7 +6,7 @@
<p id="homepage-tagline">A fast, friendly, functional language.</p>
<pre id="first-code-sample"><samp class="code-snippet">credits <span class="kw">=</span> List<span class="punctuation section">.</span>map songs <span class="kw">\</span>song <span class="kw">-></span>
<span class="string">"Performed by </span><span class="kw">$(</span>song<span class="punctuation section">.</span>artist<span class="kw">)</span><span class="string">"</span></samp></pre>
<span class="string">"Performed by </span><span class="kw">${</span>song<span class="punctuation section">.</span>artist<span class="kw">}</span><span class="string">"</span></samp></pre>
</div>
</div>

View file

@ -109,16 +109,16 @@ Note that in Roc, we don't need parentheses or commas to call functions. We don'
That said, just like in the arithmetic example above, we can use parentheses to specify how nested function calls should work. For example, we could write this:
<pre><samp><span class="repl-prompt">Str.concat "Birds: " (Num.toStr 42)</span>
<pre><samp><span class="repl-prompt">Str.concat "Birds: " (Num.to_str 42)</span>
<span class="literal">"Birds: 42"</span> <span class="colon">:</span> Str
</samp></pre>
This calls `Num.toStr` on the number `42`, which converts it into the string `"42"`, and then passes that string as the second argument to `Str.concat`.
This calls `Num.to_str` on the number `42`, which converts it into the string `"42"`, and then passes that string as the second argument to `Str.concat`.
The parentheses are important here to specify how the function calls nest. Try removing them, and see what happens:
<pre><samp><span class="repl-prompt">Str.concat "Birds: " Num.toStr 42</span>
<pre><samp><span class="repl-prompt">Str.concat "Birds: " Num.to_str 42</span>
<span class="repl-err">&lt;error&gt;</span>
</samp></pre>
@ -126,12 +126,12 @@ The parentheses are important here to specify how the function calls nest. Try r
The error tells us that we've given `Str.concat` too many arguments. Indeed we have! We've passed it three arguments:
1. The string `"Birds"`
2. The function `Num.toStr`
2. The function `Num.to_str`
3. The number `42`
That's not what we intended to do. Putting parentheses around the `Num.toStr 42` call clarifies that we want it to be evaluated as its own expression, rather than being two arguments to `Str.concat`.
That's not what we intended to do. Putting parentheses around the `Num.to_str 42` call clarifies that we want it to be evaluated as its own expression, rather than being two arguments to `Str.concat`.
Both the `Str.concat` function and the `Num.toStr` function have a dot in their names. In `Str.concat`, `Str` is the name of a _module_, and `concat` is the name of a function inside that module. Similarly, `Num` is a module, and `toStr` is a function inside that module.
Both the `Str.concat` function and the `Num.to_str` function have a dot in their names. In `Str.concat`, `Str` is the name of a _module_, and `concat` is the name of a function inside that module. Similarly, `Num` is a module, and `to_str` is a function inside that module.
We'll get into more depth about modules later, but for now you can think of a module as a named collection of functions. Eventually we'll discuss how to use them for more than that.
@ -139,7 +139,7 @@ We'll get into more depth about modules later, but for now you can think of a mo
An alternative syntax for `Str.concat` is _string interpolation_, which looks like this:
<pre><samp class="repl-prompt"><span class="literal">"<span class="str-esc">$(</span><span class="str-interp">greeting</span><span class="str-esc">)</span> there, <span class="str-esc">$(</span><span class="str-interp">audience</span><span class="str-esc">)</span>."</span></samp></pre>
<pre><samp class="repl-prompt"><span class="literal">"<span class="str-esc">${</span><span class="str-interp">greeting</span><span class="str-esc">}</span> there, <span class="str-esc">${</span><span class="str-interp">audience</span><span class="str-esc">}</span>."</span></samp></pre>
This is syntax sugar for calling `Str.concat` several times, like so:
@ -149,7 +149,7 @@ Str.concat greeting (Str.concat " there, " (Str.concat audience "."))
You can put entire single-line expressions inside the parentheses in string interpolation. For example:
<pre><samp class="repl-prompt"><span class="literal">"Two plus three is: <span class="str-esc">$(</span><span class="str-interp">Num.toStr (2 + 3)</span><span class="str-esc">)</span>"</span></samp></pre>
<pre><samp class="repl-prompt"><span class="literal">"Two plus three is: <span class="str-esc">${</span><span class="str-interp">Num.to_str(2 + 3)</span><span class="str-esc">}</span>"</span></samp></pre>
By the way, there are many other ways to put strings together! Check out the [documentation](https://www.roc-lang.org/builtins/Str) for the `Str` module for more.
@ -187,10 +187,10 @@ birds = 3
iguanas = 2
total = Num.toStr (birds + iguanas)
total = Num.to_str (birds + iguanas)
main! = \_args ->
Stdout.line! "There are $(total) animals."
Stdout.line! "There are ${total} animals."
```
Now run `roc main.roc` again. This time the "Downloading ..." message won't appear; the file has been cached from last time, and won't need to be downloaded again.
@ -204,9 +204,9 @@ You should see this:
A definition names an expression.
- The first two defs assign the names `birds` and `iguanas` to the expressions `3` and `2`.
- The next def assigns the name `total` to the expression `Num.toStr (birds + iguanas)`.
- The next def assigns the name `total` to the expression `Num.to_str (birds + iguanas)`.
Once we have a def, we can use its name in other expressions. For example, the `total` expression refers to `birds` and `iguanas`, and `Stdout.line! "There are $(total) animals."` refers to `total`.
Once we have a def, we can use its name in other expressions. For example, the `total` expression refers to `birds` and `iguanas`, and `Stdout.line! "There are ${total} animals."` refers to `total`.
You can name a def using any combination of letters and numbers, but they have to start with a lowercase letter.
@ -219,7 +219,7 @@ birds = 2
### [Defining Functions](#defining-functions) {#defining-functions}
So far we've called functions like `Num.toStr`, `Str.concat`, and `Stdout.line`. Next let's try defining a function of our own.
So far we've called functions like `Num.to_str`, `Str.concat`, and `Stdout.line`. Next let's try defining a function of our own.
```roc
birds = 3
@ -229,13 +229,13 @@ iguanas = 2
total = add_and_stringify birds iguanas
main! = \_args ->
Stdout.line! "There are $(total) animals."
Stdout.line! "There are ${total} animals."
add_and_stringify = \num1, num2 ->
Num.toStr (num1 + num2)
Num.to_str (num1 + num2)
```
This new `add_and_stringify` function we've defined accepts two numbers, adds them, calls `Num.toStr` on the result, and returns that.
This new `add_and_stringify` function we've defined accepts two numbers, adds them, calls `Num.to_str` on the result, and returns that.
The `\num1, num2 ->` syntax defines a function's arguments, and the expression after the `->` is the body of the function. Whenever a function gets called, its body expression gets evaluated and returned.
@ -250,13 +250,13 @@ add_and_stringify = \num1, num2 ->
if sum == 0 then
""
else
Num.toStr sum
Num.to_str sum
```
We did two things here:
- We introduced a _local def_ named `sum`, and set it equal to `num1 + num2`. Because we defined `sum` inside `add_and_stringify`, it's _local_ to that scope and can't be accessed outside that function.
- We added an `if`\-`then`\-`else` conditional to return either `""` or `Num.toStr sum` depending on whether `sum == 0`.
- We added an `if`\-`then`\-`else` conditional to return either `""` or `Num.to_str sum` depending on whether `sum == 0`.
Every `if` must be accompanied by both `then` and also `else`. Having an `if` without an `else` is an error, because `if` is an expression, and all expressions must evaluate to a value. If there were ever an `if` without an `else`, that would be an expression that might not evaluate to a value!
@ -273,7 +273,7 @@ add_and_stringify = \num1, num2 ->
else if sum < 0 then
"negative"
else
Num.toStr sum
Num.to_str sum
```
Note that `else if` is not a separate language keyword! It's just an `if`/`else` where the `else` branch contains another `if`/`else`. This is easier to see with different indentation:
@ -288,7 +288,7 @@ add_and_stringify = \num1, num2 ->
if sum < 0 then
"negative"
else
Num.toStr sum
Num.to_str sum
```
This differently-indented version is equivalent to writing `else if sum < 0 then` on the same line, although the convention is to use the original version's style.
@ -325,7 +325,7 @@ Currently our `add_and_stringify` function takes two arguments. We can instead m
total = add_and_stringify { birds: 5, iguanas: 7 }
add_and_stringify = \counts ->
Num.toStr (counts.birds + counts.iguanas)
Num.to_str (counts.birds + counts.iguanas)
```
The function now takes a _record_, which is a group of named values. Records are not [objects](<https://en.wikipedia.org/wiki/Object_(computer_science)>); they don't have methods or inheritance, they just store information.
@ -349,7 +349,7 @@ total = add_and_stringify { birds: 5, iguanas: 7 }
total_with_note = add_and_stringify { birds: 4, iguanas: 3, note: "Whee!" }
add_and_stringify = \counts ->
Num.toStr (counts.birds + counts.iguanas)
Num.to_str (counts.birds + counts.iguanas)
```
This works because `add_and_stringify` only uses `counts.birds` and `counts.iguanas`. If we were to use `counts.note` inside `add_and_stringify`, then we would get an error because `total` is calling `add_and_stringify` passing a record that doesn't have a `note` field.
@ -383,14 +383,14 @@ We can use _destructuring_ to avoid naming a record in a function argument, inst
```roc
add_and_stringify = \{ birds, iguanas } ->
Num.toStr (birds + iguanas)
Num.to_str (birds + iguanas)
```
Here, we've _destructured_ the record to create a `birds` def that's assigned to its `birds` field, and an `iguanas` def that's assigned to its `iguanas` field. We can customize this if we like:
```roc
add_and_stringify = \{ birds, iguanas: lizards } ->
Num.toStr (birds + lizards)
Num.to_str (birds + lizards)
```
In this version, we created a `lizards` def that's assigned to the record's `iguanas` field. (We could also do something similar with the `birds` field if we like.)
@ -810,7 +810,7 @@ Here's how calling `List.get` can look in practice:
```roc
when List.get ["a", "b", "c"] index is
Ok str -> "I got this string: $(str)"
Ok str -> "I got this string: ${str}"
Err OutOfBounds -> "That index was out of bounds, sorry!"
```
@ -1017,7 +1017,7 @@ Sometimes you may want to document the type of a definition. For example, you mi
```roc
# Takes a first_name string and a last_name string, and returns a string
full_name = \first_name, last_name ->
"$(first_name) $(last_name)"
"${first_name} ${last_name}"
```
Comments can be valuable documentation, but they can also get out of date and become misleading. If someone changes this function and forgets to update the comment, it will no longer be accurate.
@ -1029,7 +1029,7 @@ Here's another way to document this function's type, which doesn't have that pro
```roc
full_name : Str, Str -> Str
full_name = \first_name, last_name ->
"$(first_name) $(last_name)"
"${first_name} ${last_name}"
```
The `full_name :` line is a _type annotation_. It's a strictly optional piece of metadata we can add above a def to describe its type. Unlike a comment, the Roc compiler will check type annotations for accuracy. If the annotation ever doesn't fit with the implementation, we'll get a compile-time error.
@ -1410,12 +1410,12 @@ You can write automated tests for your Roc code like so:
```roc
pluralize = \singular, plural, count ->
count_str = Num.toStr count
count_str = Num.to_str count
if count == 1 then
"$(count_str) $(singular)"
"${count_str} ${singular}"
else
"$(count_str) $(plural)"
"${count_str} ${plural}"
expect pluralize "cactus" "cacti" 1 == "1 cactus"
@ -1439,14 +1439,14 @@ Expects do not have to be at the top level:
```roc
pluralize = \singular, plural, count ->
count_str = Num.toStr count
count_str = Num.to_str count
if count == 1 then
"$(count_str) $(singular)"
"${count_str} ${singular}"
else
expect count > 0
"$(count_str) $(plural)"
"${count_str} ${plural}"
```
This `expect` will fail if you call `pluralize` passing a count of 0.
@ -1625,7 +1625,7 @@ There are two types of functions in roc, "pure" and "effectful". Consider these
```roc
with_extension : Str -> Str
with_extension = \filename ->
"$(filename).roc"
"${filename}.roc"
read_file! : Str => Str
read_file! = \path ->
@ -1690,7 +1690,7 @@ import pf.Stdin
main! = \_args ->
try Stdout.line! "Type in something and press Enter:"
input = try Stdin.line! {}
try Stdout.line! "Your input was: $(input)"
try Stdout.line! "Your input was: ${input}"
Ok {}
```
@ -1766,7 +1766,7 @@ main! : List Arg => Result {} [Exit I32 Str]
main! = \_args ->
try Stdout.line! "Type in something and press Enter:"
input = try Stdin.line! {}
try Stdout.line! "Your input was: $(input)"
try Stdout.line! "Your input was: ${input}"
Ok {}
```
@ -1798,7 +1798,7 @@ my_function! : {} => Result {} [EndOfFile, StdinErr _, StdoutErr _]
my_function! = \{} ->
try Stdout.line! "Type in something and press Enter:"
input = try Stdin.line! {}
try Stdout.line! "Your input was: $(input)"
try Stdout.line! "Your input was: ${input}"
Ok {}
```
@ -1841,7 +1841,7 @@ import pf.Stdin
main! = \_args ->
try Stdout.line! "Type in something and press Enter:"
input = try Stdin.line! {}
try Stdout.line! "Your input was: $(input)"
try Stdout.line! "Your input was: ${input}"
Ok {}
```
@ -1856,7 +1856,7 @@ main! = \_args ->
_ = Stdout.line! "Type in something and press Enter:"
when Stdin.line! {} is
Ok input ->
_ = Stdout.line! "Your input was: $(input)"
_ = Stdout.line! "Your input was: ${input}"
Ok {}
Err _ ->
Ok {}
@ -1870,7 +1870,7 @@ Although it's rare, it is possible that either of the `Stdout.line!` operations
main! = \_args ->
try Stdout.line! "Type something and press Enter."
input = try Stdin.line! {}
try Stdout.line! "You entered: $(input)"
try Stdout.line! "You entered: ${input}"
Ok {}
```
@ -1897,7 +1897,7 @@ main! = \_args ->
|> Result.mapErr UnableToReadInput
|> try
Stdout.line! "You entered: $(input)"
Stdout.line! "You entered: ${input}"
|> Result.mapErr UnableToPrintInput
|> try
@ -1920,14 +1920,14 @@ This code is doing three things:
See the [Error Handling example](https://www.roc-lang.org/examples/ErrorHandling/README.html) for a more detailed explanation of error handling in a larger program.
### [Displaying Roc values with `Inspect.toStr`](#inspect) {#inspect}
### [Displaying Roc values with `Inspect.to_str`](#inspect) {#inspect}
The [`Inspect.toStr`](https://www.roc-lang.org/builtins/Inspect#toStr) function returns a `Str` representation of any Roc value using its [`Inspect` ability](/abilities#inspect-ability). It's useful for things like debugging and logging (although [`dbg`](https://www.roc-lang.org/tutorial#debugging) is often nicer for debugging in particular), but its output is almost never something that should be shown to end users! In this case we're just using it for our own learning, but it would be better to run a `when` on `e` and display a more helpful message.
The [`Inspect.to_str`](https://www.roc-lang.org/builtins/Inspect#to_str) function returns a `Str` representation of any Roc value using its [`Inspect` ability](/abilities#inspect-ability). It's useful for things like debugging and logging (although [`dbg`](https://www.roc-lang.org/tutorial#debugging) is often nicer for debugging in particular), but its output is almost never something that should be shown to end users! In this case we're just using it for our own learning, but it would be better to run a `when` on `e` and display a more helpful message.
```roc
when err is
StdoutErr e -> Exit 1 "Error writing to stdout: $(Inspect.toStr e)"
StdinErr e -> Exit 2 "Error writing to stdin: $(Inspect.toStr e)"
StdoutErr e -> Exit 1 "Error writing to stdout: ${Inspect.to_str e}"
StdinErr e -> Exit 2 "Error writing to stdin: ${Inspect.to_str e}"
```
### [The early `return` keyword](#the-early-return-keyword) {#the-early-return-keyword}
@ -1988,7 +1988,7 @@ Let's say I write a function which takes a record with a `first_name` and `last_
```roc
full_name = \user ->
"$(user.first_name) $(user.last_name)"
"${user.first_name} ${user.last_name}"
```
I can pass this function a record that has more fields than just `first_name` and `last_name`, as long as it has _at least_ both of those fields (and both of them are strings). So any of these calls would work:
@ -2007,14 +2007,14 @@ If we add a type annotation to this `full_name` function, we can choose to have
# Closed record
full_name : { first_name : Str, last_name : Str } -> Str
full_name = \user ->
"$(user.first_name) $(user.last_name)"
"${user.first_name} ${user.last_name}"
```
```roc
# Open record (because of the `*`)
full_name : { first_name : Str, last_name : Str }* -> Str
full_name = \user ->
"$(user.first_name) $(user.last_name)"
"${user.first_name} ${user.last_name}"
```
The `*` in the type `{ first_name : Str, last_name : Str }*` is what makes it an open record type. This `*` is the _wildcard type_ we saw earlier with empty lists. (An empty list has the type `List *`, in contrast to something like `List Str` which is a list of strings.)
@ -2030,7 +2030,7 @@ The type variable can also be a named type variable, like so:
```roc
add_https : { url : Str }a -> { url : Str }a
add_https = \record ->
{ record & url: "https://$(record.url)" }
{ record & url: "https://${record.url}" }
```
This function uses _constrained records_ in its type. The annotation is saying:

View file

@ -64,11 +64,11 @@ get_page_info = \page_path_str ->
Str.split_on(page_path_str, "/")
|> List.take_last(2)
|> List.first # we use the folder for name for the page title, e.g. Json from examples/Json/README.html
|> unwrap_or_crash("This List.first should never fail. pagePathStr ($(page_path_str)) did not contain any `/`.")
|> unwrap_or_crash("This List.first should never fail. page_path_str (${page_path_str}) did not contain any `/`.")
|> (\page_title ->
{ title: "$(page_title) | Roc", description: "$(page_title) example in the Roc programming language." })
{ title: "${page_title} | Roc", description: "${page_title} example in the Roc programming language." })
else
crash("Web page $(page_path_str) did not have a title and description specified in the pageData Dict. Please add one.")
crash("Web page ${page_path_str} did not have a title and description specified in the pageData Dict. Please add one.")
unwrap_or_crash : Result a b, Str -> a where b implements Inspect
unwrap_or_crash = \result, error_msg ->
@ -77,7 +77,7 @@ unwrap_or_crash = \result, error_msg ->
val
Err(err) ->
crash("$(Inspect.to_str(err)): $(error_msg)")
crash("${Inspect.to_str(err)}: ${error_msg}")
transform : Str, Str -> Str
transform = \page_path_str, html_content ->

View file

@ -40,7 +40,7 @@ const repl = {
},
{
match: (input) => input.replace(/ /g, "").match(/^name="/i),
show: '<p>This created a new <a href="https://www.roc-lang.org/tutorial#defs">definition</a>&mdash;<code>name</code> is now defined to be equal to the <a href="/tutorial#strings-and-numbers">string</a> you entered.</p><p>Try using this definition by entering <code>"Hi, \$(name)!"</code></p>',
show: '<p>This created a new <a href="https://www.roc-lang.org/tutorial#defs">definition</a>&mdash;<code>name</code> is now defined to be equal to the <a href="/tutorial#strings-and-numbers">string</a> you entered.</p><p>Try using this definition by entering <code>"Hi, \${name}!"</code></p>',
},
{
match: (input) => input.match(/^"[^\$]+\$\(name\)/i),
@ -388,9 +388,9 @@ function updateHistoryEntry(index, ok, outputText) {
bounds.top >= 0 &&
bounds.left >= 0 &&
bounds.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
(window.innerHeight || document.documentElement.clientHeight) &&
bounds.right <=
(window.innerWidth || document.documentElement.clientWidth);
(window.innerWidth || document.documentElement.clientWidth);
if (!isInView) {
repl.elemSourceInput.scrollIntoView({