Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions ci_scripts/all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -79,17 +79,17 @@ 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/
$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
Expand All @@ -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
Expand All @@ -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/
Expand All @@ -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
Expand Down
68 changes: 68 additions & 0 deletions examples/CustomLiterals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 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 &amp; friends &lt;3`, while `<p>` and `</p>` 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.

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

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: <p>Hello, Roc &amp; friends &lt;3!</p>
```

Run the unit tests with `roc test main.roc`.
171 changes: 171 additions & 0 deletions examples/CustomLiterals/main.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
### 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 # 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!

### 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" # 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!

### 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("&", "&amp;")
.replace_each("<", "&lt;")
.replace_each(">", "&gt;")
.replace_each("\"", "&quot;")
.replace_each("'", "&#39;")
}

to_str = |Html.(html)| html
}

name = "Roc & friends <3"

greeting1 : Html
greeting1 = "<p>Hello, ${name}!</p>"

greeting2 = "<p>Hello, ${name}!</p>".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
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("99:99:99").is_err()
expect Time.from_quote("ab:cd:ef").is_err()

# Test HTML
expect greeting1 == greeting2
expect greeting1.to_str() == "<p>Hello, Roc &amp; friends &lt;3!</p>"
expect Html.escape("&<>'\"") == "&amp;&lt;&gt;&#39;&quot;"
expect {
first = "<one>"
second = "&two"
html : Html
html = "${first} / ${second}"
html.to_str() == "&lt;one&gt; / &amp;two"
}

main! = |_| {
echo!("Temperature: ${temp1.to_dec().to_str()}\n")
echo!("Time: ${Str.inspect(time1)}\n")
echo!("HTML: ${greeting1.to_str()}\n")
Ok({})
}
Loading