From 393f719936315ca20af25f0e3dd517f223c226ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Sat, 3 Oct 2026 22:56:56 +1300 Subject: [PATCH 1/5] Add custom literals example --- ci_scripts/all_tests.sh | 26 ++--- examples/CustomLiterals/README.md | 66 ++++++++++++ examples/CustomLiterals/main.roc | 161 ++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 12 deletions(-) create mode 100644 examples/CustomLiterals/README.md create mode 100644 examples/CustomLiterals/main.roc diff --git a/ci_scripts/all_tests.sh b/ci_scripts/all_tests.sh index 9f861ec..1a0f780 100755 --- a/ci_scripts/all_tests.sh +++ b/ci_scripts/all_tests.sh @@ -23,10 +23,10 @@ expect ci_scripts/expect_scripts/HelloWorld.exp cd ./examples/FizzBuzz/ $ROC build --no-cache main.roc cd ../.. -$ROC test ./examples/FizzBuzz/main.roc +$ROC test --no-cache ./examples/FizzBuzz/main.roc expect ci_scripts/expect_scripts/FizzBuzz.exp -$ROC test ./examples/GraphTraversal/Graph.roc +$ROC test --no-cache ./examples/GraphTraversal/Graph.roc cd ./examples/Json/ $ROC build --no-cache main.roc @@ -46,10 +46,10 @@ expect ci_scripts/expect_scripts/IngestFiles.exp cd ./examples/Parser/ $ROC build --no-cache main.roc cd ../.. -$ROC test ./examples/Parser/main.roc +$ROC test --no-cache ./examples/Parser/main.roc expect ci_scripts/expect_scripts/Parser.exp -$ROC test ./examples/PatternMatching/PatternMatching.roc +$ROC test --no-cache ./examples/PatternMatching/PatternMatching.roc cd ./examples/AllSyntax/ $ROC build --no-cache --opt=dev main.roc @@ -79,7 +79,7 @@ expect ci_scripts/expect_scripts/CommandLineArgsFile.exp cd ./examples/TryOperatorDesugaring/ $ROC build --no-cache main.roc cd ../.. -$ROC test ./examples/TryOperatorDesugaring/main.roc +$ROC test --no-cache ./examples/TryOperatorDesugaring/main.roc expect ci_scripts/expect_scripts/TryOperatorDesugaring.exp cd ./examples/Tuples/ @@ -87,9 +87,9 @@ $ROC build --no-cache main.roc cd ../.. expect ci_scripts/expect_scripts/Tuples.exp -$ROC test ./examples/TowersOfHanoi/Hanoi.roc +$ROC test --no-cache ./examples/TowersOfHanoi/Hanoi.roc -$ROC test ./examples/ErrorHandlingBasic/ErrorHandlingBasic.roc +$ROC test --no-cache ./examples/ErrorHandlingBasic/ErrorHandlingBasic.roc cd ./examples/ErrorHandlingRealWorld/ $ROC build --no-cache main.roc @@ -104,12 +104,12 @@ expect ci_scripts/expect_scripts/LoopEffect.exp cd ./examples/Snake/ $ROC build --no-cache main.roc cd ../.. -$ROC test ./examples/Snake/main.roc +$ROC test --no-cache ./examples/Snake/main.roc expect ci_scripts/expect_scripts/Snake.exp -$ROC test ./examples/RecordBuilder/DateParser.roc +$ROC test --no-cache ./examples/RecordBuilder/DateParser.roc -$ROC test ./examples/BasicDict/BasicDict.roc +$ROC test --no-cache ./examples/BasicDict/BasicDict.roc cd ./examples/MultipleRocFiles/ $ROC build --no-cache main.roc @@ -129,7 +129,7 @@ expect ci_scripts/expect_scripts/EncodeDecode.exp cd ./examples/SafeMath/ $ROC build --no-cache main.roc cd ../.. -$ROC test ./examples/SafeMath/main.roc +$ROC test --no-cache ./examples/SafeMath/main.roc expect ci_scripts/expect_scripts/SafeMath.exp cd ./examples/HelloWeb/ @@ -142,7 +142,9 @@ $ROC build --no-cache main.roc cd ../.. expect ci_scripts/expect_scripts/ImportPackageFromModule.exp -$ROC test ./examples/CustomInspect/OpaqueTypes.roc +$ROC test --no-cache ./examples/CustomInspect/OpaqueTypes.roc + +$ROC test --no-cache ./examples/CustomLiterals/main.roc cd ./examples/SortStrings/ #$ROC build --no-cache main.roc diff --git a/examples/CustomLiterals/README.md b/examples/CustomLiterals/README.md new file mode 100644 index 0000000..ebde56b --- /dev/null +++ b/examples/CustomLiterals/README.md @@ -0,0 +1,66 @@ +# Custom Literals + +Number literals, quoted strings, and interpolated strings can create values of your own types. + +## Number Literals: `from_numeral` + +`Celsius.from_numeral` converts a number literal to a temperature, rejecting temperatures below absolute zero. + +```roc +file:main.roc:snippet:numeral +``` + +The compiler passes a `Numeral` to `from_numeral`, which returns a `Try`. For a literal, Roc evaluates this method at compile time: `Ok(value)` becomes the literal's value, while `Err(_)` causes a compilation error. So `temp` has type `Celsius`, not `Try(Celsius, _)`, and needs no runtime parsing or validation. + +Delegating to `Dec.from_numeral` handles the number syntax and checks that the number fits in a `Dec` before we check the temperature by calling `create`. + +Note that in case of error `from_numeral` must return `Err(InvalidNumeral(Str))`. This is why we call `map_err` on the return value of `create` to convert `Err(BelowAbsoluteZero)` to `InvalidNumeral("...")`. At runtime, you are free to call `from_numeral` like any function, but in general other functions (like `create`) will have more actionable errors (i.e., tags instead of strings). + +## Quoted Strings: `from_quote` + +`Time.from_quote` accepts a 24-hour time in exactly `HH:MM:SS` format. In this implementation, it calls `from_str` (which parses the string and uses `from_hms` to validate the ranges) then it maps errors to the expected `BadQuotedBytes(Str)` type. This implementation allows the user to create a `Time` using a string literal at compile time (using `from_quote`), or using a `Str` at runtime (using `from_str`), or using integers at runtime (using `from_hms`). + +```roc +file:main.roc:snippet:quote +``` + +Like `from_numeral`, `from_quote` returns a `Try`, but literal syntax unwraps `Ok` at compile time and rejects `Err`. The resulting `time` is already a validated `Time` when the program runs. + +For example, changing `time` to `"02:60:00"`, or `temp` to `-300`, makes compilation fail. An explicit call such as `Time.from_quote("02:60:00")` instead returns an `Err` that the program can handle. + +## Calling a Constructor at Compile Time + +Compile-time evaluation also works with ordinary pure functions when their inputs are known at compile time and the result is defined at the top level. + +```roc +file:main.roc:snippet:constructor +``` + +The ordinary call preserves its `Try`, so `maybe_time` contains `Ok(time)`. The `Ok(validated_time)` pattern unwraps the result, giving `validated_time` the inferred type `Time`. If the constructor returns `Err`, this top-level pattern fails at compile time. + +## Interpolated Strings: `from_interpolation` + +An interpolated literal calls `from_interpolation`. Its first argument is the text before the first interpolation. The iterator then provides each interpolated value paired with the literal text following it. + +This `Html` type escapes the interpolated values while preserving the literal markup: + +```roc +file:main.roc:snippet:interpolation +``` + +Here `name` becomes `Roc & friends <3`, while `

` and `

` remain markup. This example is for inserting text into HTML elements. Other contexts, such as URLs, scripts, or styles, could each handle interpolations in their own way. + +Unlike `from_numeral` and `from_quote`, this `from_interpolation` returns `Html` directly. There is no automatic `Try` unwrapping or rejection of `Err`. Our top-level `greeting` is evaluated at compile time because all its inputs are known. Interpolation can also use runtime values, in which case escaping and assembly happen at runtime. + +## Output + +Run this from the directory that has `main.roc` in it: + +``` +$ roc main.roc +Temperature: 37.0 +Time: { hour: 2, minute: 59, second: 57 } +HTML:

Hello, Roc & friends <3!

+``` + +Run the unit tests with `roc test main.roc`. diff --git a/examples/CustomLiterals/main.roc b/examples/CustomLiterals/main.roc new file mode 100644 index 0000000..2062503 --- /dev/null +++ b/examples/CustomLiterals/main.roc @@ -0,0 +1,161 @@ +### start snippet numeral +Celsius :: Dec.{ + is_eq : _ + + from_numeral : Numeral -> Try(Celsius, [InvalidNumeral(Str)]) + from_numeral = |numeral| { + degrees = Dec.from_numeral(numeral)? + Celsius.create(degrees).map_err(|BelowAbsoluteZero| + InvalidNumeral("Temperature must be at least absolute zero")) + } + + create : Dec -> Try(Celsius, [BelowAbsoluteZero]) + create = |degrees| { + if degrees < -273.15 { + Err(BelowAbsoluteZero) + } else { + Ok(Celsius.(degrees)) + } + } + + to_dec : Celsius -> Dec + to_dec = |Celsius.(degrees)| degrees +} + +temp1 : Celsius +temp1 = 37 + +temp2 = 37.Celsius + +### end snippet numeral + +### start snippet quote +Time := { hour : U8, minute : U8, second : U8 }.{ + is_eq : _ + + from_quote : Str -> Try(Time, [BadQuotedBytes(Str)]) + from_quote = |text| { + from_str(text).map_err( + |err| match err { + InvalidTimeFormat => BadQuotedBytes("Invalid time format") + InvalidTime => BadQuotedBytes("Invalid time") + }, + ) + } + + from_str : Str -> Try(Time, [InvalidTimeFormat, InvalidTime]) + from_str = |text| { + parts = text.split_on(":") + if parts.any(|part| part.count_utf8_bytes() != 2) { + return Err(InvalidTimeFormat) + } + nums = parts.map_try(U8.from_str) ? |BadNumStr| InvalidTimeFormat + match nums { + [hour, minute, second] => { + if hour < 24 and minute < 60 and second < 60 { + from_hms({ hour, minute, second }) + } else { + Err(InvalidTimeFormat) + } + } + _ => Err(InvalidTimeFormat) + } + } + + from_hms : { hour : U8, minute : U8, second : U8 } -> Try(Time, [InvalidTime]) + from_hms = |hms| { + if hms.hour < 24 and hms.minute < 60 and hms.second < 60 { + Ok(Time.(hms)) + } else { + Err(InvalidTime) + } + } +} + +time1 : Time +time1 = "02:59:57" + +time2 = "02:59:57".Time + +### end snippet quote + +### start snippet constructor +maybe_time : Try(Time, _) +maybe_time = Time.from_hms({ hour: 2, minute: 59, second: 57 }) + +Ok(validated_time) = Time.from_hms({ hour: 2, minute: 59, second: 57 }) + +### end snippet constructor + +### start snippet interpolation +Html :: Str.{ + is_eq : _ + + from_interpolation : Str, Iter((Str, Str)) -> Html + from_interpolation = |first, rest| { + Html.( + rest.fold( + first, + |html, (value, following)| { + html.concat(Html.escape(value)).concat(following) + }, + ), + ) + } + + # Escape interpolated text, preserving the literal markup. + escape : Str -> Str + escape = |text| { + text + .replace_each("&", "&") + .replace_each("<", "<") + .replace_each(">", ">") + .replace_each("\"", """) + .replace_each("'", "'") + } + + to_str = |Html.(html)| html +} + +name = "Roc & friends <3" + +greeting1 : Html +greeting1 = "

Hello, ${name}!

" + +greeting2 = "

Hello, ${name}!

".Html + +### end snippet interpolation + +expect temp1.to_dec() == 37 +expect time1.hour == 2 +expect time1.minute == 59 +expect time1.second == 57 +expect temp1 == temp2 +expect time1 == time2 +expect time1 == validated_time +expect maybe_time == Ok(time1) +expect Time.from_hms({ hour: 0, minute: 0, second: 0 }).is_ok() +expect Time.from_quote("23:59:59").is_ok() +expect Time.from_quote("24:00:00").is_err() +expect Time.from_quote("02:60:00").is_err() +expect Time.from_quote("02:59:60").is_err() +expect Time.from_quote("2:59:60").is_err() +expect Time.from_quote("2:9:0001").is_err() +expect Time.from_quote("ab:cd:ef").is_err() +expect greeting1 == greeting2 +expect greeting1.to_str() == "

Hello, Roc & friends <3!

" +expect Html.escape("&<>'\"") == "&<>'"" +expect { + first = "" + second = "&two" + html : Html + html = "${first} / ${second}" + html.to_str() == "<one> / &two" +} + +main! = |_| { + echo!("Temperature: ${temp1.to_dec().to_str()}\n") + echo!("Time: ${Str.inspect(time1)}\n") + echo!("HTML: ${greeting1.to_str()}\n") + Ok({}) +} From 408dc1547f7cdb0b4211e62535c0a5ca9bc58794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Sat, 3 Oct 2026 23:16:22 +1300 Subject: [PATCH 2/5] Add a few comments and one more test --- examples/CustomLiterals/main.roc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/CustomLiterals/main.roc b/examples/CustomLiterals/main.roc index 2062503..7b2cf50 100644 --- a/examples/CustomLiterals/main.roc +++ b/examples/CustomLiterals/main.roc @@ -23,9 +23,11 @@ Celsius :: Dec.{ } temp1 : Celsius -temp1 = 37 +temp1 = 37 # calls Celsius.from_quote at compile time -temp2 = 37.Celsius +temp2 = 37.Celsius # also calls Celsius.from_quote at compile time + +#temp3 = -1000 # this code would cause a compilation error: it's too cold! ### end snippet numeral @@ -77,6 +79,8 @@ time1 = "02:59:57" time2 = "02:59:57".Time +#time3 = "99:99:99" # compilation error + ### end snippet quote ### start snippet constructor @@ -126,7 +130,10 @@ greeting2 = "

Hello, ${name}!

".Html ### end snippet interpolation +# Test Celsius expect temp1.to_dec() == 37 + +# Test Time expect time1.hour == 2 expect time1.minute == 59 expect time1.second == 57 @@ -141,7 +148,10 @@ expect Time.from_quote("02:60:00").is_err() expect Time.from_quote("02:59:60").is_err() expect Time.from_quote("2:59:60").is_err() expect Time.from_quote("2:9:0001").is_err() +expect Time.from_quote("99:99:99").is_err() expect Time.from_quote("ab:cd:ef").is_err() + +# Test HTML expect greeting1 == greeting2 expect greeting1.to_str() == "

Hello, Roc & friends <3!

" expect Html.escape("&<>'\"") == "&<>'"" From ae1f12051452ad0976533c2ff2e828170109b44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Sat, 3 Oct 2026 23:19:12 +1300 Subject: [PATCH 3/5] Fix comments --- examples/CustomLiterals/main.roc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/CustomLiterals/main.roc b/examples/CustomLiterals/main.roc index 7b2cf50..3f26c88 100644 --- a/examples/CustomLiterals/main.roc +++ b/examples/CustomLiterals/main.roc @@ -23,11 +23,11 @@ Celsius :: Dec.{ } temp1 : Celsius -temp1 = 37 # calls Celsius.from_quote at compile time +temp1 = 37 # calls Celsius.from_numeral at compile time -temp2 = 37.Celsius # also calls Celsius.from_quote at compile time +temp2 = 37.Celsius # also calls Celsius.from_numeral at compile time -#temp3 = -1000 # this code would cause a compilation error: it's too cold! +#temp3 = -1000 # this would cause a compilation error: it's too cold! ### end snippet numeral @@ -75,11 +75,11 @@ Time := { hour : U8, minute : U8, second : U8 }.{ } time1 : Time -time1 = "02:59:57" +time1 = "02:59:57" # calls Time.from_quote at compilation time -time2 = "02:59:57".Time +time2 = "02:59:57".Time # also calls Time.from_quote at compilation time -#time3 = "99:99:99" # compilation error +#time3 = "99:99:99" # this would cause a compilation error: invalid time! ### end snippet quote From 3950b6e814212492aedf97e2349b69aa472035bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Sat, 3 Oct 2026 23:21:37 +1300 Subject: [PATCH 4/5] Format the code example --- examples/CustomLiterals/main.roc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/CustomLiterals/main.roc b/examples/CustomLiterals/main.roc index 3f26c88..5511a1d 100644 --- a/examples/CustomLiterals/main.roc +++ b/examples/CustomLiterals/main.roc @@ -27,7 +27,7 @@ temp1 = 37 # calls Celsius.from_numeral at compile time temp2 = 37.Celsius # also calls Celsius.from_numeral at compile time -#temp3 = -1000 # this would cause a compilation error: it's too cold! +# temp3 = -1000 # this would cause a compilation error: it's too cold! ### end snippet numeral @@ -79,7 +79,7 @@ time1 = "02:59:57" # calls Time.from_quote at compilation time time2 = "02:59:57".Time # also calls Time.from_quote at compilation time -#time3 = "99:99:99" # this would cause a compilation error: invalid time! +# time3 = "99:99:99" # this would cause a compilation error: invalid time! ### end snippet quote From 34ff03efca4d466e0bd7efa6bae43b336ab970ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Sun, 4 Oct 2026 12:07:35 +1300 Subject: [PATCH 5/5] Fix comment about from_interpolation --- examples/CustomLiterals/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/CustomLiterals/README.md b/examples/CustomLiterals/README.md index ebde56b..40d4c06 100644 --- a/examples/CustomLiterals/README.md +++ b/examples/CustomLiterals/README.md @@ -50,7 +50,9 @@ file:main.roc:snippet:interpolation Here `name` becomes `Roc & friends <3`, while `

` and `

` remain markup. This example is for inserting text into HTML elements. Other contexts, such as URLs, scripts, or styles, could each handle interpolations in their own way. -Unlike `from_numeral` and `from_quote`, this `from_interpolation` returns `Html` directly. There is no automatic `Try` unwrapping or rejection of `Err`. Our top-level `greeting` is evaluated at compile time because all its inputs are known. Interpolation can also use runtime values, in which case escaping and assembly happen at runtime. +Our top-level `greeting` is evaluated at compile time because all its inputs are known. Interpolation can also use runtime values, in which case escaping and assembly happen at runtime. + +Note: Unlike `from_numeral` and `from_quote`, the `from_interpolation` function doesn't have to return a `Try`. This will change shortly: it will require a `Try` and the compiler will automatically unwrap `Ok` and reject `Err` (see [issue #12044](https://github.com/roc-lang/roc/issues/12044)). ## Output