diff --git a/Cargo.lock b/Cargo.lock index a9415ee182..7a84114468 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3315,6 +3315,7 @@ name = "roc_build" version = "0.0.1" dependencies = [ "bumpalo", + "const_format", "inkwell", "libloading", "roc_builtins", @@ -4159,6 +4160,7 @@ version = "0.1.0" dependencies = [ "bitvec 1.0.1", "bumpalo", + "clap 3.2.20", "roc_wasm_module", ] diff --git a/README.md b/README.md index 57a4c6f867..0ce48d4b8c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Roc is not ready for a 0.1 release yet, but we do have: - [**installation** guide](https://github.com/roc-lang/roc/tree/main/getting_started) -- [**tutorial**](https://github.com/roc-lang/roc/blob/main/TUTORIAL.md) +- [**tutorial**](https://roc-lang.org/tutorial) - [**docs** for the standard library](https://www.roc-lang.org/builtins/Str) - [frequently asked questions](https://github.com/roc-lang/roc/blob/main/FAQ.md) - [Zulip chat](https://roc.zulipchat.com) for help, questions and discussions diff --git a/TUTORIAL.md b/TUTORIAL.md deleted file mode 100644 index 4215a07e5e..0000000000 --- a/TUTORIAL.md +++ /dev/null @@ -1,2142 +0,0 @@ -# Tutorial - -This is a tutorial to learn how to build Roc applications. -It covers the REPL, basic types like strings, lists, tags, and functions, syntax like `when` and `if then else`, and more! - -Enjoy! - -## Getting started - -Learn how to install roc on your machine [here](https://github.com/roc-lang/roc/tree/main/getting_started#installation). - -## REPL (Read - Eval - Print - Loop) - -Let’s start by getting acquainted with Roc’s Read Eval Print Loop, or REPL for -short. Run this in a terminal: - -```sh -$ roc repl -``` - -You should see this: - -```sh -The rockin’ roc repl -``` - -Try typing this in and pressing enter: - -```coffee ->> "Hello, World!" -"Hello, World!" : Str -``` - -Congratulations! You've just written your first Roc code! - -## Strings and Numbers - -Previously you entered the *expression* `"Hello, World!"` into the REPL, -and the REPL printed it back out. It also printed `: Str`, which is the -expression's type. We'll talk about types later; for now, we'll ignore the `:` -and whatever comes after it whenever the REPL prints them. - -Let's try a more complicated expression: - -```coffee ->> 1 + 1 -2 : Num * -``` - -According to the Roc REPL, one plus one equals two. Checks out! - -Roc will respect [order of operations](https://en.wikipedia.org/wiki/Order_of_operations) when using multiple arithmetic operators -like `+` and `-`, but you can use parentheses to specify exactly how they should -be grouped. - -```coffee ->> 1 + 2 * (3 - 4) --1 : Num * -``` - -Let's try calling a function: - -```coffee ->> Str.concat "Hi " "there!" -"Hi there!" : Str -``` - -In this expression, we're calling the `Str.concat` function -passing two arguments: the string `"Hi "` and the string `"there!"`. The -`Str.concat` function *concatenates* two strings together (that is, it puts -one after the other) and returns the resulting combined string of -`"Hi there!"`. - -Note that in Roc, we don't need parentheses or commas to call functions. -We don't write `Str.concat("Hi ", "there!")` but rather `Str.concat "Hi " "there!"`. - -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: - -```coffee ->> Str.concat "Birds: " (Num.toStr 42) -"Birds: 42" : Str -``` - -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`. -The parentheses are important here to specify how the function calls nest! -Try removing them, and see what happens: - -```coffee ->> Str.concat "Birds: " Num.toStr 42 - -``` - -This error message says that we've given `Str.concat` too many arguments. -Indeed we have! We've passed it three arguments: the string `"Birds"`, the -function `Num.toStr`, and the number `42`. That's not what we wanted 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`. - -Both the `Str.concat` function and the `Num.toStr` function have a `.` 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 different -module, and `toStr` 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. It'll be awhile before we want -to use them for more than that anyway! - -## Building an Application - -Let's move out of the REPL and create our first Roc application. - -Create a new file called `Hello.roc` and put the following code inside it: - -```coffee -app "hello" - packages { pf: "examples/cli/cli-platform/main.roc" } - imports [pf.Stdout, pf.Program] - provides [main] to pf - -main = Stdout.line "I'm a Roc application!" |> Program.quick -``` - -> **NOTE:** This assumes you've put Hello.roc in the root directory of the Roc -> source code. If you'd like to put it somewhere else, you'll need to replace -> `"examples/cli/cli-platform/main.roc"` with the path to the -> `examples/cli/cli-platform/main.roc` file in that source code. In the future, -> Roc will have the tutorial built in, and this aside will no longer be -> necessary! - -Try running this with: - -```sh -$ roc Hello.roc -``` - -You should see this: - -```sh -I'm a Roc application! -``` - -Congratulations - you've now written your first Roc application! We'll go over what the parts of -this file above `main` do later, but first let's play around a bit. -Try replacing the `main` line with this: - -```coffee -main = Stdout.line "There are \(total) animals." |> Program.quick - -birds = 3 - -iguanas = 2 - -total = Num.toStr (birds + iguanas) -``` - -Now if you run `roc Hello.roc`, you should see this: - -```sh -There are 5 animals. -``` - -`Hello.roc` now has four definitions - or *defs* for -short - namely, `main`, `birds`, `iguanas`, and `total`. - -A definition names an expression. - -- The first def assigns the name `main` to the expression `Stdout.line "There are \(total) animals." |> Program.quick`. The `Stdout.line` function takes a string and prints it as a line to [`stdout`] (the terminal's standard output device). Then `Program.quick` wrap this expression into an executable Roc program. -- The next two defs assign the names `birds` and `iguanas` to the expressions `3` and `2`. -- The last def assigns the name `total` to the expression `Num.toStr (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`. - -We can also refer to defs inside strings using *string interpolation*. The -string `"There are \(total) animals."` evaluates to the same thing as calling -`Str.concat "There are " (Str.concat total " animals.")` directly. - -You can name a def using any combination of letters and numbers, but they have -to start with a letter. Note that definitions are constant; once we've assigned -a name to an expression, we can't reassign it! We'd get an error if we wrote this: - -```coffee -birds = 3 - -birds = 2 -``` - -Order of defs doesn't matter. We defined `birds` and `iguanas` before -`total` (which uses both of them), but we defined `main` before `total` even though -it uses `total`. If you like, you can change the order of these defs to anything -you like, and everything will still work the same way! - -This works because Roc expressions don't have *side effects*. We'll talk more -about side effects later. - -## 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. - -```coffee -main = Stdout.line "There are \(total) animals." |> Program.quick - -birds = 3 - -iguanas = 2 - -total = addAndStringify birds iguanas - -addAndStringify = \num1, num2 -> - Num.toStr (num1 + num2) -``` - -This new `addAndStringify` function we've defined takes two numbers, adds them, -calls `Num.toStr` 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. The expression at the end of the body (`Num.toStr (num1 + num2)` -in this case) is returned automatically. - -Note that there is no separate syntax for named and anonymous functions in Roc. - -## if then else - -Let's modify the function to return an empty string if the numbers add to zero. - -```coffee -addAndStringify = \num1, num2 -> - sum = num1 + num2 - - if sum == 0 then - "" - else - Num.toStr 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 `addAndStringify`, it will not be accessible outside that function. -- We added an `if` / `then` / `else` conditional to return either `""` or `Num.toStr sum` depending on whether `sum == 0`. - -Of note, we couldn't have done `total = num1 + num2` because that would be -redefining `total` in the global scope, and defs can't be redefined. (However, we could use the name -`sum` for a def in a different function, because then they'd be in completely -different scopes and wouldn't affect each other.) - -Also note that every `if` must be accompanied by both `then` and also `else`. -Having an `if` without an `else` is an error, because in Roc, everything is -an expression - which means it 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! - -We can combine `if` and `else` to get `else if`, like so: - -```coffee -addAndStringify = \num1, num2 -> - sum = num1 + num2 - - if sum == 0 then - "" - else if sum < 0 then - "negative" - else - Num.toStr 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: - -```coffee -addAndStringify = \num1, num2 -> - sum = num1 + num2 - - if sum == 0 then - "" - else - if sum < 0 then - "negative" - else - Num.toStr sum -``` - -This code is equivalent to writing `else if sum < 0 then` on one line, although the stylistic -convention is to write `else if` on the same line. - -> *Note*: In Roc, `if` conditions must always be booleans. (Roc doesn't have a concept of "truthiness," -> so the compiler will report an error for conditionals like `if 1 then` or `if "true" then`.) - -## Records - -Currently our `addAndStringify` function takes two arguments. We can instead make -it take one argument like so: - -```coffee -total = addAndStringify { birds: 5, iguanas: 7 } - -addAndStringify = \counts -> - Num.toStr (counts.birds + counts.iguanas) -``` - -The function now takes a *record*, which is a group of values that travel together. -Records are not objects; they don't have methods or inheritance, they just store values. - -We create the record when we write `{ birds: 5, iguanas: 7 }`. This defines -a record with two *fields* - namely, the `birds` field and the `iguanas` field - -and then assigns the number `5` to the `birds` field and the number `7` to the -`iguanas` field. Order doesn't matter with record fields; we could have also specified -`iguanas` first and `birds` second, and Roc would consider it the exact same record. - -When we write `counts.birds`, it accesses the `birds` field of the `counts` record, -and when we write `counts.iguanas` it accesses the `iguanas` field. When we use `==` -on records, it compares all the fields in both records with `==`, and only returns true -if all fields on both records return true for their `==` comparisons. If one record has -more fields than the other, or if the types associated with a given field are different -between one field and the other, the Roc compiler will give an error at build time. - -> **Note:** Some other languages have a concept of "identity equality" that's separate from -> the "structural equality" we just described. Roc does not have a concept of identity equality; -> this is the only way equality works! - -The `addAndStringify` function will accept any record with at least the fields `birds` and -`iguanas`, but it will also accept records with more fields. For example: - -```coffee -total = addAndStringify { birds: 5, iguanas: 7 } - -totalWithNote = addAndStringify { birds: 4, iguanas: 3, note: "Whee!" } - -addAndStringify = \counts -> - Num.toStr (counts.birds + counts.iguanas) -``` - -This works because `addAndStringify` only uses `counts.birds` and `counts.iguanas`. -If we were to use `counts.note` inside `addAndStringify`, then we would get an error -because `total` is calling `addAndStringify` passing a record that doesn't have a `note` field. - -Record fields can have any combination of types we want. `totalWithNote` uses a record that -has a mixture of numbers and strings, but we can also have record fields with other types of -values - including other records, or even functions! - -```coffee -{ birds: 4, nestedRecord: { someFunction: (\arg -> arg + 1), name: "Sam" } } -``` - -### Record shorthands - -Roc has a couple of shorthands you can use to express some record-related operations more concisely. - -Instead of writing `\record -> record.x` we can write `.x` and it will evaluate to the same thing: -a function that takes a record and returns its `x` field. You can do this with any field you want. -For example: - -```elm -# function returnFoo takes a Record and returns the 'foo' field of that record. -returnFoo = .foo - -returnFoo { foo: "hi!", bar: "blah" } -# returns "hi!" -``` - -Whenever we're setting a field to be a def that has the same name as the field - -for example, `{ x: x }` - we can shorten it to just writing the name of the def alone - -for example, `{ x }`. We can do this with as many fields as we like, e.g. -`{ x: x, y: y }` can alternately be written `{ x, y }`, `{ x: x, y }`, or `{ x, y: y }`. - -### Record destructuring - -We can use *destructuring* to avoid naming a record in a function argument, instead -giving names to its individual fields: - -```coffee -addAndStringify = \{ birds, iguanas } -> - Num.toStr (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: - -```coffee -addAndStringify = \{ birds, iguanas: lizards } -> - Num.toStr (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.) - -Finally, destructuring can be used in defs too: - -```coffee -{ x, y } = { x: 5, y: 10 } -``` - -### Building records from other records - -So far we've only constructed records from scratch, by specifying all of their fields. We can -also construct new records by using another record to use as a starting point, and then -specifying only the fields we want to be different. For example, here are two ways to -get the same record: - -```coffee -original = { birds: 5, iguanas: 7, zebras: 2, goats: 1 } - -fromScratch = { birds: 4, iguanas: 3, zebras: 2, goats: 1 } -fromOriginal = { original & birds: 4, iguanas: 3 } -``` - -The `fromScratch` and `fromOriginal` records are equal, although they're assembled in -different ways. - -- `fromScratch` was built using the same record syntax we've been using up to this point. -- `fromOriginal` created a new record using the contents of `original` as defaults for fields that it didn't specify after the `&`. - -Note that when we do this, the fields you're overriding must all be present on the original record, -and their values must have the same type as the corresponding values in the original record. - -## Tags - -Sometimes we want to represent that something can have one of several values. For example: - -```coffee -stoplightColor = - if something > 0 then - Red - else if something == 0 then - Yellow - else - Green -``` - -Here, `stoplightColor` can have one of three values: `Red`, `Yellow`, or `Green`. -The capitalization is very important! If these were lowercase (`red`, `yellow`, `green`), -then they would refer to defs. However, because they are capitalized, they instead -refer to *tags*. - -A tag is a literal value just like a number or a string. Similarly to how I can write -the number `42` or the string `"forty-two"` without defining them first, I can also write -the tag `FortyTwo` without defining it first. Also, similarly to how `42 == 42` and -`"forty-two" == "forty-two"`, it's also the case that `FortyTwo == FortyTwo`. - -Let's say we wanted to turn `stoplightColor` from a `Red`, `Green`, or `Yellow` into -a string. Here's one way we could do that: - -```elm -stoplightStr = - if stoplightColor == Red then - "red" - else if stoplightColor == Green then - "green" - else - "yellow" -``` - -### Pattern matching -We can express the same logic more concisely using `when`/`is` instead of `if`/`then`: - -```elm -stoplightStr = - when stoplightColor is - Red -> "red" - Green -> "green" - Yellow -> "yellow" -``` - -This results in the same value for `stoplightStr`. In both the `when` version and the `if` version, we -have three conditional branches, and each of them evaluates to a string. The difference is how the -conditions are specified; here, we specify between `when` and `is` that we're making comparisons against -`stoplightColor`, and then we specify the different things we're comparing it to: `Red`, `Green`, and `Yellow`. - -Besides being more concise, there are other advantages to using `when` here. - -1. We don't have to specify an `else` branch, so the code can be more self-documenting about exactly what all the options are. -2. We get more compiler help. If we try deleting any of these branches, we'll get a compile-time error saying that we forgot to cover a case that could come up. For example, if we delete the `Green ->` branch, the compiler will say that we didn't handle the possibility that `stoplightColor` could be `Green`. It knows this because `Green` is one of the possibilities in the `stoplightColor` definition we made earlier. - -We can still have the equivalent of an `else` branch in our `when` if we like. Instead of writing "else", we write -"_ ->" like so: - -```coffee -stoplightStr = - when stoplightColor is - Red -> "red" - _ -> "not red" -``` - -This lets us more concisely handle multiple cases. However, it has the downside that if we add a new case - -for example, if we introduce the possibility of `stoplightColor` being `Orange`, the compiler can no longer -tell us we forgot to handle that possibility in our `when`. After all, we are handling it - just maybe not -in the way we'd decide to if the compiler had drawn our attention to it! - -We can make this `when` *exhaustive* (that is, covering all possibilities) without using `_ ->` by using -`|` to specify multiple matching conditions for the same branch: - -```coffee -stoplightStr = - when stoplightColor is - Red -> "red" - Green | Yellow -> "not red" -``` - -You can read `Green | Yellow` as "either `Green` or `Yellow`". By writing it this way, if we introduce the -possibility that `stoplightColor` can be `Orange`, we'll get a compiler error telling us we forgot to cover -that case in this `when`, and then we can handle it however we think is best. - -We can also combine `if` and `when` to make branches more specific: - -```coffee -stoplightStr = - when stoplightColor is - Red -> "red" - Green | Yellow if contrast > 75 -> "not red, but very high contrast" - Green | Yellow if contrast > 50 -> "not red, but high contrast" - Green | Yellow -> "not red" -``` - -This will give the same answer for `stoplightStr` as if we had written the following: - -```coffee -stoplightStr = - when stoplightColor is - Red -> "red" - Green | Yellow -> - if contrast > 75 then - "not red, but very high contrast" - else if contrast > 50 then - "not red, but high contrast" - else - "not red" -``` - -Either style can be a reasonable choice depending on the circumstances. - -### Tags with payloads - -Tags can have *payloads* - that is, values contained within them. For example: - -```coffee -stoplightColor = - if something > 100 then - Red - else if something > 0 then - Yellow - else if something == 0 then - Green - else - Custom "some other color" - -stoplightStr = - when stoplightColor is - Red -> "red" - Green | Yellow -> "not red" - Custom description -> description -``` - -This makes two changes to our earlier `stoplightColor` / `stoplightStr` example. - -1. We sometimes set `stoplightColor` to be `Custom "some other color"`. When we did this, we gave the `Custom` tag a *payload* of the string `"some other color"`. -2. We added a `Custom` tag in our `when`, with a payload which we named `description`. Because we did this, we were able to refer to `description` in the body of the branch (that is, the part after the `->`) just like any other def. - -Any tag can be given a payload like this. A payload doesn't have to be a string; we could also have said (for example) `Custom { r: 40, g: 60, b: 80 }` to specify an RGB color instead of a string. Then in our `when` we could have written `Custom record ->` and then after the `->` used `record.r`, `record.g`, and `record.b` to access the `40`, `60`, `80` values. We could also have written `Custom { r, g, b } ->` to *destructure* the record, and then -accessed these `r`, `g`, and `b` defs after the `->` instead. - -A tag can also have a payload with more than one value. Instead of `Custom { r: 40, g: 60, b: 80 }` we could -write `Custom 40 60 80`. If we did that, then instead of destructuring a record with `Custom { r, g, b } ->` -inside a `when`, we would write `Custom r g b ->` to destructure the values directly out of the payload. - -We refer to whatever comes before a `->` in a `when` expression as a *pattern* - so for example, in the -`Custom description -> description` branch, `Custom description` would be a pattern. In programming, using -patterns in branching conditionals like `when` is known as [pattern matching](https://en.wikipedia.org/wiki/Pattern_matching). You may hear people say things like "let's pattern match on `Custom` here" as a way to -suggest making a `when` branch that begins with something like `Custom description ->`. - -### Pattern Matching on Lists -You can also pattern match on lists, like so: - - when myList is - [] -> 0 # the list is empty - [Foo, ..] -> 1 # it starts with a Foo tag - [_, ..] -> 2 # it contains at least one element, which we ignore - [Foo, Bar, ..] -> 3 # it starts with a Foo tag followed by a Bar tag - [Foo, Bar, Baz] -> 4 # it contains exactly three elements: Foo, Bar, and Baz - [Foo, a, ..] -> 5 # it starts with a Foo tag followed by something we name `a` - [Ok a, ..] -> 6 # it starts with an Ok tag containing a payload named `a` - [.., Foo] -> 7 # it ends with a Foo tag - [A, B, .., C, D] -> 8 # it has certain elements at the beginning and and - -This can be both more concise and more efficient (at runtime) than calling [`List.get`](https://www.roc-lang.org/builtins/List#get) -multiple times, since each call to `get` requires a separate conditional to handle the different -`Result`s they return. - -> **Note:** Each list pattern can only have one `..`, which is known as the "rest pattern" because it's where the _rest_ of the list goes. - -## Booleans - -In many programming languages, `true` and `false` are special language keywords that refer to -the two boolean values. In Roc, booleans do not get special keywords; instead, they are exposed -as the ordinary values `Bool.true` and `Bool.false`. - -This design is partly to keep the number of special keywords in the language smaller, but mainly -to suggest how booleans are intended be used in Roc: for -[*boolean logic*](https://en.wikipedia.org/wiki/Boolean_algebra) (`&&`, `||`, and so on) as opposed -to for data modeling. Tags are the preferred choice for data modeling, and having tag values be -more concise than boolean values helps make this preference clear. - -As an example of why tags are encouraged for data modeling, in many languages it would be common -to write a record like `{ name: "Richard", isAdmin: Bool.true }`, but in Roc it would be preferable -to write something like `{ name: "Richard", role: Admin }`. At first, the `role` field might only -ever be set to `Admin` or `Normal`, but because the data has been modeled using tags instead of -booleans, it's much easier to add other alternatives in the future, like `Guest` or `Moderator` - -some of which might also want payloads. - -## Lists - -Another thing we can do in Roc is to make a *list* of values. Here's an example: - -```coffee -names = ["Sam", "Lee", "Ari"] -``` - -This is a list with three elements in it, all strings. We can add a fourth -element using `List.append` like so: - -```coffee -List.append names "Jess" -``` - -This returns a **new** list with `"Jess"` after `"Ari"`, and doesn't modify the original list at all. -All values in Roc (including lists, but also records, strings, numbers, and so on) are immutable, -meaning whenever we want to "change" them, we want to instead pass them to a function which returns some -variation of what was passed in. - -### List.map - -A common way to transform one list into another is to use `List.map`. Here's an example of how to -use it: - -```coffee -List.map [1, 2, 3] \num -> num * 2 -``` - -This returns `[2, 4, 6]`. `List.map` takes two arguments: - -1. An input list -2. A function that will be called on each element of that list - -It then returns a list which it creates by calling the given function on each element in the input list. -In this example, `List.map` calls the function `\num -> num * 2` on each element in -`[1, 2, 3]` to get a new list of `[2, 4, 6]`. - -We can also give `List.map` a named function, instead of an anonymous one: - -For example, the `Num.isOdd` function returns `true` if it's given an odd number, and `false` otherwise. -So `Num.isOdd 5` returns `true` and `Num.isOdd 2` returns `false`. - -As such, calling `List.map [1, 2, 3] Num.isOdd` returns a new list of `[Bool.true, Bool.false, Bool.true]`. - -### List element type compatibility - -If we tried to give `List.map` a function that didn't work on the elements in the list, then we'd get -an error at compile time. Here's a valid, and then an invalid example: - -```coffee -# working example -List.map [-1, 2, 3, -4] Num.isNegative -# returns [Bool.true, Bool.false, Bool.false, Bool.true] -``` - -```coffee -# invalid example -List.map ["A", "B", "C"] Num.isNegative -# error: isNegative doesn't work on strings! -``` - -Because `Num.isNegative` works on numbers and not strings, calling `List.map` with `Num.isNegative` and a -list of numbers works, but doing the same with a list of strings doesn't work. - -This wouldn't work either: - -```coffee -List.map ["A", "B", "C", 1, 2, 3] Num.isNegative -``` - -In fact, this wouldn't work for a more fundamental reason: every element in a Roc list has to share the same type. -For example, we can have a list of strings like `["Sam", "Lee", "Ari"]`, or a list of numbers like -`[1, 2, 3, 4, 5]` but we can't have a list which mixes strings and numbers like `["Sam", 1, "Lee", 2, 3]` - -that would be a compile-time error. - -Ensuring all elements in a list share a type eliminates entire categories of problems. -For example, it means that whenever you use `List.append` to -add elements to a list, as long as you don't have any compile-time errors, you won't get any runtime errors -from calling `List.map` afterwards - no matter what you appended to the list! More generally, it's safe to assume -that unless you run out of memory, `List.map` will run successfully unless you got a compile-time error about an -incompatibility (like `Num.negate` on a list of strings). - -### Lists that hold elements of different types - -We can use tags with payloads to make a list that contains a mixture of different types. For example: - -```coffee -List.map [StrElem "A", StrElem "b", NumElem 1, StrElem "c", NumElem -3] \elem -> - when elem is - NumElem num -> Num.isNegative num - StrElem str -> Str.isCapitalized str - -# returns [Bool.true, Bool.false, Bool.false, Bool.false, Bool.true] -``` - -Compare this with the example from earlier, which caused a compile-time error: - -```coffee -List.map ["A", "B", "C", 1, 2, 3] Num.isNegative -``` - -The version that uses tags works because we aren't trying to call `Num.isNegative` on each element. -Instead, we're using a `when` to tell when we've got a string or a number, and then calling either -`Num.isNegative` or `Str.isCapitalized` depending on which type we have. - -We could take this as far as we like, adding more different tags (e.g. `BoolElem Bool.true`) and then adding -more branches to the `when` to handle them appropriately. - -### Using tags as functions - -Let's say I want to apply a tag to a bunch of elements in a list. For example: - -```elm -List.map ["a", "b", "c"] \str -> Foo str -``` - -This is a perfectly reasonable way to write it, but I can also write it like this: - -```elm -List.map ["a", "b", "c"] Foo -``` - -These two versions compile to the same thing. As a convenience, Roc lets you specify -a tag name where a function is expected; when you do this, the compiler infers that you -want a function which uses all of its arguments as the payload to the given tag. - -### `List.any` and `List.all` - -There are several functions that work like `List.map` - they walk through each element of a list and do -something with it. Another is `List.any`, which returns `Bool.true` if calling the given function on any element -in the list returns `true`: - -```coffee -List.any [1, 2, 3] Num.isOdd -# returns `Bool.true` because 1 and 3 are odd -``` - -```coffee -List.any [1, 2, 3] Num.isNegative -# returns `Bool.false` because none of these is negative -``` - -There's also `List.all` which only returns `true` if all the elements in the list pass the test: - -```coffee -List.all [1, 2, 3] Num.isOdd -# returns `Bool.false` because 2 is not odd -``` - -```coffee -List.all [1, 2, 3] Num.isPositive -# returns `Bool.true` because all of these are positive -``` - -### Removing elements from a list - -You can also drop elements from a list. One way is `List.dropAt` - for example: - -```coffee -List.dropAt ["Sam", "Lee", "Ari"] 1 -# drops the element at offset 1 ("Lee") and returns ["Sam", "Ari"] -``` - -Another way is to use `List.keepIf`, which passes each of the list's elements to the given -function, and then keeps them only if that function returns `true`. - -```coffee -List.keepIf [1, 2, 3, 4, 5] Num.isEven -# returns [2, 4] -``` - -There's also `List.dropIf`, which does the reverse: - -```coffee -List.dropIf [1, 2, 3, 4, 5] Num.isEven -# returns [1, 3, 5] -``` - -### Custom operations that walk over a list - -You can make your own custom operations that walk over all the elements in a list, using `List.walk`. -Let's look at an example and then walk (ha!) through it. - -```coffee -List.walk [1, 2, 3, 4, 5] { evens: [], odds: [] } \state, elem -> - if Num.isEven elem then - { state & evens: List.append state.evens elem } - else - { state & odds: List.append state.odds elem } - -# returns { evens: [2, 4], odds: [1, 3, 5] } -``` - -`List.walk` walks through each element of the list, building up a state as it goes. At the end, -it returns the final state - whatever it ended up being after processing the last element. The `\state, elem ->` -function that `List.walk` takes as its last argument accepts both the current state as well as the current list element -it's looking at, and then returns the new state based on whatever it decides to do with that element. - -In this example, we walk over the list `[1, 2, 3, 4, 5]` and add each element to either the `evens` or `odds` -field of a `state` record `{ evens, odds }`. By the end, that record has a list of all the even numbers in the -list as well as a list of all the odd numbers. - -The state doesn't have to be a record; it can be anything you want. For example, if you made it a boolean, you -could implement `List.any` using `List.walk`. You could also make the state be a list, and implement `List.map`, -`List.keepIf`, or `List.dropIf`. There are a lot of things you can do with `List.walk` - it's very flexible! - -It can be tricky to remember the argument order for `List.walk` at first. A helpful trick is that the arguments -follow the same pattern as what we've seen with `List.map`, `List.any`, `List.keepIf`, and `List.dropIf`: the -first argument is a list, and the last argument is a function. The difference here is that `List.walk` has one -more argument than those other functions; the only place it could go while preserving that pattern is the middle! - -That third argument specifies the initial `state` - what it's set to before the `\state, elem ->` function has -been called on it even once. (If the list is empty, the `\state, elem ->` function will never get called and -the initial state gets returned immediately.) - -> **Note:** Other languages give this operation different names, such as "fold," "reduce," "accumulate," -> "aggregate," "compress," and "inject." - -### Getting an element from a List - -Another thing we can do with a list is to get an individual element out of it. `List.get` is a common way to do this; -it takes a list and an index, and then returns the element at that index...if there is one. But what if there isn't? - -For example, what do each of these return? - -```coffee -List.get ["a", "b", "c"] 1 -``` - -```coffee -List.get ["a", "b", "c"] 100 -``` - -The answer is that the first one returns `Ok "b"` and the second one returns `Err OutOfBounds`. -They both return tags! This is done so that the caller becomes responsible for handling the possibility that -the index is outside the bounds of that particular list. - -Here's how calling `List.get` can look in practice: - -```coffee -when List.get ["a", "b", "c"] index is - Ok str -> "I got this string: \(str)" - Err OutOfBounds -> "That index was out of bounds, sorry!" -``` - -There's also `List.first`, which always gets the first element, and `List.last` which always gets the last. -They return `Err ListWasEmpty` instead of `Err OutOfBounds`, because the only way they can fail is if you -pass them an empty list! - -These functions demonstrate a common pattern in Roc: operations that can fail returning either an `Ok` tag -with the answer (if successful), or an `Err` tag with another tag describing what went wrong (if unsuccessful). -In fact, it's such a common pattern that there's a whole module called `Result` which deals with these two tags. -Here are some examples of `Result` functions: - -```coffee -Result.withDefault (List.get ["a", "b", "c"] 100) "" -# returns "" because that's the default we said to use if List.get returned an Err -``` - -```coffee -Result.isOk (List.get ["a", "b", "c"] 1) -# returns `Bool.true` because `List.get` returned an `Ok` tag. (The payload gets ignored.) - -# Note: There's a Result.isErr function that works similarly. -``` - -### The pipe operator - -When you have nested function calls, sometimes it can be clearer to write them in a "pipelined" -style using the `|>` operator. Here are three examples of writing the same expression; they all -compile to exactly the same thing, but two of them use the `|>` operator to change how the calls look. - -```coffee -Result.withDefault (List.get ["a", "b", "c"] 1) "" -``` - -```coffee -List.get ["a", "b", "c"] 1 - |> Result.withDefault "" -``` - -The `|>` operator takes the value that comes before the `|>` and passes it as the first argument to whatever -comes after the `|>` - so in the example above, the `|>` takes `List.get ["a", "b", "c"] 1` and passes that -value as the first argument to `Result.withDefault` - making `""` the second argument to `Result.withDefault`. - -We can take this a step further like so: - -```coffee -["a", "b", "c"] - |> List.get 1 - |> Result.withDefault "" -``` - -This is still equivalent to the first expression. Since `|>` is known as the "pipe operator," we can read -this as "start with `["a", "b", "c"]`, then pipe it to `List.get`, then pipe it to `Result.withDefault`." - -One reason the `|>` operator injects the value as the first argument is to make it work better with -functions where argument order matters. For example, these two uses of `List.append` are equivalent: - -```coffee -List.append ["a", "b", "c"] "d" -``` - -```coffee -["a", "b", "c"] - |> List.append "d" -``` - -Another example is `Num.div`. All three of the following do the same thing, because `a / b` in Roc is syntax -sugar for `Num.div a b`: - -```coffee -first / second -``` - -```coffee -Num.div first second -``` - -```coffee -first - |> Num.div second -``` - -All operators in Roc are syntax sugar for normal function calls. See the "Operator Desugaring Table" -at the end of this tutorial for a complete list of them. - -## Types - -Sometimes you may want to document the type of a definition. For example, you might write: - -```ruby -# Takes a firstName string and a lastName string, and returns a string -fullName = \firstName, lastName -> - "\(firstName) \(lastName)" -``` - -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. - -### Type annotations - -Here's another way to document this function's type, which doesn't have that problem: - -```coffee -fullName : Str, Str -> Str -fullName = \firstName, lastName -> - "\(firstName) \(lastName)" -``` - -The `fullName :` 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. - -The annotation `fullName : Str, Str -> Str` says "`fullName` is a function that takes two strings as -arguments and returns a string." - -#### Strings - -We can give type annotations to any value, not just functions. For example: - -```coffee -firstName : Str -firstName = "Amy" - -lastName : Str -lastName = "Lee" -``` - -These annotations say that both `firstName` and `lastName` have the type `Str`. - -#### Records - -We can annotate records similarly. For example, we could move `firstName` and `lastName` into a record like so: - -```coffee -amy : { firstName : Str, lastName : Str } -amy = { firstName: "Amy", lastName: "Lee" } - -jen : { firstName : Str, lastName : Str } -jen = { firstName: "Jen", lastName: "Majura" } -``` - -#### Type Aliasing - -When we have a recurring type annotation like before, it can be nice to give it its own name. We do this like -so: - -```coffee -Musician : { firstName : Str, lastName : Str } - -amy : Musician -amy = { firstName: "Amy", lastName: "Lee" } - -simone : Musician -simone = { firstName: "Simone", lastName: "Simons" } -``` - -Here, `Musician` is a *type alias*. A type alias is like a def, except it gives a name to a type -instead of to a value. Just like how you can read `name : Str` as "`name` has the type `Str`," -you can also read `Musician : { firstName : Str, lastName : Str }` as "`Musician` has the type -`{ firstName : Str, lastName : Str }`." - -#### Tag Unions - -We can also give type annotations to tag unions: - -```coffee -colorFromStr : Str -> [Red, Green, Yellow] -colorFromStr = \string -> - when string is - "red" -> Red - "green" -> Green - _ -> Yellow -``` - -You can read the type `[Red, Green, Yellow]` as "a tag union of the tags `Red`, `Green`, and `Yellow`." - -#### List - -When we annotate a list type, we have to specify the type of its elements: - -```coffee -names : List Str -names = ["Amy", "Simone", "Tarja"] -``` - -You can read `List Str` as "a list of strings." Here, `Str` is a *type parameter* that tells us what type of -`List` we're dealing with. `List` is a *parameterized type*, which means it's a type that requires a type -parameter; there's no way to give something a type of `List` without a type parameter - you have to specify -what type of list it is, such as `List Str` or `List Bool` or `List { firstName : Str, lastName : Str }`. - -#### Wildcard type - -There are some functions that work on any list, regardless of its type parameter. For example, `List.isEmpty` -has this type: - -```coffee -isEmpty : List * -> Bool -``` - -The `*` is a *wildcard type* - that is, a type that's compatible with any other type. `List *` is compatible -with any type of `List` - so, `List Str`, `List Bool`, and so on. So you can call -`List.isEmpty ["I am a List Str"]` as well as `List.isEmpty [Bool.true]`, and they will both work fine. - -The wildcard type also comes up with empty lists. Suppose we have one function that takes a `List Str` and another -function that takes a `List Bool`. We might reasonably expect to be able to pass an empty list (that is, `[]`) to -either of these functions. And so we can! This is because a `[]` value has the type `List *` - that is, -"a list with a wildcard type parameter," or "a list whose element type could be anything." - -`List.reverse` works similarly to `List.isEmpty`, but with an important distinction. As with `isEmpty`, we can -call `List.reverse` on any list, regardless of its type parameter. However, consider these calls: - -```coffee -strings : List Str -strings = List.reverse ["a", "b"] - -bools : List Bool -bools = List.reverse [Bool.true, Bool.false] -``` - -In the `strings` example, we have `List.reverse` returning a `List Str`. In the `bools` example, it's returning a -`List Bool`. So what's the type of `List.reverse`? - -We saw that `List.isEmpty` has the type `List * -> Bool`, so we might think the type of `List.reverse` would be -`reverse : List * -> List *`. However, remember that we also saw that the type of the empty list is `List *`? -`List * -> List *` is actually the type of a function that always returns empty lists! That's not what we want. - -#### Type Variables - -What we want is something like one of these: - -```coffee -reverse : List elem -> List elem -``` - -```coffee -reverse : List value -> List value -``` - -```coffee -reverse : List a -> List a -``` - -Any of these will work, because `elem`, `value`, and `a` are all *type variables*. A type variable connects -two or more types in the same annotation. So you can read `List elem -> List elem` as "takes a list and returns -a list that has the same element type." Just like `List.reverse` does! - -You can choose any name you like for a type variable, but it has to be lowercase. (You may have noticed all the -types we've used until now are uppercase; that is no accident! Lowercase types are always type variables, so -all other named types have to be uppercase.) All three of the above type annotations are equivalent; -the only difference is that we chose different names (`elem`, `value`, and `a`) for their type variables. - -You can tell some interesting things about functions based on the type parameters involved. For example, -any function that returns `List *` definitely always returns an empty list. You don't need to look at the rest -of the type annotation, or even the function's implementation! The only way to have a function that returns -`List *` is if it returns an empty list. - -Similarly, the only way to have a function whose type is `a -> a` is if the function's implementation returns -its argument without modifying it in any way. This is known as [the identity function](https://en.wikipedia.org/wiki/Identity_function). - -## Tag Unions - -We can also annotate types that include tags: - -```coffee -colorFromStr : Str -> [Red, Green, Yellow] -colorFromStr = \string -> - when string is - "red" -> Red - "green" -> Green - _ -> Yellow -``` - -You can read the type `[Red, Green, Yellow]` as "a *tag union* of the tags `Red`, `Green`, and `Yellow`." - -Some tag unions have only one tag in them. For example: - -```coffee -redTag : [Red] -redTag = Red -``` - -Tag unions can accumulate additional tags based on how they're used in the program. Consider this `if` expression: - -```elm -\str -> - if Str.isEmpty str then - Ok "it was empty" - else - Err ["it was not empty"] -``` - -Here, Roc sees that the first branch has the type `[Ok Str]` and that the `else` branch has -the type `[Err (List Str)]`, so it concludes that the whole `if` expression evaluates to the -combination of those two tag unions: `[Ok Str, Err (List Str)]`. - -This means the entire `\str -> …` funcion here has the type `Str -> [Ok Str, Err (List Str)]`. -However, it would be most common to annotate it as `Result Str (List Str)` instead, because -the `Result` type (for operations like `Result.withDefault`, which we saw earlier) is a type -alias for a tag union with `Ok` and `Err` tags that each have one payload: - -```haskell -Result ok err : [Ok ok, Err err] -``` - -We just saw how tag unions get combined when different branches of a conditional return different tags. Another way tag unions can get combined is through pattern matching. For example: - -```coffeescript -when color is - Red -> "red" - Yellow -> "yellow" - Green -> "green" -``` - -Here, Roc's compiler will infer that `color`'s type is `[Red, Yellow, Green]`, because -those are the three possibilities this `when` handles. - -## Numeric types - -Roc has different numeric types that each have different tradeoffs. -They can all be broken down into two categories: [fractions](https://en.wikipedia.org/wiki/Fraction), -and [integers](https://en.wikipedia.org/wiki/Integer). In Roc we call these `Frac` and `Int` for short. - -### Integers - -Roc's integer types have two important characteristics: their *size* and their [*signedness*](https://en.wikipedia.org/wiki/Signedness). -Together, these two characteristics determine the range of numbers the integer type can represent. - -For example, the Roc type `U8` can represent the numbers 0 through 255, whereas the `I16` type can represent -the numbers -32768 through 32767. You can actually infer these ranges from their names (`U8` and `I16`) alone! - -The `U` in `U8` indicates that it's *unsigned*, meaning that it can't have a minus [sign](https://en.wikipedia.org/wiki/Sign_(mathematics)), and therefore can't be negative. The fact that it's unsigned tells us immediately that -its lowest value is zero. The 8 in `U8` means it is 8 [bits](https://en.wikipedia.org/wiki/Bit) in size, which -means it has room to represent 2⁸ (which is equal to 256) different numbers. Since one of those 256 different numbers -is 0, we can look at `U8` and know that it goes from `0` (since it's unsigned) to `255` (2⁸ - 1, since it's 8 bits). - -If we change `U8` to `I8`, making it a *signed* 8-bit integer, the range changes. Because it's still 8 bits, it still -has room to represent 2⁸ (that is, 256) different numbers. However, now in addition to one of those 256 numbers -being zero, about half of the rest will be negative, and the others positive. So instead of ranging from, say -255 -to 255 (which, counting zero, would represent 511 different numbers; too many to fit in 8 bits!) an `I8` value -ranges from -128 to 127. - -Notice that the negative extreme is `-128` versus `127` (not `128`) on the positive side. That's because of -needing room for zero; the slot for zero is taken from the positive range because zero doesn't have a minus sign. -So in general, you can find the lowest signed number by taking its total range (256 different numbers in the case -of an 8-bit integer) and dividing it in half (half of 256 is 128, so -128 is `I8`'s lowest number). To find the -highest number, take the positive version of the lowest number (so, convert `-128` to `128`) and then subtract 1 -to make room for zero (so, `128` becomes `127`; `I8` ranges from -128 to 127). - -Following this pattern, the 16 in `I16` means that it's a signed 16-bit integer. -That tells us it has room to represent 2¹⁶ (which is equal to 65536) different numbers. Half of 65536 is 32768, -so the lowest `I16` would be -32768, and the highest would be 32767. Knowing that, we can also quickly tell that -the lowest `U16` would be zero (since it always is for unsigned integers), and the highest `U16` would be 65535. - -Choosing a size depends on your performance needs and the range of numbers you want to represent. Consider: - -- Larger integer sizes can represent a wider range of numbers. If you absolutely need to represent numbers in a certain range, make sure to pick an integer size that can hold them! -- Smaller integer sizes take up less memory. These savings rarely matters in variables and function arguments, but the sizes of integers that you use in data structures can add up. This can also affect whether those data structures fit in [cache lines](https://en.wikipedia.org/wiki/CPU_cache#Cache_performance), which can easily be a performance bottleneck. -- Certain processors work faster on some numeric sizes than others. There isn't even a general rule like "larger numeric sizes run slower" (or the reverse, for that matter) that applies to all processors. In fact, if the CPU is taking too long to run numeric calculations, you may find a performance improvement by experimenting with numeric sizes that are larger than otherwise necessary. However, in practice, doing this typically degrades overall performance, so be careful to measure properly! - -Here are the different fixed-size integer types that Roc supports: - -| Range | Type | Size | -| -----------------------------------------------------------: | :---- | :------- | -| `-128`
`127` | `I8` | 1 Byte | -| `0`
`255` | `U8` | 1 Byte | -| `-32_768`
`32_767` | `I16` | 2 Bytes | -| `0`
`65_535` | `U16` | 2 Bytes | -| `-2_147_483_648`
`2_147_483_647` | `I32` | 4 Bytes | -| `0`
(over 4 billion) `4_294_967_295` | `U32` | 4 Bytes | -| `-9_223_372_036_854_775_808`
`9_223_372_036_854_775_807` | `I64` | 8 Bytes | -| `0`
(over 18 quintillion) `18_446_744_073_709_551_615` | `U64` | 8 Bytes | -| `-170_141_183_460_469_231_731_687_303_715_884_105_728`
`170_141_183_460_469_231_731_687_303_715_884_105_727` | `I128` | 16 Bytes | -| `0`
(over 340 undecillion) `340_282_366_920_938_463_463_374_607_431_768_211_455` | `U128` | 16 Bytes | - -#### Nat - -Roc also has one variable-size integer type: `Nat` (short for "natural number"). -The size of `Nat` is equal to the size of a memory address, which varies by system. -For example, when compiling for a 64-bit system, `Nat` works the same way as `U64`. -When compiling for a 32-bit system, it works the same way as `U32`. Most popular -computing devices today are 64-bit, so `Nat` is usually the same as `U64`, but -Web Assembly is typically 32-bit - so when running a Roc program built for Web Assembly, -`Nat` will work like a `U32` in that program. - -A common use for `Nat` is to store the length of a collection like a `List`; -there's a function `List.len : List * -> Nat` which returns the length of the given list. -64-bit systems can represent longer lists in memory than 32-bit systems can, -which is why the length of a list is represented as a `Nat`. - -If any operation would result in an integer that is either too big -or too small to fit in that range (e.g. calling `Int.maxI32 + 1`, which adds 1 to -the highest possible 32-bit integer), then the operation will *overflow*. -When an overflow occurs, the program will crash. - -As such, it's very important to design your integer operations not to exceed these bounds! - -### Fractions - -Roc has three fractional types: - -- `F32`, a 32-bit [floating-point number](https://en.wikipedia.org/wiki/IEEE_754) -- `F64`, a 64-bit [floating-point number](https://en.wikipedia.org/wiki/IEEE_754) -- `Dec`, a 128-bit decimal [fixed-point number](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) - -These are different from integers in that they can represent numbers with fractional components, -such as 1.5 and -0.123. - -`Dec` is the best default choice for representing base-10 decimal numbers -like currency, because it is base-10 under the hood. In contrast, -`F64` and `F32` are base-2 under the hood, which can lead to decimal -precision loss even when doing addition and subtraction. For example, when -using `F64`, running 0.1 + 0.2 returns 0.3000000000000000444089209850062616169452667236328125, -whereas when using `Dec`, 0.1 + 0.2 returns 0.3. - -`F32` and `F64` have direct hardware support on common processors today. There is no hardware support -for fixed-point decimals, so under the hood, a `Dec` is an `I128`; operations on it perform -[base-10 fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) -with 18 decimal places of precision. - -This means a `Dec` can represent whole numbers up to slightly over 170 -quintillion, along with 18 decimal places. (To be precise, it can store -numbers between `-170_141_183_460_469_231_731.687303715884105728` -and `170_141_183_460_469_231_731.687303715884105727`.) Why 18 -decimal places? It's the highest number of decimal places where you can still -convert any `U64` to a `Dec` without losing information. - -While the fixed-point `Dec` has a fixed range, the floating-point `F32` and `F64` do not. -Instead, outside of a certain range they start to lose precision instead of immediately overflowing -the way integers and `Dec` do. `F64` can represent [between 15 and 17 significant digits](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) before losing precision, whereas `F32` can only represent [between 6 and 9](https://en.wikipedia.org/wiki/Single-precision_floating-point_format#IEEE_754_single-precision_binary_floating-point_format:_binary32). - -There are some use cases where `F64` and `F32` can be better choices than `Dec` -despite their precision drawbacks. For example, in graphical applications they -can be a better choice for representing coordinates because they take up less memory, -various relevant calculations run faster, and decimal precision loss isn't as big a concern -when dealing with screen coordinates as it is when dealing with something like currency. - -### Num, Int, and Frac - -Some operations work on specific numeric types - such as `I64` or `Dec` - but operations support -multiple numeric types. For example, the `Num.abs` function works on any number, since you can -take the [absolute value](https://en.wikipedia.org/wiki/Absolute_value) of integers and fractions alike. -Its type is: - -```elm -abs : Num a -> Num a -``` - -This type says `abs` takes a number and then returns a number of the same type. That's because the -`Num` type is compatible with both integers and fractions. - -There's also an `Int` type which is only compatible with integers, and a `Frac` type which is only -compatible with fractions. For example: - -```elm -Num.xor : Int a, Int a -> Int a -``` - -```elm -Num.cos : Frac a -> Frac a -``` - -When you write a number literal in Roc, it has the type `Num *`. So you could call `Num.xor 1 1` -and also `Num.cos 1` and have them all work as expected; the number literal `1` has the type -`Num *`, which is compatible with the more constrained types `Int` and `Frac`. For the same reason, -you can pass number literals to functions expecting even more constrained types, like `I32` or `F64`. - -### Typed Number Literals - -When writing a number literal in Roc you can specify the numeric type as a suffix of the literal. -`1u8` specifies `1` as an unsigned 8-bit integer, `5i32` specifies `5` as a signed 32-bit integer, etc. -The full list of possible suffixes includes: -`i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `nat`, `f32`, `f64`, `dec` - -### Hexadecimal Integer Literals - -Integer literals can be written in hexadecimal form by prefixing with `0x` followed by hexadecimal characters. -`0xFE` evaluates to decimal `254` -The integer type can be specified as a suffix to the hexadecimal literal, -so `0xC8u8` evaluates to decimal `200` as an unsigned 8-bit integer. - -### Binary Integer Literals - -Integer literals can be written in binary form by prefixing with `0b` followed by the 1's and 0's representing -each bit. `0b0000_1000` evaluates to decimal `8` -The integer type can be specified as a suffix to the binary literal, -so `0b0100u8` evaluates to decimal `4` as an unsigned 8-bit integer. - -## Tests and expectations - -You can write automated tests for your Roc code like so: - -```swift -pluralize = \singular, plural, count -> - countStr = Num.toStr count - - if count == 1 then - "\(countStr) \(singular)" - else - "\(countStr) \(plural)" - -expect pluralize "cactus" "cacti" 1 == "1 cactus" - -expect pluralize "cactus" "cacti" 2 == "2 cacti" -``` - -If you put this in a file named `main.roc` and run `roc test`, Roc will execute the two `expect` -expressions (that is, the two `pluralize` calls) and report any that returned `false`. - -### Inline `expect`s - -For example: - -```swift - if count == 1 then - "\(countStr) \(singular)" - else - expect count > 0 - - "\(countStr) \(plural)" -``` - -This `expect` will fail if you call `pluralize` passing a count of 0, and it will fail -regardless of whether the inline `expect` is reached when running your program via `roc dev` -or in the course of running a test (with `roc test`). - -So for example, if we added this top-level `expect`... - -```swift -expect pluralize "cactus" "cacti" 0 == "0 cacti" -``` - -...it would hit the inline `expect count > 0`, which would then fail the test. - -Note that inline `expect`s do not halt the program! They are designed to inform, not to affect -control flow. In fact, if you do `roc build`, they are not even included in the final binary. - -If you try this code out, you may note that when an `expect` fails (either a top-level or inline -one), the failure message includes the values of any named variables - such as `count` here. -This leads to a useful technique, which we will see next. - -### Quick debugging with inline `expect`s - -An age-old debugging technique is printing out a variable to the terminal. In Roc you can use -`expect` to do this. Here's an example: - -```elm -\arg -> - x = arg - 1 - - # Reports the value of `x` without stopping the program - expect x != x - - Num.abs x -``` - -The failure output will include both the value of `x` as well as the comment immediately above it, -which lets you use that comment for extra context in your output. - -## Roc Modules - -Every `.roc` file is a *module*. There are three types of modules: - - builtin - - app - - interface - -### App module - -Let's take a closer look at the part of `Hello.roc` above `main`: - -```coffee -app "hello" - packages { pf: "examples/cli/cli-platform/main.roc" } - imports [pf.Stdout, pf.Program] - provides main to pf -``` - -This is known as a *module header*. We know this particular one is an *application module* -(or *app module* for short) because it begins with the `app` keyword. -Every Roc program has one app module. - -The line `app "hello"` states that this module defines a Roc application, and -that building this application should produce an executable named `hello`. This -means when you run `roc Hello.roc`, the Roc compiler will build an executable -named `hello` (or `hello.exe` on Windows) and run it. You can also build the executable -without running it by running `roc build Hello.roc`. - -The remaining lines all involve the *platform* this application is built on: - -```coffee -packages { pf: "examples/cli/cli-platform/main.roc" } -imports [pf.Stdout, pf.Program] -provides main to pf -``` - -The `packages { pf: "examples/cli/cli-platform/main.roc" }` part says two things: - -- We're going to be using a *package* (that is, a collection of modules) called `"examples/cli/cli-platform/main.roc"` -- We're going to name that package `pf` so we can refer to it more concisely in the future. - -The `imports [pf.Stdout, pf.Program]` line says that we want to import the `Stdout` and `Program` modules -from the `pf` package, and make them available in the current module. - -This import has a direct interaction with our definition of `main`. Let's look -at that again: - -```coffee -main = Stdout.line "I'm a Roc application!" |> Program.quick -``` - -Here, `main` is calling a function called `Stdout.line`. More specifically, it's -calling a function named `line` which is exposed by a module named -`Stdout`. -Then the result of that function call is passed to the `quick` function of the `Program` module, -which effectively makes it a simple Roc program. - -When we write `imports [pf.Stdout, pf.Program]`, it specifies that the `Stdout` -and `Program` modules come from the `pf` package. - -Since `pf` was the name we chose for the `examples/cli/cli-platform/main.roc` -package (when we wrote `packages { pf: "examples/cli/cli-platform/main.roc" }`), -this `imports` line tells the Roc compiler that when we call `Stdout.line`, it -should look for that `line` function in the `Stdout` module of the -`examples/cli/cli-platform/main.roc` package. - -If we would like to include other modules in our application, say `AdditionalModule.roc` -and `AnotherModule.roc`, then they can be imported directly in `imports` like this: - -```coffee -packages { pf: "examples/cli/cli-platform/main.roc" } -imports [pf.Stdout, pf.Program, AdditionalModule, AnotherModule] -provides main to pf -``` - -### Interface module - -Let's take a look at the following module header: - -```coffee -interface Parser.Core - exposes [ - Parser, - ParseResult, - buildPrimitiveParser - ] - imports [] -``` - -This says that the current .roc file is an *interface module* because it begins with the `interface` keyword. -We are naming this module when we write `interface Parser.Core`. It means that this file is in -a package `Parser` and the current module is named `core`. -When we write `exposes [Parser, ParseResult, ...]`, it specifies the definitions we -want to *expose*. Exposing makes them importable from other modules. - -Now lets import this interface from an *app module*: - -```coffee -app 'interface-example' - packages { pf: "examples/cli/cli-platform/main.roc" } - imports [Parser.Core.{ Parser, buildPrimitiveParser }] - provides main to pf -``` -Here we are importing a type and a function from the 'Core' module from the package 'Parser'. Now we can use e.g. -`buildPrimitiveParser` in this module without having to write `Parser.Core.buildPrimitiveParser`. - -### Builtin modules - -There are several modules that are built into the Roc compiler, which are imported automatically into every -Roc module. They are: - -1. `Bool` -2. `Str` -3. `Num` -4. `List` -5. `Result` -6. `Dict` -7. `Set` - -You may have noticed that we already used the first five - for example, when we wrote `Str.concat` and `Num.isEven`, -we were referencing functions stored in the `Str` and `Num` modules. - -These modules are not ordinary `.roc` files that live on your filesystem. Rather, they are built directly into the -Roc compiler. That's why they're called "builtins!" - -Besides being built into the compiler, the builtin modules are different from other modules in that: - -- They are always imported. You never need to add them to `imports`. -- All their types are imported unqualified automatically. So you never need to write `Num.Nat`, because it's as if the `Num` module was imported using `imports [Num.{ Nat }]` (and the same for all the other types in the `Num` module). - -## Platforms - -TODO - -## Comments - -Comments that begin with `##` will be included in generated documentation (```roc docs```). They require a single space after the `##`, and can include code blocks by adding five spaces after `##`. - -```coffee -## This is a comment for documentation, and includes a code block. -## -## x = 2 -## expect x == 2 -``` - -Roc also supports inline comments and line comments with `#`. They can be used to add information that won't be included in documentation. - -```coffee -# This is a line comment that won't appear in documentation. -myFunction : U8 -> U8 -myFunction = \bit -> bit % 2 # this is an inline comment -``` - -Roc does not have multiline comment syntax. - -## Tasks - -Tasks are technically not part of the Roc language, but they're very common in -platforms. Let's use the CLI platform in `examples/cli/cli-platform/main.roc` as an example! - -In the CLI platform, we have four operations we can do: - -- Write a string to the console -- Read a string from user input -- Write a string to a file -- Read a string from a file - -We'll use these four operations to learn about tasks. - -First, let's do a basic "Hello World" using the tutorial app. - -```coffee -app "cli-tutorial" - packages { pf: "examples/cli/cli-platform/main.roc" } - imports [pf.Stdout, pf.Program] - provides [main] to pf - -main = - Stdout.line "Hello, World!" - |> Program.quick -``` - -The `Stdout.line` function takes a `Str` and writes it to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). -It has this type: - -```coffee -Stdout.line : Str -> Task {} * -``` - -A `Task` represents an *effect* - that is, an interaction with state outside your Roc program, -such as the console's standard output, or a file. - -When we set `main` to be a `Task`, the task will get run when we run our program. Here, we've set -`main` to be a task that writes `"Hello, World!"` to `stdout` when it gets run, so that's what -our program does! - -`Task` has two type parameters: the type of value it produces when it finishes running, and any -errors that might happen when running it. `Stdout.line` has the type `Task {} *` because it doesn't -produce any values when it finishes (hence the `{}`) and there aren't any errors that can happen -when it runs (hence the `*`). - -In contrast, `Stdin.line` produces a `Str` when it finishes reading from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). That `Str` is reflected in its type: - -```coffee -Stdin.line : Task Str * -``` - -Let's change `main` to read a line from `stdin`, and then print it back out again: - -```swift -app "cli-tutorial" - packages { pf: "examples/cli/cli-platform/main.roc" } - imports [pf.Stdout, pf.Stdin, pf.Task, pf.Program] - provides [main] to pf - -main = Program.quick task - -task = - Task.await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -If you run this program, at first it won't do anything. It's waiting for you to type something -in and press Enter! Once you do, it should print back out what you entered. - -The `Task.await` function combines two tasks into one bigger `Task` which first runs one of the -given tasks and then the other. In this case, it's combining a `Stdin.line` task with a `Stdout.line` -task into one bigger `Task`, and then setting `main` to be that bigger task. - -The type of `Task.await` is: - -```haskell -Task.await : Task a err, (a -> Task b err) -> Task b err -``` - -The second argument to `Task.await` is a "callback function" which runs after the first task -completes. This callback function receives the output of that first task, and then returns -the second task. This means the second task can make use of output from the first task, like -we did in our `\text -> …` callback function here: - -```swift -\text -> - Stdout.line "You just entered: \(text)" -``` - -Notice that, just like before, we're still building `main` from a single `Task`. This is how we'll -always do it! We'll keep building up bigger and bigger `Task`s out of smaller tasks, and then setting -`main` to be that one big `Task`. - -For example, we can print a prompt before we pause to read from `stdin`, so it no longer looks like -the program isn't doing anything when we start it up: - -```swift -task = - Task.await (Stdout.line "Type something press Enter:") \_ -> - Task.await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -This works, but we can make it a little nicer to read. Let's change it to the following: - -```haskell -app "cli-tutorial" - packages { pf: "examples/cli/cli-platform/main.roc" } - imports [pf.Stdout, pf.Stdin, pf.Task.{ await }, pf.Program] - provides [main] to pf - -main = Program.quick task - -task = - await (Stdout.line "Type something press Enter:") \_ -> - await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -Here we've changed how we're importing the `Task` module. Before it was -`pf.Task` and now it's `pf.Task.{ await }`. The difference is that we're -importing `await` in an *unqualified* way, meaning now whenever we write `await` -in this module, it will refer to `Task.await` - so we no longer need to write -`Task.` every time we want to `await`. - -It's most common in Roc to call functions from other modules in a *qualified* way -(`Task.await`) rather than unqualified (`await`) like this, but it can be nice -for a function with an uncommon name (like "await") which often gets called repeatedly -across a small number of lines of code. - -### Backpassing - -Speaking of calling `await` repeatedly, if we keep calling it more and more on this -code, we'll end up doing a lot of indenting. If we'd rather not indent so much, we -can rewrite `task` into this style which looks different but does the same thing: - -```swift -task = - _ <- await (Stdout.line "Type something press Enter:") - text <- await Stdin.line - - Stdout.line "You just entered: \(text)" -``` - -This `<-` syntax is called *backpassing*. The `<-` is a way to define an -anonymous function, just like `\ … ->` is. - -Here, we're using backpassing to define two anonymous functions. Here's one of them: - -```swift -text <- - -Stdout.line "You just entered: \(text)" -``` - -It may not look like it, but this code is defining an anonymous function! You might -remember it as the anonymous function we previously defined like this: - -```swift -\text -> - Stdout.line "You just entered: \(text)" -``` - -These two anonymous functions are the same, just defined using different syntax. - -The reason the `<-` syntax is called *backpassing* is because it both defines a -function and passes that function *back* as an argument to whatever comes after -the `<-` (which in this case is `await Stdin.line`). - -Let's look at these two complete expressions side by side. They are both -saying exactly the same thing, with different syntax! - -Here's the original: - -```swift -await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -And here's the equivalent expression with backpassing syntax: - -```swift -text <- await Stdin.line - -Stdout.line "You just entered: \(text)" -``` - -Here's the other function we're defining with backpassing: - -```swift -_ <- -text <- await Stdin.line - -Stdout.line "You just entered: \(text)" -``` - -We could also have written that function this way if we preferred: - -```swift -_ <- - -await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -This is using a mix of a backpassing function `_ <-` and a normal function `\text ->`, -which is totally allowed! Since backpassing is nothing more than syntax sugar for -defining a function and passing back as an argument to another function, there's no -reason we can't mix and match if we like. - -That said, the typical style in which this `task` would be written in Roc is using -backpassing for all the `await` calls, like we had above: - -```swift -task = - _ <- await (Stdout.line "Type something press Enter:") - text <- await Stdin.line - - Stdout.line "You just entered: \(text)" -``` - -This way, it reads like a series of instructions: - -1. First, run the `Stdout.line` task and await its completion. Ignore its output (hence the underscore in `_ <-`) -2. Next, run the `Stdin.line` task and await its completion. Name its output `text`. -3. Finally, run the `Stdout.line` task again, using the `text` value we got from the `Stdin.line` effect. - -Some important things to note about backpassing and `await`: - -- `await` is not a language keyword in Roc! It's referring to the `Task.await` function, which we imported unqualified by writing `Task.{ await }` in our module imports. (That said, it is playing a similar role here to the `await` keyword in languages that have `async`/`await` keywords, even though in this case it's a function instead of a special keyword.) -- Backpassing syntax does not need to be used with `await` in particular. It can be used with any function. -- Roc's compiler treats functions defined with backpassing exactly the same way as functions defined the other way. The only difference between `\text ->` and `text <-` is how they look, so feel free to use whichever looks nicer to you! - -### Empty Tag Unions - -If you look up the type of [`Program.exit`](https://www.roc-lang.org/examples/cli/Program#exit), -you may notice that it takes a `Task` where the error type is `[]`. What does that mean? - -Just like how `{}` is the type of an empty record, `[]` is the type of an empty tag union. -There is no way to create an empty tag union at runtime, since creating a tag union requires -making an actual tag, and an empty tag union has no tags in it! - -This means if you have a function with the type `[] -> Str`, you can be sure that it will -never execute. It requires an argument that can't be provided! Similarly, if you have a -function with the type `Str -> []`, you can call it, but you can be sure it will not terminate -normally. The only way to implement a function like that is using [infinite recursion](https://en.wikipedia.org/wiki/Infinite_loop#Infinite_recursion), which will either run indefinitely or else crash with a [stack overflow](https://en.wikipedia.org/wiki/Stack_overflow). - -Empty tag unions can be useful as type parameters. For example, a function with the type -`List [] -> Str` can be successfully called, but only if you pass it an empty list. That's because -an empty list has the type `List *`, which means it can be used wherever any type of `List` is -needed - even a `List []`! - -Similarly, a function which accepts a `Result Str []` only accepts a "Result which is always `Ok`" - so you could call that function passing something like `Ok "hello"` with no problem, -but if you tried to give it an `Err`, you'd get a type mismatch. - -Applying this to `Task`, a task with `[]` for its error type is a "task which can never fail." The only way to obtain one is by obtaining a task with an error type of `*`, since that works with any task. You can get one of these "tasks that can never fail" by using [`Task.succeed`](https://www.roc-lang.org/examples/cli/Task#succeed) or, more commonly, by handling all possible errors using [`Task.attempt`](https://www.roc-lang.org/examples/cli/Task#attempt). - -## What now? - -That's it, you can start writing Roc apps now! -Modifying an example from the [examples folder](./examples) is probably a good place to start. -[Advent of Code](https://adventofcode.com/2021) problems can also be fun to get to know Roc. - -If you are hungry for more, check out the Advanced Concepts below. - -## Appendix: Advanced Concepts - -Here are some concepts you likely won't need as a beginner, but may want to know about eventually. -This is listed as an appendix rather than the main tutorial, to emphasize that it's totally fine -to stop reading here and go build things! - -### Open Records and Closed Records - -Let's say I write a function which takes a record with a `firstName` -and `lastName` field, and puts them together with a space in between: - -```swift -fullName = \user -> - "\(user.firstName) \(user.lastName)" -``` - -I can pass this function a record that has more fields than just -`firstName` and `lastName`, as long as it has *at least* both of those fields -(and both of them are strings). So any of these calls would work: - -- `fullName { firstName: "Sam", lastName: "Sample" }` -- `fullName { firstName: "Sam", lastName: "Sample", email: "blah@example.com" }` -- `fullName { age: 5, firstName: "Sam", things: 3, lastName: "Sample", role: Admin }` - -This `user` argument is an *open record* - that is, a description of a minimum set of fields -on a record, and their types. When a function takes an open record as an argument, -it's okay if you pass it a record with more fields than just the ones specified. - -In contrast, a *closed record* is one that requires an exact set of fields (and their types), -with no additional fields accepted. - -If we add a type annotation to this `fullName` function, we can choose to have it accept either -an open record or a closed record: - -```coffee -# Closed record -fullName : { firstName : Str, lastName : Str } -> Str -fullName = \user -> - "\(user.firstName) \(user.lastName)" -``` - -```coffee -# Open record (because of the `*`) -fullName : { firstName : Str, lastName : Str }* -> Str -fullName = \user -> - "\(user.firstName) \(user.lastName)" -``` - -The `*` in the type `{ firstName : Str, lastName : 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.) - -This is because record types can optionally end in a type variable. Just like how we can have `List *` -or `List a -> List a`, we can also have `{ first : Str, last : Str }*` or -`{ first : Str, last : Str }a -> { first: Str, last : Str }a`. The differences are that in `List a`, -the type variable is required and appears with a space after `List`; in a record, the type variable -is optional, and appears (with no space) immediately after `}`. - -If the type variable in a record type is a `*` (such as in `{ first : Str, last : Str }*`), then -it's an open record. If the type variable is missing, then it's a closed record. You can also specify -a closed record by putting a `{}` as the type variable (so for example, `{ email : Str }{}` is another way to write -`{ email : Str }`). In practice, closed records are basically always written without the `{}` on the end, -but later on we'll see a situation where putting types other than `*` in that spot can be useful. - -### Constrained Records - -The type variable can also be a named type variable, like so: - -```coffee -addHttps : { url : Str }a -> { url : Str }a -addHttps = \record -> - { record & url: "https://\(record.url)" } -``` - -This function uses *constrained records* in its type. The annotation is saying: - -- This function takes a record which has at least a `url` field, and possibly others -- That `url` field has the type `Str` -- It returns a record of exactly the same type as the one it was given - -So if we give this function a record with five fields, it will return a record with those -same five fields. The only requirement is that one of those fields must be `url : Str`. - -In practice, constrained records appear in type annotations much less often than open or closed records do. - -Here's when you can typically expect to encounter these three flavors of type variables in records: - -- *Open records* are what the compiler infers when you use a record as an argument, or when destructuring it (for example, `{ x, y } =`). -- *Closed records* are what the compiler infers when you create a new record (for example, `{ x: 5, y: 6 }`) -- *Constrained records* are what the compiler infers when you do a record update (for example, `{ user & email: newEmail }`) - -Of note, you can pass a closed record to a function that accepts a smaller open record, but not the reverse. -So a function `{ a : Str, b : Bool }* -> Str` can accept an `{ a : Str, b : Bool, c : Bool }` record, -but a function `{ a : Str, b : Bool, c : Bool } -> Str` would not accept an `{ a : Str, b : Bool }*` record. - -This is because if a function accepts `{ a : Str, b : Bool, c : Bool }`, that means it might access the `c` -field of that record. So if you passed it a record that was not guaranteed to have all three of those fields -present (such as an `{ a : Str, b : Bool }*` record, which only guarantees that the fields `a` and `b` are present), -the function might try to access a `c` field at runtime that did not exist! - -### Type Variables in Record Annotations - -You can add type annotations to make record types less flexible than what the compiler infers, but not more -flexible. For example, you can use an annotation to tell the compiler to treat a record as closed when it would -be inferred as open (or constrained), but you can't use an annotation to make a record open when it would be -inferred as closed. - -If you like, you can always annotate your functions as accepting open records. However, in practice this may not -always be the nicest choice. For example, let's say you have a `User` type alias, like so: - -```coffee -User : { - email : Str, - firstName : Str, - lastName : Str, -} -``` - -This defines `User` to be a closed record, which in practice is the most common way records named `User` -tend to be defined. - -If you want to have a function take a `User`, you might write its type like so: - -```elm -isValid : User -> Bool -``` - -If you want to have a function return a `User`, you might write its type like so: - -```elm -userFromEmail : Str -> User -``` - -A function which takes a user and returns a user might look like this: - -```elm -capitalizeNames : User -> User -``` - -This is a perfectly reasonable way to write all of these functions. However, I -might decide that I really want the `isValid` function to take an open record - -that is, a record with *at least* the fields of this `User` record, but possibly others as well. - -Since open records have a type variable (like `*` in `{ email : Str }*` or `a` in -`{ email : Str }a -> { email : Str }a`), in order to do this I'd need to add a -type variable to the `User` type alias: - -```coffee -User a : { - email : Str, - firstName : Str, - lastName : Str, -}a -``` - -Notice that the `a` type variable appears not only in `User a` but also in `}a` at the end of the -record type! - -Using `User a` type alias, I can still write the same three functions, but now their types need to look different. -This is what the first one would look like: - -```elm -isValid : User * -> Bool -``` - -Here, the `User *` type alias substitutes `*` for the type variable `a` in the type alias, -which takes it from `{ email : Str, … }a` to `{ email : Str, … }*`. Now I can pass it any -record that has at least the fields in `User`, and possibly others as well, which was my goal. - -```elm -userFromEmail : Str -> User {} -``` - -Here, the `User {}` type alias substitutes `{}` for the type variable `a` in the type alias, -which takes it from `{ email : Str, … }a` to `{ email : Str, … }{}`. As noted earlier, -this is another way to specify a closed record: putting a `{}` after it, in the same place that -you'd find a `*` in an open record. - -> **Aside:** This works because you can form new record types by replacing the type variable with -> other record types. For example, `{ a : Str, b : Str }` can also be written `{ a : Str }{ b : Str }`. -> You can chain these more than once, e.g. `{ a : Str }{ b : Str }{ c : Str, d : Str }`. -> This is more useful when used with type annotations; for example, `{ a : Str, b : Str }User` describes -> a closed record consisting of all the fields in the closed record `User`, plus `a : Str` and `b : Str`. - -This function still returns the same record as it always did, it just needs to be annotated as -`User {}` now instead of just `User`, because the `User` type alias has a variable in it that must be -specified. - -The third function might need to use a named type variable: - -```elm -capitalizeNames : User a -> User a -``` - -If this function does a record update on the given user, and returns that - for example, if its -definition were `capitalizeNames = \user -> { user & email: "blah" }` - then it needs to use the -same named type variable for both the argument and return value. - -However, if returns a new `User` that it created from scratch, then its type could instead be: - -```elm -capitalizeNames : User * -> User {} -``` - -This says that it takes a record with at least the fields specified in the `User` type alias, -and possibly others...and then returns a record with exactly the fields specified in the `User` -type alias, and no others. - -These three examples illustrate why it's relatively uncommon to use open records for type aliases: -it makes a lot of types need to incorporate a type variable that otherwise they could omit, -all so that `isValid` can be given something that has not only the fields `User` has, but -some others as well. (In the case of a `User` record in particular, it may be that the extra -fields were included due to a mistake rather than on purpose, and accepting an open record could -prevent the compiler from raising an error that would have revealed the mistake.) - -That said, this is a useful technique to know about if you want to (for example) make a record -type that accumulates more and more fields as it progresses through a series of operations. - -### Open and Closed Tag Unions - -Just like how Roc has open records and closed records, it also has open and closed tag unions. Similarly to how an open record can have other fields besides the ones explicitly listed, an open tag union can have other tags beyond the ones explicitly listed. - -For example, here `[Red, Green]` is a closed union like the ones we saw earlier: - -```coffee -colorToStr : [Red, Green] -> String -colorToStr = \color -> - when color is - Red -> "red" - Green -> "green" - -Now let's compare to an *open union* version: - -```coffee -colorOrOther : [Red, Green]* -> String -colorOrOther = \color -> - when color is - Red -> "red" - Green -> "green" - _ -> "other" -``` - -Two things have changed compared to the first example. -1. The `when color is` now has an extra branch: `_ -> "other"` -2. Since this branch matches any tag, the type annotation for the `color` argument changed from the closed union `[Red, Green]` to the _open union_ `[Red, Green]*`. - -Also like with open records, you can name the type variable in an open tag union. For example: - -```coffee -stopGoOther : [Red, Green]a -> [Stop, Go]a -stopGoOther = \color -> - when color is - Red -> Stop - Green -> Go - other -> other -``` - -You can read this type annotation as "`stopGoOther` takes either a `Red` tag, a `Green` tag, or some other tag. It returns either a `Stop` tag, a `Go` tag, or any one of the tags it received in its argument." - -So let's say you called this `stopGoOther` function passing `Foo "hello"`. Then the `a` type variable would be the closed union `[Foo Str]`, and `stopGoOther` would return a union with the type `[Stop, Go][Foo Str]` - which is equivalent to `[Stop, Go, Foo Str]`. - -Just like with records, you can replace the type variable in tag union types with a concrete type. -For example, `[Foo Str][Bar Bool][Baz (List Str)]` is equivalent to `[Foo Str, Bar Bool, Baz (List Str)]`. - -Also just like with records, you can use this to compose tag union type aliases. For example, you can write `NetworkError : [Timeout, Disconnected]` and then `Problem : [InvalidInput, UnknownFormat]NetworkError`. - -Note that that a function which accepts an open union does not accept "all possible tags." -For example, if I have a function `[Ok Str]* -> Bool` and I pass it -`Ok 5`, that will still be a type mismatch. A `when` on that function's argument might -have the branch `Ok str ->` which assumes there's a string inside that `Ok`, -and if `Ok 5` type-checked, then that assumption would be false and things would break! - -So `[Ok Str]*` is more restrictive than `[]*`. It's basically saying "this may or may not be an `Ok` tag, but if it _is_ an `Ok` tag, then it's guaranteed to have a payload of exactly `Str`." - -> **Note:** As with open and closed records, we can use type annotations to make tag union types less flexible -> than what the compiler would infer. For example, if we changed the type of the second -> `colorOrOther` function from the open `[Red, Green]*` to the closed `[Red, Green]`, Roc's compiler -> would accept it as a valid annotation, but it would give a warning that the `_ -> "other"` -> branch had become unreachable. - -### Phantom Types - -[This part of the tutorial has not been written yet. Coming soon!] - -### Operator Desugaring Table - -Here are various Roc expressions involving operators, and what they desugar to. - -| Expression | Desugars to | -| --------------- | ---------------- | -| `a + b` | `Num.add a b` | -| `a - b` | `Num.sub a b` | -| `a * b` | `Num.mul a b` | -| `a / b` | `Num.div a b` | -| `a // b` | `Num.divTrunc a b` | -| `a ^ b` | `Num.pow a b` | -| `a % b` | `Num.rem a b` | -| `a >> b` | `Num.shr a b` | -| `a << b` | `Num.shl a b` | -| `-a` | `Num.neg a` | -| `-f x y` | `Num.neg (f x y)` | -| `a == b` | `Bool.isEq a b` | -| `a != b` | `Bool.isNotEq a b` | -| `a && b` | `Bool.and a b` | -| `a \|\| b` | `Bool.or a b` | -| `!a` | `Bool.not a` | -| `!f x y` | `Bool.not (f x y)` | -| `a \|> b` | `b a` | -| `a b c \|> f x y` | `f (a b c) x y` | diff --git a/ci/benchmarks/prep_folder.sh b/ci/benchmarks/prep_folder.sh index 926c000cc5..486abe152c 100755 --- a/ci/benchmarks/prep_folder.sh +++ b/ci/benchmarks/prep_folder.sh @@ -14,12 +14,10 @@ cd crates/cli && cargo criterion --no-run && cd ../.. mkdir -p bench-folder/crates/cli_testing_examples/benchmarks mkdir -p bench-folder/crates/compiler/builtins/bitcode/src mkdir -p bench-folder/target/release/deps -mkdir -p bench-folder/target/release/lib cp "crates/cli_testing_examples/benchmarks/"*".roc" bench-folder/crates/cli_testing_examples/benchmarks/ cp -r crates/cli_testing_examples/benchmarks/platform bench-folder/crates/cli_testing_examples/benchmarks/ cp crates/compiler/builtins/bitcode/src/str.zig bench-folder/crates/compiler/builtins/bitcode/src cp target/release/roc bench-folder/target/release -cp -r target/release/lib bench-folder/target/release # copy the most recent time bench to bench-folder cp target/release/deps/`ls -t target/release/deps/ | grep time_bench | head -n 1` bench-folder/target/release/deps/time_bench diff --git a/ci/package_release.sh b/ci/package_release.sh index ff3755f64e..a7ed841e8f 100755 --- a/ci/package_release.sh +++ b/ci/package_release.sh @@ -4,5 +4,4 @@ set -euxo pipefail cp target/release/roc ./roc # to be able to exclude "target" later in the tar command -cp -r target/release/lib ./lib -tar -czvf $1 --exclude="target" --exclude="zig-cache" roc lib LICENSE LEGAL_DETAILS examples/helloWorld.roc examples/platform-switching examples/cli crates/roc_std +tar -czvf $1 --exclude="target" --exclude="zig-cache" roc LICENSE LEGAL_DETAILS examples/helloWorld.roc examples/platform-switching examples/cli crates/roc_std diff --git a/crates/ast/src/lang/core/def/def.rs b/crates/ast/src/lang/core/def/def.rs index 14b38b01c2..1510f6b704 100644 --- a/crates/ast/src/lang/core/def/def.rs +++ b/crates/ast/src/lang/core/def/def.rs @@ -278,6 +278,7 @@ fn to_pending_def<'a>( Type(TypeDef::Opaque { .. }) => internal_error!("opaques not implemented"), Type(TypeDef::Ability { .. }) => todo_abilities!(), + Value(AstValueDef::Dbg { .. }) => todo!(), Value(AstValueDef::Expect { .. }) => todo!(), Value(AstValueDef::ExpectFx { .. }) => todo!(), diff --git a/crates/ast/src/lang/core/types.rs b/crates/ast/src/lang/core/types.rs index 36f8f89490..7ef0b52e51 100644 --- a/crates/ast/src/lang/core/types.rs +++ b/crates/ast/src/lang/core/types.rs @@ -405,6 +405,9 @@ pub fn to_type2<'a>( Type2::Variable(var) } + Tuple { fields: _, ext: _ } => { + todo!("tuple type"); + } Record { fields, ext, .. } => { let field_types_map = can_assigned_fields(env, scope, references, &fields.items, region); diff --git a/crates/cli/src/build.rs b/crates/cli/src/build.rs index 36de753bb4..6b8a1c12bc 100644 --- a/crates/cli/src/build.rs +++ b/crates/cli/src/build.rs @@ -1,6 +1,9 @@ use bumpalo::Bump; use roc_build::{ - link::{link, preprocess_host_wasm32, rebuild_host, LinkType, LinkingStrategy}, + link::{ + legacy_host_filename, link, preprocess_host_wasm32, preprocessed_host_filename, + rebuild_host, LinkType, LinkingStrategy, + }, program::{self, CodeGenOptions}, }; use roc_builtins::bitcode; @@ -106,28 +109,27 @@ pub fn build_file<'a>( } }; - use target_lexicon::Architecture; - let emit_wasm = matches!(target.architecture, Architecture::Wasm32); - - // TODO wasm host extension should be something else ideally - // .bc does not seem to work because - // - // > Non-Emscripten WebAssembly hasn't implemented __builtin_return_address - // - // and zig does not currently emit `.a` webassembly static libraries - let (host_extension, app_extension, extension) = { + let (app_extension, extension, host_filename) = { use roc_target::OperatingSystem::*; match roc_target::OperatingSystem::from(target.operating_system) { Wasi => { if matches!(code_gen_options.opt_level, OptLevel::Development) { - ("wasm", "wasm", Some("wasm")) + ("wasm", Some("wasm"), "host.zig".to_string()) } else { - ("zig", "bc", Some("wasm")) + ("bc", Some("wasm"), "host.zig".to_string()) } } - Unix => ("o", "o", None), - Windows => ("obj", "obj", Some("exe")), + Unix => ( + "o", + None, + legacy_host_filename(target, code_gen_options.opt_level).unwrap(), + ), + Windows => ( + "obj", + Some("exe"), + legacy_host_filename(target, code_gen_options.opt_level).unwrap(), + ), } }; @@ -140,9 +142,7 @@ pub fn build_file<'a>( let host_input_path = if let EntryPoint::Executable { platform_path, .. } = &loaded.entry_point { - cwd.join(platform_path) - .with_file_name("host") - .with_extension(host_extension) + cwd.join(platform_path).with_file_name(host_filename) } else { unreachable!(); }; @@ -172,23 +172,43 @@ pub fn build_file<'a>( }) .collect(); - let preprocessed_host_path = if emit_wasm { - host_input_path.with_file_name("preprocessedhost.o") + let preprocessed_host_path = if linking_strategy == LinkingStrategy::Legacy { + host_input_path + .with_file_name(legacy_host_filename(target, code_gen_options.opt_level).unwrap()) } else { - host_input_path.with_file_name("preprocessedhost") + host_input_path.with_file_name(preprocessed_host_filename(target).unwrap()) }; - let rebuild_thread = spawn_rebuild_thread( - code_gen_options.opt_level, - linking_strategy, - prebuilt, - host_input_path.clone(), - preprocessed_host_path.clone(), - binary_path.clone(), - target, - exposed_values, - exposed_closure_types, - ); + // We don't need to spawn a rebuild thread when using a prebuilt host. + let rebuild_thread = if prebuilt { + if !preprocessed_host_path.exists() { + eprintln!( + "\nBecause I was run with --prebuilt-platform=true, I was expecting this file to exist:\n\n {}\n\nHowever, it was not there!\n\nIf you have the platform's source code locally, you may be able to regenerate it by re-running this command with --prebuilt-platform=false\n", + preprocessed_host_path.to_string_lossy() + ); + + std::process::exit(1); + } + + if linking_strategy == LinkingStrategy::Surgical { + // Copy preprocessed host to executable location. + // The surgical linker will modify that copy in-place. + std::fs::copy(&preprocessed_host_path, binary_path.as_path()).unwrap(); + } + + None + } else { + Some(spawn_rebuild_thread( + code_gen_options.opt_level, + linking_strategy, + host_input_path.clone(), + preprocessed_host_path.clone(), + binary_path.clone(), + target, + exposed_values, + exposed_closure_types, + )) + }; let buf = &mut String::with_capacity(1024); @@ -247,19 +267,24 @@ pub fn build_file<'a>( ConcurrentWithApp(JoinHandle), } - let rebuild_timing = if linking_strategy == LinkingStrategy::Additive { - let rebuild_duration = rebuild_thread - .join() - .expect("Failed to (re)build platform."); - if emit_timings && !prebuilt { - println!( - "Finished rebuilding the platform in {} ms\n", - rebuild_duration - ); + let opt_rebuild_timing = if let Some(rebuild_thread) = rebuild_thread { + if linking_strategy == LinkingStrategy::Additive { + let rebuild_duration = rebuild_thread + .join() + .expect("Failed to (re)build platform."); + if emit_timings && !prebuilt { + println!( + "Finished rebuilding the platform in {} ms\n", + rebuild_duration + ); + } + + Some(HostRebuildTiming::BeforeApp(rebuild_duration)) + } else { + Some(HostRebuildTiming::ConcurrentWithApp(rebuild_thread)) } - HostRebuildTiming::BeforeApp(rebuild_duration) } else { - HostRebuildTiming::ConcurrentWithApp(rebuild_thread) + None }; let (roc_app_bytes, code_gen_timing, expect_metadata) = program::gen_from_mono_module( @@ -300,7 +325,7 @@ pub fn build_file<'a>( ); } - if let HostRebuildTiming::ConcurrentWithApp(thread) = rebuild_timing { + if let Some(HostRebuildTiming::ConcurrentWithApp(thread)) = opt_rebuild_timing { let rebuild_duration = thread.join().expect("Failed to (re)build platform."); if emit_timings && !prebuilt { println!( @@ -361,9 +386,8 @@ pub fn build_file<'a>( inputs.push(builtins_host_tempfile.path().to_str().unwrap()); } - let (mut child, _) = // TODO use lld - link(target, binary_path.clone(), &inputs, link_type) - .map_err(|_| todo!("gracefully handle `ld` failing to spawn."))?; + let (mut child, _) = link(target, binary_path.clone(), &inputs, link_type) + .map_err(|_| todo!("gracefully handle `ld` failing to spawn."))?; let exit_status = child .wait() @@ -376,12 +400,10 @@ pub fn build_file<'a>( if exit_status.success() { problems } else { - let mut problems = problems; - - // Add an error for `ld` failing - problems.errors += 1; - - problems + todo!( + "gracefully handle `ld` (or `zig` in the case of wasm with --optimize) returning exit code {:?}", + exit_status.code() + ); } } }; @@ -406,7 +428,6 @@ pub fn build_file<'a>( fn spawn_rebuild_thread( opt_level: OptLevel, linking_strategy: LinkingStrategy, - prebuilt: bool, host_input_path: PathBuf, preprocessed_host_path: PathBuf, binary_path: PathBuf, @@ -416,54 +437,49 @@ fn spawn_rebuild_thread( ) -> std::thread::JoinHandle { let thread_local_target = target.clone(); std::thread::spawn(move || { - if !prebuilt { - // Printing to stderr because we want stdout to contain only the output of the roc program. - // We are aware of the trade-offs. - // `cargo run` follows the same approach - eprintln!("🔨 Rebuilding platform..."); - } + // Printing to stderr because we want stdout to contain only the output of the roc program. + // We are aware of the trade-offs. + // `cargo run` follows the same approach + eprintln!("🔨 Rebuilding platform..."); let rebuild_host_start = Instant::now(); - if !prebuilt { - match linking_strategy { - LinkingStrategy::Additive => { - let host_dest = rebuild_host( - opt_level, - &thread_local_target, - host_input_path.as_path(), - None, - ); + match linking_strategy { + LinkingStrategy::Additive => { + let host_dest = rebuild_host( + opt_level, + &thread_local_target, + host_input_path.as_path(), + None, + ); - preprocess_host_wasm32(host_dest.as_path(), &preprocessed_host_path); - } - LinkingStrategy::Surgical => { - roc_linker::build_and_preprocess_host( - opt_level, - &thread_local_target, - host_input_path.as_path(), - preprocessed_host_path.as_path(), - exported_symbols, - exported_closure_types, - ); - } - LinkingStrategy::Legacy => { - rebuild_host( - opt_level, - &thread_local_target, - host_input_path.as_path(), - None, - ); - } + preprocess_host_wasm32(host_dest.as_path(), &preprocessed_host_path); + } + LinkingStrategy::Surgical => { + roc_linker::build_and_preprocess_host( + opt_level, + &thread_local_target, + host_input_path.as_path(), + preprocessed_host_path.as_path(), + exported_symbols, + exported_closure_types, + ); + + // Copy preprocessed host to executable location. + // The surgical linker will modify that copy in-place. + std::fs::copy(&preprocessed_host_path, binary_path.as_path()).unwrap(); + } + LinkingStrategy::Legacy => { + rebuild_host( + opt_level, + &thread_local_target, + host_input_path.as_path(), + None, + ); } } - if linking_strategy == LinkingStrategy::Surgical { - // Copy preprocessed host to executable location. - std::fs::copy(preprocessed_host_path, binary_path.as_path()).unwrap(); - } - let rebuild_host_end = rebuild_host_start.elapsed(); - rebuild_host_end.as_millis() + rebuild_host_start.elapsed().as_millis() }) } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 15eb1b57a5..f4d0fd3969 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -940,7 +940,11 @@ fn roc_dev_native( expect_metadata: ExpectMetadata, ) -> ! { use roc_repl_expect::run::ExpectMemory; - use signal_hook::{consts::signal::SIGCHLD, consts::signal::SIGUSR1, iterator::Signals}; + use signal_hook::{ + consts::signal::SIGCHLD, + consts::signal::{SIGUSR1, SIGUSR2}, + iterator::Signals, + }; let ExpectMetadata { mut expectations, @@ -948,7 +952,7 @@ fn roc_dev_native( layout_interner, } = expect_metadata; - let mut signals = Signals::new(&[SIGCHLD, SIGUSR1]).unwrap(); + let mut signals = Signals::new(&[SIGCHLD, SIGUSR1, SIGUSR2]).unwrap(); // let shm_name = let shm_name = format!("/roc_expect_buffer_{}", std::process::id()); @@ -994,6 +998,19 @@ fn roc_dev_native( ) .unwrap(); } + SIGUSR2 => { + // this is the signal we use for a dbg + + roc_repl_expect::run::render_dbgs_in_memory( + &mut writer, + arena, + &mut expectations, + &interns, + &layout_interner, + &memory, + ) + .unwrap(); + } _ => println!("received signal {}", sig), } } diff --git a/crates/cli/tests/cli_run.rs b/crates/cli/tests/cli_run.rs index 0b4330dca9..abb58f7a65 100644 --- a/crates/cli/tests/cli_run.rs +++ b/crates/cli/tests/cli_run.rs @@ -1056,50 +1056,29 @@ mod cli_run { &[], indoc!( r#" - ── TYPE MISMATCH ─ ...known_bad/../../../../examples/cli/cli-platform/main.roc ─ + ── TYPE MISMATCH ─────────────────────────────── tests/known_bad/TypeError.roc ─ - Something is off with the type annotation of the main required symbol: + This 2nd argument to attempt has an unexpected type: - 2│ requires {} { main : InternalProgram } - ^^^^^^^^^^^^^^^ + 15│> Task.attempt task /result -> + 16│> when result is + 17│> Ok {} -> Stdout.line "Done!" + 18│> # Type mismatch because the File.readUtf8 error case is not handled + 19│> Err {} -> Stdout.line "Problem!" - This #UserApp.main value is a: + The argument is an anonymous function of type: - Task.Task {} * [Write [Stdout]] + [Err {}a, Ok {}] -> Task {} * - But the type annotation on main says it should be: + But attempt needs its 2nd argument to be: - InternalProgram.InternalProgram - - Tip: Type comparisons between an opaque type are only ever equal if - both types are the same opaque type. Did you mean to create an opaque - type by wrapping it? If I have an opaque type Age := U32 I can create - an instance of this opaque type by doing @Age 23. - - - ── TYPE MISMATCH ─ ...known_bad/../../../../examples/cli/cli-platform/main.roc ─ - - This 1st argument to toEffect has an unexpected type: - - 9│ mainForHost = InternalProgram.toEffect main - ^^^^ - - This #UserApp.main value is a: - - Task.Task {} * [Write [Stdout]] - - But toEffect needs its 1st argument to be: - - InternalProgram.InternalProgram - - Tip: Type comparisons between an opaque type are only ever equal if - both types are the same opaque type. Did you mean to create an opaque - type by wrapping it? If I have an opaque type Age := U32 I can create - an instance of this opaque type by doing @Age 23. + Result {} [FileReadErr Path.Path InternalFile.ReadErr, + FileReadUtf8Err Path.Path [BadUtf8 Utf8ByteProblem Nat]*]* -> + Task {} * ──────────────────────────────────────────────────────────────────────────────── - 2 errors and 1 warning found in ms."# + 1 error and 0 warnings found in ms."# ), ); } diff --git a/crates/cli/tests/known_bad/TypeError.roc b/crates/cli/tests/known_bad/TypeError.roc index 0d7e15f6d7..c07a8fb773 100644 --- a/crates/cli/tests/known_bad/TypeError.roc +++ b/crates/cli/tests/known_bad/TypeError.roc @@ -1,13 +1,19 @@ app "type-error" packages { pf: "../../../../examples/cli/cli-platform/main.roc" } - imports [pf.Stdout.{ line }, pf.Task.{ await }, pf.Program] + imports [pf.Stdout.{ line }, pf.Task.{ await }, pf.Path, pf.File] provides [main] to pf main = - _ <- await (line "a") - _ <- await (line "b") - _ <- await (line "c") - _ <- await (line "d") - line "e" - # Type mismatch because this line is missing: - # |> Program.quick + task = + _ <- await (line "a") + _ <- await (line "b") + _ <- await (line "c") + _ <- await (line "d") + _ <- await (File.readUtf8 (Path.fromStr "blah.txt")) + line "e" + + Task.attempt task \result -> + when result is + Ok {} -> Stdout.line "Done!" + # Type mismatch because the File.readUtf8 error case is not handled + Err {} -> Stdout.line "Problem!" diff --git a/crates/compiler/alias_analysis/src/lib.rs b/crates/compiler/alias_analysis/src/lib.rs index d3f91bb538..33a6247843 100644 --- a/crates/compiler/alias_analysis/src/lib.rs +++ b/crates/compiler/alias_analysis/src/lib.rs @@ -640,10 +640,13 @@ fn stmt_spec<'a>( let jpid = env.join_points[id]; builder.add_jump(block, jpid, argument, ret_type_id) } - RuntimeError(_) => { - let type_id = layout_spec(env, builder, interner, layout, &WhenRecursive::Unreachable)?; + Crash(msg, _) => { + // Model this as a foreign call rather than TERMINATE because + // we want ownership of the message. + let result_type = + layout_spec(env, builder, interner, layout, &WhenRecursive::Unreachable)?; - builder.add_terminate(block, type_id) + builder.add_unknown_with(block, &[env.symbols[msg]], result_type) } } } diff --git a/crates/compiler/build/Cargo.toml b/crates/compiler/build/Cargo.toml index ff6ef6d59d..de8e82e4d7 100644 --- a/crates/compiler/build/Cargo.toml +++ b/crates/compiler/build/Cargo.toml @@ -31,11 +31,12 @@ roc_utils = { path = "../../utils" } wasi_libc_sys = { path = "../../wasi-libc-sys" } +const_format.workspace = true bumpalo.workspace = true libloading.workspace = true tempfile.workspace = true target-lexicon.workspace = true -inkwell.workspace = true +inkwell.workspace = true [target.'cfg(target_os = "macos")'.dependencies] serde_json = "1.0.85" diff --git a/crates/compiler/build/src/link.rs b/crates/compiler/build/src/link.rs index 579d3b75fc..062fcc102f 100644 --- a/crates/compiler/build/src/link.rs +++ b/crates/compiler/build/src/link.rs @@ -1,4 +1,5 @@ use crate::target::{arch_str, target_zig_str}; +use const_format::concatcp; use libloading::{Error, Library}; use roc_builtins::bitcode; use roc_error_macros::internal_error; @@ -59,6 +60,86 @@ pub fn link( } } +const fn legacy_host_filename_ext( + os: roc_target::OperatingSystem, + opt_level: OptLevel, +) -> &'static str { + use roc_target::OperatingSystem::*; + + match os { + Wasi => { + // TODO wasm host extension should be something else ideally + // .bc does not seem to work because + // + // > Non-Emscripten WebAssembly hasn't implemented __builtin_return_address + // + // and zig does not currently emit `.a` webassembly static libraries + if matches!(opt_level, OptLevel::Development) { + "wasm" + } else { + "zig" + } + } + Unix => "o", + Windows => "obj", + } +} + +const PRECOMPILED_HOST_EXT: &str = "rh1"; // Short for "roc host version 1" (so we can change format in the future) + +pub const fn preprocessed_host_filename(target: &Triple) -> Option<&'static str> { + match target { + Triple { + architecture: Architecture::Wasm32, + .. + } => Some(concatcp!("wasm32", '.', PRECOMPILED_HOST_EXT)), + Triple { + operating_system: OperatingSystem::Linux, + architecture: Architecture::X86_64, + .. + } => Some(concatcp!("linux-x64", '.', PRECOMPILED_HOST_EXT)), + Triple { + operating_system: OperatingSystem::Linux, + architecture: Architecture::Aarch64(_), + .. + } => Some(concatcp!("linux-arm64", '.', PRECOMPILED_HOST_EXT)), + Triple { + operating_system: OperatingSystem::Darwin, + architecture: Architecture::Aarch64(_), + .. + } => Some(concatcp!("macos-arm64", '.', PRECOMPILED_HOST_EXT)), + Triple { + operating_system: OperatingSystem::Darwin, + architecture: Architecture::X86_64, + .. + } => Some(concatcp!("macos-x64", '.', PRECOMPILED_HOST_EXT)), + Triple { + operating_system: OperatingSystem::Windows, + architecture: Architecture::X86_64, + .. + } => Some(concatcp!("windows-x64", '.', PRECOMPILED_HOST_EXT)), + Triple { + operating_system: OperatingSystem::Windows, + architecture: Architecture::X86_32(_), + .. + } => Some(concatcp!("windows-x86", '.', PRECOMPILED_HOST_EXT)), + Triple { + operating_system: OperatingSystem::Windows, + architecture: Architecture::Aarch64(_), + .. + } => Some(concatcp!("windows-arm64", '.', PRECOMPILED_HOST_EXT)), + _ => None, + } +} + +/// Same format as the precompiled host filename, except with a file extension like ".o" or ".obj" +pub fn legacy_host_filename(target: &Triple, opt_level: OptLevel) -> Option { + let os = roc_target::OperatingSystem::from(target.operating_system); + let ext = legacy_host_filename_ext(os, opt_level); + + Some(preprocessed_host_filename(target)?.replace(PRECOMPILED_HOST_EXT, ext)) +} + fn find_zig_str_path() -> PathBuf { // First try using the lib path relative to the executable location. let lib_path_opt = get_lib_path(); @@ -489,6 +570,10 @@ pub fn build_swift_host_native( .arg("swiftc") .args(sources) .arg("-emit-object") + // `-module-name host` renames the .o file to "host" - otherwise you get an error like: + // error: module name "legacy_macos-arm64" is not a valid identifier; use -module-name flag to specify an alternate name + .arg("-module-name") + .arg("host") .arg("-parse-as-library") .args(["-o", dest]); @@ -527,26 +612,18 @@ pub fn rebuild_host( roc_target::OperatingSystem::Wasi => "", }; - let object_extension = match os { - roc_target::OperatingSystem::Windows => "obj", - roc_target::OperatingSystem::Unix => "o", - roc_target::OperatingSystem::Wasi => "o", - }; - let host_dest = if matches!(target.architecture, Architecture::Wasm32) { if matches!(opt_level, OptLevel::Development) { - host_input_path.with_file_name("host.o") + host_input_path.with_extension("o") } else { - host_input_path.with_file_name("host.bc") + host_input_path.with_extension("bc") } } else if shared_lib_path.is_some() { host_input_path .with_file_name("dynhost") .with_extension(executable_extension) } else { - host_input_path - .with_file_name("host") - .with_extension(object_extension) + host_input_path.with_file_name(legacy_host_filename(target, opt_level).unwrap()) }; let env_path = env::var("PATH").unwrap_or_else(|_| "".to_string()); diff --git a/crates/compiler/builtins/bitcode/src/dec.zig b/crates/compiler/builtins/bitcode/src/dec.zig index 92738c7351..3910d98a2d 100644 --- a/crates/compiler/builtins/bitcode/src/dec.zig +++ b/crates/compiler/builtins/bitcode/src/dec.zig @@ -7,7 +7,7 @@ const math = std.math; const always_inline = std.builtin.CallOptions.Modifier.always_inline; const RocStr = str.RocStr; const WithOverflow = utils.WithOverflow; -const roc_panic = utils.panic; +const roc_panic = @import("panic.zig").panic_help; const U256 = num_.U256; const mul_u128 = num_.mul_u128; @@ -233,7 +233,7 @@ pub const RocDec = extern struct { const answer = RocDec.addWithOverflow(self, other); if (answer.has_overflowed) { - roc_panic("Decimal addition overflowed!", 1); + roc_panic("Decimal addition overflowed!", 0); unreachable; } else { return answer.value; @@ -265,7 +265,7 @@ pub const RocDec = extern struct { const answer = RocDec.subWithOverflow(self, other); if (answer.has_overflowed) { - roc_panic("Decimal subtraction overflowed!", 1); + roc_panic("Decimal subtraction overflowed!", 0); unreachable; } else { return answer.value; @@ -329,7 +329,7 @@ pub const RocDec = extern struct { const answer = RocDec.mulWithOverflow(self, other); if (answer.has_overflowed) { - roc_panic("Decimal multiplication overflowed!", 1); + roc_panic("Decimal multiplication overflowed!", 0); unreachable; } else { return answer.value; diff --git a/crates/compiler/builtins/bitcode/src/expect.zig b/crates/compiler/builtins/bitcode/src/expect.zig index 3e250b5b72..769f54e5b9 100644 --- a/crates/compiler/builtins/bitcode/src/expect.zig +++ b/crates/compiler/builtins/bitcode/src/expect.zig @@ -2,6 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const SIGUSR1: c_int = if (builtin.os.tag.isDarwin()) 30 else 10; +const SIGUSR2: c_int = if (builtin.os.tag.isDarwin()) 31 else 12; const O_RDWR: c_int = 2; const O_CREAT: c_int = 64; @@ -87,3 +88,11 @@ pub fn expectFailedFinalize() callconv(.C) void { _ = roc_send_signal(parent_pid, SIGUSR1); } } + +pub fn sendDbg() callconv(.C) void { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + const parent_pid = roc_getppid(); + + _ = roc_send_signal(parent_pid, SIGUSR2); + } +} diff --git a/crates/compiler/builtins/bitcode/src/main.zig b/crates/compiler/builtins/bitcode/src/main.zig index 152284aebc..5b2ebb5bef 100644 --- a/crates/compiler/builtins/bitcode/src/main.zig +++ b/crates/compiler/builtins/bitcode/src/main.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); const math = std.math; const utils = @import("utils.zig"); const expect = @import("expect.zig"); +const panic_utils = @import("panic.zig"); const ROC_BUILTINS = "roc_builtins"; const NUM = "num"; @@ -166,12 +167,13 @@ comptime { exportUtilsFn(utils.decrefCheckNullC, "decref_check_null"); exportUtilsFn(utils.allocateWithRefcountC, "allocate_with_refcount"); - @export(utils.panic, .{ .name = "roc_builtins.utils." ++ "panic", .linkage = .Weak }); + @export(panic_utils.panic, .{ .name = "roc_builtins.utils." ++ "panic", .linkage = .Weak }); if (builtin.target.cpu.arch != .wasm32) { exportUtilsFn(expect.expectFailedStartSharedBuffer, "expect_failed_start_shared_buffer"); exportUtilsFn(expect.expectFailedStartSharedFile, "expect_failed_start_shared_file"); exportUtilsFn(expect.expectFailedFinalize, "expect_failed_finalize"); + exportUtilsFn(expect.sendDbg, "send_dbg"); // sets the buffer used for expect failures @export(expect.setSharedBuffer, .{ .name = "set_shared_buffer", .linkage = .Weak }); diff --git a/crates/compiler/builtins/bitcode/src/num.zig b/crates/compiler/builtins/bitcode/src/num.zig index 23670f4142..cb8c2498f2 100644 --- a/crates/compiler/builtins/bitcode/src/num.zig +++ b/crates/compiler/builtins/bitcode/src/num.zig @@ -4,7 +4,7 @@ const math = std.math; const RocList = @import("list.zig").RocList; const RocStr = @import("str.zig").RocStr; const WithOverflow = @import("utils.zig").WithOverflow; -const roc_panic = @import("utils.zig").panic; +const roc_panic = @import("panic.zig").panic_help; pub fn NumParseResult(comptime T: type) type { // on the roc side we sort by alignment; putting the errorcode last @@ -284,7 +284,7 @@ pub fn exportAddOrPanic(comptime T: type, comptime name: []const u8) void { fn func(self: T, other: T) callconv(.C) T { const result = addWithOverflow(T, self, other); if (result.has_overflowed) { - roc_panic("integer addition overflowed!", 1); + roc_panic("integer addition overflowed!", 0); unreachable; } else { return result.value; @@ -343,7 +343,7 @@ pub fn exportSubOrPanic(comptime T: type, comptime name: []const u8) void { fn func(self: T, other: T) callconv(.C) T { const result = subWithOverflow(T, self, other); if (result.has_overflowed) { - roc_panic("integer subtraction overflowed!", 1); + roc_panic("integer subtraction overflowed!", 0); unreachable; } else { return result.value; @@ -451,7 +451,7 @@ pub fn exportMulOrPanic(comptime T: type, comptime W: type, comptime name: []con fn func(self: T, other: T) callconv(.C) T { const result = @call(.{ .modifier = always_inline }, mulWithOverflow, .{ T, W, self, other }); if (result.has_overflowed) { - roc_panic("integer multiplication overflowed!", 1); + roc_panic("integer multiplication overflowed!", 0); unreachable; } else { return result.value; diff --git a/crates/compiler/builtins/bitcode/src/panic.zig b/crates/compiler/builtins/bitcode/src/panic.zig new file mode 100644 index 0000000000..918f1f0e02 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/panic.zig @@ -0,0 +1,16 @@ +const std = @import("std"); +const RocStr = @import("str.zig").RocStr; +const always_inline = std.builtin.CallOptions.Modifier.always_inline; + +// Signals to the host that the program has panicked +extern fn roc_panic(msg: *const RocStr, tag_id: u32) callconv(.C) void; + +pub fn panic_help(msg: []const u8, tag_id: u32) void { + var str = RocStr.init(msg.ptr, msg.len); + roc_panic(&str, tag_id); +} + +// must export this explicitly because right now it is not used from zig code +pub fn panic(msg: *const RocStr, alignment: u32) callconv(.C) void { + return @call(.{ .modifier = always_inline }, roc_panic, .{ msg, alignment }); +} diff --git a/crates/compiler/builtins/bitcode/src/utils.zig b/crates/compiler/builtins/bitcode/src/utils.zig index 94a6190f8d..c8ab9f0039 100644 --- a/crates/compiler/builtins/bitcode/src/utils.zig +++ b/crates/compiler/builtins/bitcode/src/utils.zig @@ -16,9 +16,6 @@ extern fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, align // This should never be passed a null pointer. extern fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void; -// Signals to the host that the program has panicked -extern fn roc_panic(c_ptr: *const anyopaque, tag_id: u32) callconv(.C) void; - // should work just like libc memcpy (we can't assume libc is present) extern fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; @@ -108,11 +105,6 @@ pub fn dealloc(c_ptr: [*]u8, alignment: u32) void { return @call(.{ .modifier = always_inline }, roc_dealloc, .{ c_ptr, alignment }); } -// must export this explicitly because right now it is not used from zig code -pub fn panic(c_ptr: *const anyopaque, alignment: u32) callconv(.C) void { - return @call(.{ .modifier = always_inline }, roc_panic, .{ c_ptr, alignment }); -} - pub fn memcpy(dst: [*]u8, src: [*]u8, size: usize) void { @call(.{ .modifier = always_inline }, roc_memcpy, .{ dst, src, size }); } diff --git a/crates/compiler/builtins/roc/Str.roc b/crates/compiler/builtins/roc/Str.roc index 1ebb83ca0a..0c3adf3352 100644 --- a/crates/compiler/builtins/roc/Str.roc +++ b/crates/compiler/builtins/roc/Str.roc @@ -139,58 +139,80 @@ Utf8ByteProblem : [ Utf8Problem : { byteIndex : Nat, problem : Utf8ByteProblem } -## Returns `Bool.true` if the string is empty, and `Bool.false` otherwise. +## Returns [Bool.true] if the string is empty, and [Bool.false] otherwise. ## ## expect Str.isEmpty "hi!" == Bool.false ## expect Str.isEmpty "" == Bool.true isEmpty : Str -> Bool -## Concatenate two [Str] values together. +## Concatenates two strings together. ## -## expect Str.concat "Hello" "World" == "HelloWorld" +## expect Str.concat "ab" "cd" == "abcd" +## expect Str.concat "hello" "" == "hello" +## expect Str.concat "" "" == "" concat : Str, Str -> Str -## Returns a [Str] of the specified capacity [Num] without any content +## Returns a string of the specified capacity without any content. withCapacity : Nat -> Str -## Combine a [List] of [Str] into a single [Str], with a separator -## [Str] in between each. +## Combines a [List] of strings into a single string, with a separator +## string in between each. ## ## expect Str.joinWith ["one", "two", "three"] ", " == "one, two, three" ## expect Str.joinWith ["1", "2", "3", "4"] "." == "1.2.3.4" joinWith : List Str, Str -> Str -## Split a [Str] around a separator. Passing `""` for the separator is not -## useful; it returns the original string wrapped in a list. To split a string +## Split a string around a separator. +## +## Passing `""` for the separator is not useful; +## it returns the original string wrapped in a [List]. To split a string ## into its individual [graphemes](https://stackoverflow.com/a/27331885/4200103), use `Str.graphemes` ## ## expect Str.split "1,2,3" "," == ["1","2","3"] ## expect Str.split "1,2,3" "" == ["1,2,3"] split : Str, Str -> List Str -## Repeat a given [Str] value [Nat] times. +## Repeats a string the given number of times. ## -## expect Str.repeat ">" 3 == ">>>" +## expect Str.repeat "z" 3 == "zzz" +## expect Str.repeat "na" 8 == "nananananananana" +## +## Returns `""` when given `""` for the string or `0` for the count. +## +## expect Str.repeat "" 10 == "" +## expect Str.repeat "anything" 0 == "" repeat : Str, Nat -> Str -## Count the number of [extended grapheme clusters](http://www.unicode.org/glossary/#extended_grapheme_cluster) +## Counts the number of [extended grapheme clusters](http://www.unicode.org/glossary/#extended_grapheme_cluster) ## in the string. ## -## expect Str.countGraphemes "Roc!" == 4 -## expect Str.countGraphemes "‰∏ÉÂ∑ßÊùø" == 9 -## expect Str.countGraphemes "üïä" == 4 +## Note that the number of extended grapheme clusters can be different from the number +## of visual glyphs rendered! Consider the following examples: +## +## expect Str.countGraphemes "Roc" == 3 +## expect Str.countGraphemes "👩‍👩‍👦‍👦" == 4 +## expect Str.countGraphemes "🕊" == 1 +## +## Note that "👩‍👩‍👦‍👦" takes up 4 graphemes (even though visually it appears as a single +## glyph) because under the hood it's represented using an emoji modifier sequence. +## In contrast, "🕊" only takes up 1 grapheme because under the hood it's represented +## using a single Unicode code point. countGraphemes : Str -> Nat ## Split a string into its constituent grapheme clusters graphemes : Str -> List Str ## If the string begins with a [Unicode code point](http://www.unicode.org/glossary/#code_point) -## equal to the given [U32], return `Bool.true`. Otherwise return `Bool.false`. +## equal to the given [U32], returns [Bool.true]. Otherwise returns [Bool.false]. ## -## If the given [Str] is empty, or if the given [U32] is not a valid -## code point, this will return `Bool.false`. +## If the given string is empty, or if the given [U32] is not a valid +## code point, returns [Bool.false]. ## -## **Performance Note:** This runs slightly faster than `Str.startsWith`, so +## expect Str.startsWithScalar "鹏 means 'roc'" 40527 # "鹏" is Unicode scalar 40527 +## expect !Str.startsWithScalar "9" 9 # the Unicode scalar for "9" is 57, not 9 +## expect !Str.startsWithScalar "" 40527 +## +## **Performance Note:** This runs slightly faster than [Str.startsWith], so ## if you want to check whether a string begins with something that's representable ## in a single code point, you can use (for example) `Str.startsWithScalar '鹏'` ## instead of `Str.startsWith "鹏"`. ('鹏' evaluates to the [U32] value `40527`.) @@ -200,26 +222,41 @@ graphemes : Str -> List Str ## You'd need to use `Str.startsWithScalar "🕊"` instead. startsWithScalar : Str, U32 -> Bool -## Return a [List] of the [unicode scalar values](https://unicode.org/glossary/#unicode_scalar_value) -## in the given string. Strings contain only scalar values, not [surrogate code points](https://unicode.org/glossary/#surrogate_code_point), -## so this is equivalent to returning a list of the string's [code points](https://unicode.org/glossary/#code_point). +## Returns a [List] of the [Unicode scalar values](https://unicode.org/glossary/#unicode_scalar_value) +## in the given string. ## +## (Roc strings contain only scalar values, not [surrogate code points](https://unicode.org/glossary/#surrogate_code_point), +## so this is equivalent to returning a list of the string's [code points](https://unicode.org/glossary/#code_point).) +## +## expect Str.toScalars "Roc" == [82, 111, 99] +## expect Str.toScalars "鹏" == [40527] +## expect Str.toScalars "சி" == [2970, 3007] +## expect Str.toScalars "🐦" == [128038] +## expect Str.toScalars "👩‍👩‍👦‍👦" == [128105, 8205, 128105, 8205, 128102, 8205, 128102] ## expect Str.toScalars "I ♥ Roc" == [73, 32, 9829, 32, 82, 111, 99] +## expect Str.toScalars "" == [] toScalars : Str -> List U32 -## Return a [List] of the string's [U8] UTF-8 [code units](https://unicode.org/glossary/#code_unit). -## To split the string into a [List] of smaller [Str] values instead of [U8] values, -## see `Str.split`. +## Returns a [List] of the string's [U8] UTF-8 [code units](https://unicode.org/glossary/#code_unit). +## (To split the string into a [List] of smaller [Str] values instead of [U8] values, +## see [Str.split].) ## +## expect Str.toUtf8 "Roc" == [82, 111, 99] ## expect Str.toUtf8 "鹏" == [233, 185, 143] +## expect Str.toUtf8 "சி" == [224, 174, 154, 224, 174, 191] ## expect Str.toUtf8 "🐦" == [240, 159, 144, 166] toUtf8 : Str -> List U8 -## Encode a [List] of [U8] UTF-8 [code units](https://unicode.org/glossary/#code_unit) -## into a [Str] +## Converts a [List] of [U8] UTF-8 [code units](https://unicode.org/glossary/#code_unit) to a string. ## +## Returns `Err` if the given bytes are invalid UTF-8, and returns `Ok ""` when given `[]`. +## +## expect Str.fromUtf8 [82, 111, 99] == Ok "Roc" ## expect Str.fromUtf8 [233, 185, 143] == Ok "鹏" -## expect Str.fromUtf8 [0xb0] == Err (BadUtf8 InvalidStartByte 0) +## expect Str.fromUtf8 [224, 174, 154, 224, 174, 191] == Ok "சி" +## expect Str.fromUtf8 [240, 159, 144, 166] == Ok "🐦" +## expect Str.fromUtf8 [] == Ok "" +## expect Str.fromUtf8 [255] |> Result.isErr fromUtf8 : List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat] fromUtf8 = \bytes -> result = fromUtf8RangeLowlevel bytes 0 (List.len bytes) @@ -673,14 +710,14 @@ walkUtf8WithIndexHelp = \string, state, step, index, length -> else state -## Enlarge the given [Str] for at least capacity additional bytes. +## Enlarge a string for at least the given number additional bytes. reserve : Str, Nat -> Str ## is UB when the scalar is invalid appendScalarUnsafe : Str, U32 -> Str -## Append a [U32] scalar to the given [Str]. If the given scalar is not a valid -## unicode value, it will return [Err InvalidScalar]. +## Append a [U32] scalar to the given string. If the given scalar is not a valid +## unicode value, it returns [Err InvalidScalar]. ## ## expect Str.appendScalar "H" 105 == Ok "Hi" ## expect Str.appendScalar "😢" 0xabcdef == Err InvalidScalar diff --git a/crates/compiler/builtins/src/bitcode.rs b/crates/compiler/builtins/src/bitcode.rs index 67ee584e9a..1a77570dc1 100644 --- a/crates/compiler/builtins/src/bitcode.rs +++ b/crates/compiler/builtins/src/bitcode.rs @@ -426,6 +426,7 @@ pub const UTILS_EXPECT_FAILED_START_SHARED_FILE: &str = "roc_builtins.utils.expect_failed_start_shared_file"; pub const UTILS_EXPECT_FAILED_FINALIZE: &str = "roc_builtins.utils.expect_failed_finalize"; pub const UTILS_EXPECT_READ_ENV_SHARED_BUFFER: &str = "roc_builtins.utils.read_env_shared_buffer"; +pub const UTILS_SEND_DBG: &str = "roc_builtins.utils.send_dbg"; pub const UTILS_LONGJMP: &str = "longjmp"; pub const UTILS_SETJMP: &str = "setjmp"; diff --git a/crates/compiler/can/src/annotation.rs b/crates/compiler/can/src/annotation.rs index 15e24ea0fd..a71a2c8f66 100644 --- a/crates/compiler/can/src/annotation.rs +++ b/crates/compiler/can/src/annotation.rs @@ -448,6 +448,9 @@ pub fn find_type_def_symbols( As(actual, _, _) => { stack.push(&actual.value); } + Tuple { fields: _, ext: _ } => { + todo!("find_type_def_symbols: Tuple"); + } Record { fields, ext } => { let mut inner_stack = Vec::with_capacity(fields.items.len()); @@ -869,6 +872,9 @@ fn can_annotation_help( } } + Tuple { fields: _, ext: _ } => { + todo!("tuple"); + } Record { fields, ext } => { let ext_type = can_extension_type( env, diff --git a/crates/compiler/can/src/builtins.rs b/crates/compiler/can/src/builtins.rs index 05688ed656..087bd7e836 100644 --- a/crates/compiler/can/src/builtins.rs +++ b/crates/compiler/can/src/builtins.rs @@ -87,6 +87,7 @@ macro_rules! map_symbol_to_lowlevel_and_arity { LowLevel::PtrCast => unimplemented!(), LowLevel::RefCountInc => unimplemented!(), LowLevel::RefCountDec => unimplemented!(), + LowLevel::Dbg => unimplemented!(), // these are not implemented, not sure why LowLevel::StrFromInt => unimplemented!(), diff --git a/crates/compiler/can/src/copy.rs b/crates/compiler/can/src/copy.rs index 71c18ef9dc..83e8ab9d71 100644 --- a/crates/compiler/can/src/copy.rs +++ b/crates/compiler/can/src/copy.rs @@ -376,6 +376,10 @@ fn deep_copy_expr_help(env: &mut C, copied: &mut Vec, expr *called_via, ) } + Crash { msg, ret_var } => Crash { + msg: Box::new(msg.map(|m| go_help!(m))), + ret_var: sub!(*ret_var), + }, RunLowLevel { op, args, ret_var } => RunLowLevel { op: *op, args: args @@ -611,6 +615,18 @@ fn deep_copy_expr_help(env: &mut C, copied: &mut Vec, expr lookups_in_cond: lookups_in_cond.to_vec(), }, + Dbg { + loc_condition, + loc_continuation, + variable, + symbol, + } => Dbg { + loc_condition: Box::new(loc_condition.map(|e| go_help!(e))), + loc_continuation: Box::new(loc_continuation.map(|e| go_help!(e))), + variable: sub!(*variable), + symbol: *symbol, + }, + TypedHole(v) => TypedHole(sub!(*v)), RuntimeError(err) => RuntimeError(err.clone()), diff --git a/crates/compiler/can/src/def.rs b/crates/compiler/can/src/def.rs index b49b888cac..c9b389cee1 100644 --- a/crates/compiler/can/src/def.rs +++ b/crates/compiler/can/src/def.rs @@ -88,20 +88,21 @@ pub struct Annotation { #[derive(Debug)] pub(crate) struct CanDefs { defs: Vec>, - expects: Expects, - expects_fx: Expects, + dbgs: ExpectsOrDbgs, + expects: ExpectsOrDbgs, + expects_fx: ExpectsOrDbgs, def_ordering: DefOrdering, aliases: VecMap, } #[derive(Clone, Debug)] -pub struct Expects { +pub struct ExpectsOrDbgs { pub conditions: Vec, pub regions: Vec, pub preceding_comment: Vec, } -impl Expects { +impl ExpectsOrDbgs { fn with_capacity(capacity: usize) -> Self { Self { conditions: Vec::with_capacity(capacity), @@ -239,8 +240,8 @@ pub enum Declaration { Declare(Def), DeclareRec(Vec, IllegalCycleMark), Builtin(Def), - Expects(Expects), - ExpectsFx(Expects), + Expects(ExpectsOrDbgs), + ExpectsFx(ExpectsOrDbgs), /// If we know a cycle is illegal during canonicalization. /// Otherwise we will try to detect this during solving; see [`IllegalCycleMark`]. InvalidCycle(Vec), @@ -1017,6 +1018,7 @@ fn canonicalize_value_defs<'a>( // the ast::Expr values in pending_exprs for further canonicalization // once we've finished assembling the entire scope. let mut pending_value_defs = Vec::with_capacity(value_defs.len()); + let mut pending_dbgs = Vec::with_capacity(value_defs.len()); let mut pending_expects = Vec::with_capacity(value_defs.len()); let mut pending_expect_fx = Vec::with_capacity(value_defs.len()); @@ -1030,10 +1032,12 @@ fn canonicalize_value_defs<'a>( pending_value_defs.push(pending_def); } PendingValue::SignatureDefMismatch => { /* skip */ } + PendingValue::Dbg(pending_dbg) => { + pending_dbgs.push(pending_dbg); + } PendingValue::Expect(pending_expect) => { pending_expects.push(pending_expect); } - PendingValue::ExpectFx(pending_expect) => { pending_expect_fx.push(pending_expect); } @@ -1094,8 +1098,23 @@ fn canonicalize_value_defs<'a>( def_ordering.insert_symbol_references(def_id as u32, &temp_output.references) } - let mut expects = Expects::with_capacity(pending_expects.len()); - let mut expects_fx = Expects::with_capacity(pending_expects.len()); + let mut dbgs = ExpectsOrDbgs::with_capacity(pending_dbgs.len()); + let mut expects = ExpectsOrDbgs::with_capacity(pending_expects.len()); + let mut expects_fx = ExpectsOrDbgs::with_capacity(pending_expects.len()); + + for pending in pending_dbgs { + let (loc_can_condition, can_output) = canonicalize_expr( + env, + var_store, + scope, + pending.condition.region, + &pending.condition.value, + ); + + dbgs.push(loc_can_condition, pending.preceding_comment); + + output.union(can_output); + } for pending in pending_expects { let (loc_can_condition, can_output) = canonicalize_expr( @@ -1127,6 +1146,7 @@ fn canonicalize_value_defs<'a>( let can_defs = CanDefs { defs, + dbgs, expects, expects_fx, def_ordering, @@ -1534,6 +1554,7 @@ pub(crate) fn sort_can_defs_new( ) -> (Declarations, Output) { let CanDefs { defs, + dbgs: _, expects, expects_fx, def_ordering, @@ -1750,6 +1771,7 @@ pub(crate) fn sort_can_defs( ) -> (Vec, Output) { let CanDefs { mut defs, + dbgs, expects, expects_fx, def_ordering, @@ -1852,6 +1874,10 @@ pub(crate) fn sort_can_defs( } } + if !dbgs.conditions.is_empty() { + declarations.push(Declaration::Expects(dbgs)); + } + if !expects.conditions.is_empty() { declarations.push(Declaration::Expects(expects)); } @@ -2581,12 +2607,13 @@ fn to_pending_type_def<'a>( enum PendingValue<'a> { Def(PendingValueDef<'a>), - Expect(PendingExpect<'a>), - ExpectFx(PendingExpect<'a>), + Dbg(PendingExpectOrDbg<'a>), + Expect(PendingExpectOrDbg<'a>), + ExpectFx(PendingExpectOrDbg<'a>), SignatureDefMismatch, } -struct PendingExpect<'a> { +struct PendingExpectOrDbg<'a> { condition: &'a Loc>, preceding_comment: Region, } @@ -2684,10 +2711,18 @@ fn to_pending_value_def<'a>( } } + Dbg { + condition, + preceding_comment, + } => PendingValue::Dbg(PendingExpectOrDbg { + condition, + preceding_comment: *preceding_comment, + }), + Expect { condition, preceding_comment, - } => PendingValue::Expect(PendingExpect { + } => PendingValue::Expect(PendingExpectOrDbg { condition, preceding_comment: *preceding_comment, }), @@ -2695,7 +2730,7 @@ fn to_pending_value_def<'a>( ExpectFx { condition, preceding_comment, - } => PendingValue::ExpectFx(PendingExpect { + } => PendingValue::ExpectFx(PendingExpectOrDbg { condition, preceding_comment: *preceding_comment, }), diff --git a/crates/compiler/can/src/expr.rs b/crates/compiler/can/src/expr.rs index 4b7839f47b..e7b7a262f8 100644 --- a/crates/compiler/can/src/expr.rs +++ b/crates/compiler/can/src/expr.rs @@ -166,6 +166,12 @@ pub enum Expr { /// Empty record constant EmptyRecord, + /// The "crash" keyword + Crash { + msg: Box>, + ret_var: Variable, + }, + /// Look up exactly one field on a record, e.g. (expr).foo. Access { record_var: Variable, @@ -240,6 +246,13 @@ pub enum Expr { lookups_in_cond: Vec, }, + Dbg { + loc_condition: Box>, + loc_continuation: Box>, + variable: Variable, + symbol: Symbol, + }, + /// Rendered as empty box in editor TypedHole(Variable), @@ -254,6 +267,14 @@ pub struct ExpectLookup { pub ability_info: Option, } +#[derive(Clone, Copy, Debug)] +pub struct DbgLookup { + pub symbol: Symbol, + pub var: Variable, + pub region: Region, + pub ability_info: Option, +} + impl Expr { pub fn category(&self) -> Category { match self { @@ -294,6 +315,9 @@ impl Expr { } Self::Expect { .. } => Category::Expect, Self::ExpectFx { .. } => Category::Expect, + Self::Crash { .. } => Category::Crash, + + Self::Dbg { .. } => Category::Expect, // these nodes place no constraints on the expression's type Self::TypedHole(_) | Self::RuntimeError(..) => Category::Unknown, @@ -767,6 +791,47 @@ pub fn canonicalize_expr<'a>( } } } + } else if let ast::Expr::Crash = loc_fn.value { + // We treat crash specially, since crashing must be applied with one argument. + + debug_assert!(!args.is_empty()); + + let mut args = Vec::new(); + let mut output = Output::default(); + + for loc_arg in loc_args.iter() { + let (arg_expr, arg_out) = + canonicalize_expr(env, var_store, scope, loc_arg.region, &loc_arg.value); + + args.push(arg_expr); + output.references.union_mut(&arg_out.references); + } + + let crash = if args.len() > 1 { + let args_region = Region::span_across( + &loc_args.first().unwrap().region, + &loc_args.last().unwrap().region, + ); + env.problem(Problem::OverAppliedCrash { + region: args_region, + }); + // Still crash, just with our own message, and drop the references. + Crash { + msg: Box::new(Loc::at( + region, + Expr::Str(String::from("hit a crash!").into_boxed_str()), + )), + ret_var: var_store.fresh(), + } + } else { + let msg = args.pop().unwrap(); + Crash { + msg: Box::new(msg), + ret_var: var_store.fresh(), + } + }; + + (crash, output) } else { // Canonicalize the function expression and its arguments let (fn_expr, fn_expr_output) = @@ -857,6 +922,22 @@ pub fn canonicalize_expr<'a>( (RuntimeError(problem), Output::default()) } + ast::Expr::Crash => { + // Naked crashes aren't allowed; we'll admit this with our own message, but yield an + // error. + env.problem(Problem::UnappliedCrash { region }); + + ( + Crash { + msg: Box::new(Loc::at( + region, + Expr::Str(String::from("hit a crash!").into_boxed_str()), + )), + ret_var: var_store.fresh(), + }, + Output::default(), + ) + } ast::Expr::Defs(loc_defs, loc_ret) => { // The body expression gets a new scope for canonicalization, scope.inner_scope(|inner_scope| { @@ -1031,6 +1112,41 @@ pub fn canonicalize_expr<'a>( output, ) } + ast::Expr::Dbg(condition, continuation) => { + let mut output = Output::default(); + + let (loc_condition, output1) = + canonicalize_expr(env, var_store, scope, condition.region, &condition.value); + + let (loc_continuation, output2) = canonicalize_expr( + env, + var_store, + scope, + continuation.region, + &continuation.value, + ); + + output.union(output1); + output.union(output2); + + // the symbol is used to bind the condition `x = condition`, and identify this `dbg`. + // That would cause issues if we dbg a variable, like `dbg y`, because in the IR we + // cannot alias variables. Hence, we make the dbg use that same variable `y` + let symbol = match &loc_condition.value { + Expr::Var(symbol, _) => *symbol, + _ => scope.gen_unique_symbol(), + }; + + ( + Dbg { + loc_condition: Box::new(loc_condition), + loc_continuation: Box::new(loc_continuation), + variable: var_store.fresh(), + symbol, + }, + output, + ) + } ast::Expr::If(if_thens, final_else_branch) => { let mut branches = Vec::with_capacity(if_thens.len()); let mut output = Output::default(); @@ -1676,7 +1792,8 @@ pub fn inline_calls(var_store: &mut VarStore, expr: Expr) -> Expr { | other @ RunLowLevel { .. } | other @ TypedHole { .. } | other @ ForeignCall { .. } - | other @ OpaqueWrapFunction(_) => other, + | other @ OpaqueWrapFunction(_) + | other @ Crash { .. } => other, List { elem_var, @@ -1826,6 +1943,30 @@ pub fn inline_calls(var_store: &mut VarStore, expr: Expr) -> Expr { } } + Dbg { + loc_condition, + loc_continuation, + variable, + symbol, + } => { + let loc_condition = Loc { + region: loc_condition.region, + value: inline_calls(var_store, loc_condition.value), + }; + + let loc_continuation = Loc { + region: loc_continuation.region, + value: inline_calls(var_store, loc_continuation.value), + }; + + Dbg { + loc_condition: Box::new(loc_condition), + loc_continuation: Box::new(loc_continuation), + variable, + symbol, + } + } + LetRec(defs, loc_expr, mark) => { let mut new_defs = Vec::with_capacity(defs.len()); @@ -2552,9 +2693,10 @@ impl Declarations { }) } - pub fn expects(&self) -> VecMap> { + pub fn expects(&self) -> ExpectCollector { let mut collector = ExpectCollector { expects: VecMap::default(), + dbgs: VecMap::default(), }; let var = Variable::EMPTY_RECORD; @@ -2587,7 +2729,7 @@ impl Declarations { } } - collector.expects + collector } } @@ -2740,12 +2882,16 @@ fn get_lookup_symbols(expr: &Expr) -> Vec { } | Expr::ExpectFx { loc_continuation, .. + } + | Expr::Dbg { + loc_continuation, .. } => { stack.push(&loc_continuation.value); // Intentionally ignore the lookups in the nested `expect` condition itself, // because they couldn't possibly influence the outcome of this `expect`! } + Expr::Crash { msg, .. } => stack.push(&msg.value), Expr::Num(_, _, _, _) | Expr::Float(_, _, _, _, _) | Expr::Int(_, _, _, _, _) @@ -2864,8 +3010,9 @@ fn toplevel_expect_to_inline_expect_help(mut loc_expr: Loc, has_effects: b loc_expr } -struct ExpectCollector { - expects: VecMap>, +pub struct ExpectCollector { + pub expects: VecMap>, + pub dbgs: VecMap, } impl crate::traverse::Visitor for ExpectCollector { @@ -2884,6 +3031,21 @@ impl crate::traverse::Visitor for ExpectCollector { self.expects .insert(loc_condition.region, lookups_in_cond.to_vec()); } + Expr::Dbg { + loc_condition, + variable, + symbol, + .. + } => { + let lookup = DbgLookup { + symbol: *symbol, + var: *variable, + region: loc_condition.region, + ability_info: None, + }; + + self.dbgs.insert(*symbol, lookup); + } _ => (), } diff --git a/crates/compiler/can/src/module.rs b/crates/compiler/can/src/module.rs index 914c1b06e4..998e3e0f06 100644 --- a/crates/compiler/can/src/module.rs +++ b/crates/compiler/can/src/module.rs @@ -3,7 +3,9 @@ use crate::annotation::{canonicalize_annotation, AnnotationFor}; use crate::def::{canonicalize_defs, Def}; use crate::effect_module::HostedGeneratedFunctions; use crate::env::Env; -use crate::expr::{ClosureData, Declarations, ExpectLookup, Expr, Output, PendingDerives}; +use crate::expr::{ + ClosureData, DbgLookup, Declarations, ExpectLookup, Expr, Output, PendingDerives, +}; use crate::pattern::{BindingsFromPattern, Pattern}; use crate::scope::Scope; use bumpalo::Bump; @@ -131,6 +133,7 @@ pub struct Module { pub rigid_variables: RigidVariables, pub abilities_store: PendingAbilitiesStore, pub loc_expects: VecMap>, + pub loc_dbgs: VecMap, } #[derive(Debug, Default)] @@ -153,6 +156,7 @@ pub struct ModuleOutput { pub pending_derives: PendingDerives, pub scope: Scope, pub loc_expects: VecMap>, + pub loc_dbgs: VecMap, } fn validate_generate_with<'a>( @@ -776,7 +780,7 @@ pub fn canonicalize_module_defs<'a>( } } - let loc_expects = declarations.expects(); + let collected = declarations.expects(); ModuleOutput { scope, @@ -789,7 +793,8 @@ pub fn canonicalize_module_defs<'a>( problems: env.problems, symbols_from_requires, pending_derives, - loc_expects, + loc_expects: collected.expects, + loc_dbgs: collected.dbgs, } } @@ -952,7 +957,17 @@ fn fix_values_captured_in_closure_expr( Expect { loc_condition, loc_continuation, - lookups_in_cond: _, + .. + } + | ExpectFx { + loc_condition, + loc_continuation, + .. + } + | Dbg { + loc_condition, + loc_continuation, + .. } => { fix_values_captured_in_closure_expr( &mut loc_condition.value, @@ -966,18 +981,9 @@ fn fix_values_captured_in_closure_expr( ); } - ExpectFx { - loc_condition, - loc_continuation, - lookups_in_cond: _, - } => { + Crash { msg, ret_var: _ } => { fix_values_captured_in_closure_expr( - &mut loc_condition.value, - no_capture_symbols, - closure_captures, - ); - fix_values_captured_in_closure_expr( - &mut loc_continuation.value, + &mut msg.value, no_capture_symbols, closure_captures, ); diff --git a/crates/compiler/can/src/operator.rs b/crates/compiler/can/src/operator.rs index 596c031923..f16e1c3c26 100644 --- a/crates/compiler/can/src/operator.rs +++ b/crates/compiler/can/src/operator.rs @@ -82,6 +82,16 @@ fn desugar_value_def<'a>(arena: &'a Bump, def: &'a ValueDef<'a>) -> ValueDef<'a> body_pattern, body_expr: desugar_expr(arena, body_expr), }, + Dbg { + condition, + preceding_comment, + } => { + let desugared_condition = &*arena.alloc(desugar_expr(arena, condition)); + Dbg { + condition: desugared_condition, + preceding_comment: *preceding_comment, + } + } Expect { condition, preceding_comment, @@ -128,7 +138,8 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc>) -> &'a Loc | MalformedClosure | PrecedenceConflict { .. } | Tag(_) - | OpaqueRef(_) => loc_expr, + | OpaqueRef(_) + | Crash => loc_expr, TupleAccess(_sub_expr, _paths) => todo!("Handle TupleAccess"), RecordAccess(sub_expr, paths) => { @@ -348,6 +359,14 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc>) -> &'a Loc region: loc_expr.region, }) } + Dbg(condition, continuation) => { + let desugared_condition = &*arena.alloc(desugar_expr(arena, condition)); + let desugared_continuation = &*arena.alloc(desugar_expr(arena, continuation)); + arena.alloc(Loc { + value: Dbg(desugared_condition, desugared_continuation), + region: loc_expr.region, + }) + } } } diff --git a/crates/compiler/can/src/traverse.rs b/crates/compiler/can/src/traverse.rs index 28c5ec6b78..cefb62a24f 100644 --- a/crates/compiler/can/src/traverse.rs +++ b/crates/compiler/can/src/traverse.rs @@ -203,6 +203,9 @@ pub fn walk_expr(visitor: &mut V, expr: &Expr, var: Variable) { let (fn_var, loc_fn, _closure_var, _ret_var) = &**f; walk_call(visitor, *fn_var, loc_fn, args); } + Expr::Crash { msg, .. } => { + visitor.visit_expr(&msg.value, msg.region, Variable::STR); + } Expr::RunLowLevel { op: _, args, @@ -268,8 +271,7 @@ pub fn walk_expr(visitor: &mut V, expr: &Expr, var: Variable) { loc_continuation, lookups_in_cond: _, } => { - // TODO: what type does an expect have? bool - visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::NULL); + visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::BOOL); visitor.visit_expr( &loc_continuation.value, loc_continuation.region, @@ -281,8 +283,20 @@ pub fn walk_expr(visitor: &mut V, expr: &Expr, var: Variable) { loc_continuation, lookups_in_cond: _, } => { - // TODO: what type does an expect have? bool - visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::NULL); + visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::BOOL); + visitor.visit_expr( + &loc_continuation.value, + loc_continuation.region, + Variable::NULL, + ); + } + Expr::Dbg { + variable, + loc_condition, + loc_continuation, + symbol: _, + } => { + visitor.visit_expr(&loc_condition.value, loc_condition.region, *variable); visitor.visit_expr( &loc_continuation.value, loc_continuation.region, diff --git a/crates/compiler/constrain/src/expr.rs b/crates/compiler/constrain/src/expr.rs index 30b1a1f247..ae4a72987b 100644 --- a/crates/compiler/constrain/src/expr.rs +++ b/crates/compiler/constrain/src/expr.rs @@ -482,6 +482,28 @@ pub fn constrain_expr( let and_constraint = constraints.and_constraint(and_cons); constraints.exists(vars, and_constraint) } + Expr::Crash { msg, ret_var } => { + let str_index = constraints.push_type(types, Types::STR); + let expected_msg = constraints.push_expected_type(Expected::ForReason( + Reason::CrashArg, + str_index, + msg.region, + )); + + let msg_is_str = constrain_expr( + types, + constraints, + env, + msg.region, + &msg.value, + expected_msg, + ); + let magic = constraints.equal_types_var(*ret_var, expected, Category::Crash, region); + + let and = constraints.and_constraint([msg_is_str, magic]); + + constraints.exists([*ret_var], and) + } Var(symbol, variable) => { // Save the expectation in the variable, then lookup the symbol's type in the environment let expected_type = *constraints[expected].get_type_ref(); @@ -656,6 +678,36 @@ pub fn constrain_expr( constraints.exists_many(vars, all_constraints) } + Dbg { + loc_condition, + loc_continuation, + variable, + symbol: _, + } => { + let dbg_type = constraints.push_variable(*variable); + let expected_dbg = constraints.push_expected_type(Expected::NoExpectation(dbg_type)); + + let cond_con = constrain_expr( + types, + constraints, + env, + loc_condition.region, + &loc_condition.value, + expected_dbg, + ); + + let continuation_con = constrain_expr( + types, + constraints, + env, + loc_continuation.region, + &loc_continuation.value, + expected, + ); + + constraints.exists_many([], [cond_con, continuation_con]) + } + If { cond_var, branch_var, diff --git a/crates/compiler/fmt/src/annotation.rs b/crates/compiler/fmt/src/annotation.rs index e3cd669f67..6be2cb6945 100644 --- a/crates/compiler/fmt/src/annotation.rs +++ b/crates/compiler/fmt/src/annotation.rs @@ -176,6 +176,15 @@ impl<'a> Formattable for TypeAnnotation<'a> { annot.is_multiline() || has_clauses.iter().any(|has| has.is_multiline()) } + Tuple { fields, ext } => { + match ext { + Some(ann) if ann.value.is_multiline() => return true, + _ => {} + } + + fields.items.iter().any(|field| field.value.is_multiline()) + } + Record { fields, ext } => { match ext { Some(ann) if ann.value.is_multiline() => return true, @@ -297,6 +306,14 @@ impl<'a> Formattable for TypeAnnotation<'a> { } } + Tuple { fields, ext } => { + fmt_collection(buf, indent, Braces::Round, *fields, newlines); + + if let Some(loc_ext_ann) = *ext { + loc_ext_ann.value.format(buf, indent); + } + } + Record { fields, ext } => { fmt_collection(buf, indent, Braces::Curly, *fields, newlines); diff --git a/crates/compiler/fmt/src/def.rs b/crates/compiler/fmt/src/def.rs index d93c932f19..43f3e4eb05 100644 --- a/crates/compiler/fmt/src/def.rs +++ b/crates/compiler/fmt/src/def.rs @@ -168,6 +168,7 @@ impl<'a> Formattable for ValueDef<'a> { AnnotatedBody { .. } => true, Expect { condition, .. } => condition.is_multiline(), ExpectFx { condition, .. } => condition.is_multiline(), + Dbg { condition, .. } => condition.is_multiline(), } } @@ -241,6 +242,7 @@ impl<'a> Formattable for ValueDef<'a> { Body(loc_pattern, loc_expr) => { fmt_body(buf, &loc_pattern.value, &loc_expr.value, indent); } + Dbg { condition, .. } => fmt_dbg_in_def(buf, condition, self.is_multiline(), indent), Expect { condition, .. } => fmt_expect(buf, condition, self.is_multiline(), indent), ExpectFx { condition, .. } => { fmt_expect_fx(buf, condition, self.is_multiline(), indent) @@ -294,6 +296,27 @@ impl<'a> Formattable for ValueDef<'a> { } } +fn fmt_dbg_in_def<'a, 'buf>( + buf: &mut Buf<'buf>, + condition: &'a Loc>, + is_multiline: bool, + indent: u16, +) { + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("dbg"); + + let return_indent = if is_multiline { + buf.newline(); + indent + INDENT + } else { + buf.spaces(1); + indent + }; + + condition.format(buf, return_indent); +} + fn fmt_expect<'a, 'buf>( buf: &mut Buf<'buf>, condition: &'a Loc>, diff --git a/crates/compiler/fmt/src/expr.rs b/crates/compiler/fmt/src/expr.rs index 75c8201f8e..c30d28c68e 100644 --- a/crates/compiler/fmt/src/expr.rs +++ b/crates/compiler/fmt/src/expr.rs @@ -43,7 +43,8 @@ impl<'a> Formattable for Expr<'a> { | MalformedIdent(_, _) | MalformedClosure | Tag(_) - | OpaqueRef(_) => false, + | OpaqueRef(_) + | Crash => false, // These expressions always have newlines Defs(_, _) | When(_, _) => true, @@ -71,6 +72,7 @@ impl<'a> Formattable for Expr<'a> { Expect(condition, continuation) => { condition.is_multiline() || continuation.is_multiline() } + Dbg(condition, continuation) => condition.is_multiline() || continuation.is_multiline(), If(branches, final_else) => { final_else.is_multiline() @@ -190,6 +192,10 @@ impl<'a> Formattable for Expr<'a> { buf.push('_'); buf.push_str(name); } + Crash => { + buf.indent(indent); + buf.push_str("crash"); + } Apply(loc_expr, loc_args, _) => { buf.indent(indent); if apply_needs_parens && !loc_args.is_empty() { @@ -379,6 +385,9 @@ impl<'a> Formattable for Expr<'a> { Expect(condition, continuation) => { fmt_expect(buf, condition, continuation, self.is_multiline(), indent); } + Dbg(condition, continuation) => { + fmt_dbg(buf, condition, continuation, self.is_multiline(), indent); + } If(branches, final_else) => { fmt_if(buf, branches, final_else, self.is_multiline(), indent); } @@ -843,6 +852,33 @@ fn fmt_when<'a, 'buf>( } } +fn fmt_dbg<'a, 'buf>( + buf: &mut Buf<'buf>, + condition: &'a Loc>, + continuation: &'a Loc>, + is_multiline: bool, + indent: u16, +) { + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("dbg"); + + let return_indent = if is_multiline { + buf.newline(); + indent + INDENT + } else { + buf.spaces(1); + indent + }; + + condition.format(buf, return_indent); + + // Always put a blank line after the `dbg` line(s) + buf.ensure_ends_with_blank_line(); + + continuation.format(buf, indent); +} + fn fmt_expect<'a, 'buf>( buf: &mut Buf<'buf>, condition: &'a Loc>, diff --git a/crates/compiler/fmt/src/spaces.rs b/crates/compiler/fmt/src/spaces.rs index 5b83fd43dc..1ca70d6ee8 100644 --- a/crates/compiler/fmt/src/spaces.rs +++ b/crates/compiler/fmt/src/spaces.rs @@ -540,6 +540,13 @@ impl<'a> RemoveSpaces<'a> for ValueDef<'a> { body_pattern: arena.alloc(body_pattern.remove_spaces(arena)), body_expr: arena.alloc(body_expr.remove_spaces(arena)), }, + Dbg { + condition, + preceding_comment: _, + } => Dbg { + condition: arena.alloc(condition.remove_spaces(arena)), + preceding_comment: Region::zero(), + }, Expect { condition, preceding_comment: _, @@ -659,6 +666,7 @@ impl<'a> RemoveSpaces<'a> for Expr<'a> { arena.alloc(a.remove_spaces(arena)), arena.alloc(b.remove_spaces(arena)), ), + Expr::Crash => Expr::Crash, Expr::Defs(a, b) => { let mut defs = a.clone(); defs.space_before = vec![Default::default(); defs.len()]; @@ -685,6 +693,10 @@ impl<'a> RemoveSpaces<'a> for Expr<'a> { arena.alloc(a.remove_spaces(arena)), arena.alloc(b.remove_spaces(arena)), ), + Expr::Dbg(a, b) => Expr::Dbg( + arena.alloc(a.remove_spaces(arena)), + arena.alloc(b.remove_spaces(arena)), + ), Expr::Apply(a, b, c) => Expr::Apply( arena.alloc(a.remove_spaces(arena)), b.remove_spaces(arena), @@ -776,6 +788,10 @@ impl<'a> RemoveSpaces<'a> for TypeAnnotation<'a> { vars: vars.remove_spaces(arena), }, ), + TypeAnnotation::Tuple { fields, ext } => TypeAnnotation::Tuple { + fields: fields.remove_spaces(arena), + ext: ext.remove_spaces(arena), + }, TypeAnnotation::Record { fields, ext } => TypeAnnotation::Record { fields: fields.remove_spaces(arena), ext: ext.remove_spaces(arena), diff --git a/crates/compiler/fmt/tests/test_fmt.rs b/crates/compiler/fmt/tests/test_fmt.rs index bc615c3a09..aa0dc1f897 100644 --- a/crates/compiler/fmt/tests/test_fmt.rs +++ b/crates/compiler/fmt/tests/test_fmt.rs @@ -5829,6 +5829,42 @@ mod test_fmt { ); } + #[test] + fn format_crash() { + expr_formats_same(indoc!( + r#" + _ = crash + _ = crash "" + + crash "" "" + "# + )); + + expr_formats_to( + indoc!( + r#" + _ = crash + _ = crash "" + _ = crash "" "" + try + foo + (\_ -> crash "") + "# + ), + indoc!( + r#" + _ = crash + _ = crash "" + _ = crash "" "" + + try + foo + (\_ -> crash "") + "# + ), + ); + } + // this is a parse error atm // #[test] // fn multiline_apply() { diff --git a/crates/compiler/gen_dev/src/lib.rs b/crates/compiler/gen_dev/src/lib.rs index a8147cdab7..03c32d23e7 100644 --- a/crates/compiler/gen_dev/src/lib.rs +++ b/crates/compiler/gen_dev/src/lib.rs @@ -1110,7 +1110,7 @@ trait Backend<'a> { Stmt::Expect { .. } => todo!("expect is not implemented in the dev backend"), Stmt::ExpectFx { .. } => todo!("expect-fx is not implemented in the dev backend"), - Stmt::RuntimeError(_) => {} + Stmt::Crash(..) => todo!("crash is not implemented in the dev backend"), } } diff --git a/crates/compiler/gen_llvm/src/llvm/build.rs b/crates/compiler/gen_llvm/src/llvm/build.rs index bb2b0ec22d..e41f2783dd 100644 --- a/crates/compiler/gen_llvm/src/llvm/build.rs +++ b/crates/compiler/gen_llvm/src/llvm/build.rs @@ -39,8 +39,8 @@ use roc_debug_flags::dbg_do; use roc_debug_flags::ROC_PRINT_LLVM_FN_VERIFICATION; use roc_module::symbol::{Interns, ModuleId, Symbol}; use roc_mono::ir::{ - BranchInfo, CallType, EntryPoint, JoinPointId, ListLiteralElement, ModifyRc, OptLevel, - ProcLayout, + BranchInfo, CallType, CrashTag, EntryPoint, JoinPointId, ListLiteralElement, ModifyRc, + OptLevel, ProcLayout, }; use roc_mono::layout::{ Builtin, CapturesNiche, LambdaName, LambdaSet, Layout, LayoutIds, RawFunctionLayout, @@ -158,7 +158,7 @@ impl LlvmBackendMode { } } - fn runs_expects(self) -> bool { + pub(crate) fn runs_expects(self) -> bool { match self { LlvmBackendMode::Binary => false, LlvmBackendMode::BinaryDev => true, @@ -183,22 +183,6 @@ pub struct Env<'a, 'ctx, 'env> { pub exposed_to_host: MutSet, } -#[repr(u32)] -pub enum PanicTagId { - NullTerminatedString = 0, -} - -impl std::convert::TryFrom for PanicTagId { - type Error = (); - - fn try_from(value: u32) -> Result { - match value { - 0 => Ok(PanicTagId::NullTerminatedString), - _ => Err(()), - } - } -} - impl<'a, 'ctx, 'env> Env<'a, 'ctx, 'env> { /// The integer type representing a pointer /// @@ -344,16 +328,33 @@ impl<'a, 'ctx, 'env> Env<'a, 'ctx, 'env> { ) } - pub fn call_panic(&self, message: PointerValue<'ctx>, tag_id: PanicTagId) { + pub fn call_panic( + &self, + env: &Env<'a, 'ctx, 'env>, + message: BasicValueEnum<'ctx>, + tag: CrashTag, + ) { let function = self.module.get_function("roc_panic").unwrap(); - let tag_id = self - .context - .i32_type() - .const_int(tag_id as u32 as u64, false); + let tag_id = self.context.i32_type().const_int(tag as u32 as u64, false); + + let msg = match env.target_info.ptr_width() { + PtrWidth::Bytes4 => { + // we need to pass the message by reference, but we currently hold the value. + let alloca = env + .builder + .build_alloca(message.get_type(), "alloca_panic_msg"); + env.builder.build_store(alloca, message); + alloca.into() + } + PtrWidth::Bytes8 => { + // string is already held by reference + message + } + }; let call = self .builder - .build_call(function, &[message.into(), tag_id.into()], "roc_panic"); + .build_call(function, &[msg.into(), tag_id.into()], "roc_panic"); call.set_call_convention(C_CALL_CONV); } @@ -750,25 +751,30 @@ pub fn build_exp_literal<'a, 'ctx, 'env>( } Bool(b) => env.context.bool_type().const_int(*b as u64, false).into(), Byte(b) => env.context.i8_type().const_int(*b as u64, false).into(), - Str(str_literal) => { - if str_literal.len() < env.small_str_bytes() as usize { - match env.small_str_bytes() { - 24 => small_str_ptr_width_8(env, parent, str_literal).into(), - 12 => small_str_ptr_width_4(env, str_literal).into(), - _ => unreachable!("incorrect small_str_bytes"), - } - } else { - let ptr = define_global_str_literal_ptr(env, str_literal); - let number_of_elements = env.ptr_int().const_int(str_literal.len() as u64, false); + Str(str_literal) => build_string_literal(env, parent, str_literal), + } +} - let alloca = - const_str_alloca_ptr(env, parent, ptr, number_of_elements, number_of_elements); +fn build_string_literal<'a, 'ctx, 'env>( + env: &Env<'a, 'ctx, 'env>, + parent: FunctionValue<'ctx>, + str_literal: &str, +) -> BasicValueEnum<'ctx> { + if str_literal.len() < env.small_str_bytes() as usize { + match env.small_str_bytes() { + 24 => small_str_ptr_width_8(env, parent, str_literal).into(), + 12 => small_str_ptr_width_4(env, str_literal).into(), + _ => unreachable!("incorrect small_str_bytes"), + } + } else { + let ptr = define_global_str_literal_ptr(env, str_literal); + let number_of_elements = env.ptr_int().const_int(str_literal.len() as u64, false); - match env.target_info.ptr_width() { - PtrWidth::Bytes4 => env.builder.build_load(alloca, "load_const_str"), - PtrWidth::Bytes8 => alloca.into(), - } - } + let alloca = const_str_alloca_ptr(env, parent, ptr, number_of_elements, number_of_elements); + + match env.target_info.ptr_width() { + PtrWidth::Bytes4 => env.builder.build_load(alloca, "load_const_str"), + PtrWidth::Bytes8 => alloca.into(), } } } @@ -2621,7 +2627,7 @@ pub fn build_exp_stmt<'a, 'ctx, 'env>( } roc_target::PtrWidth::Bytes4 => { // temporary WASM implementation - throw_exception(env, "An expectation failed!"); + throw_internal_exception(env, parent, "An expectation failed!"); } } } else { @@ -2683,7 +2689,7 @@ pub fn build_exp_stmt<'a, 'ctx, 'env>( } roc_target::PtrWidth::Bytes4 => { // temporary WASM implementation - throw_exception(env, "An expectation failed!"); + throw_internal_exception(env, parent, "An expectation failed!"); } } } else { @@ -2703,8 +2709,8 @@ pub fn build_exp_stmt<'a, 'ctx, 'env>( ) } - RuntimeError(error_msg) => { - throw_exception(env, error_msg); + Crash(sym, tag) => { + throw_exception(env, scope, sym, *tag); // unused value (must return a BasicValue) let zero = env.context.i64_type().const_zero(); @@ -3336,7 +3342,7 @@ fn expose_function_to_host_help_c_abi_generic<'a, 'ctx, 'env>( builder.position_at_end(entry); - let wrapped_layout = roc_result_layout(env.arena, return_layout, env.target_info); + let wrapped_layout = roc_call_result_layout(env.arena, return_layout, env.target_info); call_roc_function(env, roc_function, &wrapped_layout, arguments_for_call) } else { call_roc_function(env, roc_function, &return_layout, arguments_for_call) @@ -3366,7 +3372,8 @@ fn expose_function_to_host_help_c_abi_gen_test<'a, 'ctx, 'env>( // a tagged union to indicate to the test loader that a panic occurred. // especially when running 32-bit binaries on a 64-bit machine, there // does not seem to be a smarter solution - let wrapper_return_type = roc_result_type(env, basic_type_from_layout(env, &return_layout)); + let wrapper_return_type = + roc_call_result_type(env, basic_type_from_layout(env, &return_layout)); let mut cc_argument_types = Vec::with_capacity_in(arguments.len(), env.arena); for layout in arguments { @@ -3755,7 +3762,7 @@ fn expose_function_to_host_help_c_abi<'a, 'ctx, 'env>( let return_type = match env.mode { LlvmBackendMode::GenTest | LlvmBackendMode::WasmGenTest | LlvmBackendMode::CliTest => { - roc_result_type(env, roc_function.get_type().get_return_type().unwrap()).into() + roc_call_result_type(env, roc_function.get_type().get_return_type().unwrap()).into() } LlvmBackendMode::Binary | LlvmBackendMode::BinaryDev => { @@ -3862,14 +3869,29 @@ pub fn build_setjmp_call<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> BasicValu } } -/// Pointer to pointer of the panic message. +/// Pointer to RocStr which is the panic message. pub fn get_panic_msg_ptr<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> PointerValue<'ctx> { - let ptr_to_u8_ptr = env.context.i8_type().ptr_type(AddressSpace::Generic); + let str_typ = zig_str_type(env); - let global_name = "roc_panic_msg_ptr"; + let global_name = "roc_panic_msg_str"; let global = env.module.get_global(global_name).unwrap_or_else(|| { - let global = env.module.add_global(ptr_to_u8_ptr, None, global_name); - global.set_initializer(&ptr_to_u8_ptr.const_zero()); + let global = env.module.add_global(str_typ, None, global_name); + global.set_initializer(&str_typ.const_zero()); + global + }); + + global.as_pointer_value() +} + +/// Pointer to the panic tag. +/// Only non-zero values must be written into here. +pub fn get_panic_tag_ptr<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> PointerValue<'ctx> { + let i64_typ = env.context.i64_type(); + + let global_name = "roc_panic_msg_tag"; + let global = env.module.get_global(global_name).unwrap_or_else(|| { + let global = env.module.add_global(i64_typ, None, global_name); + global.set_initializer(&i64_typ.const_zero()); global }); @@ -3887,7 +3909,7 @@ fn set_jump_and_catch_long_jump<'a, 'ctx, 'env>( let builder = env.builder; let return_type = basic_type_from_layout(env, &return_layout); - let call_result_type = roc_result_type(env, return_type.as_basic_type_enum()); + let call_result_type = roc_call_result_type(env, return_type.as_basic_type_enum()); let result_alloca = builder.build_alloca(call_result_type, "result"); let then_block = context.append_basic_block(parent, "then_block"); @@ -3922,26 +3944,21 @@ fn set_jump_and_catch_long_jump<'a, 'ctx, 'env>( { builder.position_at_end(catch_block); - let error_msg = { - // u8** - let ptr_int_ptr = get_panic_msg_ptr(env); - - // u8* again - builder.build_load(ptr_int_ptr, "ptr_int") - }; + // RocStr* global + let error_msg_ptr = get_panic_msg_ptr(env); + // i64* global + let error_tag_ptr = get_panic_tag_ptr(env); let return_value = { let v1 = call_result_type.const_zero(); - // flag is non-zero, indicating failure - let flag = context.i64_type().const_int(1, false); + // tag must be non-zero, indicating failure + let tag = builder.build_load(error_tag_ptr, "load_panic_tag"); - let v2 = builder - .build_insert_value(v1, flag, 0, "set_error") - .unwrap(); + let v2 = builder.build_insert_value(v1, tag, 0, "set_error").unwrap(); let v3 = builder - .build_insert_value(v2, error_msg, 1, "set_exception") + .build_insert_value(v2, error_msg_ptr, 1, "set_exception") .unwrap(); v3 }; @@ -3971,7 +3988,7 @@ fn make_exception_catcher<'a, 'ctx, 'env>( function_value } -fn roc_result_layout<'a>( +fn roc_call_result_layout<'a>( arena: &'a Bump, return_layout: Layout<'a>, target_info: TargetInfo, @@ -3981,14 +3998,14 @@ fn roc_result_layout<'a>( Layout::struct_no_name_order(arena.alloc(elements)) } -fn roc_result_type<'a, 'ctx, 'env>( +fn roc_call_result_type<'a, 'ctx, 'env>( env: &Env<'a, 'ctx, 'env>, return_type: BasicTypeEnum<'ctx>, ) -> StructType<'ctx> { env.context.struct_type( &[ env.context.i64_type().into(), - env.context.i8_type().ptr_type(AddressSpace::Generic).into(), + zig_str_type(env).ptr_type(AddressSpace::Generic).into(), return_type, ], false, @@ -4003,7 +4020,7 @@ fn make_good_roc_result<'a, 'ctx, 'env>( let context = env.context; let builder = env.builder; - let v1 = roc_result_type(env, basic_type_from_layout(env, &return_layout)).const_zero(); + let v1 = roc_call_result_type(env, basic_type_from_layout(env, &return_layout)).const_zero(); let v2 = builder .build_insert_value(v1, context.i64_type().const_zero(), 0, "set_no_error") @@ -4050,7 +4067,8 @@ fn make_exception_catching_wrapper<'a, 'ctx, 'env>( } }; - let wrapper_return_type = roc_result_type(env, basic_type_from_layout(env, &return_layout)); + let wrapper_return_type = + roc_call_result_type(env, basic_type_from_layout(env, &return_layout)); // argument_types.push(wrapper_return_type.ptr_type(AddressSpace::Generic).into()); @@ -5520,51 +5538,33 @@ fn define_global_str_literal<'a, 'ctx, 'env>( } } -fn define_global_error_str<'a, 'ctx, 'env>( +pub(crate) fn throw_internal_exception<'a, 'ctx, 'env>( env: &Env<'a, 'ctx, 'env>, + parent: FunctionValue<'ctx>, message: &str, -) -> inkwell::values::GlobalValue<'ctx> { - let module = env.module; - - // hash the name so we don't re-define existing messages - let name = { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - let mut hasher = DefaultHasher::new(); - message.hash(&mut hasher); - let hash = hasher.finish(); - - format!("_Error_message_{}", hash) - }; - - match module.get_global(&name) { - Some(current) => current, - None => unsafe { env.builder.build_global_string(message, name.as_str()) }, - } -} - -pub(crate) fn throw_exception<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>, message: &str) { +) { let builder = env.builder; - // define the error message as a global - // (a hash is used such that the same value is not defined repeatedly) - let error_msg_global = define_global_error_str(env, message); + let str = build_string_literal(env, parent, message); - let cast = env - .builder - .build_bitcast( - error_msg_global.as_pointer_value(), - env.context.i8_type().ptr_type(AddressSpace::Generic), - "cast_void", - ) - .into_pointer_value(); - - env.call_panic(cast, PanicTagId::NullTerminatedString); + env.call_panic(env, str, CrashTag::Roc); builder.build_unreachable(); } +pub(crate) fn throw_exception<'a, 'ctx, 'env>( + env: &Env<'a, 'ctx, 'env>, + scope: &mut Scope<'a, 'ctx>, + message: &Symbol, + tag: CrashTag, +) { + let msg_val = load_symbol(scope, message); + + env.call_panic(env, msg_val, tag); + + env.builder.build_unreachable(); +} + fn get_foreign_symbol<'a, 'ctx, 'env>( env: &Env<'a, 'ctx, 'env>, foreign_symbol: roc_module::ident::ForeignSymbol, diff --git a/crates/compiler/gen_llvm/src/llvm/expect.rs b/crates/compiler/gen_llvm/src/llvm/expect.rs index 5fa64a01c0..606e0aab52 100644 --- a/crates/compiler/gen_llvm/src/llvm/expect.rs +++ b/crates/compiler/gen_llvm/src/llvm/expect.rs @@ -104,6 +104,13 @@ pub(crate) fn finalize(env: &Env) { .build_call(func, &[], "call_expect_failed_finalize"); } +pub(crate) fn send_dbg(env: &Env) { + let func = env.module.get_function(bitcode::UTILS_SEND_DBG).unwrap(); + + env.builder + .build_call(func, &[], "call_expect_failed_finalize"); +} + pub(crate) fn clone_to_shared_memory<'a, 'ctx, 'env>( env: &Env<'a, 'ctx, 'env>, scope: &Scope<'a, 'ctx>, diff --git a/crates/compiler/gen_llvm/src/llvm/externs.rs b/crates/compiler/gen_llvm/src/llvm/externs.rs index 42f386ba60..bb3d5fe011 100644 --- a/crates/compiler/gen_llvm/src/llvm/externs.rs +++ b/crates/compiler/gen_llvm/src/llvm/externs.rs @@ -1,5 +1,5 @@ use crate::llvm::bitcode::call_void_bitcode_fn; -use crate::llvm::build::{add_func, get_panic_msg_ptr, C_CALL_CONV}; +use crate::llvm::build::{add_func, get_panic_msg_ptr, get_panic_tag_ptr, C_CALL_CONV}; use crate::llvm::build::{CCReturn, Env, FunctionSpec}; use inkwell::module::Linkage; use inkwell::types::BasicType; @@ -193,10 +193,9 @@ pub fn add_sjlj_roc_panic(env: &Env<'_, '_, '_>) { // already been defined by the builtins, which rely on it. let fn_val = module.get_function("roc_panic").unwrap(); let mut params = fn_val.get_param_iter(); - let ptr_arg = params.next().unwrap(); + let roc_str_arg = params.next().unwrap(); - // in debug mode, this is assumed to be NullTerminatedString - let _tag_id_arg = params.next().unwrap(); + let tag_id_arg = params.next().unwrap(); debug_assert!(params.next().is_none()); @@ -210,8 +209,38 @@ pub fn add_sjlj_roc_panic(env: &Env<'_, '_, '_>) { builder.position_at_end(entry); - // write our error message pointer - env.builder.build_store(get_panic_msg_ptr(env), ptr_arg); + // write our error message to the RocStr pointer + { + let loaded_roc_str = match env.target_info.ptr_width() { + roc_target::PtrWidth::Bytes4 => roc_str_arg, + // On 64-bit we pass RocStrs by reference internally + roc_target::PtrWidth::Bytes8 => { + builder.build_load(roc_str_arg.into_pointer_value(), "load_roc_str") + } + }; + + env.builder + .build_store(get_panic_msg_ptr(env), loaded_roc_str); + } + + // write the panic tag. + // increment by 1, since the tag we'll get from the Roc program is 0-based, + // but we use 0 for marking a successful call. + { + let cast_tag_id = builder.build_int_z_extend( + tag_id_arg.into_int_value(), + env.context.i64_type(), + "zext_panic_tag", + ); + + let inc_tag_id = builder.build_int_add( + cast_tag_id, + env.context.i64_type().const_int(1, false), + "inc_panic_tag", + ); + + env.builder.build_store(get_panic_tag_ptr(env), inc_tag_id); + } build_longjmp_call(env); diff --git a/crates/compiler/gen_llvm/src/llvm/lowlevel.rs b/crates/compiler/gen_llvm/src/llvm/lowlevel.rs index 4d51257f60..5eb6fbb2b6 100644 --- a/crates/compiler/gen_llvm/src/llvm/lowlevel.rs +++ b/crates/compiler/gen_llvm/src/llvm/lowlevel.rs @@ -41,9 +41,9 @@ use crate::llvm::{ }, }; -use super::convert::zig_with_overflow_roc_dec; +use super::{build::throw_internal_exception, convert::zig_with_overflow_roc_dec}; use super::{ - build::{load_symbol, load_symbol_and_layout, throw_exception, Env, Scope}, + build::{load_symbol, load_symbol_and_layout, Env, Scope}, convert::zig_dec_type, }; @@ -1119,6 +1119,27 @@ pub(crate) fn run_low_level<'a, 'ctx, 'env>( ptr.into() } }, + Dbg => { + // now what + arguments!(condition); + + if env.mode.runs_expects() { + let region = unsafe { std::mem::transmute::<_, roc_region::all::Region>(args[0]) }; + + crate::llvm::expect::clone_to_shared_memory( + env, + scope, + layout_ids, + args[0], + region, + &[args[0]], + ); + + crate::llvm::expect::send_dbg(env); + } + + condition + } } } @@ -1536,7 +1557,7 @@ fn throw_on_overflow<'a, 'ctx, 'env>( bd.position_at_end(throw_block); - throw_exception(env, message); + throw_internal_exception(env, parent, message); bd.position_at_end(then_block); @@ -1982,8 +2003,9 @@ fn int_neg_raise_on_overflow<'a, 'ctx, 'env>( builder.position_at_end(then_block); - throw_exception( + throw_internal_exception( env, + parent, "integer negation overflowed because its argument is the minimum value", ); @@ -2012,8 +2034,9 @@ fn int_abs_raise_on_overflow<'a, 'ctx, 'env>( builder.position_at_end(then_block); - throw_exception( + throw_internal_exception( env, + parent, "integer absolute overflowed because its argument is the minimum value", ); diff --git a/crates/compiler/gen_llvm/src/run_roc.rs b/crates/compiler/gen_llvm/src/run_roc.rs index d4954c7e5f..dfbfcadec8 100644 --- a/crates/compiler/gen_llvm/src/run_roc.rs +++ b/crates/compiler/gen_llvm/src/run_roc.rs @@ -1,6 +1,7 @@ -use std::ffi::CStr; use std::mem::MaybeUninit; -use std::os::raw::c_char; + +use roc_mono::ir::CrashTag; +use roc_std::RocStr; /// This must have the same size as the repr() of RocCallResult! pub const ROC_CALL_RESULT_DISCRIMINANT_SIZE: usize = std::mem::size_of::(); @@ -8,7 +9,7 @@ pub const ROC_CALL_RESULT_DISCRIMINANT_SIZE: usize = std::mem::size_of::(); #[repr(C)] pub struct RocCallResult { tag: u64, - error_msg: *mut c_char, + error_msg: *mut RocStr, value: MaybeUninit, } @@ -32,14 +33,18 @@ impl Default for RocCallResult { } } -impl From> for Result { +impl From> for Result { fn from(call_result: RocCallResult) -> Self { match call_result.tag { 0 => Ok(unsafe { call_result.value.assume_init() }), - _ => Err({ - let raw = unsafe { CStr::from_ptr(call_result.error_msg) }; + n => Err({ + let msg: &RocStr = unsafe { &*call_result.error_msg }; + let tag = (n - 1) as u32; + let tag = tag + .try_into() + .unwrap_or_else(|_| panic!("received illegal tag: {tag}")); - raw.to_str().unwrap().to_owned() + (msg.as_str().to_owned(), tag) }), } } @@ -120,7 +125,7 @@ macro_rules! run_jit_function { $transform(success) } - Err(error_msg) => { + Err((error_msg, _)) => { eprintln!("This Roc code crashed with: \"{error_msg}\""); Expr::MalformedClosure diff --git a/crates/compiler/gen_wasm/README.md b/crates/compiler/gen_wasm/README.md index 620ee0dfe1..5957c02cb3 100644 --- a/crates/compiler/gen_wasm/README.md +++ b/crates/compiler/gen_wasm/README.md @@ -208,3 +208,18 @@ The diagram below illustrates this process.   ![Diagram showing how host-to-app calls are linked.](./docs/host-to-app-calls.svg) + +## Tips for debugging Wasm code generation + +In general, WebAssembly runtimes often have terrible error messages. Especially command-line ones. And most especially Wasm3, which we use nonetheless because it's fast. + +- Install the WABT (WebAssembly Binary Toolkit) + - We have a debug setting to dump out the test binary. In `gen_wasm/src/lib.rs`, set `DEBUG_LOG_SETTINGS.keep_test_binary` to `true` + - Run `wasm-validate` to make sure the module is valid WebAssembly + - Use `wasm-objdump` with options `-d`, `-x`, or `-s` depending on the issue +- Browsers are **much** better for debugging Wasm than any of the command line tools. + - I highly recommend this, even if you are more comfortable with the command line than the browser! + - Browsers have by far the best error messages and debugging tools. There is nothing comparable on the command line. + - We have a web page that can run gen_wasm unit tests: + crates/compiler/test_gen/src/helpers/debug-wasm-test.html + - The page itself contains instructions explaining how to open the browser debug tools. No web dev background should be required. If there's something useful missing, let Brian Carroll know or add him as a reviewer on a PR. diff --git a/crates/compiler/gen_wasm/src/backend.rs b/crates/compiler/gen_wasm/src/backend.rs index f8ef7eed8d..a163df1c18 100644 --- a/crates/compiler/gen_wasm/src/backend.rs +++ b/crates/compiler/gen_wasm/src/backend.rs @@ -8,8 +8,8 @@ use roc_module::low_level::{LowLevel, LowLevelWrapperType}; use roc_module::symbol::{Interns, Symbol}; use roc_mono::code_gen_help::{CodeGenHelp, HelperOp, REFCOUNT_MAX}; use roc_mono::ir::{ - BranchInfo, CallType, Expr, JoinPointId, ListLiteralElement, Literal, ModifyRc, Param, Proc, - ProcLayout, Stmt, + BranchInfo, CallType, CrashTag, Expr, JoinPointId, ListLiteralElement, Literal, ModifyRc, + Param, Proc, ProcLayout, Stmt, }; use roc_mono::layout::{Builtin, Layout, LayoutIds, TagIdIntType, UnionLayout}; use roc_std::RocDec; @@ -717,7 +717,7 @@ impl<'a> WasmBackend<'a> { Stmt::Expect { .. } => todo!("expect is not implemented in the wasm backend"), Stmt::ExpectFx { .. } => todo!("expect-fx is not implemented in the wasm backend"), - Stmt::RuntimeError(msg) => self.stmt_runtime_error(msg), + Stmt::Crash(sym, tag) => self.stmt_crash(*sym, *tag), } } @@ -987,19 +987,31 @@ impl<'a> WasmBackend<'a> { self.stmt(rc_stmt); } - pub fn stmt_runtime_error(&mut self, msg: &'a str) { - // Create a zero-terminated version of the message string - let mut bytes = Vec::with_capacity_in(msg.len() + 1, self.env.arena); - bytes.extend_from_slice(msg.as_bytes()); - bytes.push(0); + pub fn stmt_internal_error(&mut self, msg: &'a str) { + let msg_sym = self.create_symbol("panic_str"); + let msg_storage = self.storage.allocate_var( + self.env.layout_interner, + Layout::Builtin(Builtin::Str), + msg_sym, + StoredVarKind::Variable, + ); - // Store it in the app's data section - let elements_addr = self.store_bytes_in_data_section(&bytes); + // Store the message as a RocStr on the stack + let (local_id, offset) = match msg_storage { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(self.storage.stack_frame_pointer) + } + _ => internal_error!("String must always have stack memory"), + }; + self.expr_string_literal(msg, local_id, offset); - // Pass its address to roc_panic - let tag_id = 0; - self.code_builder.i32_const(elements_addr as i32); - self.code_builder.i32_const(tag_id); + self.stmt_crash(msg_sym, CrashTag::Roc); + } + + pub fn stmt_crash(&mut self, msg: Symbol, tag: CrashTag) { + // load the pointer + self.storage.load_symbols(&mut self.code_builder, &[msg]); + self.code_builder.i32_const(tag as _); self.call_host_fn_after_loading_args("roc_panic", 2, false); self.code_builder.unreachable_(); @@ -1128,45 +1140,7 @@ impl<'a> WasmBackend<'a> { let (local_id, offset) = location.local_and_offset(self.storage.stack_frame_pointer); - let len = string.len(); - if len < 12 { - // Construct the bytes of the small string - let mut bytes = [0; 12]; - bytes[0..len].clone_from_slice(string.as_bytes()); - bytes[11] = 0x80 | (len as u8); - - // Transform into two integers, to minimise number of instructions - let bytes_split: &([u8; 8], [u8; 4]) = - unsafe { std::mem::transmute(&bytes) }; - let int64 = i64::from_le_bytes(bytes_split.0); - let int32 = i32::from_le_bytes(bytes_split.1); - - // Write the integers to memory - self.code_builder.get_local(local_id); - self.code_builder.i64_const(int64); - self.code_builder.i64_store(Align::Bytes4, offset); - self.code_builder.get_local(local_id); - self.code_builder.i32_const(int32); - self.code_builder.i32_store(Align::Bytes4, offset + 8); - } else { - let bytes = string.as_bytes(); - let elements_addr = self.store_bytes_in_data_section(bytes); - - // ptr - self.code_builder.get_local(local_id); - self.code_builder.i32_const(elements_addr as i32); - self.code_builder.i32_store(Align::Bytes4, offset); - - // len - self.code_builder.get_local(local_id); - self.code_builder.i32_const(string.len() as i32); - self.code_builder.i32_store(Align::Bytes4, offset + 4); - - // capacity - self.code_builder.get_local(local_id); - self.code_builder.i32_const(string.len() as i32); - self.code_builder.i32_store(Align::Bytes4, offset + 8); - }; + self.expr_string_literal(string, local_id, offset); } // Bools and bytes should not be stored in the stack frame Literal::Bool(_) | Literal::Byte(_) => invalid_error(), @@ -1177,6 +1151,47 @@ impl<'a> WasmBackend<'a> { }; } + fn expr_string_literal(&mut self, string: &str, local_id: LocalId, offset: u32) { + let len = string.len(); + if len < 12 { + // Construct the bytes of the small string + let mut bytes = [0; 12]; + bytes[0..len].clone_from_slice(string.as_bytes()); + bytes[11] = 0x80 | (len as u8); + + // Transform into two integers, to minimise number of instructions + let bytes_split: &([u8; 8], [u8; 4]) = unsafe { std::mem::transmute(&bytes) }; + let int64 = i64::from_le_bytes(bytes_split.0); + let int32 = i32::from_le_bytes(bytes_split.1); + + // Write the integers to memory + self.code_builder.get_local(local_id); + self.code_builder.i64_const(int64); + self.code_builder.i64_store(Align::Bytes4, offset); + self.code_builder.get_local(local_id); + self.code_builder.i32_const(int32); + self.code_builder.i32_store(Align::Bytes4, offset + 8); + } else { + let bytes = string.as_bytes(); + let elements_addr = self.store_bytes_in_data_section(bytes); + + // ptr + self.code_builder.get_local(local_id); + self.code_builder.i32_const(elements_addr as i32); + self.code_builder.i32_store(Align::Bytes4, offset); + + // len + self.code_builder.get_local(local_id); + self.code_builder.i32_const(string.len() as i32); + self.code_builder.i32_store(Align::Bytes4, offset + 4); + + // capacity + self.code_builder.get_local(local_id); + self.code_builder.i32_const(string.len() as i32); + self.code_builder.i32_store(Align::Bytes4, offset + 8); + }; + } + /// Create a string constant in the module data section /// Return the data we need for code gen: linker symbol index and memory address fn store_bytes_in_data_section(&mut self, bytes: &[u8]) -> u32 { diff --git a/crates/compiler/gen_wasm/src/lib.rs b/crates/compiler/gen_wasm/src/lib.rs index 69e35f12d2..e039e1febf 100644 --- a/crates/compiler/gen_wasm/src/lib.rs +++ b/crates/compiler/gen_wasm/src/lib.rs @@ -56,7 +56,8 @@ impl Env<'_> { /// Parse the preprocessed host binary /// If successful, the module can be passed to build_app_binary pub fn parse_host<'a>(arena: &'a Bump, host_bytes: &[u8]) -> Result, ParseError> { - WasmModule::preload(arena, host_bytes) + let require_relocatable = true; + WasmModule::preload(arena, host_bytes, require_relocatable) } /// Generate a Wasm module in binary form, ready to write to a file. Entry point from roc_build. diff --git a/crates/compiler/gen_wasm/src/low_level.rs b/crates/compiler/gen_wasm/src/low_level.rs index 99be9dea8c..82efa09c8b 100644 --- a/crates/compiler/gen_wasm/src/low_level.rs +++ b/crates/compiler/gen_wasm/src/low_level.rs @@ -1362,7 +1362,7 @@ impl<'a> LowLevelCall<'a> { backend.code_builder.i32_const(i32::MIN); backend.code_builder.i32_eq(); backend.code_builder.if_(); - backend.stmt_runtime_error(PANIC_MSG); + backend.stmt_internal_error(PANIC_MSG); backend.code_builder.end(); // x @@ -1388,7 +1388,7 @@ impl<'a> LowLevelCall<'a> { backend.code_builder.i64_const(i64::MIN); backend.code_builder.i64_eq(); backend.code_builder.if_(); - backend.stmt_runtime_error(PANIC_MSG); + backend.stmt_internal_error(PANIC_MSG); backend.code_builder.end(); // x @@ -1422,7 +1422,7 @@ impl<'a> LowLevelCall<'a> { backend.code_builder.i32_const(i32::MIN); backend.code_builder.i32_eq(); backend.code_builder.if_(); - backend.stmt_runtime_error(PANIC_MSG); + backend.stmt_internal_error(PANIC_MSG); backend.code_builder.end(); backend.code_builder.i32_const(0); @@ -1433,7 +1433,7 @@ impl<'a> LowLevelCall<'a> { backend.code_builder.i64_const(i64::MIN); backend.code_builder.i64_eq(); backend.code_builder.if_(); - backend.stmt_runtime_error(PANIC_MSG); + backend.stmt_internal_error(PANIC_MSG); backend.code_builder.end(); backend.code_builder.i64_const(0); @@ -1874,6 +1874,8 @@ impl<'a> LowLevelCall<'a> { }, StoredValue::StackMemory { .. } => { /* do nothing */ } }, + + Dbg => todo!("{:?}", self.lowlevel), } } diff --git a/crates/compiler/load_internal/src/docs.rs b/crates/compiler/load_internal/src/docs.rs index 0295fa9c47..81fbe95c88 100644 --- a/crates/compiler/load_internal/src/docs.rs +++ b/crates/compiler/load_internal/src/docs.rs @@ -200,6 +200,11 @@ fn generate_entry_docs<'a>( ValueDef::Body(_, _) => (), + ValueDef::Dbg { .. } => { + + // Don't generate docs for `dbg`s + } + ValueDef::Expect { .. } => { // Don't generate docs for `expect`s } diff --git a/crates/compiler/load_internal/src/file.rs b/crates/compiler/load_internal/src/file.rs index b6ad939c11..7e2558babe 100644 --- a/crates/compiler/load_internal/src/file.rs +++ b/crates/compiler/load_internal/src/file.rs @@ -7,7 +7,7 @@ use parking_lot::Mutex; use roc_builtins::roc::module_source; use roc_can::abilities::{AbilitiesStore, PendingAbilitiesStore, ResolvedImpl}; use roc_can::constraint::{Constraint as ConstraintSoa, Constraints, TypeOrVar}; -use roc_can::expr::PendingDerives; +use roc_can::expr::{DbgLookup, PendingDerives}; use roc_can::expr::{Declarations, ExpectLookup}; use roc_can::module::{ canonicalize_module_defs, ExposedByModule, ExposedForModule, ExposedModuleTypes, Module, @@ -731,6 +731,7 @@ pub struct Expectations { pub subs: roc_types::subs::Subs, pub path: PathBuf, pub expectations: VecMap>, + pub dbgs: VecMap, pub ident_ids: IdentIds, } @@ -775,6 +776,7 @@ struct ParsedModule<'a> { } type LocExpects = VecMap>; +type LocDbgs = VecMap; /// A message sent out _from_ a worker thread, /// representing a result of work done, or a request for further work @@ -794,6 +796,7 @@ enum Msg<'a> { module_timing: ModuleTiming, abilities_store: AbilitiesStore, loc_expects: LocExpects, + loc_dbgs: LocDbgs, }, FinishedAllTypeChecking { solved_subs: Solved, @@ -2403,6 +2406,7 @@ fn update<'a>( mut module_timing, abilities_store, loc_expects, + loc_dbgs, } => { log!("solved types for {:?}", module_id); module_timing.end_time = Instant::now(); @@ -2412,7 +2416,7 @@ fn update<'a>( .type_problems .insert(module_id, solved_module.problems); - let should_include_expects = !loc_expects.is_empty() && { + let should_include_expects = (!loc_expects.is_empty() || !loc_dbgs.is_empty()) && { let modules = state.arc_modules.lock(); modules .package_eq(module_id, state.root_id) @@ -2424,6 +2428,7 @@ fn update<'a>( let expectations = Expectations { expectations: loc_expects, + dbgs: loc_dbgs, subs: solved_subs.clone().into_inner(), path: path.to_owned(), ident_ids: ident_ids.clone(), @@ -4552,6 +4557,7 @@ fn run_solve<'a>( let mut module = module; let loc_expects = std::mem::take(&mut module.loc_expects); + let loc_dbgs = std::mem::take(&mut module.loc_dbgs); let module = module; let (solved_subs, solved_implementations, exposed_vars_by_symbol, problems, abilities_store) = { @@ -4626,6 +4632,7 @@ fn run_solve<'a>( module_timing, abilities_store, loc_expects, + loc_dbgs, } } @@ -4832,6 +4839,7 @@ fn canonicalize_and_constrain<'a>( rigid_variables: module_output.rigid_variables, abilities_store: module_output.scope.abilities_store, loc_expects: module_output.loc_expects, + loc_dbgs: module_output.loc_dbgs, }; let constrained_module = ConstrainedModule { diff --git a/crates/compiler/module/src/low_level.rs b/crates/compiler/module/src/low_level.rs index e29c9816a0..bf85a11403 100644 --- a/crates/compiler/module/src/low_level.rs +++ b/crates/compiler/module/src/low_level.rs @@ -112,6 +112,7 @@ pub enum LowLevel { RefCountDec, BoxExpr, UnboxExpr, + Dbg, Unreachable, } @@ -208,11 +209,13 @@ macro_rules! map_symbol_to_lowlevel { LowLevel::NumToIntChecked => unreachable!(), LowLevel::NumToFloatChecked => unreachable!(), + // these are used internally and not tied to a symbol LowLevel::Hash => unimplemented!(), LowLevel::PtrCast => unimplemented!(), LowLevel::RefCountInc => unimplemented!(), LowLevel::RefCountDec => unimplemented!(), + LowLevel::Dbg => unreachable!(), // these are not implemented, not sure why LowLevel::StrFromInt => unimplemented!(), diff --git a/crates/compiler/mono/src/borrow.rs b/crates/compiler/mono/src/borrow.rs index fc37792f57..0bf91f12df 100644 --- a/crates/compiler/mono/src/borrow.rs +++ b/crates/compiler/mono/src/borrow.rs @@ -321,7 +321,7 @@ impl<'a> ParamMap<'a> { } Refcounting(_, _) => unreachable!("these have not been introduced yet"), - Ret(_) | Jump(_, _) | RuntimeError(_) => { + Ret(_) | Jump(_, _) | Crash(..) => { // these are terminal, do nothing } } @@ -827,7 +827,12 @@ impl<'a> BorrowInfState<'a> { Refcounting(_, _) => unreachable!("these have not been introduced yet"), - Ret(_) | RuntimeError(_) => { + Crash(msg, _) => { + // Crash is a foreign call, so we must own the argument. + self.own_var(*msg); + } + + Ret(_) => { // these are terminal, do nothing } } @@ -937,6 +942,8 @@ pub fn lowlevel_borrow_signature(arena: &Bump, op: LowLevel) -> &[bool] { ListIsUnique => arena.alloc_slice_copy(&[borrowed]), + Dbg => arena.alloc_slice_copy(&[borrowed]), + BoxExpr | UnboxExpr => { unreachable!("These lowlevel operations are turned into mono Expr's") } @@ -999,7 +1006,7 @@ fn call_info_stmt<'a>(arena: &'a Bump, stmt: &Stmt<'a>, info: &mut CallInfo<'a>) Refcounting(_, _) => unreachable!("these have not been introduced yet"), - Ret(_) | Jump(_, _) | RuntimeError(_) => { + Ret(_) | Jump(_, _) | Crash(..) => { // these are terminal, do nothing } } diff --git a/crates/compiler/mono/src/inc_dec.rs b/crates/compiler/mono/src/inc_dec.rs index bbba37e80a..8e277e5b91 100644 --- a/crates/compiler/mono/src/inc_dec.rs +++ b/crates/compiler/mono/src/inc_dec.rs @@ -158,7 +158,9 @@ pub fn occurring_variables(stmt: &Stmt<'_>) -> (MutSet, MutSet) stack.push(default_branch.1); } - RuntimeError(_) => {} + Crash(sym, _) => { + result.insert(*sym); + } } } @@ -1240,7 +1242,20 @@ impl<'a, 'i> Context<'a, 'i> { (expect, b_live_vars) } - RuntimeError(_) | Refcounting(_, _) => (stmt, MutSet::default()), + Crash(x, _) => { + let info = self.get_var_info(*x); + + let mut live_vars = MutSet::default(); + live_vars.insert(*x); + + if info.reference && !info.consume { + (self.add_inc(*x, 1, stmt), live_vars) + } else { + (stmt, live_vars) + } + } + + Refcounting(_, _) => (stmt, MutSet::default()), } } } @@ -1411,7 +1426,10 @@ pub fn collect_stmt( vars } - RuntimeError(_) => vars, + Crash(m, _) => { + vars.insert(*m); + vars + } } } diff --git a/crates/compiler/mono/src/ir.rs b/crates/compiler/mono/src/ir.rs index 150cf89596..14654d201d 100644 --- a/crates/compiler/mono/src/ir.rs +++ b/crates/compiler/mono/src/ir.rs @@ -71,6 +71,16 @@ roc_error_macros::assert_sizeof_non_wasm!(ProcLayout, 8 * 8); roc_error_macros::assert_sizeof_non_wasm!(Call, 9 * 8); roc_error_macros::assert_sizeof_non_wasm!(CallType, 7 * 8); +fn runtime_error<'a>(env: &mut Env<'a, '_>, msg: &'a str) -> Stmt<'a> { + let sym = env.unique_symbol(); + Stmt::Let( + sym, + Expr::Literal(Literal::Str(msg)), + Layout::Builtin(Builtin::Str), + env.arena.alloc(Stmt::Crash(sym, CrashTag::Roc)), + ) +} + macro_rules! return_on_layout_error { ($env:expr, $layout_result:expr, $context_msg:expr) => { match $layout_result { @@ -84,15 +94,17 @@ macro_rules! return_on_layout_error_help { ($env:expr, $error:expr, $context_msg:expr) => {{ match $error { LayoutProblem::UnresolvedTypeVar(_) => { - return Stmt::RuntimeError( + return runtime_error( + $env, $env.arena .alloc(format!("UnresolvedTypeVar: {}", $context_msg,)), - ); + ) } LayoutProblem::Erroneous => { - return Stmt::RuntimeError( + return runtime_error( + $env, $env.arena.alloc(format!("Erroneous: {}", $context_msg,)), - ); + ) } } }}; @@ -1611,6 +1623,7 @@ pub fn cond<'a>( } pub type Stores<'a> = &'a [(Symbol, Layout<'a>, Expr<'a>)]; + #[derive(Clone, Debug, PartialEq)] pub enum Stmt<'a> { Let(Symbol, Expr<'a>, Layout<'a>, &'a Stmt<'a>), @@ -1655,7 +1668,29 @@ pub enum Stmt<'a> { remainder: &'a Stmt<'a>, }, Jump(JoinPointId, &'a [Symbol]), - RuntimeError(&'a str), + Crash(Symbol, CrashTag), +} + +/// Source of crash, and its runtime representation to roc_panic. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum CrashTag { + /// The crash is due to Roc, either via a builtin or type error. + Roc = 0, + /// The crash is user-defined. + User = 1, +} + +impl TryFrom for CrashTag { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Roc), + 1 => Ok(Self::User), + _ => Err(()), + } + } } /// in the block below, symbol `scrutinee` is assumed be be of shape `tag_id` @@ -2303,7 +2338,7 @@ impl<'a> Stmt<'a> { } } - RuntimeError(s) => alloc.text(format!("Error {}", s)), + Crash(s, _src) => alloc.text("Crash ").append(symbol_to_doc(alloc, *s)), Join { id, @@ -3152,7 +3187,7 @@ fn generate_runtime_error_function<'a>( ); }); - let runtime_error = Stmt::RuntimeError(msg.into_bump_str()); + let runtime_error = runtime_error(env, msg.into_bump_str()); let (args, ret_layout) = match layout { RawFunctionLayout::Function(arg_layouts, lambda_set, ret_layout) => { @@ -4300,7 +4335,7 @@ pub fn with_hole<'a>( }; let sorted_fields = match sorted_fields_result { Ok(fields) => fields, - Err(_) => return Stmt::RuntimeError("Can't create record with improper layout"), + Err(_) => return runtime_error(env, "Can't create record with improper layout"), }; let mut field_symbols = Vec::with_capacity_in(fields.len(), env.arena); @@ -4352,7 +4387,7 @@ pub fn with_hole<'a>( // creating a record from the var will unpack it if it's just a single field. let layout = match layout_cache.from_var(env.arena, record_var, env.subs) { Ok(layout) => layout, - Err(_) => return Stmt::RuntimeError("Can't create record with improper layout"), + Err(_) => return runtime_error(env, "Can't create record with improper layout"), }; let field_symbols = field_symbols.into_bump_slice(); @@ -4403,6 +4438,7 @@ pub fn with_hole<'a>( Expect { .. } => unreachable!("I think this is unreachable"), ExpectFx { .. } => unreachable!("I think this is unreachable"), + Dbg { .. } => unreachable!("I think this is unreachable"), If { cond_var, @@ -4530,8 +4566,8 @@ pub fn with_hole<'a>( } } } - (Err(_), _) => Stmt::RuntimeError("invalid ret_layout"), - (_, Err(_)) => Stmt::RuntimeError("invalid cond_layout"), + (Err(_), _) => runtime_error(env, "invalid ret_layout"), + (_, Err(_)) => runtime_error(env, "invalid cond_layout"), } } @@ -4697,7 +4733,7 @@ pub fn with_hole<'a>( }; let sorted_fields = match sorted_fields_result { Ok(fields) => fields, - Err(_) => return Stmt::RuntimeError("Can't access record with improper layout"), + Err(_) => return runtime_error(env, "Can't access record with improper layout"), }; let mut index = None; @@ -4815,7 +4851,8 @@ pub fn with_hole<'a>( } } - Err(_error) => Stmt::RuntimeError( + Err(_error) => runtime_error( + env, "TODO convert anonymous function error to a RuntimeError string", ), } @@ -4871,7 +4908,8 @@ pub fn with_hole<'a>( } } - Err(_error) => Stmt::RuntimeError( + Err(_error) => runtime_error( + env, "TODO convert anonymous function error to a RuntimeError string", ), } @@ -4906,7 +4944,7 @@ pub fn with_hole<'a>( let sorted_fields = match sorted_fields_result { Ok(fields) => fields, - Err(_) => return Stmt::RuntimeError("Can't update record with improper layout"), + Err(_) => return runtime_error(env, "Can't update record with improper layout"), }; let mut field_layouts = Vec::with_capacity_in(sorted_fields.len(), env.arena); @@ -5092,10 +5130,10 @@ pub fn with_hole<'a>( layout_cache, ); - if let Err(runtime_error) = inserted { - return Stmt::RuntimeError( - env.arena - .alloc(format!("RuntimeError: {:?}", runtime_error,)), + if let Err(e) = inserted { + return runtime_error( + env, + env.arena.alloc(format!("RuntimeError: {:?}", e,)), ); } else { drop(inserted); @@ -5559,8 +5597,20 @@ pub fn with_hole<'a>( } } } - TypedHole(_) => Stmt::RuntimeError("Hit a blank"), - RuntimeError(e) => Stmt::RuntimeError(env.arena.alloc(e.runtime_message())), + TypedHole(_) => runtime_error(env, "Hit a blank"), + RuntimeError(e) => runtime_error(env, env.arena.alloc(e.runtime_message())), + Crash { msg, ret_var: _ } => { + let msg_sym = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &msg.value, + Variable::STR, + ); + let stmt = Stmt::Crash(msg_sym, CrashTag::User); + + assign_to_symbol(env, procs, layout_cache, Variable::STR, *msg, msg_sym, stmt) + } } } @@ -5819,16 +5869,22 @@ fn convert_tag_union<'a>( let variant = match res_variant { Ok(cached) => cached, Err(LayoutProblem::UnresolvedTypeVar(_)) => { - return Stmt::RuntimeError(env.arena.alloc(format!( - "Unresolved type variable for tag {}", - tag_name.0.as_str() - ))) + return runtime_error( + env, + env.arena.alloc(format!( + "Unresolved type variable for tag {}", + tag_name.0.as_str() + )), + ) } Err(LayoutProblem::Erroneous) => { - return Stmt::RuntimeError(env.arena.alloc(format!( - "Tag {} was part of a type error!", - tag_name.0.as_str() - ))); + return runtime_error( + env, + env.arena.alloc(format!( + "Tag {} was part of a type error!", + tag_name.0.as_str() + )), + ); } }; @@ -5856,7 +5912,7 @@ fn convert_tag_union<'a>( Layout::Builtin(Builtin::Int(IntWidth::U8)), hole, ), - None => Stmt::RuntimeError("tag must be in its own type"), + None => runtime_error(env, "tag must be in its own type"), } } @@ -5896,7 +5952,7 @@ fn convert_tag_union<'a>( if dataful_tag != tag_name { // this tag is not represented, and hence will never be reached, at runtime. - Stmt::RuntimeError("voided tag constructor is unreachable") + runtime_error(env, "voided tag constructor is unreachable") } else { let field_symbols_temp = sorted_field_symbols(env, procs, layout_cache, args); @@ -6160,10 +6216,13 @@ fn tag_union_to_function<'a>( } } - Err(runtime_error) => Stmt::RuntimeError(env.arena.alloc(format!( - "Could not produce tag function due to a runtime error: {:?}", - runtime_error, - ))), + Err(e) => runtime_error( + env, + env.arena.alloc(format!( + "Could not produce tag function due to a runtime error: {:?}", + e, + )), + ), } } @@ -6546,6 +6605,50 @@ pub fn from_can<'a>( stmt } + Dbg { + loc_condition, + loc_continuation, + variable, + symbol: dbg_symbol, + } => { + let rest = from_can(env, variable, loc_continuation.value, procs, layout_cache); + + let call = crate::ir::Call { + call_type: CallType::LowLevel { + op: LowLevel::Dbg, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([dbg_symbol]), + }; + + let dbg_layout = layout_cache + .from_var(env.arena, variable, env.subs) + .expect("invalid dbg_layout"); + + let expr = Expr::Call(call); + let mut stmt = Stmt::Let(dbg_symbol, expr, dbg_layout, env.arena.alloc(rest)); + + let symbol_is_reused = matches!( + can_reuse_symbol(env, procs, &loc_condition.value, variable), + ReuseSymbol::Value(_) + ); + + // skip evaluating the condition if it's just a symbol + if !symbol_is_reused { + stmt = with_hole( + env, + loc_condition.value, + variable, + procs, + layout_cache, + dbg_symbol, + env.arena.alloc(stmt), + ); + } + + stmt + } + LetRec(defs, cont, _cycle_mark) => { // because Roc is strict, only functions can be recursive! for def in defs.into_iter() { @@ -6677,7 +6780,7 @@ fn from_can_when<'a>( if branches.is_empty() { // A when-expression with no branches is a runtime error. // We can't know what to return! - return Stmt::RuntimeError("Hit a 0-branch when expression"); + return runtime_error(env, "Hit a 0-branch when expression"); } let opt_branches = to_opt_branches(env, procs, branches, exhaustive_mark, layout_cache); @@ -6980,8 +7083,7 @@ fn substitute_in_stmt_help<'a>( None } } - - RuntimeError(_) => None, + Crash(msg, tag) => substitute(subs, *msg).map(|new| &*arena.alloc(Crash(new, *tag))), } } @@ -8364,7 +8466,7 @@ fn evaluate_arguments_then_runtime_error<'a>( let arena = env.arena; // eventually we will throw this runtime error - let result = Stmt::RuntimeError(env.arena.alloc(msg)); + let result = runtime_error(env, env.arena.alloc(msg)); // but, we also still evaluate and specialize the arguments to give better error messages let arg_symbols = Vec::from_iter_in( @@ -8589,7 +8691,7 @@ fn call_by_name_help<'a>( Err(_) => { // One of this function's arguments code gens to a runtime error, // so attempting to call it will immediately crash. - return Stmt::RuntimeError("TODO runtime error for invalid layout"); + return runtime_error(env, "TODO runtime error for invalid layout"); } } } @@ -10088,7 +10190,7 @@ where ToLowLevelCall: Fn(ToLowLevelCallArguments<'a>) -> Call<'a> + Copy, { match lambda_set.call_by_name_options(&layout_cache.interner) { - ClosureCallOptions::Void => empty_lambda_set_error(), + ClosureCallOptions::Void => empty_lambda_set_error(env), ClosureCallOptions::Union(union_layout) => { let closure_tag_id_symbol = env.unique_symbol(); @@ -10267,9 +10369,9 @@ where } } -fn empty_lambda_set_error() -> Stmt<'static> { +fn empty_lambda_set_error<'a>(env: &mut Env<'a, '_>) -> Stmt<'a> { let msg = "a Lambda Set is empty. Most likely there is a type error in your program."; - Stmt::RuntimeError(msg) + runtime_error(env, msg) } /// Use the lambda set to figure out how to make a call-by-name @@ -10287,7 +10389,7 @@ fn match_on_lambda_set<'a>( hole: &'a Stmt<'a>, ) -> Stmt<'a> { match lambda_set.call_by_name_options(&layout_cache.interner) { - ClosureCallOptions::Void => empty_lambda_set_error(), + ClosureCallOptions::Void => empty_lambda_set_error(env), ClosureCallOptions::Union(union_layout) => { let closure_tag_id_symbol = env.unique_symbol(); @@ -10441,7 +10543,7 @@ fn union_lambda_set_to_switch<'a>( // there is really nothing we can do here. We generate a runtime error here which allows // code gen to proceed. We then assume that we hit another (more descriptive) error before // hitting this one - return empty_lambda_set_error(); + return empty_lambda_set_error(env); } let join_point_id = JoinPointId(env.unique_symbol()); diff --git a/crates/compiler/mono/src/reset_reuse.rs b/crates/compiler/mono/src/reset_reuse.rs index 6b2447c250..c47f2e6d56 100644 --- a/crates/compiler/mono/src/reset_reuse.rs +++ b/crates/compiler/mono/src/reset_reuse.rs @@ -241,7 +241,7 @@ fn function_s<'a, 'i>( } } - Ret(_) | Jump(_, _) | RuntimeError(_) => stmt, + Ret(_) | Jump(_, _) | Crash(..) => stmt, } } @@ -535,7 +535,7 @@ fn function_d_main<'a, 'i>( (arena.alloc(new_join), found) } - Ret(_) | Jump(_, _) | RuntimeError(_) => (stmt, has_live_var(&env.jp_live_vars, stmt, x)), + Ret(_) | Jump(_, _) | Crash(..) => (stmt, has_live_var(&env.jp_live_vars, stmt, x)), } } @@ -696,7 +696,7 @@ fn function_r<'a, 'i>(env: &mut Env<'a, 'i>, stmt: &'a Stmt<'a>) -> &'a Stmt<'a> arena.alloc(expect) } - Ret(_) | Jump(_, _) | RuntimeError(_) => { + Ret(_) | Jump(_, _) | Crash(..) => { // terminals stmt } @@ -761,7 +761,7 @@ fn has_live_var<'a>(jp_live_vars: &JPLiveVarMap, stmt: &'a Stmt<'a>, needle: Sym Jump(id, arguments) => { arguments.iter().any(|s| *s == needle) || jp_live_vars[id].contains(&needle) } - RuntimeError(_) => false, + Crash(m, _) => *m == needle, } } diff --git a/crates/compiler/mono/src/tail_recursion.rs b/crates/compiler/mono/src/tail_recursion.rs index 834c0dc6c8..96bc7df8f2 100644 --- a/crates/compiler/mono/src/tail_recursion.rs +++ b/crates/compiler/mono/src/tail_recursion.rs @@ -299,6 +299,6 @@ fn insert_jumps<'a>( Ret(_) => None, Jump(_, _) => None, - RuntimeError(_) => None, + Crash(..) => None, } } diff --git a/crates/compiler/parse/fuzz/dict.txt b/crates/compiler/parse/fuzz/dict.txt index c2e458bbda..4bb264389b 100644 --- a/crates/compiler/parse/fuzz/dict.txt +++ b/crates/compiler/parse/fuzz/dict.txt @@ -5,6 +5,7 @@ "as" "is" "expect" +"dbg" "app" "platform" diff --git a/crates/compiler/parse/src/ast.rs b/crates/compiler/parse/src/ast.rs index c952f75fce..5655b9f2e9 100644 --- a/crates/compiler/parse/src/ast.rs +++ b/crates/compiler/parse/src/ast.rs @@ -196,6 +196,9 @@ pub enum Expr<'a> { Underscore(&'a str), + // The "crash" keyword + Crash, + // Tags Tag(&'a str), @@ -208,6 +211,7 @@ pub enum Expr<'a> { Defs(&'a Defs<'a>, &'a Loc>), Backpassing(&'a [Loc>], &'a Loc>, &'a Loc>), Expect(&'a Loc>, &'a Loc>), + Dbg(&'a Loc>, &'a Loc>), // Application /// To apply by name, do Apply(Var(...), ...) @@ -339,6 +343,11 @@ pub enum ValueDef<'a> { body_expr: &'a Loc>, }, + Dbg { + condition: &'a Loc>, + preceding_comment: Region, + }, + Expect { condition: &'a Loc>, preceding_comment: Region, @@ -527,6 +536,13 @@ pub enum TypeAnnotation<'a> { ext: Option<&'a Loc>>, }, + Tuple { + fields: Collection<'a, Loc>>, + /// The row type variable in an open tuple, e.g. the `r` in `( Str, Str )r`. + /// This is None if it's a closed tuple annotation like `( Str, Str )`. + ext: Option<&'a Loc>>, + }, + /// A tag union, e.g. `[ TagUnion { /// The row type variable in an open tag union, e.g. the `a` in `[Foo, Bar]a`. diff --git a/crates/compiler/parse/src/expr.rs b/crates/compiler/parse/src/expr.rs index 20094f8c2a..08274a5b17 100644 --- a/crates/compiler/parse/src/expr.rs +++ b/crates/compiler/parse/src/expr.rs @@ -6,7 +6,7 @@ use crate::blankspace::{ space0_after_e, space0_around_e_no_after_indent_check, space0_around_ee, space0_before_e, space0_before_optional_after, space0_e, }; -use crate::ident::{lowercase_ident, parse_ident, Ident}; +use crate::ident::{integer_ident, lowercase_ident, parse_ident, Accessor, Ident}; use crate::keyword; use crate::parser::{ self, backtrackable, increment_min_indent, line_min_indent, optional, reset_min_indent, @@ -124,13 +124,9 @@ fn loc_expr_in_parens_etc_help<'a>() -> impl Parser<'a, Loc>, EExpr<'a> map_with_arena!( loc!(and!( specialize(EExpr::InParens, loc_expr_in_parens_help()), - one_of![record_field_access_chain(), |a, s, _m| Ok(( - NoProgress, - Vec::new_in(a), - s - ))] + record_field_access_chain() )), - move |arena: &'a Bump, value: Loc<(Loc>, Vec<'a, &'a str>)>| { + move |arena: &'a Bump, value: Loc<(Loc>, Vec<'a, Accessor<'a>>)>| { let Loc { mut region, value: (loc_expr, field_accesses), @@ -143,12 +139,7 @@ fn loc_expr_in_parens_etc_help<'a>() -> impl Parser<'a, Loc>, EExpr<'a> if field_accesses.is_empty() { region = loc_expr.region; } else { - for field in field_accesses { - // Wrap the previous answer in the new one, so we end up - // with a nested Expr. That way, `foo.bar.baz` gets represented - // in the AST as if it had been written (foo.bar).baz all along. - value = Expr::RecordAccess(arena.alloc(value), field); - } + value = apply_expr_access_chain(arena, value, field_accesses); } Loc::at(region, value) @@ -156,39 +147,17 @@ fn loc_expr_in_parens_etc_help<'a>() -> impl Parser<'a, Loc>, EExpr<'a> ) } -fn record_field_access_chain<'a>() -> impl Parser<'a, Vec<'a, &'a str>, EExpr<'a>> { - |arena, state: State<'a>, min_indent| match record_field_access().parse( - arena, - state.clone(), - min_indent, - ) { - Ok((_, initial, state)) => { - let mut accesses = Vec::with_capacity_in(1, arena); - - accesses.push(initial); - - let mut loop_state = state; - loop { - match record_field_access().parse(arena, loop_state.clone(), min_indent) { - Ok((_, next, state)) => { - accesses.push(next); - loop_state = state; - } - Err((MadeProgress, fail)) => return Err((MadeProgress, fail)), - Err((NoProgress, _)) => return Ok((MadeProgress, accesses, loop_state)), - } - } - } - Err((MadeProgress, fail)) => Err((MadeProgress, fail)), - Err((NoProgress, _)) => Err((NoProgress, EExpr::Access(state.pos()))), - } -} - -fn record_field_access<'a>() -> impl Parser<'a, &'a str, EExpr<'a>> { - skip_first!( +fn record_field_access_chain<'a>() -> impl Parser<'a, Vec<'a, Accessor<'a>>, EExpr<'a>> { + zero_or_more!(skip_first!( word1(b'.', EExpr::Access), - specialize(|_, pos| EExpr::Access(pos), lowercase_ident()) - ) + specialize( + |_, pos| EExpr::Access(pos), + one_of!( + map!(lowercase_ident(), Accessor::RecordField), + map!(integer_ident(), Accessor::TupleIndex), + ) + ) + )) } /// In some contexts we want to parse the `_` as an expression, so it can then be turned into a @@ -204,6 +173,7 @@ fn loc_term_or_underscore_or_conditional<'a>( loc!(specialize(EExpr::SingleQuote, single_quote_literal_help())), loc!(specialize(EExpr::Number, positive_number_literal_help())), loc!(specialize(EExpr::Closure, closure_help(options))), + loc!(crash_kw()), loc!(underscore_expression()), loc!(record_literal_help()), loc!(specialize(EExpr::List, list_literal_help())), @@ -269,6 +239,15 @@ fn underscore_expression<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { } } +fn crash_kw<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { + move |arena: &'a Bump, state: State<'a>, min_indent: u32| { + let (_, _, next_state) = crate::parser::keyword_e(crate::keyword::CRASH, EExpr::Crash) + .parse(arena, state, min_indent)?; + + Ok((MadeProgress, Expr::Crash, next_state)) + } +} + fn loc_possibly_negative_or_negated_term<'a>( options: ExprParseOptions, ) -> impl Parser<'a, Loc>, EExpr<'a>> { @@ -327,6 +306,7 @@ fn expr_start<'a>(options: ExprParseOptions) -> impl Parser<'a, Loc>, E loc!(specialize(EExpr::If, if_expr_help(options))), loc!(specialize(EExpr::When, when::expr_help(options))), loc!(specialize(EExpr::Expect, expect_help(options))), + loc!(specialize(EExpr::Dbg, dbg_help(options))), loc!(specialize(EExpr::Closure, closure_help(options))), loc!(expr_operator_chain(options)), fail_expr_start_e() @@ -586,6 +566,7 @@ pub fn parse_single_def<'a>( let start = state.pos(); + let parse_dbg = crate::parser::keyword_e(crate::keyword::DBG, EExpect::Dbg); let parse_expect_vanilla = crate::parser::keyword_e(crate::keyword::EXPECT, EExpect::Expect); let parse_expect_fx = crate::parser::keyword_e(crate::keyword::EXPECT_FX, EExpect::Expect); let parse_expect = either!(parse_expect_fx, parse_expect_vanilla); @@ -596,37 +577,35 @@ pub fn parse_single_def<'a>( min_indent, ) { Err((NoProgress, _)) => { - match parse_expect.parse(arena, state, min_indent) { + match parse_expect.parse(arena, state.clone(), min_indent) { Err((_, _)) => { - // a hacky way to get expression-based error messages. TODO fix this - Ok((NoProgress, None, initial)) - } - Ok((_, expect_flavor, state)) => { - let parse_def_expr = - space0_before_e(increment_min_indent(loc_expr()), EExpr::IndentEnd); - - let (_, loc_def_expr, state) = - parse_def_expr.parse(arena, state, min_indent)?; - let end = loc_def_expr.region.end(); - let region = Region::new(start, end); - - // drop newlines before the preceding comment - let spaces_before_start = spaces_before_current_start.offset as usize; - let spaces_before_end = start.offset as usize; - let mut spaces_before_current_start = spaces_before_current_start; - - for byte in &state.original_bytes()[spaces_before_start..spaces_before_end] { - match byte { - b' ' | b'\n' => { - spaces_before_current_start.offset += 1; - } - _ => break, + match parse_dbg.parse(arena, state, min_indent) { + Ok((_, _, state)) => parse_statement_inside_def( + arena, + state, + min_indent, + start, + spaces_before_current_start, + spaces_before_current, + |preceding_comment, loc_def_expr| ValueDef::Dbg { + condition: arena.alloc(loc_def_expr), + preceding_comment, + }, + ), + Err((_, _)) => { + // a hacky way to get expression-based error messages. TODO fix this + Ok((NoProgress, None, initial)) } } - - let preceding_comment = Region::new(spaces_before_current_start, start); - - let value_def = match expect_flavor { + } + Ok((_, expect_flavor, state)) => parse_statement_inside_def( + arena, + state, + min_indent, + start, + spaces_before_current_start, + spaces_before_current, + |preceding_comment, loc_def_expr| match expect_flavor { Either::Second(_) => ValueDef::Expect { condition: arena.alloc(loc_def_expr), preceding_comment, @@ -635,18 +614,8 @@ pub fn parse_single_def<'a>( condition: arena.alloc(loc_def_expr), preceding_comment, }, - }; - - Ok(( - MadeProgress, - Some(SingleDef { - type_or_value: Either::Second(value_def), - region, - spaces_before: spaces_before_current, - }), - state, - )) - } + }, + ), } } Err((MadeProgress, _)) => { @@ -870,6 +839,49 @@ pub fn parse_single_def<'a>( } } +/// e.g. Things that can be on their own line in a def, e.g. `expect`, `expect-fx`, or `dbg` +fn parse_statement_inside_def<'a>( + arena: &'a Bump, + state: State<'a>, + min_indent: u32, + start: Position, + spaces_before_current_start: Position, + spaces_before_current: &'a [CommentOrNewline<'a>], + get_value_def: impl Fn(Region, Loc>) -> ValueDef<'a>, +) -> Result<(Progress, Option>, State<'a>), (Progress, EExpr<'a>)> { + let parse_def_expr = space0_before_e(increment_min_indent(loc_expr()), EExpr::IndentEnd); + let (_, loc_def_expr, state) = parse_def_expr.parse(arena, state, min_indent)?; + let end = loc_def_expr.region.end(); + let region = Region::new(start, end); + + // drop newlines before the preceding comment + let spaces_before_start = spaces_before_current_start.offset as usize; + let spaces_before_end = start.offset as usize; + let mut spaces_before_current_start = spaces_before_current_start; + + for byte in &state.original_bytes()[spaces_before_start..spaces_before_end] { + match byte { + b' ' | b'\n' => { + spaces_before_current_start.offset += 1; + } + _ => break, + } + } + + let preceding_comment = Region::new(spaces_before_current_start, start); + let value_def = get_value_def(preceding_comment, loc_def_expr); + + Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::Second(value_def), + region, + spaces_before: spaces_before_current, + }), + state, + )) +} + // This is a macro only because trying to make it be a function caused lifetime issues. #[macro_export] macro_rules! join_ann_to_body { @@ -1880,10 +1892,12 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result Err(()), + | Expr::UnaryOp(_, _) + | Expr::Crash => Err(()), Expr::Str(string) => Ok(Pattern::StrLiteral(*string)), Expr::SingleQuote(string) => Ok(Pattern::SingleQuote(string)), @@ -2298,6 +2312,36 @@ fn expect_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EExpe } } +fn dbg_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EExpect<'a>> { + move |arena: &'a Bump, state: State<'a>, min_indent| { + let start_column = state.column(); + + let (_, _, state) = + parser::keyword_e(keyword::DBG, EExpect::Dbg).parse(arena, state, min_indent)?; + + let (_, condition, state) = space0_before_e( + specialize_ref( + EExpect::Condition, + set_min_indent(start_column + 1, expr_start(options)), + ), + EExpect::IndentCondition, + ) + .parse(arena, state, start_column + 1) + .map_err(|(_, f)| (MadeProgress, f))?; + + let parse_cont = specialize_ref( + EExpect::Continuation, + space0_before_e(loc_expr(), EExpr::IndentEnd), + ); + + let (_, loc_cont, state) = parse_cont.parse(arena, state, min_indent)?; + + let expr = Expr::Dbg(arena.alloc(condition), arena.alloc(loc_cont)); + + Ok((MadeProgress, expr, state)) + } +} + fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<'a>> { move |arena: &'a Bump, state, min_indent| { let (_, _, state) = @@ -2557,13 +2601,13 @@ fn record_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { and!( loc!(specialize(EExpr::Record, record_help())), // there can be field access, e.g. `{ x : 4 }.x` - optional(record_field_access_chain()) + record_field_access_chain() ), - move |arena, state, _, (loc_record, accesses)| { + move |arena, state, _, (loc_record, accessors)| { let (opt_update, loc_assigned_fields_with_comments) = loc_record.value; // This is a record literal, not a destructure. - let mut value = match opt_update { + let value = match opt_update { Some(update) => Expr::RecordUpdate { update: &*arena.alloc(update), fields: Collection::with_items_and_comments( @@ -2579,20 +2623,26 @@ fn record_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { )), }; - if let Some(fields) = accesses { - for field in fields { - // Wrap the previous answer in the new one, so we end up - // with a nested Expr. That way, `foo.bar.baz` gets represented - // in the AST as if it had been written (foo.bar).baz all along. - value = Expr::RecordAccess(arena.alloc(value), field); - } - } + let value = apply_expr_access_chain(arena, value, accessors); Ok((MadeProgress, value, state)) }, ) } +fn apply_expr_access_chain<'a>( + arena: &'a Bump, + value: Expr<'a>, + accessors: Vec<'a, Accessor<'a>>, +) -> Expr<'a> { + accessors + .into_iter() + .fold(value, |value, accessor| match accessor { + Accessor::RecordField(field) => Expr::RecordAccess(arena.alloc(value), field), + Accessor::TupleIndex(field) => Expr::TupleAccess(arena.alloc(value), field), + }) +} + fn string_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EString<'a>> { map!(crate::string_literal::parse(), Expr::Str) } diff --git a/crates/compiler/parse/src/ident.rs b/crates/compiler/parse/src/ident.rs index 4f369f0bad..ea79756d7a 100644 --- a/crates/compiler/parse/src/ident.rs +++ b/crates/compiler/parse/src/ident.rs @@ -100,6 +100,17 @@ pub fn lowercase_ident<'a>() -> impl Parser<'a, &'a str, ()> { } } +/// This is a tuple accessor, e.g. "1" in `.1` +pub fn integer_ident<'a>() -> impl Parser<'a, &'a str, ()> { + move |_, state: State<'a>, _min_indent: u32| match chomp_integer_part(state.bytes()) { + Err(progress) => Err((progress, ())), + Ok(ident) => { + let width = ident.len(); + Ok((MadeProgress, ident, state.advance(width))) + } + } +} + pub fn tag_name<'a>() -> impl Parser<'a, &'a str, ()> { move |arena, state: State<'a>, min_indent: u32| { uppercase_ident().parse(arena, state, min_indent) diff --git a/crates/compiler/parse/src/keyword.rs b/crates/compiler/parse/src/keyword.rs index b9185518bd..e20ce81870 100644 --- a/crates/compiler/parse/src/keyword.rs +++ b/crates/compiler/parse/src/keyword.rs @@ -4,7 +4,9 @@ pub const ELSE: &str = "else"; pub const WHEN: &str = "when"; pub const AS: &str = "as"; pub const IS: &str = "is"; +pub const DBG: &str = "dbg"; pub const EXPECT: &str = "expect"; pub const EXPECT_FX: &str = "expect-fx"; +pub const CRASH: &str = "crash"; -pub const KEYWORDS: [&str; 8] = [IF, THEN, ELSE, WHEN, AS, IS, EXPECT, EXPECT_FX]; +pub const KEYWORDS: [&str; 10] = [IF, THEN, ELSE, WHEN, AS, IS, DBG, EXPECT, EXPECT_FX, CRASH]; diff --git a/crates/compiler/parse/src/parser.rs b/crates/compiler/parse/src/parser.rs index cf30c6a37b..a13c26848c 100644 --- a/crates/compiler/parse/src/parser.rs +++ b/crates/compiler/parse/src/parser.rs @@ -354,9 +354,11 @@ pub enum EExpr<'a> { If(EIf<'a>, Position), Expect(EExpect<'a>, Position), + Dbg(EExpect<'a>, Position), Closure(EClosure<'a>, Position), Underscore(Position), + Crash(Position), InParens(EInParens<'a>, Position), Record(ERecord<'a>, Position), @@ -544,6 +546,7 @@ pub enum EIf<'a> { #[derive(Debug, Clone, PartialEq, Eq)] pub enum EExpect<'a> { Space(BadInputError, Position), + Dbg(Position), Expect(Position), Condition(&'a EExpr<'a>, Position), Continuation(&'a EExpr<'a>, Position), @@ -672,6 +675,9 @@ pub enum ETypeTagUnion<'a> { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ETypeInParens<'a> { + /// e.g. (), which isn't a valid type + Empty(Position), + End(Position), Open(Position), /// @@ -1784,7 +1790,7 @@ macro_rules! one_or_more { move |arena, state: State<'a>, min_indent: u32| { use bumpalo::collections::Vec; - match $parser.parse(arena, state, min_indent) { + match $parser.parse(arena, state.clone(), min_indent) { Ok((_, first_output, next_state)) => { let mut state = next_state; let mut buf = Vec::with_capacity_in(1, arena); @@ -1802,14 +1808,12 @@ macro_rules! one_or_more { return Ok((MadeProgress, buf, old_state)); } Err((MadeProgress, fail)) => { - return Err((MadeProgress, fail, old_state)); + return Err((MadeProgress, fail)); } } } } - Err((progress, _, new_state)) => { - Err((progress, $to_error(new_state.pos), new_state)) - } + Err((progress, _)) => Err((progress, $to_error(state.pos()))), } } }; diff --git a/crates/compiler/parse/src/type_annotation.rs b/crates/compiler/parse/src/type_annotation.rs index 4574935919..e1f92e1b63 100644 --- a/crates/compiler/parse/src/type_annotation.rs +++ b/crates/compiler/parse/src/type_annotation.rs @@ -116,7 +116,7 @@ fn term<'a>(stop_at_surface_has: bool) -> impl Parser<'a, Loc one_of!( loc_wildcard(), loc_inferred(), - specialize(EType::TInParens, loc_type_in_parens()), + specialize(EType::TInParens, loc_type_in_parens(stop_at_surface_has)), loc!(specialize(EType::TRecord, record_type(stop_at_surface_has))), loc!(specialize( EType::TTagUnion, @@ -185,7 +185,7 @@ fn loc_applied_arg<'a>( one_of!( loc_wildcard(), loc_inferred(), - specialize(EType::TInParens, loc_type_in_parens()), + specialize(EType::TInParens, loc_type_in_parens(stop_at_surface_has)), loc!(specialize(EType::TRecord, record_type(stop_at_surface_has))), loc!(specialize( EType::TTagUnion, @@ -206,16 +206,45 @@ fn loc_applied_arg<'a>( ) } -fn loc_type_in_parens<'a>() -> impl Parser<'a, Loc>, ETypeInParens<'a>> { - between!( - word1(b'(', ETypeInParens::Open), - space0_around_ee( - specialize_ref(ETypeInParens::Type, expression(true, false)), - ETypeInParens::IndentOpen, - ETypeInParens::IndentEnd, - ), - word1(b')', ETypeInParens::IndentEnd) +fn loc_type_in_parens<'a>( + stop_at_surface_has: bool, +) -> impl Parser<'a, Loc>, ETypeInParens<'a>> { + then( + loc!(and!( + collection_trailing_sep_e!( + word1(b'(', ETypeInParens::Open), + specialize_ref(ETypeInParens::Type, expression(true, false)), + word1(b',', ETypeInParens::End), + word1(b')', ETypeInParens::End), + ETypeInParens::Open, + ETypeInParens::IndentEnd, + TypeAnnotation::SpaceBefore + ), + optional(allocated(specialize_ref( + ETypeInParens::Type, + term(stop_at_surface_has) + ))) + )), + |_arena, state, progress, item| { + let Loc { + region, + value: (fields, ext), + } = item; + if fields.len() > 1 || ext.is_some() { + Ok(( + MadeProgress, + Loc::at(region, TypeAnnotation::Tuple { fields, ext }), + state, + )) + } else if fields.len() == 1 { + Ok((MadeProgress, fields.items[0], state)) + } else { + debug_assert!(fields.is_empty()); + Err((progress, ETypeInParens::Empty(state.pos()))) + } + }, ) + .trace("type_annotation:type_in_parens") } #[inline(always)] @@ -322,29 +351,24 @@ fn record_type_field<'a>() -> impl Parser<'a, AssignedField<'a, TypeAnnotation<' fn record_type<'a>( stop_at_surface_has: bool, ) -> impl Parser<'a, TypeAnnotation<'a>, ETypeRecord<'a>> { - use crate::type_annotation::TypeAnnotation::*; - - (move |arena, state, min_indent| { - let (_, fields, state) = collection_trailing_sep_e!( - // word1_check_indent!(b'{', TRecord::Open, min_indent, TRecord::IndentOpen), - word1(b'{', ETypeRecord::Open), - loc!(record_type_field()), - word1(b',', ETypeRecord::End), - // word1_check_indent!(b'}', TRecord::End, min_indent, TRecord::IndentEnd), - word1(b'}', ETypeRecord::End), - ETypeRecord::Open, - ETypeRecord::IndentEnd, - AssignedField::SpaceBefore - ) - .parse(arena, state, min_indent)?; - - let field_term = specialize_ref(ETypeRecord::Type, term(stop_at_surface_has)); - let (_, ext, state) = optional(allocated(field_term)).parse(arena, state, min_indent)?; - - let result = Record { fields, ext }; - - Ok((MadeProgress, result, state)) - }) + map!( + and!( + collection_trailing_sep_e!( + word1(b'{', ETypeRecord::Open), + loc!(record_type_field()), + word1(b',', ETypeRecord::End), + word1(b'}', ETypeRecord::End), + ETypeRecord::Open, + ETypeRecord::IndentEnd, + AssignedField::SpaceBefore + ), + optional(allocated(specialize_ref( + ETypeRecord::Type, + term(stop_at_surface_has) + ))) + ), + |(fields, ext)| { TypeAnnotation::Record { fields, ext } } + ) .trace("type_annotation:record_type") } diff --git a/crates/compiler/parse/tests/snapshots/pass/crash.expr.formatted.roc b/crates/compiler/parse/tests/snapshots/pass/crash.expr.formatted.roc new file mode 100644 index 0000000000..6ec4234a0b --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/crash.expr.formatted.roc @@ -0,0 +1,10 @@ +_ = crash "" +_ = crash "" "" +_ = crash 15 123 +_ = try foo (\_ -> crash "") +_ = + _ = crash "" + + crash + +{ f: crash "" } \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/crash.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/crash.expr.result-ast new file mode 100644 index 0000000000..88b452be8a --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/crash.expr.result-ast @@ -0,0 +1,210 @@ +Defs( + Defs { + tags: [ + Index(2147483648), + Index(2147483649), + Index(2147483650), + Index(2147483651), + Index(2147483652), + ], + regions: [ + @0-12, + @13-28, + @29-45, + @46-74, + @75-101, + ], + space_before: [ + Slice(start = 0, length = 0), + Slice(start = 0, length = 1), + Slice(start = 1, length = 1), + Slice(start = 2, length = 1), + Slice(start = 3, length = 1), + ], + space_after: [ + Slice(start = 0, length = 0), + Slice(start = 1, length = 0), + Slice(start = 2, length = 0), + Slice(start = 3, length = 0), + Slice(start = 4, length = 0), + ], + spaces: [ + Newline, + Newline, + Newline, + Newline, + ], + type_defs: [], + value_defs: [ + Body( + @0-1 Underscore( + "", + ), + @4-12 Apply( + @4-9 Crash, + [ + @10-12 Str( + PlainLine( + "", + ), + ), + ], + Space, + ), + ), + Body( + @13-14 Underscore( + "", + ), + @17-28 Apply( + @17-22 Crash, + [ + @23-25 Str( + PlainLine( + "", + ), + ), + @26-28 Str( + PlainLine( + "", + ), + ), + ], + Space, + ), + ), + Body( + @29-30 Underscore( + "", + ), + @33-45 Apply( + @33-38 Crash, + [ + @39-41 Num( + "15", + ), + @42-45 Num( + "123", + ), + ], + Space, + ), + ), + Body( + @46-47 Underscore( + "", + ), + @50-74 Apply( + @50-53 Var { + module_name: "", + ident: "try", + }, + [ + @54-57 Var { + module_name: "", + ident: "foo", + }, + @59-73 ParensAround( + Closure( + [ + @60-61 Underscore( + "", + ), + ], + @65-73 Apply( + @65-70 Crash, + [ + @71-73 Str( + PlainLine( + "", + ), + ), + ], + Space, + ), + ), + ), + ], + Space, + ), + ), + Body( + @75-76 Underscore( + "", + ), + @81-101 SpaceBefore( + Defs( + Defs { + tags: [ + Index(2147483648), + ], + regions: [ + @81-93, + ], + space_before: [ + Slice(start = 0, length = 0), + ], + space_after: [ + Slice(start = 0, length = 0), + ], + spaces: [], + type_defs: [], + value_defs: [ + Body( + @81-82 Underscore( + "", + ), + @85-93 Apply( + @85-90 Crash, + [ + @91-93 Str( + PlainLine( + "", + ), + ), + ], + Space, + ), + ), + ], + }, + @96-101 SpaceBefore( + Crash, + [ + Newline, + ], + ), + ), + [ + Newline, + ], + ), + ), + ], + }, + @103-118 SpaceBefore( + Record( + [ + @105-116 RequiredValue( + @105-106 "f", + [], + @108-116 Apply( + @108-113 Crash, + [ + @114-116 Str( + PlainLine( + "", + ), + ), + ], + Space, + ), + ), + ], + ), + [ + Newline, + Newline, + ], + ), +) diff --git a/crates/compiler/parse/tests/snapshots/pass/crash.expr.roc b/crates/compiler/parse/tests/snapshots/pass/crash.expr.roc new file mode 100644 index 0000000000..234227f786 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/crash.expr.roc @@ -0,0 +1,9 @@ +_ = crash "" +_ = crash "" "" +_ = crash 15 123 +_ = try foo (\_ -> crash "") +_ = + _ = crash "" + crash + +{ f: crash "" } diff --git a/crates/compiler/parse/tests/snapshots/pass/dbg.expr.formatted.roc b/crates/compiler/parse/tests/snapshots/pass/dbg.expr.formatted.roc new file mode 100644 index 0000000000..78c37e5eb4 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/dbg.expr.formatted.roc @@ -0,0 +1,4 @@ +dbg + 1 == 1 + +4 \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/dbg.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/dbg.expr.result-ast new file mode 100644 index 0000000000..e928afdee5 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/dbg.expr.result-ast @@ -0,0 +1,24 @@ +Dbg( + @4-10 BinOps( + [ + ( + @4-5 Num( + "1", + ), + @6-8 Equals, + ), + ], + @9-10 Num( + "1", + ), + ), + @12-13 SpaceBefore( + Num( + "4", + ), + [ + Newline, + Newline, + ], + ), +) diff --git a/crates/compiler/parse/tests/snapshots/pass/dbg.expr.roc b/crates/compiler/parse/tests/snapshots/pass/dbg.expr.roc new file mode 100644 index 0000000000..c1dc7243cb --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/dbg.expr.roc @@ -0,0 +1,3 @@ +dbg 1 == 1 + +4 diff --git a/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_ext_type.expr.formatted.roc b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_ext_type.expr.formatted.roc new file mode 100644 index 0000000000..0f097ea6a8 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_ext_type.expr.formatted.roc @@ -0,0 +1,4 @@ +f : (Str)a -> (Str)a +f = \x -> x + +f ("Str", 42) \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_ext_type.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_ext_type.expr.result-ast new file mode 100644 index 0000000000..e37c89546d --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_ext_type.expr.result-ast @@ -0,0 +1,136 @@ +Defs( + Defs { + tags: [ + Index(2147483649), + ], + regions: [ + @0-32, + ], + space_before: [ + Slice(start = 0, length = 0), + ], + space_after: [ + Slice(start = 0, length = 0), + ], + spaces: [], + type_defs: [], + value_defs: [ + Annotation( + @0-1 Identifier( + "f", + ), + @4-20 Function( + [ + @4-10 Tuple { + fields: [ + @5-8 Apply( + "", + "Str", + [], + ), + ], + ext: Some( + @9-10 BoundVariable( + "a", + ), + ), + }, + ], + @14-20 Tuple { + fields: [ + @15-18 Apply( + "", + "Str", + [], + ), + ], + ext: Some( + @19-20 BoundVariable( + "a", + ), + ), + }, + ), + ), + AnnotatedBody { + ann_pattern: @0-1 Identifier( + "f", + ), + ann_type: @4-20 Function( + [ + @4-10 Tuple { + fields: [ + @5-8 Apply( + "", + "Str", + [], + ), + ], + ext: Some( + @9-10 BoundVariable( + "a", + ), + ), + }, + ], + @14-20 Tuple { + fields: [ + @15-18 Apply( + "", + "Str", + [], + ), + ], + ext: Some( + @19-20 BoundVariable( + "a", + ), + ), + }, + ), + comment: None, + body_pattern: @21-22 Identifier( + "f", + ), + body_expr: @25-32 Closure( + [ + @26-27 Identifier( + "x", + ), + ], + @31-32 Var { + module_name: "", + ident: "x", + }, + ), + }, + ], + }, + @34-47 SpaceBefore( + Apply( + @34-35 Var { + module_name: "", + ident: "f", + }, + [ + @36-47 Tuple( + [ + @37-42 Str( + PlainLine( + "Str", + ), + ), + @44-46 Num( + "42", + ), + ], + ), + ], + Space, + ), + [ + Newline, + Newline, + ], + ), +) diff --git a/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_ext_type.expr.roc b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_ext_type.expr.roc new file mode 100644 index 0000000000..9357c626e1 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_ext_type.expr.roc @@ -0,0 +1,4 @@ +f : (Str)a -> (Str)a +f = \x -> x + +f ("Str", 42) diff --git a/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_type.expr.formatted.roc b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_type.expr.formatted.roc new file mode 100644 index 0000000000..98cad54ca6 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_type.expr.formatted.roc @@ -0,0 +1,4 @@ +f : I64 -> (I64, I64) +f = \x -> (x, x + 1) + +f 42 \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_type.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_type.expr.result-ast new file mode 100644 index 0000000000..50ccc54f6f --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_type.expr.result-ast @@ -0,0 +1,129 @@ +Defs( + Defs { + tags: [ + Index(2147483649), + ], + regions: [ + @0-42, + ], + space_before: [ + Slice(start = 0, length = 0), + ], + space_after: [ + Slice(start = 0, length = 0), + ], + spaces: [], + type_defs: [], + value_defs: [ + Annotation( + @0-1 Identifier( + "f", + ), + @4-21 Function( + [ + @4-7 Apply( + "", + "I64", + [], + ), + ], + @11-21 Tuple { + fields: [ + @12-15 Apply( + "", + "I64", + [], + ), + @17-20 Apply( + "", + "I64", + [], + ), + ], + ext: None, + }, + ), + ), + AnnotatedBody { + ann_pattern: @0-1 Identifier( + "f", + ), + ann_type: @4-21 Function( + [ + @4-7 Apply( + "", + "I64", + [], + ), + ], + @11-21 Tuple { + fields: [ + @12-15 Apply( + "", + "I64", + [], + ), + @17-20 Apply( + "", + "I64", + [], + ), + ], + ext: None, + }, + ), + comment: None, + body_pattern: @22-23 Identifier( + "f", + ), + body_expr: @26-42 Closure( + [ + @27-28 Identifier( + "x", + ), + ], + @32-42 Tuple( + [ + @33-34 Var { + module_name: "", + ident: "x", + }, + @36-41 BinOps( + [ + ( + @36-37 Var { + module_name: "", + ident: "x", + }, + @38-39 Plus, + ), + ], + @40-41 Num( + "1", + ), + ), + ], + ), + ), + }, + ], + }, + @44-48 SpaceBefore( + Apply( + @44-45 Var { + module_name: "", + ident: "f", + }, + [ + @46-48 Num( + "42", + ), + ], + Space, + ), + [ + Newline, + Newline, + ], + ), +) diff --git a/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_type.expr.roc b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_type.expr.roc new file mode 100644 index 0000000000..481fddbb80 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/function_with_tuple_type.expr.roc @@ -0,0 +1,4 @@ +f : I64 -> (I64, I64) +f = \x -> (x, x + 1) + +f 42 diff --git a/crates/compiler/parse/tests/snapshots/pass/record_access_after_tuple.expr.formatted.roc b/crates/compiler/parse/tests/snapshots/pass/record_access_after_tuple.expr.formatted.roc new file mode 100644 index 0000000000..435594dc08 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/record_access_after_tuple.expr.formatted.roc @@ -0,0 +1 @@ +({ a: 0 }, { b: 1 }).0.a \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/record_access_after_tuple.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/record_access_after_tuple.expr.result-ast new file mode 100644 index 0000000000..4a0a71c0d0 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/record_access_after_tuple.expr.result-ast @@ -0,0 +1,32 @@ +RecordAccess( + TupleAccess( + Tuple( + [ + @1-7 Record( + [ + @2-6 RequiredValue( + @2-3 "a", + [], + @5-6 Num( + "0", + ), + ), + ], + ), + @9-15 Record( + [ + @10-14 RequiredValue( + @10-11 "b", + [], + @13-14 Num( + "1", + ), + ), + ], + ), + ], + ), + "0", + ), + "a", +) diff --git a/crates/compiler/parse/tests/snapshots/pass/record_access_after_tuple.expr.roc b/crates/compiler/parse/tests/snapshots/pass/record_access_after_tuple.expr.roc new file mode 100644 index 0000000000..78c88dac1e --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/record_access_after_tuple.expr.roc @@ -0,0 +1 @@ +({a: 0}, {b: 1}).0.a \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/tuple_access_after_record.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/tuple_access_after_record.expr.result-ast new file mode 100644 index 0000000000..3e46aae1d1 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/tuple_access_after_record.expr.result-ast @@ -0,0 +1,24 @@ +TupleAccess( + RecordAccess( + Record( + [ + @2-11 RequiredValue( + @2-3 "a", + [], + @5-11 Tuple( + [ + @6-7 Num( + "1", + ), + @9-10 Num( + "2", + ), + ], + ), + ), + ], + ), + "a", + ), + "0", +) diff --git a/crates/compiler/parse/tests/snapshots/pass/tuple_access_after_record.expr.roc b/crates/compiler/parse/tests/snapshots/pass/tuple_access_after_record.expr.roc new file mode 100644 index 0000000000..51052a21f2 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/tuple_access_after_record.expr.roc @@ -0,0 +1 @@ +{ a: (1, 2) }.a.0 \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/tuple_type.expr.formatted.roc b/crates/compiler/parse/tests/snapshots/pass/tuple_type.expr.formatted.roc new file mode 100644 index 0000000000..7f1f6e3191 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/tuple_type.expr.formatted.roc @@ -0,0 +1,4 @@ +f : (Str, Str) -> (Str, Str) +f = \x -> x + +f (1, 2) \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/tuple_type.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/tuple_type.expr.result-ast new file mode 100644 index 0000000000..3aae783860 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/tuple_type.expr.result-ast @@ -0,0 +1,138 @@ +Defs( + Defs { + tags: [ + Index(2147483649), + ], + regions: [ + @0-39, + ], + space_before: [ + Slice(start = 0, length = 0), + ], + space_after: [ + Slice(start = 0, length = 0), + ], + spaces: [], + type_defs: [], + value_defs: [ + Annotation( + @0-1 Identifier( + "f", + ), + @3-27 Function( + [ + @3-13 Tuple { + fields: [ + @4-7 Apply( + "", + "Str", + [], + ), + @9-12 Apply( + "", + "Str", + [], + ), + ], + ext: None, + }, + ], + @17-27 Tuple { + fields: [ + @18-21 Apply( + "", + "Str", + [], + ), + @23-26 Apply( + "", + "Str", + [], + ), + ], + ext: None, + }, + ), + ), + AnnotatedBody { + ann_pattern: @0-1 Identifier( + "f", + ), + ann_type: @3-27 Function( + [ + @3-13 Tuple { + fields: [ + @4-7 Apply( + "", + "Str", + [], + ), + @9-12 Apply( + "", + "Str", + [], + ), + ], + ext: None, + }, + ], + @17-27 Tuple { + fields: [ + @18-21 Apply( + "", + "Str", + [], + ), + @23-26 Apply( + "", + "Str", + [], + ), + ], + ext: None, + }, + ), + comment: None, + body_pattern: @28-29 Identifier( + "f", + ), + body_expr: @32-39 Closure( + [ + @33-34 Identifier( + "x", + ), + ], + @38-39 Var { + module_name: "", + ident: "x", + }, + ), + }, + ], + }, + @41-49 SpaceBefore( + Apply( + @41-42 Var { + module_name: "", + ident: "f", + }, + [ + @43-49 Tuple( + [ + @44-45 Num( + "1", + ), + @47-48 Num( + "2", + ), + ], + ), + ], + Space, + ), + [ + Newline, + Newline, + ], + ), +) diff --git a/crates/compiler/parse/tests/snapshots/pass/tuple_type.expr.roc b/crates/compiler/parse/tests/snapshots/pass/tuple_type.expr.roc new file mode 100644 index 0000000000..df9c603722 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/tuple_type.expr.roc @@ -0,0 +1,4 @@ +f: (Str, Str) -> (Str, Str) +f = \x -> x + +f (1, 2) \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/tuple_type_ext.expr.formatted.roc b/crates/compiler/parse/tests/snapshots/pass/tuple_type_ext.expr.formatted.roc new file mode 100644 index 0000000000..434088cf8f --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/tuple_type_ext.expr.formatted.roc @@ -0,0 +1,4 @@ +f : (Str, Str)a -> (Str, Str)a +f = \x -> x + +f (1, 2) \ No newline at end of file diff --git a/crates/compiler/parse/tests/snapshots/pass/tuple_type_ext.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/tuple_type_ext.expr.result-ast new file mode 100644 index 0000000000..3d9bab17da --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/tuple_type_ext.expr.result-ast @@ -0,0 +1,154 @@ +Defs( + Defs { + tags: [ + Index(2147483649), + ], + regions: [ + @0-41, + ], + space_before: [ + Slice(start = 0, length = 0), + ], + space_after: [ + Slice(start = 0, length = 0), + ], + spaces: [], + type_defs: [], + value_defs: [ + Annotation( + @0-1 Identifier( + "f", + ), + @3-29 Function( + [ + @3-14 Tuple { + fields: [ + @4-7 Apply( + "", + "Str", + [], + ), + @9-12 Apply( + "", + "Str", + [], + ), + ], + ext: Some( + @13-14 BoundVariable( + "a", + ), + ), + }, + ], + @18-29 Tuple { + fields: [ + @19-22 Apply( + "", + "Str", + [], + ), + @24-27 Apply( + "", + "Str", + [], + ), + ], + ext: Some( + @28-29 BoundVariable( + "a", + ), + ), + }, + ), + ), + AnnotatedBody { + ann_pattern: @0-1 Identifier( + "f", + ), + ann_type: @3-29 Function( + [ + @3-14 Tuple { + fields: [ + @4-7 Apply( + "", + "Str", + [], + ), + @9-12 Apply( + "", + "Str", + [], + ), + ], + ext: Some( + @13-14 BoundVariable( + "a", + ), + ), + }, + ], + @18-29 Tuple { + fields: [ + @19-22 Apply( + "", + "Str", + [], + ), + @24-27 Apply( + "", + "Str", + [], + ), + ], + ext: Some( + @28-29 BoundVariable( + "a", + ), + ), + }, + ), + comment: None, + body_pattern: @30-31 Identifier( + "f", + ), + body_expr: @34-41 Closure( + [ + @35-36 Identifier( + "x", + ), + ], + @40-41 Var { + module_name: "", + ident: "x", + }, + ), + }, + ], + }, + @43-51 SpaceBefore( + Apply( + @43-44 Var { + module_name: "", + ident: "f", + }, + [ + @45-51 Tuple( + [ + @46-47 Num( + "1", + ), + @49-50 Num( + "2", + ), + ], + ), + ], + Space, + ), + [ + Newline, + Newline, + ], + ), +) diff --git a/crates/compiler/parse/tests/snapshots/pass/tuple_type_ext.expr.roc b/crates/compiler/parse/tests/snapshots/pass/tuple_type_ext.expr.roc new file mode 100644 index 0000000000..cce4b4c704 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/tuple_type_ext.expr.roc @@ -0,0 +1,4 @@ +f: (Str, Str)a -> (Str, Str)a +f = \x -> x + +f (1, 2) \ No newline at end of file diff --git a/crates/compiler/parse/tests/test_parse.rs b/crates/compiler/parse/tests/test_parse.rs index 4c7c8a45b1..11b41dff25 100644 --- a/crates/compiler/parse/tests/test_parse.rs +++ b/crates/compiler/parse/tests/test_parse.rs @@ -156,6 +156,7 @@ mod test_parse { pass/comment_before_op.expr, pass/comment_inside_empty_list.expr, pass/comment_with_non_ascii.expr, + pass/crash.expr, pass/destructure_tag_assignment.expr, pass/empty_app_header.header, pass/empty_hosted_header.header, @@ -168,6 +169,7 @@ mod test_parse { pass/equals.expr, pass/expect_fx.module, pass/multiline_tuple_with_comments.expr, + pass/dbg.expr, pass/expect.expr, pass/float_with_underscores.expr, pass/full_app_header_trailing_commas.header, @@ -186,6 +188,10 @@ mod test_parse { pass/list_patterns.expr, pass/lowest_float.expr, pass/lowest_int.expr, + pass/tuple_type.expr, + pass/tuple_access_after_record.expr, + pass/record_access_after_tuple.expr, + pass/tuple_type_ext.expr, pass/malformed_ident_due_to_underscore.expr, pass/malformed_pattern_field_access.expr, // See https://github.com/roc-lang/roc/issues/399 pass/malformed_pattern_module_name.expr, // See https://github.com/roc-lang/roc/issues/399 @@ -302,6 +308,8 @@ mod test_parse { pass/when_with_negative_numbers.expr, pass/when_with_numbers.expr, pass/when_with_records.expr, + pass/function_with_tuple_type.expr, + pass/function_with_tuple_ext_type.expr, pass/where_clause_function.expr, pass/where_clause_multiple_bound_abilities.expr, pass/where_clause_multiple_has_across_newlines.expr, diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs index c49efaab6e..2fd5a9483e 100644 --- a/crates/compiler/problem/src/can.rs +++ b/crates/compiler/problem/src/can.rs @@ -195,6 +195,12 @@ pub enum Problem { type_got: u8, alias_kind: AliasKind, }, + UnappliedCrash { + region: Region, + }, + OverAppliedCrash { + region: Region, + }, } impl Problem { @@ -325,7 +331,9 @@ impl Problem { } | Problem::MultipleListRestPattern { region } | Problem::BadTypeArguments { region, .. } - | Problem::UnnecessaryOutputWildcard { region } => Some(*region), + | Problem::UnnecessaryOutputWildcard { region } + | Problem::OverAppliedCrash { region } + | Problem::UnappliedCrash { region } => Some(*region), Problem::RuntimeError(RuntimeError::CircularDef(cycle_entries)) | Problem::BadRecursion(cycle_entries) => { cycle_entries.first().map(|entry| entry.expr_region) diff --git a/crates/compiler/roc_target/src/lib.rs b/crates/compiler/roc_target/src/lib.rs index c37ddb2c4c..acc160c7a7 100644 --- a/crates/compiler/roc_target/src/lib.rs +++ b/crates/compiler/roc_target/src/lib.rs @@ -12,17 +12,24 @@ pub enum OperatingSystem { Wasi, } +impl OperatingSystem { + pub const fn new(target: target_lexicon::OperatingSystem) -> Option { + match target { + target_lexicon::OperatingSystem::Windows => Some(OperatingSystem::Windows), + target_lexicon::OperatingSystem::Wasi => Some(OperatingSystem::Wasi), + target_lexicon::OperatingSystem::Linux => Some(OperatingSystem::Unix), + target_lexicon::OperatingSystem::MacOSX { .. } => Some(OperatingSystem::Unix), + target_lexicon::OperatingSystem::Darwin => Some(OperatingSystem::Unix), + target_lexicon::OperatingSystem::Unknown => Some(OperatingSystem::Unix), + _ => None, + } + } +} + impl From for OperatingSystem { fn from(target: target_lexicon::OperatingSystem) -> Self { - match target { - target_lexicon::OperatingSystem::Windows => OperatingSystem::Windows, - target_lexicon::OperatingSystem::Wasi => OperatingSystem::Wasi, - target_lexicon::OperatingSystem::Linux => OperatingSystem::Unix, - target_lexicon::OperatingSystem::MacOSX { .. } => OperatingSystem::Unix, - target_lexicon::OperatingSystem::Darwin => OperatingSystem::Unix, - target_lexicon::OperatingSystem::Unknown => OperatingSystem::Unix, - other => unreachable!("unsupported operating system {:?}", other), - } + Self::new(target) + .unwrap_or_else(|| unreachable!("unsupported operating system {:?}", target)) } } diff --git a/crates/compiler/solve/tests/solve_expr.rs b/crates/compiler/solve/tests/solve_expr.rs index 4734295b00..fa97a2b0f4 100644 --- a/crates/compiler/solve/tests/solve_expr.rs +++ b/crates/compiler/solve/tests/solve_expr.rs @@ -8352,4 +8352,20 @@ mod solve_expr { @"translateStatic : [Element (List a)] as a -[[translateStatic(0)]]-> [Element (List b)]* as b" ) } + + #[test] + fn infer_contextual_crash() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [getInfallible] to "./platform" + + getInfallible = \result -> when result is + Ok x -> x + _ -> crash "turns out this was fallible" + "# + ), + "[Ok a]* -> a", + ); + } } diff --git a/crates/compiler/test_derive/src/pretty_print.rs b/crates/compiler/test_derive/src/pretty_print.rs index 5c072c84ac..1b0a3e96d1 100644 --- a/crates/compiler/test_derive/src/pretty_print.rs +++ b/crates/compiler/test_derive/src/pretty_print.rs @@ -279,8 +279,10 @@ fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a, ) .group() ), + Crash { .. } => todo!(), ZeroArgumentTag { .. } => todo!(), OpaqueRef { .. } => todo!(), + Dbg { .. } => todo!(), Expect { .. } => todo!(), ExpectFx { .. } => todo!(), TypedHole(_) => todo!(), diff --git a/crates/compiler/test_gen/src/gen_panic.rs b/crates/compiler/test_gen/src/gen_panic.rs new file mode 100644 index 0000000000..b92501fc71 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_panic.rs @@ -0,0 +1,85 @@ +use indoc::indoc; +use roc_std::RocList; + +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic = r#"User crash with message: "hello crash""#] +fn crash_literal() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = if Bool.true then crash "hello crash" else 1u8 + "# + ), + 1u8, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic = r#"User crash with message: "hello crash""#] +fn crash_variable() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + msg = "hello crash" + if Bool.true then crash msg else 1u8 + "# + ), + 1u8, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic = r#"User crash with message: "turns out this was fallible""#] +fn crash_in_call() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + getInfallible = \result -> when result is + Ok x -> x + _ -> crash "turns out this was fallible" + + main = + x : [Ok U64, Err Str] + x = Err "" + getInfallible x + "# + ), + 1u64, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic = r#"User crash with message: "no new even primes""#] +fn crash_in_passed_closure() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = List.map [1, 2, 3] \n -> if n == 2 then crash "no new even primes" else "" + "# + ), + RocList::from_slice(&[1u8]), + RocList + ); +} diff --git a/crates/compiler/test_gen/src/gen_records.rs b/crates/compiler/test_gen/src/gen_records.rs index 823b260955..e0d25d553f 100644 --- a/crates/compiler/test_gen/src/gen_records.rs +++ b/crates/compiler/test_gen/src/gen_records.rs @@ -1,11 +1,11 @@ #[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::{assert_evals_to, expect_runtime_error_panic}; +use crate::helpers::llvm::assert_evals_to; #[cfg(feature = "gen-dev")] use crate::helpers::dev::assert_evals_to; #[cfg(feature = "gen-wasm")] -use crate::helpers::wasm::{assert_evals_to, expect_runtime_error_panic}; +use crate::helpers::wasm::assert_evals_to; // use crate::assert_wasm_evals_to as assert_evals_to; use indoc::indoc; @@ -447,7 +447,7 @@ fn optional_field_when_use_default_nested() { #[test] #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn optional_field_when_no_use_default() { +fn optional_field_destructure_module() { assert_evals_to!( indoc!( r#" @@ -468,15 +468,15 @@ fn optional_field_when_no_use_default() { #[test] #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn optional_field_when_no_use_default_nested() { +fn optional_field_destructure_expr() { assert_evals_to!( indoc!( r#" - f = \r -> + fn = \r -> { x ? 10, y } = r x + y - f { x: 4, y: 9 } + fn { x: 4, y: 9 } "# ), 13, @@ -1019,8 +1019,9 @@ fn different_proc_types_specialized_to_same_layout() { #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] #[should_panic(expected = r#"Roc failed with message: "Can't create record with improper layout""#)] fn call_with_bad_record_runtime_error() { - expect_runtime_error_panic!(indoc!( - r#" + assert_evals_to!( + indoc!( + r#" app "test" provides [main] to "./platform" main = @@ -1028,7 +1029,12 @@ fn call_with_bad_record_runtime_error() { get = \{a} -> a get {b: ""} "# - )) + ), + true, + bool, + |x| x, + true // ignore type errors + ) } #[test] @@ -1056,9 +1062,9 @@ fn update_record_that_is_a_thunk() { app "test" provides [main] to "./platform" main = Num.toStr fromOriginal.birds - + original = { birds: 5, iguanas: 7, zebras: 2, goats: 1 } - + fromOriginal = { original & birds: 4, iguanas: 3 } "# ), @@ -1076,9 +1082,9 @@ fn update_record_that_is_a_thunk_single_field() { app "test" provides [main] to "./platform" main = Num.toStr fromOriginal.birds - + original = { birds: 5 } - + fromOriginal = { original & birds: 4 } "# ), diff --git a/crates/compiler/test_gen/src/helpers/llvm.rs b/crates/compiler/test_gen/src/helpers/llvm.rs index 4c167c6925..f0ee5fd7a6 100644 --- a/crates/compiler/test_gen/src/helpers/llvm.rs +++ b/crates/compiler/test_gen/src/helpers/llvm.rs @@ -8,7 +8,7 @@ use roc_collections::all::MutSet; use roc_gen_llvm::llvm::externs::add_default_roc_externs; use roc_gen_llvm::{llvm::build::LlvmBackendMode, run_roc::RocCallResult}; use roc_load::{EntryPoint, ExecutionMode, LoadConfig, Threading}; -use roc_mono::ir::OptLevel; +use roc_mono::ir::{CrashTag, OptLevel}; use roc_region::all::LineInfo; use roc_reporting::report::{RenderTarget, DEFAULT_PALETTE}; use roc_utils::zig; @@ -544,7 +544,10 @@ macro_rules! assert_wasm_evals_to { } #[allow(dead_code)] -pub fn try_run_lib_function(main_fn_name: &str, lib: &libloading::Library) -> Result { +pub fn try_run_lib_function( + main_fn_name: &str, + lib: &libloading::Library, +) -> Result { unsafe { let main: libloading::Symbol)> = lib .get(main_fn_name.as_bytes()) @@ -565,6 +568,7 @@ macro_rules! assert_llvm_evals_to { use bumpalo::Bump; use inkwell::context::Context; use roc_gen_llvm::llvm::build::LlvmBackendMode; + use roc_mono::ir::CrashTag; let arena = Bump::new(); let context = Context::create(); @@ -594,7 +598,10 @@ macro_rules! assert_llvm_evals_to { #[cfg(windows)] std::mem::forget(given); } - Err(msg) => panic!("Roc failed with message: \"{}\"", msg), + Err((msg, tag)) => match tag { + CrashTag::Roc => panic!(r#"Roc failed with message: "{}""#, msg), + CrashTag::User => panic!(r#"User crash with message: "{}""#, msg), + }, } // artificially extend the lifetime of `lib` @@ -655,29 +662,6 @@ macro_rules! assert_evals_to { }}; } -#[allow(unused_macros)] -macro_rules! expect_runtime_error_panic { - ($src:expr) => {{ - #[cfg(feature = "gen-llvm-wasm")] - $crate::helpers::llvm::assert_wasm_evals_to!( - $src, - false, // fake value/type for eval - bool, - $crate::helpers::llvm::identity, - true // ignore problems - ); - - #[cfg(not(feature = "gen-llvm-wasm"))] - $crate::helpers::llvm::assert_llvm_evals_to!( - $src, - false, // fake value/type for eval - bool, - $crate::helpers::llvm::identity, - true // ignore problems - ); - }}; -} - #[allow(dead_code)] pub fn identity(value: T) -> T { value @@ -689,5 +673,3 @@ pub(crate) use assert_evals_to; pub(crate) use assert_llvm_evals_to; #[allow(unused_imports)] pub(crate) use assert_wasm_evals_to; -#[allow(unused_imports)] -pub(crate) use expect_runtime_error_panic; diff --git a/crates/compiler/test_gen/src/helpers/platform_functions.rs b/crates/compiler/test_gen/src/helpers/platform_functions.rs index 90387a8c87..71c0a7b03d 100644 --- a/crates/compiler/test_gen/src/helpers/platform_functions.rs +++ b/crates/compiler/test_gen/src/helpers/platform_functions.rs @@ -32,23 +32,3 @@ pub unsafe fn roc_realloc( pub unsafe fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { libc::free(c_ptr) } - -/// # Safety -/// The Roc application needs this. -#[no_mangle] -pub unsafe fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - use roc_gen_llvm::llvm::build::PanicTagId; - - use std::ffi::CStr; - use std::os::raw::c_char; - - match PanicTagId::try_from(tag_id) { - Ok(PanicTagId::NullTerminatedString) => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - Err(_) => unreachable!(), - } -} diff --git a/crates/compiler/test_gen/src/helpers/wasm.rs b/crates/compiler/test_gen/src/helpers/wasm.rs index 3c203fc790..7bc9d19e9b 100644 --- a/crates/compiler/test_gen/src/helpers/wasm.rs +++ b/crates/compiler/test_gen/src/helpers/wasm.rs @@ -5,6 +5,7 @@ use roc_gen_wasm::wasm32_result::Wasm32Result; use roc_gen_wasm::DEBUG_SETTINGS; use roc_load::{ExecutionMode, LoadConfig, Threading}; use roc_reporting::report::DEFAULT_PALETTE_HTML; +use roc_std::RocStr; use roc_wasm_module::{Export, ExportType}; use std::marker::PhantomData; use std::path::PathBuf; @@ -193,7 +194,7 @@ where let parsed = Module::parse(&env, &wasm_bytes[..]).expect("Unable to parse module"); let mut module = rt.load_module(parsed).expect("Unable to load module"); - let panic_msg: Rc>> = Default::default(); + let panic_msg: Rc>> = Default::default(); link_module(&mut module, panic_msg.clone()); let test_wrapper = module @@ -202,12 +203,18 @@ where match test_wrapper.call() { Err(e) => { - if let Some((msg_ptr, msg_len)) = *panic_msg.lock().unwrap() { + if let Some((msg_ptr, tag)) = *panic_msg.lock().unwrap() { let memory: &[u8] = get_memory(&rt); - let msg_bytes = &memory[msg_ptr as usize..][..msg_len as usize]; - let msg = std::str::from_utf8(msg_bytes).unwrap(); + let msg = RocStr::decode(memory, msg_ptr as _); - Err(format!("Roc failed with message: \"{}\"", msg)) + dbg!(tag); + let msg = match tag { + 0 => format!(r#"Roc failed with message: "{}""#, msg), + 1 => format!(r#"User crash with message: "{}""#, msg), + tag => format!(r#"Got an invald panic tag: "{}""#, tag), + }; + + Err(msg) } else { Err(format!("{}", e)) } @@ -253,7 +260,7 @@ where let parsed = Module::parse(&env, wasm_bytes).expect("Unable to parse module"); let mut module = rt.load_module(parsed).expect("Unable to load module"); - let panic_msg: Rc>> = Default::default(); + let panic_msg: Rc>> = Default::default(); link_module(&mut module, panic_msg.clone()); let expected_len = num_refcounts as i32; @@ -316,13 +323,13 @@ fn read_i32(memory: &[u8], ptr: usize) -> i32 { i32::from_le_bytes(bytes) } -fn link_module(module: &mut Module, panic_msg: Rc>>) { +fn link_module(module: &mut Module, panic_msg: Rc>>) { let try_link_panic = module.link_closure( "env", "send_panic_msg_to_rust", - move |_call_context, args: (i32, i32)| { + move |_call_context, (msg_ptr, tag): (i32, u32)| { let mut w = panic_msg.lock().unwrap(); - *w = Some(args); + *w = Some((msg_ptr, tag)); Ok(()) }, ); @@ -390,19 +397,6 @@ macro_rules! assert_evals_to { }}; } -#[allow(unused_macros)] -macro_rules! expect_runtime_error_panic { - ($src:expr) => {{ - $crate::helpers::wasm::assert_evals_to!( - $src, - false, // fake value/type for eval - bool, - $crate::helpers::wasm::identity, - true // ignore problems - ); - }}; -} - #[allow(dead_code)] pub fn identity(value: T) -> T { value @@ -430,8 +424,5 @@ macro_rules! assert_refcounts { #[allow(unused_imports)] pub(crate) use assert_evals_to; -#[allow(unused_imports)] -pub(crate) use expect_runtime_error_panic; - #[allow(unused_imports)] pub(crate) use assert_refcounts; diff --git a/crates/compiler/test_gen/src/helpers/wasm_test_platform.c b/crates/compiler/test_gen/src/helpers/wasm_test_platform.c index 372f010df6..28ea77de78 100644 --- a/crates/compiler/test_gen/src/helpers/wasm_test_platform.c +++ b/crates/compiler/test_gen/src/helpers/wasm_test_platform.c @@ -1,4 +1,5 @@ #include +#include #include #include @@ -123,12 +124,11 @@ void roc_dealloc(void *ptr, unsigned int alignment) //-------------------------- -extern void send_panic_msg_to_rust(char* msg, int len); +extern void send_panic_msg_to_rust(void* msg, uint32_t tag_id); -void roc_panic(char *msg, unsigned int tag_id) +void roc_panic(void* msg, unsigned int tag_id) { - int len = strlen(msg); - send_panic_msg_to_rust(msg, len); + send_panic_msg_to_rust(msg, tag_id); exit(101); } diff --git a/crates/compiler/test_gen/src/tests.rs b/crates/compiler/test_gen/src/tests.rs index 81c431acd7..a1cbff449d 100644 --- a/crates/compiler/test_gen/src/tests.rs +++ b/crates/compiler/test_gen/src/tests.rs @@ -9,6 +9,7 @@ pub mod gen_compare; pub mod gen_dict; pub mod gen_list; pub mod gen_num; +pub mod gen_panic; pub mod gen_primitives; pub mod gen_records; pub mod gen_refcount; diff --git a/crates/compiler/test_mono/generated/call_function_in_empty_list.txt b/crates/compiler/test_mono/generated/call_function_in_empty_list.txt index 5e5af921e0..cb3112aa9e 100644 --- a/crates/compiler/test_mono/generated/call_function_in_empty_list.txt +++ b/crates/compiler/test_mono/generated/call_function_in_empty_list.txt @@ -5,7 +5,8 @@ procedure List.5 (#Attr.2, #Attr.3): procedure Test.2 (Test.3): let Test.7 : {} = Struct {}; - Error a Lambda Set is empty. Most likely there is a type error in your program. + let Test.8 : Str = "a Lambda Set is empty. Most likely there is a type error in your program."; + Crash Test.8 procedure Test.0 (): let Test.1 : List [] = Array []; diff --git a/crates/compiler/test_mono/generated/call_function_in_empty_list_unbound.txt b/crates/compiler/test_mono/generated/call_function_in_empty_list_unbound.txt index 947e114ed7..7fa0f73c5f 100644 --- a/crates/compiler/test_mono/generated/call_function_in_empty_list_unbound.txt +++ b/crates/compiler/test_mono/generated/call_function_in_empty_list_unbound.txt @@ -5,7 +5,8 @@ procedure List.5 (#Attr.2, #Attr.3): procedure Test.2 (Test.3): let Test.7 : {} = Struct {}; - Error a Lambda Set is empty. Most likely there is a type error in your program. + let Test.8 : Str = "a Lambda Set is empty. Most likely there is a type error in your program."; + Crash Test.8 procedure Test.0 (): let Test.1 : List [] = Array []; diff --git a/crates/compiler/test_mono/generated/crash.txt b/crates/compiler/test_mono/generated/crash.txt new file mode 100644 index 0000000000..fc1b607bc6 --- /dev/null +++ b/crates/compiler/test_mono/generated/crash.txt @@ -0,0 +1,18 @@ +procedure Test.1 (Test.2): + let Test.10 : U8 = 1i64; + let Test.11 : U8 = GetTagId Test.2; + let Test.12 : Int1 = lowlevel Eq Test.10 Test.11; + if Test.12 then + let Test.3 : U64 = UnionAtIndex (Id 1) (Index 0) Test.2; + dec Test.2; + ret Test.3; + else + dec Test.2; + let Test.9 : Str = "turns out this was fallible"; + Crash Test.9 + +procedure Test.0 (): + let Test.13 : U64 = 78i64; + let Test.4 : [C Str, C U64] = TagId(1) Test.13; + let Test.6 : U64 = CallByName Test.1 Test.4; + ret Test.6; diff --git a/crates/compiler/test_mono/generated/encode_derived_nested_record_string.txt b/crates/compiler/test_mono/generated/encode_derived_nested_record_string.txt index 069b904448..695cd0e985 100644 --- a/crates/compiler/test_mono/generated/encode_derived_nested_record_string.txt +++ b/crates/compiler/test_mono/generated/encode_derived_nested_record_string.txt @@ -205,58 +205,58 @@ procedure Json.96 (Json.97, Json.473, Json.95): ret Json.475; procedure List.137 (List.138, List.139, List.136): - let List.449 : {List U8, U64} = CallByName Json.114 List.138 List.139; - ret List.449; + let List.450 : {List U8, U64} = CallByName Json.114 List.138 List.139; + ret List.450; procedure List.137 (List.138, List.139, List.136): - let List.521 : {List U8, U64} = CallByName Json.114 List.138 List.139; - ret List.521; + let List.523 : {List U8, U64} = CallByName Json.114 List.138 List.139; + ret List.523; procedure List.18 (List.134, List.135, List.136): let List.431 : {List U8, U64} = CallByName List.89 List.134 List.135 List.136; ret List.431; procedure List.18 (List.134, List.135, List.136): - let List.503 : {List U8, U64} = CallByName List.89 List.134 List.135 List.136; - ret List.503; + let List.504 : {List U8, U64} = CallByName List.89 List.134 List.135 List.136; + ret List.504; procedure List.4 (List.105, List.106): - let List.502 : U64 = 1i64; - let List.501 : List U8 = CallByName List.70 List.105 List.502; - let List.500 : List U8 = CallByName List.71 List.501 List.106; - ret List.500; + let List.503 : U64 = 1i64; + let List.502 : List U8 = CallByName List.70 List.105 List.503; + let List.501 : List U8 = CallByName List.71 List.502 List.106; + ret List.501; procedure List.6 (#Attr.2): let List.409 : U64 = lowlevel ListLen #Attr.2; ret List.409; procedure List.6 (#Attr.2): - let List.451 : U64 = lowlevel ListLen #Attr.2; - ret List.451; + let List.452 : U64 = lowlevel ListLen #Attr.2; + ret List.452; procedure List.6 (#Attr.2): - let List.524 : U64 = lowlevel ListLen #Attr.2; - ret List.524; + let List.526 : U64 = lowlevel ListLen #Attr.2; + ret List.526; procedure List.66 (#Attr.2, #Attr.3): - let List.446 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.446; + let List.447 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.447; procedure List.66 (#Attr.2, #Attr.3): - let List.518 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.518; + let List.520 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.520; procedure List.70 (#Attr.2, #Attr.3): - let List.481 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.481; + let List.482 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.482; procedure List.71 (#Attr.2, #Attr.3): - let List.479 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.479; + let List.480 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.480; procedure List.8 (#Attr.2, #Attr.3): - let List.523 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.523; + let List.525 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.525; procedure List.89 (List.385, List.386, List.387): let List.435 : U64 = 0i64; @@ -265,38 +265,38 @@ procedure List.89 (List.385, List.386, List.387): ret List.434; procedure List.89 (List.385, List.386, List.387): - let List.507 : U64 = 0i64; - let List.508 : U64 = CallByName List.6 List.385; - let List.506 : {List U8, U64} = CallByName List.90 List.385 List.386 List.387 List.507 List.508; - ret List.506; + let List.508 : U64 = 0i64; + let List.509 : U64 = CallByName List.6 List.385; + let List.507 : {List U8, U64} = CallByName List.90 List.385 List.386 List.387 List.508 List.509; + ret List.507; -procedure List.90 (List.461, List.462, List.463, List.464, List.465): +procedure List.90 (List.462, List.463, List.464, List.465, List.466): joinpoint List.437 List.388 List.389 List.390 List.391 List.392: let List.439 : Int1 = CallByName Num.22 List.391 List.392; if List.439 then - let List.445 : {Str, Str} = CallByName List.66 List.388 List.391; - let List.440 : {List U8, U64} = CallByName List.137 List.389 List.445 List.390; + let List.446 : {Str, Str} = CallByName List.66 List.388 List.391; + let List.440 : {List U8, U64} = CallByName List.137 List.389 List.446 List.390; let List.443 : U64 = 1i64; let List.442 : U64 = CallByName Num.19 List.391 List.443; jump List.437 List.388 List.440 List.390 List.442 List.392; else ret List.389; in - jump List.437 List.461 List.462 List.463 List.464 List.465; + jump List.437 List.462 List.463 List.464 List.465 List.466; -procedure List.90 (List.534, List.535, List.536, List.537, List.538): - joinpoint List.509 List.388 List.389 List.390 List.391 List.392: - let List.511 : Int1 = CallByName Num.22 List.391 List.392; - if List.511 then - let List.517 : {Str, Str} = CallByName List.66 List.388 List.391; - let List.512 : {List U8, U64} = CallByName List.137 List.389 List.517 List.390; - let List.515 : U64 = 1i64; - let List.514 : U64 = CallByName Num.19 List.391 List.515; - jump List.509 List.388 List.512 List.390 List.514 List.392; +procedure List.90 (List.536, List.537, List.538, List.539, List.540): + joinpoint List.510 List.388 List.389 List.390 List.391 List.392: + let List.512 : Int1 = CallByName Num.22 List.391 List.392; + if List.512 then + let List.519 : {Str, Str} = CallByName List.66 List.388 List.391; + let List.513 : {List U8, U64} = CallByName List.137 List.389 List.519 List.390; + let List.516 : U64 = 1i64; + let List.515 : U64 = CallByName Num.19 List.391 List.516; + jump List.510 List.388 List.513 List.390 List.515 List.392; else ret List.389; in - jump List.509 List.534 List.535 List.536 List.537 List.538; + jump List.510 List.536 List.537 List.538 List.539 List.540; procedure Num.125 (#Attr.2): let Num.282 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/encode_derived_record_one_field_string.txt b/crates/compiler/test_mono/generated/encode_derived_record_one_field_string.txt index 9b81c3f70e..fd701eca46 100644 --- a/crates/compiler/test_mono/generated/encode_derived_record_one_field_string.txt +++ b/crates/compiler/test_mono/generated/encode_derived_record_one_field_string.txt @@ -118,8 +118,8 @@ procedure Json.96 (Json.97, Json.433, Json.95): ret Json.435; procedure List.137 (List.138, List.139, List.136): - let List.455 : {List U8, U64} = CallByName Json.114 List.138 List.139; - ret List.455; + let List.456 : {List U8, U64} = CallByName Json.114 List.138 List.139; + ret List.456; procedure List.18 (List.134, List.135, List.136): let List.437 : {List U8, U64} = CallByName List.89 List.134 List.135 List.136; @@ -136,12 +136,12 @@ procedure List.6 (#Attr.2): ret List.409; procedure List.6 (#Attr.2): - let List.458 : U64 = lowlevel ListLen #Attr.2; - ret List.458; + let List.459 : U64 = lowlevel ListLen #Attr.2; + ret List.459; procedure List.66 (#Attr.2, #Attr.3): - let List.452 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.452; + let List.453 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.453; procedure List.70 (#Attr.2, #Attr.3): let List.415 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; @@ -152,8 +152,8 @@ procedure List.71 (#Attr.2, #Attr.3): ret List.413; procedure List.8 (#Attr.2, #Attr.3): - let List.457 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.457; + let List.458 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.458; procedure List.89 (List.385, List.386, List.387): let List.441 : U64 = 0i64; @@ -161,19 +161,19 @@ procedure List.89 (List.385, List.386, List.387): let List.440 : {List U8, U64} = CallByName List.90 List.385 List.386 List.387 List.441 List.442; ret List.440; -procedure List.90 (List.468, List.469, List.470, List.471, List.472): +procedure List.90 (List.469, List.470, List.471, List.472, List.473): joinpoint List.443 List.388 List.389 List.390 List.391 List.392: let List.445 : Int1 = CallByName Num.22 List.391 List.392; if List.445 then - let List.451 : {Str, Str} = CallByName List.66 List.388 List.391; - let List.446 : {List U8, U64} = CallByName List.137 List.389 List.451 List.390; + let List.452 : {Str, Str} = CallByName List.66 List.388 List.391; + let List.446 : {List U8, U64} = CallByName List.137 List.389 List.452 List.390; let List.449 : U64 = 1i64; let List.448 : U64 = CallByName Num.19 List.391 List.449; jump List.443 List.388 List.446 List.390 List.448 List.392; else ret List.389; in - jump List.443 List.468 List.469 List.470 List.471 List.472; + jump List.443 List.469 List.470 List.471 List.472 List.473; procedure Num.125 (#Attr.2): let Num.263 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/encode_derived_record_two_field_strings.txt b/crates/compiler/test_mono/generated/encode_derived_record_two_field_strings.txt index faa8c35dcf..3a44fecb08 100644 --- a/crates/compiler/test_mono/generated/encode_derived_record_two_field_strings.txt +++ b/crates/compiler/test_mono/generated/encode_derived_record_two_field_strings.txt @@ -126,8 +126,8 @@ procedure Json.96 (Json.97, Json.433, Json.95): ret Json.435; procedure List.137 (List.138, List.139, List.136): - let List.455 : {List U8, U64} = CallByName Json.114 List.138 List.139; - ret List.455; + let List.456 : {List U8, U64} = CallByName Json.114 List.138 List.139; + ret List.456; procedure List.18 (List.134, List.135, List.136): let List.437 : {List U8, U64} = CallByName List.89 List.134 List.135 List.136; @@ -144,12 +144,12 @@ procedure List.6 (#Attr.2): ret List.409; procedure List.6 (#Attr.2): - let List.458 : U64 = lowlevel ListLen #Attr.2; - ret List.458; + let List.459 : U64 = lowlevel ListLen #Attr.2; + ret List.459; procedure List.66 (#Attr.2, #Attr.3): - let List.452 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.452; + let List.453 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.453; procedure List.70 (#Attr.2, #Attr.3): let List.415 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; @@ -160,8 +160,8 @@ procedure List.71 (#Attr.2, #Attr.3): ret List.413; procedure List.8 (#Attr.2, #Attr.3): - let List.457 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.457; + let List.458 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.458; procedure List.89 (List.385, List.386, List.387): let List.441 : U64 = 0i64; @@ -169,19 +169,19 @@ procedure List.89 (List.385, List.386, List.387): let List.440 : {List U8, U64} = CallByName List.90 List.385 List.386 List.387 List.441 List.442; ret List.440; -procedure List.90 (List.468, List.469, List.470, List.471, List.472): +procedure List.90 (List.469, List.470, List.471, List.472, List.473): joinpoint List.443 List.388 List.389 List.390 List.391 List.392: let List.445 : Int1 = CallByName Num.22 List.391 List.392; if List.445 then - let List.451 : {Str, Str} = CallByName List.66 List.388 List.391; - let List.446 : {List U8, U64} = CallByName List.137 List.389 List.451 List.390; + let List.452 : {Str, Str} = CallByName List.66 List.388 List.391; + let List.446 : {List U8, U64} = CallByName List.137 List.389 List.452 List.390; let List.449 : U64 = 1i64; let List.448 : U64 = CallByName Num.19 List.391 List.449; jump List.443 List.388 List.446 List.390 List.448 List.392; else ret List.389; in - jump List.443 List.468 List.469 List.470 List.471 List.472; + jump List.443 List.469 List.470 List.471 List.472 List.473; procedure Num.125 (#Attr.2): let Num.263 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/encode_derived_tag_one_field_string.txt b/crates/compiler/test_mono/generated/encode_derived_tag_one_field_string.txt index d5e22ddb0f..e52498d8ff 100644 --- a/crates/compiler/test_mono/generated/encode_derived_tag_one_field_string.txt +++ b/crates/compiler/test_mono/generated/encode_derived_tag_one_field_string.txt @@ -127,8 +127,8 @@ procedure Json.96 (Json.97, Json.438, Json.95): ret Json.440; procedure List.137 (List.138, List.139, List.136): - let List.461 : {List U8, U64} = CallByName Json.128 List.138 List.139; - ret List.461; + let List.462 : {List U8, U64} = CallByName Json.128 List.138 List.139; + ret List.462; procedure List.18 (List.134, List.135, List.136): let List.443 : {List U8, U64} = CallByName List.89 List.134 List.135 List.136; @@ -145,12 +145,12 @@ procedure List.6 (#Attr.2): ret List.409; procedure List.6 (#Attr.2): - let List.462 : U64 = lowlevel ListLen #Attr.2; - ret List.462; + let List.463 : U64 = lowlevel ListLen #Attr.2; + ret List.463; procedure List.66 (#Attr.2, #Attr.3): - let List.458 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.458; + let List.459 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.459; procedure List.70 (#Attr.2, #Attr.3): let List.415 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; @@ -161,8 +161,8 @@ procedure List.71 (#Attr.2, #Attr.3): ret List.413; procedure List.8 (#Attr.2, #Attr.3): - let List.464 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.464; + let List.465 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.465; procedure List.89 (List.385, List.386, List.387): let List.447 : U64 = 0i64; @@ -170,19 +170,19 @@ procedure List.89 (List.385, List.386, List.387): let List.446 : {List U8, U64} = CallByName List.90 List.385 List.386 List.387 List.447 List.448; ret List.446; -procedure List.90 (List.474, List.475, List.476, List.477, List.478): +procedure List.90 (List.475, List.476, List.477, List.478, List.479): joinpoint List.449 List.388 List.389 List.390 List.391 List.392: let List.451 : Int1 = CallByName Num.22 List.391 List.392; if List.451 then - let List.457 : Str = CallByName List.66 List.388 List.391; - let List.452 : {List U8, U64} = CallByName List.137 List.389 List.457 List.390; + let List.458 : Str = CallByName List.66 List.388 List.391; + let List.452 : {List U8, U64} = CallByName List.137 List.389 List.458 List.390; let List.455 : U64 = 1i64; let List.454 : U64 = CallByName Num.19 List.391 List.455; jump List.449 List.388 List.452 List.390 List.454 List.392; else ret List.389; in - jump List.449 List.474 List.475 List.476 List.477 List.478; + jump List.449 List.475 List.476 List.477 List.478 List.479; procedure Num.125 (#Attr.2): let Num.265 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/encode_derived_tag_two_payloads_string.txt b/crates/compiler/test_mono/generated/encode_derived_tag_two_payloads_string.txt index 39d9458331..70ba47cc7f 100644 --- a/crates/compiler/test_mono/generated/encode_derived_tag_two_payloads_string.txt +++ b/crates/compiler/test_mono/generated/encode_derived_tag_two_payloads_string.txt @@ -133,8 +133,8 @@ procedure Json.96 (Json.97, Json.438, Json.95): ret Json.440; procedure List.137 (List.138, List.139, List.136): - let List.461 : {List U8, U64} = CallByName Json.128 List.138 List.139; - ret List.461; + let List.462 : {List U8, U64} = CallByName Json.128 List.138 List.139; + ret List.462; procedure List.18 (List.134, List.135, List.136): let List.443 : {List U8, U64} = CallByName List.89 List.134 List.135 List.136; @@ -151,12 +151,12 @@ procedure List.6 (#Attr.2): ret List.409; procedure List.6 (#Attr.2): - let List.462 : U64 = lowlevel ListLen #Attr.2; - ret List.462; + let List.463 : U64 = lowlevel ListLen #Attr.2; + ret List.463; procedure List.66 (#Attr.2, #Attr.3): - let List.458 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.458; + let List.459 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.459; procedure List.70 (#Attr.2, #Attr.3): let List.415 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; @@ -167,8 +167,8 @@ procedure List.71 (#Attr.2, #Attr.3): ret List.413; procedure List.8 (#Attr.2, #Attr.3): - let List.464 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.464; + let List.465 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.465; procedure List.89 (List.385, List.386, List.387): let List.447 : U64 = 0i64; @@ -176,19 +176,19 @@ procedure List.89 (List.385, List.386, List.387): let List.446 : {List U8, U64} = CallByName List.90 List.385 List.386 List.387 List.447 List.448; ret List.446; -procedure List.90 (List.474, List.475, List.476, List.477, List.478): +procedure List.90 (List.475, List.476, List.477, List.478, List.479): joinpoint List.449 List.388 List.389 List.390 List.391 List.392: let List.451 : Int1 = CallByName Num.22 List.391 List.392; if List.451 then - let List.457 : Str = CallByName List.66 List.388 List.391; - let List.452 : {List U8, U64} = CallByName List.137 List.389 List.457 List.390; + let List.458 : Str = CallByName List.66 List.388 List.391; + let List.452 : {List U8, U64} = CallByName List.137 List.389 List.458 List.390; let List.455 : U64 = 1i64; let List.454 : U64 = CallByName Num.19 List.391 List.455; jump List.449 List.388 List.452 List.390 List.454 List.392; else ret List.389; in - jump List.449 List.474 List.475 List.476 List.477 List.478; + jump List.449 List.475 List.476 List.477 List.478 List.479; procedure Num.125 (#Attr.2): let Num.265 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/unreachable_void_constructor.txt b/crates/compiler/test_mono/generated/unreachable_void_constructor.txt index 017064eed0..5009f2f7a9 100644 --- a/crates/compiler/test_mono/generated/unreachable_void_constructor.txt +++ b/crates/compiler/test_mono/generated/unreachable_void_constructor.txt @@ -5,7 +5,8 @@ procedure Bool.2 (): procedure Test.0 (): let Test.6 : Int1 = CallByName Bool.2; if Test.6 then - Error voided tag constructor is unreachable + let Test.8 : Str = "voided tag constructor is unreachable"; + Crash Test.8 else let Test.5 : Str = "abc"; ret Test.5; diff --git a/crates/compiler/test_mono/src/tests.rs b/crates/compiler/test_mono/src/tests.rs index fcae9822d4..663db0f558 100644 --- a/crates/compiler/test_mono/src/tests.rs +++ b/crates/compiler/test_mono/src/tests.rs @@ -2030,3 +2030,21 @@ fn recursive_function_and_union_with_inference_hole() { "# ) } + +#[mono_test] +fn crash() { + indoc!( + r#" + app "test" provides [main] to "./platform" + + getInfallible = \result -> when result is + Ok x -> x + _ -> crash "turns out this was fallible" + + main = + x : [Ok U64, Err Str] + x = Ok 78 + getInfallible x + "# + ) +} diff --git a/crates/compiler/types/src/subs.rs b/crates/compiler/types/src/subs.rs index 485c1233d0..9492434e71 100644 --- a/crates/compiler/types/src/subs.rs +++ b/crates/compiler/types/src/subs.rs @@ -1667,6 +1667,8 @@ impl Subs { pub const TAG_NAME_BAD_UTF_8: SubsIndex = SubsIndex::new(3); pub const TAG_NAME_OUT_OF_BOUNDS: SubsIndex = SubsIndex::new(4); + pub const STR_SLICE: VariableSubsSlice = SubsSlice::new(0, 1); + #[rustfmt::skip] pub const AB_ENCODING: SubsSlice = SubsSlice::new(0, 1); #[rustfmt::skip] @@ -1704,14 +1706,18 @@ impl Subs { let mut subs = Subs { utable: UnificationTable::default(), - variables: Vec::new(), + variables: vec![ + // Used for STR_SLICE + Variable::STR, + ], tag_names, symbol_names, field_names: Vec::new(), record_fields: Vec::new(), - // store an empty slice at the first position - // used for "TagOrFunction" - variable_slices: vec![VariableSubsSlice::default()], + variable_slices: vec![ + // used for "TagOrFunction" + VariableSubsSlice::default(), + ], unspecialized_lambda_sets: Vec::new(), tag_name_cache: Default::default(), uls_of_var: Default::default(), diff --git a/crates/compiler/types/src/types.rs b/crates/compiler/types/src/types.rs index e7dfa72a3e..c781493cd9 100644 --- a/crates/compiler/types/src/types.rs +++ b/crates/compiler/types/src/types.rs @@ -3486,6 +3486,7 @@ pub enum Reason { member_name: Symbol, def_region: Region, }, + CrashArg, } #[derive(PartialEq, Eq, Debug, Clone)] @@ -3528,7 +3529,10 @@ pub enum Category { AbilityMemberSpecialization(Symbol), + Crash, + Expect, + Dbg, Unknown, } diff --git a/crates/highlight/src/tokenizer.rs b/crates/highlight/src/tokenizer.rs index bf737d3f83..1878d0c0f3 100644 --- a/crates/highlight/src/tokenizer.rs +++ b/crates/highlight/src/tokenizer.rs @@ -33,6 +33,7 @@ pub enum Token { KeywordEffects = 0b_0011_0000, KeywordPlatform = 0b_0011_0001, KeywordRequires = 0b_0011_0010, + KeywordDbg = 0b_0111_1011, Comma = 0b_0100_0000, Colon = 0b_0100_0001, @@ -417,6 +418,7 @@ fn lex_ident(uppercase: bool, bytes: &[u8]) -> (Token, usize) { b"when" => Token::KeywordWhen, b"as" => Token::KeywordAs, b"is" => Token::KeywordIs, + b"dbg" => Token::KeywordDbg, b"expect" => Token::KeywordExpect, b"app" => Token::KeywordApp, b"interface" => Token::KeywordInterface, diff --git a/crates/linker/src/elf.rs b/crates/linker/src/elf.rs index ec014396dc..0d4baabbd3 100644 --- a/crates/linker/src/elf.rs +++ b/crates/linker/src/elf.rs @@ -1517,6 +1517,7 @@ mod tests { use super::*; use indoc::indoc; + use roc_build::link::preprocessed_host_filename; use target_lexicon::Triple; const ELF64_DYNHOST: &[u8] = include_bytes!("../dynhost_benchmarks_elf64") as &[_]; @@ -1575,7 +1576,7 @@ mod tests { } #[allow(dead_code)] - fn zig_host_app_help(dir: &Path) { + fn zig_host_app_help(dir: &Path, target: &Triple) { let host_zig = indoc!( r#" const std = @import("std"); @@ -1669,17 +1670,19 @@ mod tests { panic!("zig build-exe failed"); } + let preprocessed_host_filename = dir.join(preprocessed_host_filename(target).unwrap()); + preprocess_elf( target_lexicon::Endianness::Little, &dir.join("host"), &dir.join("metadata"), - &dir.join("preprocessedhost"), + &preprocessed_host_filename, &dir.join("libapp.so"), false, false, ); - std::fs::copy(&dir.join("preprocessedhost"), &dir.join("final")).unwrap(); + std::fs::copy(&preprocessed_host_filename, &dir.join("final")).unwrap(); surgery_elf( &roc_app, @@ -1693,10 +1696,12 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn zig_host_app() { + use std::str::FromStr; + let dir = tempfile::tempdir().unwrap(); let dir = dir.path(); - zig_host_app_help(dir); + zig_host_app_help(dir, &Triple::from_str("x86_64-unknown-linux-musl").unwrap()); let output = std::process::Command::new(&dir.join("final")) .current_dir(dir) diff --git a/crates/linker/src/lib.rs b/crates/linker/src/lib.rs index 8b35e9cbab..cfea89c784 100644 --- a/crates/linker/src/lib.rs +++ b/crates/linker/src/lib.rs @@ -77,7 +77,7 @@ pub fn build_and_preprocess_host( generate_dynamic_lib(target, &stub_dll_symbols, &stub_lib); rebuild_host(opt_level, target, host_input_path, Some(&stub_lib)); let metadata = host_input_path.with_file_name("metadata"); - // let prehost = host_input_path.with_file_name("preprocessedhost"); + // let prehost = host_input_path.with_file_name(preprocessed_host_filename(target).unwrap()); preprocess( target, @@ -198,7 +198,9 @@ fn generate_dynamic_lib(target: &Triple, stub_dll_symbols: &[String], stub_lib_p let bytes = crate::generate_dylib::generate(target, stub_dll_symbols) .unwrap_or_else(|e| internal_error!("{e}")); - std::fs::write(stub_lib_path, &bytes).unwrap_or_else(|e| internal_error!("{e}")); + if let Err(e) = std::fs::write(stub_lib_path, &bytes) { + internal_error!("failed to write stub lib to {:?}: {e}", stub_lib_path) + } if let target_lexicon::OperatingSystem::Windows = target.operating_system { generate_import_library(stub_lib_path, stub_dll_symbols); @@ -212,8 +214,9 @@ fn generate_import_library(stub_lib_path: &Path, custom_names: &[String]) { let mut def_path = stub_lib_path.to_owned(); def_path.set_extension("def"); - std::fs::write(def_path, def_file_content.as_bytes()) - .unwrap_or_else(|e| internal_error!("{e}")); + if let Err(e) = std::fs::write(&def_path, def_file_content.as_bytes()) { + internal_error!("failed to write import library to {:?}: {e}", def_path) + } let mut def_filename = PathBuf::from(generate_dylib::APP_DLL); def_filename.set_extension("def"); @@ -499,7 +502,7 @@ pub(crate) fn open_mmap(path: &Path) -> Mmap { let in_file = std::fs::OpenOptions::new() .read(true) .open(path) - .unwrap_or_else(|e| internal_error!("{e}")); + .unwrap_or_else(|e| internal_error!("failed to open file {path:?}: {e}")); unsafe { Mmap::map(&in_file).unwrap_or_else(|e| internal_error!("{e}")) } } @@ -510,7 +513,7 @@ pub(crate) fn open_mmap_mut(path: &Path, length: usize) -> MmapMut { .write(true) .create(true) .open(path) - .unwrap_or_else(|e| internal_error!("{e}")); + .unwrap_or_else(|e| internal_error!("failed to create or open file {path:?}: {e}")); out_file .set_len(length as u64) .unwrap_or_else(|e| internal_error!("{e}")); diff --git a/crates/linker/src/pe.rs b/crates/linker/src/pe.rs index a815f391b0..3c42d3b26e 100644 --- a/crates/linker/src/pe.rs +++ b/crates/linker/src/pe.rs @@ -1358,6 +1358,8 @@ mod test { use object::{pe, LittleEndian as LE, Object}; use indoc::indoc; + use roc_build::link::preprocessed_host_filename; + use target_lexicon::Triple; use super::*; @@ -1708,17 +1710,20 @@ mod test { panic!("zig build-exe failed: {}", command_str); } + let preprocessed_host_filename = + dir.join(preprocessed_host_filename(&Triple::host()).unwrap()); + preprocess_windows( &dir.join("host.exe"), &dir.join("metadata"), - &dir.join("preprocessedhost"), + &preprocessed_host_filename, &names, false, false, ) .unwrap(); - std::fs::copy(&dir.join("preprocessedhost"), &dir.join("app.exe")).unwrap(); + std::fs::copy(&preprocessed_host_filename, &dir.join("app.exe")).unwrap(); surgery_pe(&dir.join("app.exe"), &dir.join("metadata"), &roc_app); } diff --git a/crates/repl_cli/src/repl_state.rs b/crates/repl_cli/src/repl_state.rs index c05a5bf340..097b9875ff 100644 --- a/crates/repl_cli/src/repl_state.rs +++ b/crates/repl_cli/src/repl_state.rs @@ -196,6 +196,9 @@ impl ReplState { | ValueDef::AnnotatedBody { .. } => { todo!("handle pattern other than identifier (which repl doesn't support)") } + ValueDef::Dbg { .. } => { + todo!("handle receiving a `dbg` - what should the repl do for that?") + } ValueDef::Expect { .. } => { todo!("handle receiving an `expect` - what should the repl do for that?") } diff --git a/crates/repl_expect/src/run.rs b/crates/repl_expect/src/run.rs index 2ec241aca6..8d56dcbddb 100644 --- a/crates/repl_expect/src/run.rs +++ b/crates/repl_expect/src/run.rs @@ -235,7 +235,7 @@ fn run_expect_pure<'a, W: std::io::Write>( let sequence = ExpectSequence::new(shared_memory.ptr.cast()); - let result: Result<(), String> = try_run_jit_function!(lib, expect.name, (), |v: ()| v); + let result: Result<(), (String, _)> = try_run_jit_function!(lib, expect.name, (), |v: ()| v); let shared_memory_ptr: *const u8 = shared_memory.ptr.cast(); @@ -249,7 +249,7 @@ fn run_expect_pure<'a, W: std::io::Write>( let renderer = Renderer::new(arena, interns, render_target, module_id, filename, &source); - if let Err(roc_panic_message) = result { + if let Err((roc_panic_message, _roc_panic_tag)) = result { renderer.render_panic(writer, &roc_panic_message, expect.region)?; } else { let mut offset = ExpectSequence::START_OFFSET; @@ -305,9 +305,10 @@ fn run_expect_fx<'a, W: std::io::Write>( child_memory.set_shared_buffer(lib); - let result: Result<(), String> = try_run_jit_function!(lib, expect.name, (), |v: ()| v); + let result: Result<(), (String, _)> = + try_run_jit_function!(lib, expect.name, (), |v: ()| v); - if let Err(msg) = result { + if let Err((msg, _)) = result { panic!("roc panic {}", msg); } @@ -416,6 +417,44 @@ pub fn render_expects_in_memory<'a>( ) } +pub fn render_dbgs_in_memory<'a>( + writer: &mut impl std::io::Write, + arena: &'a Bump, + expectations: &mut VecMap, + interns: &'a Interns, + layout_interner: &Arc>>, + memory: &ExpectMemory, +) -> std::io::Result { + let shared_ptr = memory.ptr; + + let frame = ExpectFrame::at_offset(shared_ptr, ExpectSequence::START_OFFSET); + let module_id = frame.module_id; + + let data = expectations.get_mut(&module_id).unwrap(); + let filename = data.path.to_owned(); + let source = std::fs::read_to_string(&data.path).unwrap(); + + let renderer = Renderer::new( + arena, + interns, + RenderTarget::ColorTerminal, + module_id, + filename, + &source, + ); + + render_dbg_failure( + writer, + &renderer, + arena, + expectations, + interns, + layout_interner, + shared_ptr, + ExpectSequence::START_OFFSET, + ) +} + fn split_expect_lookups(subs: &Subs, lookups: &[ExpectLookup]) -> (Vec, Vec) { lookups .iter() @@ -437,6 +476,61 @@ fn split_expect_lookups(subs: &Subs, lookups: &[ExpectLookup]) -> (Vec, .unzip() } +#[allow(clippy::too_many_arguments)] +fn render_dbg_failure<'a>( + writer: &mut impl std::io::Write, + renderer: &Renderer, + arena: &'a Bump, + expectations: &mut VecMap, + interns: &'a Interns, + layout_interner: &Arc>>, + start: *const u8, + offset: usize, +) -> std::io::Result { + // we always run programs as the host + let target_info = (&target_lexicon::Triple::host()).into(); + + let frame = ExpectFrame::at_offset(start, offset); + let module_id = frame.module_id; + + let failure_region = frame.region; + let dbg_symbol = unsafe { std::mem::transmute::<_, Symbol>(failure_region) }; + let expect_region = Some(Region::zero()); + + let data = expectations.get_mut(&module_id).unwrap(); + + let current = match data.dbgs.get(&dbg_symbol) { + None => panic!("region {failure_region:?} not in list of dbgs"), + Some(current) => current, + }; + let failure_region = current.region; + + let subs = arena.alloc(&mut data.subs); + + let current = ExpectLookup { + symbol: current.symbol, + var: current.var, + ability_info: current.ability_info, + }; + + let (_symbols, variables) = split_expect_lookups(subs, &[current]); + + let (offset, expressions) = crate::get_values( + target_info, + arena, + subs, + interns, + layout_interner, + start, + frame.start_offset, + &variables, + ); + + renderer.render_dbg(writer, &expressions, expect_region, failure_region)?; + + Ok(offset) +} + #[allow(clippy::too_many_arguments)] fn render_expect_failure<'a>( writer: &mut impl std::io::Write, diff --git a/crates/reporting/src/error/canonicalize.rs b/crates/reporting/src/error/canonicalize.rs index b5b26e4125..cf96c09fcc 100644 --- a/crates/reporting/src/error/canonicalize.rs +++ b/crates/reporting/src/error/canonicalize.rs @@ -1086,7 +1086,36 @@ pub fn can_problem<'b>( } else { "TOO FEW TYPE ARGUMENTS".to_string() }; - + severity = Severity::RuntimeError; + } + Problem::UnappliedCrash { region } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This "), alloc.keyword("crash"), alloc.reflow(" doesn't have a message given to it:") + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.keyword("crash"), alloc.reflow(" must be passed a message to crash with at the exact place it's used. "), + alloc.keyword("crash"), alloc.reflow(" can't be used as a value that's passed around, like functions can be - it must be applied immediately!"), + ]) + ]); + title = "UNAPPLIED CRASH".to_string(); + severity = Severity::RuntimeError; + } + Problem::OverAppliedCrash { region } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This "), + alloc.keyword("crash"), + alloc.reflow(" has too many values given to it:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.keyword("crash"), + alloc.reflow(" must be given exacly one message to crash with."), + ]), + ]); + title = "OVERAPPLIED CRASH".to_string(); severity = Severity::RuntimeError; } }; diff --git a/crates/reporting/src/error/expect.rs b/crates/reporting/src/error/expect.rs index 5bde85c412..be9221d537 100644 --- a/crates/reporting/src/error/expect.rs +++ b/crates/reporting/src/error/expect.rs @@ -42,13 +42,18 @@ impl<'a> Renderer<'a> { } } + fn render_expr(&'a self, error_type: ErrorType) -> RocDocBuilder<'a> { + use crate::error::r#type::error_type_to_doc; + + error_type_to_doc(&self.alloc, error_type) + } + fn render_lookup( &'a self, symbol: Symbol, expr: &Expr<'_>, error_type: ErrorType, ) -> RocDocBuilder<'a> { - use crate::error::r#type::error_type_to_doc; use roc_fmt::annotation::Formattable; let mut buf = roc_fmt::Buf::new_in(self.arena); @@ -58,7 +63,7 @@ impl<'a> Renderer<'a> { self.alloc .symbol_unqualified(symbol) .append(" : ") - .append(error_type_to_doc(&self.alloc, error_type)), + .append(self.render_expr(error_type)), self.alloc .symbol_unqualified(symbol) .append(" = ") @@ -164,6 +169,37 @@ impl<'a> Renderer<'a> { write!(writer, "{}", buf) } + #[allow(clippy::too_many_arguments)] + pub fn render_dbg( + &self, + writer: &mut W, + expressions: &[Expr<'_>], + expect_region: Option, + dbg_expr_region: Region, + ) -> std::io::Result<()> + where + W: std::io::Write, + { + let line_col_region = self.to_line_col_region(expect_region, dbg_expr_region); + write!( + writer, + "\u{001b}[36m[{} {}:{}] \u{001b}[0m", + self.filename.display(), + line_col_region.start.line + 1, + line_col_region.start.column + 1 + )?; + + let expr = expressions[0]; + + let mut buf = roc_fmt::Buf::new_in(self.arena); + { + use roc_fmt::annotation::Formattable; + expr.format(&mut buf, 0); + } + + writeln!(writer, "{}", buf.as_str()) + } + pub fn render_panic( &self, writer: &mut W, diff --git a/crates/reporting/src/error/parse.rs b/crates/reporting/src/error/parse.rs index e9ed73dd68..17d554e3f8 100644 --- a/crates/reporting/src/error/parse.rs +++ b/crates/reporting/src/error/parse.rs @@ -2907,6 +2907,27 @@ fn to_tinparens_report<'a>( } } + ETypeInParens::Empty(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow("I am partway through parsing a parenthesized type:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"I was expecting to see an expression next."), + alloc.reflow(r"Note, Roc doesn't use '()' as a null type."), + ]), + ]); + + Report { + filename, + doc, + title: "EMPTY PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + ETypeInParens::End(pos) => { let surroundings = Region::new(start, pos); let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); diff --git a/crates/reporting/src/error/type.rs b/crates/reporting/src/error/type.rs index eebf683824..097d8faddc 100644 --- a/crates/reporting/src/error/type.rs +++ b/crates/reporting/src/error/type.rs @@ -1375,6 +1375,42 @@ fn to_expr_report<'b>( } } + Reason::CrashArg => { + let this_is = alloc.reflow("The value is"); + + let wanted = alloc.concat([ + alloc.reflow("But I can only "), + alloc.keyword("crash"), + alloc.reflow(" with messages of type"), + ]); + + let details = None; + + let lines = [ + alloc + .reflow("This value passed to ") + .append(alloc.keyword("crash")) + .append(alloc.reflow(" is not a string:")), + alloc.region(lines.convert_region(region)), + type_comparison( + alloc, + found, + expected_type, + ExpectationContext::WhenCondition, + add_category(alloc, this_is, &category), + wanted, + details, + ), + ]; + + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc: alloc.stack(lines), + severity: Severity::RuntimeError, + } + } + Reason::LowLevelOpArg { op, arg_index } => { panic!( "Compiler bug: argument #{} to low-level operation {:?} was the wrong type!", @@ -1680,6 +1716,10 @@ fn format_category<'b>( alloc.concat([this_is, alloc.text(" an uniqueness attribute")]), alloc.text(" of type:"), ), + Crash => { + internal_error!("calls to crash should be unconditionally admitted in any context, unexpected reachability!"); + } + Storage(..) | Unknown => ( alloc.concat([this_is, alloc.text(" a value")]), alloc.text(" of type:"), @@ -1696,6 +1736,10 @@ fn format_category<'b>( alloc.concat([this_is, alloc.text(" an expectation")]), alloc.text(" of type:"), ), + Dbg => ( + alloc.concat([this_is, alloc.text(" a dbg statement")]), + alloc.text(" of type:"), + ), } } diff --git a/crates/reporting/tests/test_reporting.rs b/crates/reporting/tests/test_reporting.rs index 79f5f3a91d..33c333f093 100644 --- a/crates/reporting/tests/test_reporting.rs +++ b/crates/reporting/tests/test_reporting.rs @@ -4410,13 +4410,14 @@ mod test_reporting { @r###" ── UNFINISHED PARENTHESES ────────────────── tmp/type_in_parens_start/Test.roc ─ - I just started parsing a type in parentheses, but I got stuck here: + I am partway through parsing a type in parentheses, but I got stuck + here: 4│ f : ( ^ - Tag unions look like [Many I64, None], so I was expecting to see a tag - name next. + I was expecting to see a parenthesis before this, so try adding a ) + and see if that helps? Note: I may be confused by indentation "### @@ -4436,12 +4437,12 @@ mod test_reporting { here: 4│ f : ( I64 - ^ + 5│ + 6│ + ^ - I was expecting to see a parenthesis before this, so try adding a ) - and see if that helps? - - Note: I may be confused by indentation + I was expecting to see a closing parenthesis before this, so try + adding a ) and see if that helps? "### ); @@ -6049,33 +6050,6 @@ All branches in an `if` must have the same type! "### ); - test_report!( - outdented_in_parens, - indoc!( - r#" - Box : ( - Str - ) - - 4 - "# - ), - @r###" - ── NEED MORE INDENTATION ──────────────────── tmp/outdented_in_parens/Test.roc ─ - - I am partway through parsing a type in parentheses, but I got stuck - here: - - 4│ Box : ( - 5│ Str - 6│ ) - ^ - - I need this parenthesis to be indented more. Try adding more spaces - before it! - "### - ); - test_report!( backpassing_type_error, indoc!( @@ -12479,4 +12453,67 @@ All branches in an `if` must have the same type! clause to bind the type variable, like `| e has Bool.Eq` "### ); + + test_report!( + crash_given_non_string, + indoc!( + r#" + crash {} + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This value passed to `crash` is not a string: + + 4│ crash {} + ^^ + + The value is a record of type: + + {} + + But I can only `crash` with messages of type + + Str + "### + ); + + test_report!( + crash_unapplied, + indoc!( + r#" + crash + "# + ), + @r###" + ── UNAPPLIED CRASH ─────────────────────────────────────── /code/proj/Main.roc ─ + + This `crash` doesn't have a message given to it: + + 4│ crash + ^^^^^ + + `crash` must be passed a message to crash with at the exact place it's + used. `crash` can't be used as a value that's passed around, like + functions can be - it must be applied immediately! + "### + ); + + test_report!( + crash_overapplied, + indoc!( + r#" + crash "" "" + "# + ), + @r###" + ── OVERAPPLIED CRASH ───────────────────────────────────── /code/proj/Main.roc ─ + + This `crash` has too many values given to it: + + 4│ crash "" "" + ^^^^^ + + `crash` must be given exacly one message to crash with. } diff --git a/crates/wasm_interp/Cargo.toml b/crates/wasm_interp/Cargo.toml index ab18242a86..d247ab8c7d 100644 --- a/crates/wasm_interp/Cargo.toml +++ b/crates/wasm_interp/Cargo.toml @@ -3,10 +3,13 @@ name = "roc_wasm_interp" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "roc_wasm_interp" +path = "src/main.rs" [dependencies] roc_wasm_module = { path = "../wasm_module" } bitvec.workspace = true bumpalo.workspace = true +clap.workspace = true diff --git a/crates/wasm_interp/src/call_stack.rs b/crates/wasm_interp/src/call_stack.rs index 8919636ea1..3e1dd0a570 100644 --- a/crates/wasm_interp/src/call_stack.rs +++ b/crates/wasm_interp/src/call_stack.rs @@ -1,19 +1,21 @@ use bitvec::vec::BitVec; use bumpalo::{collections::Vec, Bump}; -use roc_wasm_module::ValueType; +use roc_wasm_module::{parse::Parse, Value, ValueType}; use std::iter::repeat; -use crate::Value; +use crate::ValueStack; /// Struct-of-Arrays storage for the call stack. /// Type info is packed to avoid wasting space on padding. /// However we store 64 bits for every local, even 32-bit values, for easy random access. #[derive(Debug)] pub struct CallStack<'a> { - /// return addresses (one entry per frame) - return_addrs: Vec<'a, u32>, + /// return addresses and nested block depths (one entry per frame) + return_addrs_and_block_depths: Vec<'a, (u32, u32)>, /// frame offsets into the `locals`, `is_float`, and `is_64` vectors (one entry per frame) frame_offsets: Vec<'a, u32>, + /// base size of the value stack before executing (one entry per frame) + value_stack_bases: Vec<'a, u32>, /// binary data for local variables (one entry per local) locals_data: Vec<'a, u64>, /// int/float type info (one entry per local) @@ -35,8 +37,9 @@ Not clear if this would be better! Stack access pattern is pretty cache-friendly impl<'a> CallStack<'a> { pub fn new(arena: &'a Bump) -> Self { CallStack { - return_addrs: Vec::with_capacity_in(256, arena), + return_addrs_and_block_depths: Vec::with_capacity_in(256, arena), frame_offsets: Vec::with_capacity_in(256, arena), + value_stack_bases: Vec::with_capacity_in(256, arena), locals_data: Vec::with_capacity_in(16 * 256, arena), is_float: BitVec::with_capacity(256), is_64: BitVec::with_capacity(256), @@ -44,13 +47,39 @@ impl<'a> CallStack<'a> { } /// On entering a Wasm call, save the return address, and make space for locals - pub fn push_frame(&mut self, return_addr: u32, local_groups: &[(u32, ValueType)]) { - self.return_addrs.push(return_addr); + pub fn push_frame( + &mut self, + return_addr: u32, + return_block_depth: u32, + n_args: u32, + value_stack: &mut ValueStack<'a>, + code_bytes: &[u8], + pc: &mut usize, + ) { + self.return_addrs_and_block_depths + .push((return_addr, return_block_depth)); let frame_offset = self.is_64.len(); self.frame_offsets.push(frame_offset as u32); let mut total = 0; - for (num_locals, ty) in local_groups { - let n = *num_locals as usize; + + // Make space for arguments + self.is_64.extend(repeat(false).take(n_args as usize)); + self.is_float.extend(repeat(false).take(n_args as usize)); + self.locals_data.extend(repeat(0).take(n_args as usize)); + + // Pop arguments off the value stack and into locals + for i in (0..n_args).rev() { + let arg = value_stack.pop(); + self.set_local_help(i, arg); + } + + self.value_stack_bases.push(value_stack.len() as u32); + + // Parse local variable declarations in the function header. They're grouped by type. + let local_group_count = u32::parse((), code_bytes, pc).unwrap(); + for _ in 0..local_group_count { + let (group_size, ty) = <(u32, ValueType)>::parse((), code_bytes, pc).unwrap(); + let n = group_size as usize; total += n; self.is_64 .extend(repeat(matches!(ty, ValueType::I64 | ValueType::F64)).take(n)); @@ -61,12 +90,13 @@ impl<'a> CallStack<'a> { } /// On returning from a Wasm call, drop its locals and retrieve the return address - pub fn pop_frame(&mut self) -> u32 { - let frame_offset = self.frame_offsets.pop().unwrap() as usize; + pub fn pop_frame(&mut self) -> Option<(u32, u32)> { + let frame_offset = self.frame_offsets.pop()? as usize; + self.value_stack_bases.pop()?; self.locals_data.truncate(frame_offset); self.is_64.truncate(frame_offset); self.is_64.truncate(frame_offset); - self.return_addrs.pop().unwrap() + self.return_addrs_and_block_depths.pop() } pub fn get_local(&self, local_index: u32) -> Value { @@ -92,35 +122,46 @@ impl<'a> CallStack<'a> { } pub fn set_local(&mut self, local_index: u32, value: Value) { + let type_check_ok = self.set_local_help(local_index, value); + debug_assert!(type_check_ok); + } + + fn set_local_help(&mut self, local_index: u32, value: Value) -> bool { let frame_offset = *self.frame_offsets.last().unwrap(); let index = (frame_offset + local_index) as usize; match value { Value::I32(x) => { self.locals_data[index] = u64::from_ne_bytes((x as i64).to_ne_bytes()); - debug_assert!(!self.is_64[index]); - debug_assert!(!self.is_float[index]); + !self.is_64[index] && !self.is_float[index] } Value::I64(x) => { self.locals_data[index] = u64::from_ne_bytes((x).to_ne_bytes()); - debug_assert!(!self.is_float[index]); - debug_assert!(self.is_64[index]); + !self.is_float[index] && self.is_64[index] } Value::F32(x) => { self.locals_data[index] = x.to_bits() as u64; - debug_assert!(self.is_float[index]); - debug_assert!(!self.is_64[index]); + self.is_float[index] && !self.is_64[index] } Value::F64(x) => { self.locals_data[index] = x.to_bits(); - debug_assert!(self.is_float[index]); - debug_assert!(self.is_64[index]); + self.is_float[index] && self.is_64[index] } } } + + pub fn value_stack_base(&self) -> u32 { + *self.value_stack_bases.last().unwrap_or(&0) + } + + pub fn is_empty(&self) -> bool { + self.is_64.is_empty() + } } #[cfg(test)] mod tests { + use roc_wasm_module::Serialize; + use super::*; const RETURN_ADDR: u32 = 0x12345; @@ -130,27 +171,38 @@ mod tests { assert_eq!(call_stack.get_local(index), value); } - fn setup(call_stack: &mut CallStack<'_>) { + fn setup<'a>(arena: &'a Bump, call_stack: &mut CallStack<'a>) { + let mut buffer = vec![]; + let mut cursor = 0; + let mut vs = ValueStack::new(arena); + // Push a other few frames before the test frame, just to make the scenario more typical. - call_stack.push_frame(0x11111, &[(1, ValueType::I32)]); - call_stack.push_frame(0x22222, &[(2, ValueType::I32)]); - call_stack.push_frame(0x33333, &[(3, ValueType::I32)]); + [(1u32, ValueType::I32)].serialize(&mut buffer); + call_stack.push_frame(0x11111, 0, 0, &mut vs, &buffer, &mut cursor); + + [(2u32, ValueType::I32)].serialize(&mut buffer); + call_stack.push_frame(0x22222, 0, 0, &mut vs, &buffer, &mut cursor); + + [(3u32, ValueType::I32)].serialize(&mut buffer); + call_stack.push_frame(0x33333, 0, 0, &mut vs, &buffer, &mut cursor); // Create a test call frame with local variables of every type - let current_frame_local_decls = [ - (8, ValueType::I32), - (4, ValueType::I64), - (2, ValueType::F32), - (1, ValueType::F64), - ]; - call_stack.push_frame(RETURN_ADDR, ¤t_frame_local_decls); + [ + (8u32, ValueType::I32), + (4u32, ValueType::I64), + (2u32, ValueType::F32), + (1u32, ValueType::F64), + ] + .serialize(&mut buffer); + call_stack.push_frame(RETURN_ADDR, 0, 0, &mut vs, &buffer, &mut cursor); } #[test] fn test_all() { let arena = Bump::new(); let mut call_stack = CallStack::new(&arena); - setup(&mut call_stack); + + setup(&arena, &mut call_stack); test_get_set(&mut call_stack, 0, Value::I32(123)); test_get_set(&mut call_stack, 8, Value::I64(123456)); @@ -169,7 +221,7 @@ mod tests { test_get_set(&mut call_stack, 14, Value::F64(f64::MIN)); test_get_set(&mut call_stack, 14, Value::F64(f64::MAX)); - assert_eq!(call_stack.pop_frame(), RETURN_ADDR); + assert_eq!(call_stack.pop_frame(), Some((RETURN_ADDR, 0))); } #[test] @@ -177,7 +229,7 @@ mod tests { fn test_type_error_i32() { let arena = Bump::new(); let mut call_stack = CallStack::new(&arena); - setup(&mut call_stack); + setup(&arena, &mut call_stack); test_get_set(&mut call_stack, 0, Value::F32(1.01)); } @@ -186,7 +238,7 @@ mod tests { fn test_type_error_i64() { let arena = Bump::new(); let mut call_stack = CallStack::new(&arena); - setup(&mut call_stack); + setup(&arena, &mut call_stack); test_get_set(&mut call_stack, 8, Value::F32(1.01)); } @@ -195,7 +247,7 @@ mod tests { fn test_type_error_f32() { let arena = Bump::new(); let mut call_stack = CallStack::new(&arena); - setup(&mut call_stack); + setup(&arena, &mut call_stack); test_get_set(&mut call_stack, 12, Value::I32(123)); } @@ -204,7 +256,7 @@ mod tests { fn test_type_error_f64() { let arena = Bump::new(); let mut call_stack = CallStack::new(&arena); - setup(&mut call_stack); + setup(&arena, &mut call_stack); test_get_set(&mut call_stack, 14, Value::I32(123)); } } diff --git a/crates/wasm_interp/src/execute.rs b/crates/wasm_interp/src/execute.rs index c2868a74bd..78f59b5580 100644 --- a/crates/wasm_interp/src/execute.rs +++ b/crates/wasm_interp/src/execute.rs @@ -1,310 +1,518 @@ use bumpalo::{collections::Vec, Bump}; +use std::fmt::{self, Write}; +use std::iter; + use roc_wasm_module::opcodes::OpCode; use roc_wasm_module::parse::Parse; -use roc_wasm_module::sections::MemorySection; -use roc_wasm_module::WasmModule; +use roc_wasm_module::sections::{ImportDesc, MemorySection}; +use roc_wasm_module::Value; +use roc_wasm_module::{ExportType, WasmModule}; use crate::call_stack::CallStack; use crate::value_stack::ValueStack; -use crate::Value; + +pub enum Action { + Continue, + Break, +} #[derive(Debug)] pub struct ExecutionState<'a> { - #[allow(dead_code)] - memory: Vec<'a, u8>, - - #[allow(dead_code)] - call_stack: CallStack<'a>, - + pub memory: Vec<'a, u8>, + pub call_stack: CallStack<'a>, pub value_stack: ValueStack<'a>, - program_counter: usize, + pub globals: Vec<'a, Value>, + pub program_counter: usize, + block_depth: u32, + import_signatures: Vec<'a, u32>, + debug_string: Option, } impl<'a> ExecutionState<'a> { - pub fn new(arena: &'a Bump, memory_pages: u32, program_counter: usize) -> Self { + pub fn new(arena: &'a Bump, memory_pages: u32, program_counter: usize, globals: G) -> Self + where + G: IntoIterator, + { let mem_bytes = memory_pages * MemorySection::PAGE_SIZE; ExecutionState { - memory: Vec::with_capacity_in(mem_bytes as usize, arena), + memory: Vec::from_iter_in(iter::repeat(0).take(mem_bytes as usize), arena), call_stack: CallStack::new(arena), value_stack: ValueStack::new(arena), + globals: Vec::from_iter_in(globals, arena), program_counter, + block_depth: 0, + import_signatures: Vec::new_in(arena), + debug_string: None, } } - pub fn execute_next_instruction(&mut self, module: &WasmModule<'a>) { + pub fn for_module( + arena: &'a Bump, + module: &WasmModule<'a>, + start_fn_name: &str, + is_debug_mode: bool, + ) -> Result { + let mem_bytes = module.memory.min_bytes().map_err(|e| { + format!( + "Error parsing Memory section at offset {:#x}:\n{}", + e.offset, e.message + ) + })?; + let mut memory = Vec::from_iter_in(iter::repeat(0).take(mem_bytes as usize), arena); + module.data.load_into(&mut memory)?; + + let globals = module.global.initial_values(arena); + + // Gather imported function signatures into a vector, for simpler lookup + let import_signatures = { + let imports_iter = module.import.imports.iter(); + let sig_iter = imports_iter.filter_map(|imp| match imp.description { + ImportDesc::Func { signature_index } => Some(signature_index), + _ => None, + }); + Vec::from_iter_in(sig_iter, arena) + }; + + let mut program_counter = { + let mut export_iter = module.export.exports.iter(); + let start_fn_index = export_iter + .find_map(|ex| { + if ex.ty == ExportType::Func && ex.name == start_fn_name { + Some(ex.index) + } else { + None + } + }) + .ok_or(format!( + "I couldn't find an exported function '{}' in this WebAssembly module", + start_fn_name + ))?; + let internal_fn_index = start_fn_index as usize - module.import.function_count(); + let mut cursor = module.code.function_offsets[internal_fn_index] as usize; + let _start_fn_byte_length = u32::parse((), &module.code.bytes, &mut cursor); + cursor + }; + + let mut value_stack = ValueStack::new(arena); + let mut call_stack = CallStack::new(arena); + call_stack.push_frame( + 0, // return_addr + 0, // return_block_depth + 0, // n_args + &mut value_stack, + &module.code.bytes, + &mut program_counter, + ); + + let debug_string = if is_debug_mode { + Some(String::new()) + } else { + None + }; + + Ok(ExecutionState { + memory, + call_stack, + value_stack, + globals, + program_counter, + block_depth: 0, + import_signatures, + debug_string, + }) + } + + fn fetch_immediate_u32(&mut self, module: &WasmModule<'a>) -> u32 { + let x = u32::parse((), &module.code.bytes, &mut self.program_counter).unwrap(); + if let Some(debug_string) = self.debug_string.as_mut() { + write!(debug_string, "{}", x).unwrap(); + } + x + } + + fn do_return(&mut self) -> Action { + if let Some((return_addr, block_depth)) = self.call_stack.pop_frame() { + if self.call_stack.is_empty() { + // We just popped the stack frame for the entry function. Terminate the program. + Action::Break + } else { + self.program_counter = return_addr as usize; + self.block_depth = block_depth; + Action::Continue + } + } else { + // We should never get here with real programs but maybe in tests. Terminate the program. + Action::Break + } + } + + fn get_load_address(&mut self, module: &WasmModule<'a>) -> u32 { + // Alignment is not used in the execution steps from the spec! Maybe it's just an optimization hint? + // https://webassembly.github.io/spec/core/exec/instructions.html#memory-instructions + // Also note: in the text format we can specify the useless `align=` but not the useful `offset=`! + let _alignment = self.fetch_immediate_u32(module); + let offset = self.fetch_immediate_u32(module); + let base_addr = self.value_stack.pop_u32(); + base_addr + offset + } + + fn get_store_addr_value(&mut self, module: &WasmModule<'a>) -> (usize, Value) { + // Alignment is not used in the execution steps from the spec! Maybe it's just an optimization hint? + // https://webassembly.github.io/spec/core/exec/instructions.html#memory-instructions + // Also note: in the text format we can specify the useless `align=` but not the useful `offset=`! + let _alignment = self.fetch_immediate_u32(module); + let offset = self.fetch_immediate_u32(module); + let value = self.value_stack.pop(); + let base_addr = self.value_stack.pop_u32(); + let addr = (base_addr + offset) as usize; + (addr, value) + } + + fn write_debug(&mut self, value: T) { + if let Some(debug_string) = self.debug_string.as_mut() { + std::write!(debug_string, "{:?} ", value).unwrap(); + } + } + + pub fn execute_next_instruction(&mut self, module: &WasmModule<'a>) -> Action { use OpCode::*; + let file_offset = self.program_counter as u32 + module.code.section_offset; let op_code = OpCode::from(module.code.bytes[self.program_counter]); self.program_counter += 1; + if let Some(debug_string) = self.debug_string.as_mut() { + debug_string.clear(); + self.write_debug(op_code); + } + + let mut action = Action::Continue; + match op_code { UNREACHABLE => { - unreachable!("WebAssembly tried to execute an `unreachable` instruction."); + unreachable!( + "WebAssembly `unreachable` instruction at file offset {:#x?}.", + file_offset + ); } NOP => {} BLOCK => { - todo!("{:?}", op_code); + self.block_depth += 1; + todo!("{:?} @ {:#x}", op_code, file_offset); } LOOP => { - todo!("{:?}", op_code); - } - IF => { - todo!("{:?}", op_code); - } - ELSE => { - todo!("{:?}", op_code); + self.block_depth += 1; + todo!("{:?} @ {:#x}", op_code, file_offset); } + IF => todo!("{:?} @ {:#x}", op_code, file_offset), + ELSE => todo!("{:?} @ {:#x}", op_code, file_offset), END => { - todo!("{:?}", op_code); - } - BR => { - todo!("{:?}", op_code); - } - BRIF => { - todo!("{:?}", op_code); - } - BRTABLE => { - todo!("{:?}", op_code); + if self.block_depth == 0 { + // implicit RETURN at end of function + action = self.do_return(); + } else { + self.block_depth -= 1; + } } + BR => todo!("{:?} @ {:#x}", op_code, file_offset), + BRIF => todo!("{:?} @ {:#x}", op_code, file_offset), + BRTABLE => todo!("{:?} @ {:#x}", op_code, file_offset), RETURN => { - todo!("{:?}", op_code); + action = self.do_return(); } CALL => { - todo!("{:?}", op_code); - } - CALLINDIRECT => { - todo!("{:?}", op_code); + let index = self.fetch_immediate_u32(module) as usize; + + let signature_index = if index < self.import_signatures.len() { + self.import_signatures[index] + } else { + let internal_fn_index = index - self.import_signatures.len(); + module.function.signatures[internal_fn_index] + }; + let arg_count = module.types.look_up_arg_count(signature_index); + + let return_addr = self.program_counter as u32; + self.program_counter = module.code.function_offsets[index] as usize; + + let return_block_depth = self.block_depth; + self.block_depth = 0; + + let _function_byte_length = + u32::parse((), &module.code.bytes, &mut self.program_counter).unwrap(); + self.call_stack.push_frame( + return_addr, + return_block_depth, + arg_count, + &mut self.value_stack, + &module.code.bytes, + &mut self.program_counter, + ); } + CALLINDIRECT => todo!("{:?} @ {:#x}", op_code, file_offset), DROP => { - todo!("{:?}", op_code); - } - SELECT => { - todo!("{:?}", op_code); + self.value_stack.pop(); } + SELECT => todo!("{:?} @ {:#x}", op_code, file_offset), GETLOCAL => { - todo!("{:?}", op_code); + let index = self.fetch_immediate_u32(module); + let value = self.call_stack.get_local(index); + self.value_stack.push(value); } SETLOCAL => { - todo!("{:?}", op_code); + let index = self.fetch_immediate_u32(module); + let value = self.value_stack.pop(); + self.call_stack.set_local(index, value); } TEELOCAL => { - todo!("{:?}", op_code); + let index = self.fetch_immediate_u32(module); + let value = self.value_stack.peek(); + self.call_stack.set_local(index, value); } GETGLOBAL => { - todo!("{:?}", op_code); + let index = self.fetch_immediate_u32(module); + self.value_stack.push(self.globals[index as usize]); } SETGLOBAL => { - todo!("{:?}", op_code); + let index = self.fetch_immediate_u32(module); + self.globals[index as usize] = self.value_stack.pop(); } I32LOAD => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 4]; + bytes.copy_from_slice(&self.memory[addr..][..4]); + let value = i32::from_le_bytes(bytes); + self.value_stack.push(Value::I32(value)); } I64LOAD => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 8]; + bytes.copy_from_slice(&self.memory[addr..][..8]); + let value = i64::from_le_bytes(bytes); + self.value_stack.push(Value::I64(value)); } F32LOAD => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 4]; + bytes.copy_from_slice(&self.memory[addr..][..4]); + let value = f32::from_le_bytes(bytes); + self.value_stack.push(Value::F32(value)); } F64LOAD => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 8]; + bytes.copy_from_slice(&self.memory[addr..][..8]); + let value = f64::from_le_bytes(bytes); + self.value_stack.push(Value::F64(value)); } I32LOAD8S => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 1]; + bytes.copy_from_slice(&self.memory[addr..][..1]); + let value = i8::from_le_bytes(bytes); + self.value_stack.push(Value::I32(value as i32)); } I32LOAD8U => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let value = self.memory[addr]; + self.value_stack.push(Value::I32(value as i32)); } I32LOAD16S => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 2]; + bytes.copy_from_slice(&self.memory[addr..][..2]); + let value = i16::from_le_bytes(bytes); + self.value_stack.push(Value::I32(value as i32)); } I32LOAD16U => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 2]; + bytes.copy_from_slice(&self.memory[addr..][..2]); + let value = u16::from_le_bytes(bytes); + self.value_stack.push(Value::I32(value as i32)); } I64LOAD8S => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 1]; + bytes.copy_from_slice(&self.memory[addr..][..1]); + let value = i8::from_le_bytes(bytes); + self.value_stack.push(Value::I64(value as i64)); } I64LOAD8U => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let value = self.memory[addr]; + self.value_stack.push(Value::I64(value as i64)); } I64LOAD16S => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 2]; + bytes.copy_from_slice(&self.memory[addr..][..2]); + let value = i16::from_le_bytes(bytes); + self.value_stack.push(Value::I64(value as i64)); } I64LOAD16U => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 2]; + bytes.copy_from_slice(&self.memory[addr..][..2]); + let value = u16::from_le_bytes(bytes); + self.value_stack.push(Value::I64(value as i64)); } I64LOAD32S => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 4]; + bytes.copy_from_slice(&self.memory[addr..][..4]); + let value = i32::from_le_bytes(bytes); + self.value_stack.push(Value::I64(value as i64)); } I64LOAD32U => { - todo!("{:?}", op_code); + let addr = self.get_load_address(module) as usize; + let mut bytes = [0; 4]; + bytes.copy_from_slice(&self.memory[addr..][..4]); + let value = u32::from_le_bytes(bytes); + self.value_stack.push(Value::I64(value as i64)); } I32STORE => { - todo!("{:?}", op_code); + let (addr, value) = self.get_store_addr_value(module); + let unwrapped = value.unwrap_i32(); + let target = &mut self.memory[addr..][..4]; + target.copy_from_slice(&unwrapped.to_le_bytes()); } I64STORE => { - todo!("{:?}", op_code); + let (addr, value) = self.get_store_addr_value(module); + let unwrapped = value.unwrap_i64(); + let target = &mut self.memory[addr..][..8]; + target.copy_from_slice(&unwrapped.to_le_bytes()); } F32STORE => { - todo!("{:?}", op_code); + let (addr, value) = self.get_store_addr_value(module); + let unwrapped = value.unwrap_f32(); + let target = &mut self.memory[addr..][..4]; + target.copy_from_slice(&unwrapped.to_le_bytes()); } F64STORE => { - todo!("{:?}", op_code); + let (addr, value) = self.get_store_addr_value(module); + let unwrapped = value.unwrap_f64(); + let target = &mut self.memory[addr..][..8]; + target.copy_from_slice(&unwrapped.to_le_bytes()); } I32STORE8 => { - todo!("{:?}", op_code); + let (addr, value) = self.get_store_addr_value(module); + let unwrapped = value.unwrap_i32(); + let target = &mut self.memory[addr..][..1]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..1]); } I32STORE16 => { - todo!("{:?}", op_code); + let (addr, value) = self.get_store_addr_value(module); + let unwrapped = value.unwrap_i32(); + let target = &mut self.memory[addr..][..2]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..2]); } I64STORE8 => { - todo!("{:?}", op_code); + let (addr, value) = self.get_store_addr_value(module); + let unwrapped = value.unwrap_i64(); + let target = &mut self.memory[addr..][..1]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..1]); } I64STORE16 => { - todo!("{:?}", op_code); + let (addr, value) = self.get_store_addr_value(module); + let unwrapped = value.unwrap_i64(); + let target = &mut self.memory[addr..][..2]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..2]); } I64STORE32 => { - todo!("{:?}", op_code); + let (addr, value) = self.get_store_addr_value(module); + let unwrapped = value.unwrap_i64(); + let target = &mut self.memory[addr..][..4]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..4]); } CURRENTMEMORY => { - todo!("{:?}", op_code); + let size = self.memory.len() as i32 / MemorySection::PAGE_SIZE as i32; + self.value_stack.push(Value::I32(size)); } GROWMEMORY => { - todo!("{:?}", op_code); + let old_bytes = self.memory.len() as u32; + let old_pages = old_bytes / MemorySection::PAGE_SIZE as u32; + let grow_pages = self.value_stack.pop_u32(); + let grow_bytes = grow_pages * MemorySection::PAGE_SIZE; + let new_bytes = old_bytes + grow_bytes; + + let success = match module.memory.max_bytes().unwrap() { + Some(max_bytes) => new_bytes <= max_bytes, + None => true, + }; + if success { + self.memory + .extend(iter::repeat(0).take(grow_bytes as usize)); + self.value_stack.push(Value::I32(old_pages as i32)); + } else { + self.value_stack.push(Value::I32(-1)); + } } I32CONST => { let value = i32::parse((), &module.code.bytes, &mut self.program_counter).unwrap(); + self.write_debug(value); self.value_stack.push(Value::I32(value)); } I64CONST => { let value = i64::parse((), &module.code.bytes, &mut self.program_counter).unwrap(); + self.write_debug(value); self.value_stack.push(Value::I64(value)); } F32CONST => { let mut bytes = [0; 4]; bytes.copy_from_slice(&module.code.bytes[self.program_counter..][..4]); - self.value_stack.push(Value::F32(f32::from_le_bytes(bytes))); + let value = f32::from_le_bytes(bytes); + self.write_debug(value); + self.value_stack.push(Value::F32(value)); self.program_counter += 4; } F64CONST => { let mut bytes = [0; 8]; bytes.copy_from_slice(&module.code.bytes[self.program_counter..][..8]); - self.value_stack.push(Value::F64(f64::from_le_bytes(bytes))); + let value = f64::from_le_bytes(bytes); + self.write_debug(value); + self.value_stack.push(Value::F64(value)); self.program_counter += 8; } - I32EQZ => { - todo!("{:?}", op_code); - } - I32EQ => { - todo!("{:?}", op_code); - } - I32NE => { - todo!("{:?}", op_code); - } - I32LTS => { - todo!("{:?}", op_code); - } - I32LTU => { - todo!("{:?}", op_code); - } - I32GTS => { - todo!("{:?}", op_code); - } - I32GTU => { - todo!("{:?}", op_code); - } - I32LES => { - todo!("{:?}", op_code); - } - I32LEU => { - todo!("{:?}", op_code); - } - I32GES => { - todo!("{:?}", op_code); - } - I32GEU => { - todo!("{:?}", op_code); - } - I64EQZ => { - todo!("{:?}", op_code); - } - I64EQ => { - todo!("{:?}", op_code); - } - I64NE => { - todo!("{:?}", op_code); - } - I64LTS => { - todo!("{:?}", op_code); - } - I64LTU => { - todo!("{:?}", op_code); - } - I64GTS => { - todo!("{:?}", op_code); - } - I64GTU => { - todo!("{:?}", op_code); - } - I64LES => { - todo!("{:?}", op_code); - } - I64LEU => { - todo!("{:?}", op_code); - } - I64GES => { - todo!("{:?}", op_code); - } - I64GEU => { - todo!("{:?}", op_code); - } + I32EQZ => todo!("{:?} @ {:#x}", op_code, file_offset), + I32EQ => todo!("{:?} @ {:#x}", op_code, file_offset), + I32NE => todo!("{:?} @ {:#x}", op_code, file_offset), + I32LTS => todo!("{:?} @ {:#x}", op_code, file_offset), + I32LTU => todo!("{:?} @ {:#x}", op_code, file_offset), + I32GTS => todo!("{:?} @ {:#x}", op_code, file_offset), + I32GTU => todo!("{:?} @ {:#x}", op_code, file_offset), + I32LES => todo!("{:?} @ {:#x}", op_code, file_offset), + I32LEU => todo!("{:?} @ {:#x}", op_code, file_offset), + I32GES => todo!("{:?} @ {:#x}", op_code, file_offset), + I32GEU => todo!("{:?} @ {:#x}", op_code, file_offset), + I64EQZ => todo!("{:?} @ {:#x}", op_code, file_offset), + I64EQ => todo!("{:?} @ {:#x}", op_code, file_offset), + I64NE => todo!("{:?} @ {:#x}", op_code, file_offset), + I64LTS => todo!("{:?} @ {:#x}", op_code, file_offset), + I64LTU => todo!("{:?} @ {:#x}", op_code, file_offset), + I64GTS => todo!("{:?} @ {:#x}", op_code, file_offset), + I64GTU => todo!("{:?} @ {:#x}", op_code, file_offset), + I64LES => todo!("{:?} @ {:#x}", op_code, file_offset), + I64LEU => todo!("{:?} @ {:#x}", op_code, file_offset), + I64GES => todo!("{:?} @ {:#x}", op_code, file_offset), + I64GEU => todo!("{:?} @ {:#x}", op_code, file_offset), - F32EQ => { - todo!("{:?}", op_code); - } - F32NE => { - todo!("{:?}", op_code); - } - F32LT => { - todo!("{:?}", op_code); - } - F32GT => { - todo!("{:?}", op_code); - } - F32LE => { - todo!("{:?}", op_code); - } - F32GE => { - todo!("{:?}", op_code); - } + F32EQ => todo!("{:?} @ {:#x}", op_code, file_offset), + F32NE => todo!("{:?} @ {:#x}", op_code, file_offset), + F32LT => todo!("{:?} @ {:#x}", op_code, file_offset), + F32GT => todo!("{:?} @ {:#x}", op_code, file_offset), + F32LE => todo!("{:?} @ {:#x}", op_code, file_offset), + F32GE => todo!("{:?} @ {:#x}", op_code, file_offset), - F64EQ => { - todo!("{:?}", op_code); - } - F64NE => { - todo!("{:?}", op_code); - } - F64LT => { - todo!("{:?}", op_code); - } - F64GT => { - todo!("{:?}", op_code); - } - F64LE => { - todo!("{:?}", op_code); - } - F64GE => { - todo!("{:?}", op_code); - } + F64EQ => todo!("{:?} @ {:#x}", op_code, file_offset), + F64NE => todo!("{:?} @ {:#x}", op_code, file_offset), + F64LT => todo!("{:?} @ {:#x}", op_code, file_offset), + F64GT => todo!("{:?} @ {:#x}", op_code, file_offset), + F64LE => todo!("{:?} @ {:#x}", op_code, file_offset), + F64GE => todo!("{:?} @ {:#x}", op_code, file_offset), - I32CLZ => { - todo!("{:?}", op_code); - } - I32CTZ => { - todo!("{:?}", op_code); - } - I32POPCNT => { - todo!("{:?}", op_code); - } + I32CLZ => todo!("{:?} @ {:#x}", op_code, file_offset), + I32CTZ => todo!("{:?} @ {:#x}", op_code, file_offset), + I32POPCNT => todo!("{:?} @ {:#x}", op_code, file_offset), I32ADD => { let x = self.value_stack.pop_i32(); let y = self.value_stack.pop_i32(); @@ -320,258 +528,100 @@ impl<'a> ExecutionState<'a> { let y = self.value_stack.pop_i32(); self.value_stack.push(Value::I32(y * x)); } - I32DIVS => { - todo!("{:?}", op_code); - } - I32DIVU => { - todo!("{:?}", op_code); - } - I32REMS => { - todo!("{:?}", op_code); - } - I32REMU => { - todo!("{:?}", op_code); - } - I32AND => { - todo!("{:?}", op_code); - } - I32OR => { - todo!("{:?}", op_code); - } - I32XOR => { - todo!("{:?}", op_code); - } - I32SHL => { - todo!("{:?}", op_code); - } - I32SHRS => { - todo!("{:?}", op_code); - } - I32SHRU => { - todo!("{:?}", op_code); - } - I32ROTL => { - todo!("{:?}", op_code); - } - I32ROTR => { - todo!("{:?}", op_code); - } + I32DIVS => todo!("{:?} @ {:#x}", op_code, file_offset), + I32DIVU => todo!("{:?} @ {:#x}", op_code, file_offset), + I32REMS => todo!("{:?} @ {:#x}", op_code, file_offset), + I32REMU => todo!("{:?} @ {:#x}", op_code, file_offset), + I32AND => todo!("{:?} @ {:#x}", op_code, file_offset), + I32OR => todo!("{:?} @ {:#x}", op_code, file_offset), + I32XOR => todo!("{:?} @ {:#x}", op_code, file_offset), + I32SHL => todo!("{:?} @ {:#x}", op_code, file_offset), + I32SHRS => todo!("{:?} @ {:#x}", op_code, file_offset), + I32SHRU => todo!("{:?} @ {:#x}", op_code, file_offset), + I32ROTL => todo!("{:?} @ {:#x}", op_code, file_offset), + I32ROTR => todo!("{:?} @ {:#x}", op_code, file_offset), - I64CLZ => { - todo!("{:?}", op_code); - } - I64CTZ => { - todo!("{:?}", op_code); - } - I64POPCNT => { - todo!("{:?}", op_code); - } - I64ADD => { - todo!("{:?}", op_code); - } - I64SUB => { - todo!("{:?}", op_code); - } - I64MUL => { - todo!("{:?}", op_code); - } - I64DIVS => { - todo!("{:?}", op_code); - } - I64DIVU => { - todo!("{:?}", op_code); - } - I64REMS => { - todo!("{:?}", op_code); - } - I64REMU => { - todo!("{:?}", op_code); - } - I64AND => { - todo!("{:?}", op_code); - } - I64OR => { - todo!("{:?}", op_code); - } - I64XOR => { - todo!("{:?}", op_code); - } - I64SHL => { - todo!("{:?}", op_code); - } - I64SHRS => { - todo!("{:?}", op_code); - } - I64SHRU => { - todo!("{:?}", op_code); - } - I64ROTL => { - todo!("{:?}", op_code); - } - I64ROTR => { - todo!("{:?}", op_code); - } - F32ABS => { - todo!("{:?}", op_code); - } - F32NEG => { - todo!("{:?}", op_code); - } - F32CEIL => { - todo!("{:?}", op_code); - } - F32FLOOR => { - todo!("{:?}", op_code); - } - F32TRUNC => { - todo!("{:?}", op_code); - } - F32NEAREST => { - todo!("{:?}", op_code); - } - F32SQRT => { - todo!("{:?}", op_code); - } - F32ADD => { - todo!("{:?}", op_code); - } - F32SUB => { - todo!("{:?}", op_code); - } - F32MUL => { - todo!("{:?}", op_code); - } - F32DIV => { - todo!("{:?}", op_code); - } - F32MIN => { - todo!("{:?}", op_code); - } - F32MAX => { - todo!("{:?}", op_code); - } - F32COPYSIGN => { - todo!("{:?}", op_code); - } - F64ABS => { - todo!("{:?}", op_code); - } - F64NEG => { - todo!("{:?}", op_code); - } - F64CEIL => { - todo!("{:?}", op_code); - } - F64FLOOR => { - todo!("{:?}", op_code); - } - F64TRUNC => { - todo!("{:?}", op_code); - } - F64NEAREST => { - todo!("{:?}", op_code); - } - F64SQRT => { - todo!("{:?}", op_code); - } - F64ADD => { - todo!("{:?}", op_code); - } - F64SUB => { - todo!("{:?}", op_code); - } - F64MUL => { - todo!("{:?}", op_code); - } - F64DIV => { - todo!("{:?}", op_code); - } - F64MIN => { - todo!("{:?}", op_code); - } - F64MAX => { - todo!("{:?}", op_code); - } - F64COPYSIGN => { - todo!("{:?}", op_code); - } + I64CLZ => todo!("{:?} @ {:#x}", op_code, file_offset), + I64CTZ => todo!("{:?} @ {:#x}", op_code, file_offset), + I64POPCNT => todo!("{:?} @ {:#x}", op_code, file_offset), + I64ADD => todo!("{:?} @ {:#x}", op_code, file_offset), + I64SUB => todo!("{:?} @ {:#x}", op_code, file_offset), + I64MUL => todo!("{:?} @ {:#x}", op_code, file_offset), + I64DIVS => todo!("{:?} @ {:#x}", op_code, file_offset), + I64DIVU => todo!("{:?} @ {:#x}", op_code, file_offset), + I64REMS => todo!("{:?} @ {:#x}", op_code, file_offset), + I64REMU => todo!("{:?} @ {:#x}", op_code, file_offset), + I64AND => todo!("{:?} @ {:#x}", op_code, file_offset), + I64OR => todo!("{:?} @ {:#x}", op_code, file_offset), + I64XOR => todo!("{:?} @ {:#x}", op_code, file_offset), + I64SHL => todo!("{:?} @ {:#x}", op_code, file_offset), + I64SHRS => todo!("{:?} @ {:#x}", op_code, file_offset), + I64SHRU => todo!("{:?} @ {:#x}", op_code, file_offset), + I64ROTL => todo!("{:?} @ {:#x}", op_code, file_offset), + I64ROTR => todo!("{:?} @ {:#x}", op_code, file_offset), + F32ABS => todo!("{:?} @ {:#x}", op_code, file_offset), + F32NEG => todo!("{:?} @ {:#x}", op_code, file_offset), + F32CEIL => todo!("{:?} @ {:#x}", op_code, file_offset), + F32FLOOR => todo!("{:?} @ {:#x}", op_code, file_offset), + F32TRUNC => todo!("{:?} @ {:#x}", op_code, file_offset), + F32NEAREST => todo!("{:?} @ {:#x}", op_code, file_offset), + F32SQRT => todo!("{:?} @ {:#x}", op_code, file_offset), + F32ADD => todo!("{:?} @ {:#x}", op_code, file_offset), + F32SUB => todo!("{:?} @ {:#x}", op_code, file_offset), + F32MUL => todo!("{:?} @ {:#x}", op_code, file_offset), + F32DIV => todo!("{:?} @ {:#x}", op_code, file_offset), + F32MIN => todo!("{:?} @ {:#x}", op_code, file_offset), + F32MAX => todo!("{:?} @ {:#x}", op_code, file_offset), + F32COPYSIGN => todo!("{:?} @ {:#x}", op_code, file_offset), + F64ABS => todo!("{:?} @ {:#x}", op_code, file_offset), + F64NEG => todo!("{:?} @ {:#x}", op_code, file_offset), + F64CEIL => todo!("{:?} @ {:#x}", op_code, file_offset), + F64FLOOR => todo!("{:?} @ {:#x}", op_code, file_offset), + F64TRUNC => todo!("{:?} @ {:#x}", op_code, file_offset), + F64NEAREST => todo!("{:?} @ {:#x}", op_code, file_offset), + F64SQRT => todo!("{:?} @ {:#x}", op_code, file_offset), + F64ADD => todo!("{:?} @ {:#x}", op_code, file_offset), + F64SUB => todo!("{:?} @ {:#x}", op_code, file_offset), + F64MUL => todo!("{:?} @ {:#x}", op_code, file_offset), + F64DIV => todo!("{:?} @ {:#x}", op_code, file_offset), + F64MIN => todo!("{:?} @ {:#x}", op_code, file_offset), + F64MAX => todo!("{:?} @ {:#x}", op_code, file_offset), + F64COPYSIGN => todo!("{:?} @ {:#x}", op_code, file_offset), - I32WRAPI64 => { - todo!("{:?}", op_code); - } - I32TRUNCSF32 => { - todo!("{:?}", op_code); - } - I32TRUNCUF32 => { - todo!("{:?}", op_code); - } - I32TRUNCSF64 => { - todo!("{:?}", op_code); - } - I32TRUNCUF64 => { - todo!("{:?}", op_code); - } - I64EXTENDSI32 => { - todo!("{:?}", op_code); - } - I64EXTENDUI32 => { - todo!("{:?}", op_code); - } - I64TRUNCSF32 => { - todo!("{:?}", op_code); - } - I64TRUNCUF32 => { - todo!("{:?}", op_code); - } - I64TRUNCSF64 => { - todo!("{:?}", op_code); - } - I64TRUNCUF64 => { - todo!("{:?}", op_code); - } - F32CONVERTSI32 => { - todo!("{:?}", op_code); - } - F32CONVERTUI32 => { - todo!("{:?}", op_code); - } - F32CONVERTSI64 => { - todo!("{:?}", op_code); - } - F32CONVERTUI64 => { - todo!("{:?}", op_code); - } - F32DEMOTEF64 => { - todo!("{:?}", op_code); - } - F64CONVERTSI32 => { - todo!("{:?}", op_code); - } - F64CONVERTUI32 => { - todo!("{:?}", op_code); - } - F64CONVERTSI64 => { - todo!("{:?}", op_code); - } - F64CONVERTUI64 => { - todo!("{:?}", op_code); - } - F64PROMOTEF32 => { - todo!("{:?}", op_code); - } + I32WRAPI64 => todo!("{:?} @ {:#x}", op_code, file_offset), + I32TRUNCSF32 => todo!("{:?} @ {:#x}", op_code, file_offset), + I32TRUNCUF32 => todo!("{:?} @ {:#x}", op_code, file_offset), + I32TRUNCSF64 => todo!("{:?} @ {:#x}", op_code, file_offset), + I32TRUNCUF64 => todo!("{:?} @ {:#x}", op_code, file_offset), + I64EXTENDSI32 => todo!("{:?} @ {:#x}", op_code, file_offset), + I64EXTENDUI32 => todo!("{:?} @ {:#x}", op_code, file_offset), + I64TRUNCSF32 => todo!("{:?} @ {:#x}", op_code, file_offset), + I64TRUNCUF32 => todo!("{:?} @ {:#x}", op_code, file_offset), + I64TRUNCSF64 => todo!("{:?} @ {:#x}", op_code, file_offset), + I64TRUNCUF64 => todo!("{:?} @ {:#x}", op_code, file_offset), + F32CONVERTSI32 => todo!("{:?} @ {:#x}", op_code, file_offset), + F32CONVERTUI32 => todo!("{:?} @ {:#x}", op_code, file_offset), + F32CONVERTSI64 => todo!("{:?} @ {:#x}", op_code, file_offset), + F32CONVERTUI64 => todo!("{:?} @ {:#x}", op_code, file_offset), + F32DEMOTEF64 => todo!("{:?} @ {:#x}", op_code, file_offset), + F64CONVERTSI32 => todo!("{:?} @ {:#x}", op_code, file_offset), + F64CONVERTUI32 => todo!("{:?} @ {:#x}", op_code, file_offset), + F64CONVERTSI64 => todo!("{:?} @ {:#x}", op_code, file_offset), + F64CONVERTUI64 => todo!("{:?} @ {:#x}", op_code, file_offset), + F64PROMOTEF32 => todo!("{:?} @ {:#x}", op_code, file_offset), - I32REINTERPRETF32 => { - todo!("{:?}", op_code); - } - I64REINTERPRETF64 => { - todo!("{:?}", op_code); - } - F32REINTERPRETI32 => { - todo!("{:?}", op_code); - } - F64REINTERPRETI64 => { - todo!("{:?}", op_code); - } + I32REINTERPRETF32 => todo!("{:?} @ {:#x}", op_code, file_offset), + I64REINTERPRETF64 => todo!("{:?} @ {:#x}", op_code, file_offset), + F32REINTERPRETI32 => todo!("{:?} @ {:#x}", op_code, file_offset), + F64REINTERPRETI64 => todo!("{:?} @ {:#x}", op_code, file_offset), } + + if let Some(debug_string) = &self.debug_string { + let base = self.call_stack.value_stack_base(); + let slice = self.value_stack.get_slice(base as usize); + eprintln!("{:#07x} {:17} {:?}", file_offset, debug_string, slice); + } + + action } } diff --git a/crates/wasm_interp/src/lib.rs b/crates/wasm_interp/src/lib.rs index a11c3d0267..dbd01480ea 100644 --- a/crates/wasm_interp/src/lib.rs +++ b/crates/wasm_interp/src/lib.rs @@ -4,13 +4,5 @@ mod value_stack; // Exposed for testing only. Should eventually become private. pub use call_stack::CallStack; -pub use execute::ExecutionState; +pub use execute::{Action, ExecutionState}; pub use value_stack::ValueStack; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Value { - I32(i32), - I64(i64), - F32(f32), - F64(f64), -} diff --git a/crates/wasm_interp/src/main.rs b/crates/wasm_interp/src/main.rs new file mode 100644 index 0000000000..4717df6d9e --- /dev/null +++ b/crates/wasm_interp/src/main.rs @@ -0,0 +1,110 @@ +use bumpalo::Bump; +use clap::ArgAction; +use clap::{Arg, Command}; +use roc_wasm_interp::Action; +use std::ffi::OsString; +use std::fs; +use std::io; +use std::process; + +use roc_wasm_interp::ExecutionState; +use roc_wasm_module::WasmModule; + +pub const FLAG_FUNCTION: &str = "function"; +pub const FLAG_DEBUG: &str = "debug"; +pub const FLAG_HEX: &str = "hex"; +pub const WASM_FILE: &str = "WASM_FILE"; + +fn main() -> io::Result<()> { + // Define the command line arguments + + let flag_function = Arg::new(FLAG_FUNCTION) + .long(FLAG_FUNCTION) + .help("Call a specific function exported from the WebAssembly module") + .default_value("_start") + .required(false); + + let flag_debug = Arg::new(FLAG_DEBUG) + .long(FLAG_DEBUG) + .help("Print a log of every instruction executed, for debugging purposes.") + .action(ArgAction::SetTrue) + .required(false); + + let flag_hex = Arg::new(FLAG_HEX) + .long(FLAG_HEX) + .help("If the called function returns a value, print it in hexadecimal format.") + .action(ArgAction::SetTrue) + .required(false); + + let wasm_file_to_run = Arg::new(WASM_FILE) + .help("The .wasm file to run") + .allow_invalid_utf8(true) + .required(true); + + let app = Command::new("roc_wasm_interp") + .about("Run the given .wasm file") + .arg(flag_function) + .arg(flag_debug) + .arg(flag_hex) + .arg(wasm_file_to_run); + + // Parse the command line arguments + + let matches = app.get_matches(); + let start_fn_name = matches.get_one::(FLAG_FUNCTION).unwrap(); + let is_debug_mode = matches.get_flag(FLAG_DEBUG); + let is_hex_format = matches.get_flag(FLAG_HEX); + + // Load the WebAssembly binary file + + let wasm_path = matches.get_one::(WASM_FILE).unwrap(); + let module_bytes = fs::read(wasm_path)?; + + // Parse the binary data + + let arena = Bump::new(); + let require_relocatable = false; + let module = match WasmModule::preload(&arena, &module_bytes, require_relocatable) { + Ok(m) => m, + Err(e) => { + eprintln!("I couldn't parse this WebAssembly module! There's something wrong at byte offset {:#x}.", e.offset); + eprintln!("{}", e.message); + eprintln!("If you think this could be a code generation problem in the Roc compiler, see crates/compiler/gen_wasm/README.md for debugging tips."); + process::exit(1); + } + }; + + // Initialise the execution state + + let mut state = ExecutionState::for_module(&arena, &module, start_fn_name, is_debug_mode) + .unwrap_or_else(|e| { + eprintln!("{}", e); + process::exit(2); + }); + + // Run + + while let Action::Continue = state.execute_next_instruction(&module) {} + + // Print out return value(s), if any + + match state.value_stack.len() { + 0 => {} + 1 => { + if is_hex_format { + println!("{:#x?}", state.value_stack.pop()) + } else { + println!("{:?}", state.value_stack.pop()) + } + } + _ => { + if is_hex_format { + println!("{:#x?}", &state.value_stack) + } else { + println!("{:?}", &state.value_stack) + } + } + } + + Ok(()) +} diff --git a/crates/wasm_interp/src/value_stack.rs b/crates/wasm_interp/src/value_stack.rs index 29efb656ff..b5195f5b57 100644 --- a/crates/wasm_interp/src/value_stack.rs +++ b/crates/wasm_interp/src/value_stack.rs @@ -1,10 +1,8 @@ use bitvec::vec::BitVec; use bumpalo::{collections::Vec, Bump}; -use roc_wasm_module::ValueType; +use roc_wasm_module::{Value, ValueType}; use std::{fmt::Debug, mem::size_of}; -use crate::Value; - /// Memory-efficient Struct-of-Arrays storage for the value stack. /// Pack the values and their types as densely as possible, /// to get better cache usage, at the expense of some extra logic. @@ -34,6 +32,14 @@ impl<'a> ValueStack<'a> { } } + pub fn len(&self) -> usize { + self.is_64.len() + } + + pub fn is_empty(&self) -> bool { + self.is_64.is_empty() + } + pub fn push(&mut self, value: Value) { match value { Value::I32(x) => { @@ -69,6 +75,14 @@ impl<'a> ValueStack<'a> { value } + pub fn peek(&self) -> Value { + let is_64 = *self.is_64.last().unwrap(); + let is_float = *self.is_float.last().unwrap(); + let size = if is_64 { 8 } else { 4 }; + let bytes_idx = self.bytes.len() - size; + self.get(is_64, is_float, bytes_idx) + } + fn get(&self, is_64: bool, is_float: bool, bytes_idx: usize) -> Value { if is_64 { let mut b = [0; 8]; @@ -89,6 +103,18 @@ impl<'a> ValueStack<'a> { } } + /// Memory addresses etc + pub fn pop_u32(&mut self) -> u32 { + match (self.is_float.pop(), self.is_64.pop()) { + (Some(false), Some(false)) => pop_bytes!(u32, self.bytes), + (Some(is_float), Some(is_64)) => panic!( + "Expected I32 but found {:?}", + type_from_flags(is_float, is_64) + ), + _ => panic!("Expected I32 but value stack was empty"), + } + } + pub fn pop_i32(&mut self) -> i32 { match (self.is_float.pop(), self.is_64.pop()) { (Some(false), Some(false)) => pop_bytes!(i32, self.bytes), @@ -132,6 +158,36 @@ impl<'a> ValueStack<'a> { _ => panic!("Expected F64 but value stack was empty"), } } + + fn fmt_from_index( + &self, + f: &mut std::fmt::Formatter<'_>, + from_index: usize, + ) -> std::fmt::Result { + write!(f, "[")?; + let mut bytes_index = 0; + assert_eq!(self.is_64.len(), self.is_float.len()); + if from_index < self.is_64.len() { + let iter_64 = self.is_64.iter().by_vals(); + let iter_float = self.is_float.iter().by_vals(); + for (i, (is_64, is_float)) in iter_64.zip(iter_float).enumerate() { + if i < from_index { + continue; + } + let value = self.get(is_64, is_float, bytes_index); + bytes_index += if is_64 { 8 } else { 4 }; + value.fmt(f)?; + if i < self.is_64.len() - 1 { + write!(f, ", ")?; + } + } + } + write!(f, "]") + } + + pub fn get_slice<'b>(&'b self, index: usize) -> ValueStackSlice<'a, 'b> { + ValueStackSlice { stack: self, index } + } } fn type_from_flags(is_float: bool, is_64: bool) -> ValueType { @@ -145,20 +201,18 @@ fn type_from_flags(is_float: bool, is_64: bool) -> ValueType { impl Debug for ValueStack<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[")?; - let mut index = 0; - assert_eq!(self.is_64.len(), self.is_float.len()); - let iter_64 = self.is_64.iter().by_vals(); - let iter_float = self.is_float.iter().by_vals(); - for (i, (is_64, is_float)) in iter_64.zip(iter_float).enumerate() { - let value = self.get(is_64, is_float, index); - index += if is_64 { 8 } else { 4 }; - value.fmt(f)?; - if i < self.is_64.len() - 1 { - write!(f, ", ")?; - } - } - write!(f, "]") + self.fmt_from_index(f, 0) + } +} + +pub struct ValueStackSlice<'a, 'b> { + stack: &'b ValueStack<'a>, + index: usize, +} + +impl Debug for ValueStackSlice<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.stack.fmt_from_index(f, self.index) } } diff --git a/crates/wasm_interp/tests/test_opcodes.rs b/crates/wasm_interp/tests/test_opcodes.rs index 40159d7930..3d83d9320e 100644 --- a/crates/wasm_interp/tests/test_opcodes.rs +++ b/crates/wasm_interp/tests/test_opcodes.rs @@ -1,17 +1,20 @@ #![cfg(test)] -use bumpalo::Bump; -use roc_wasm_interp::{ExecutionState, Value}; -use roc_wasm_module::{opcodes::OpCode, SerialBuffer, WasmModule}; +use bumpalo::{collections::Vec, Bump}; +use roc_wasm_interp::{Action, ExecutionState, ValueStack}; +use roc_wasm_module::{ + opcodes::OpCode, + sections::{DataMode, DataSegment, MemorySection}, + ConstExpr, Export, ExportType, SerialBuffer, Serialize, Signature, Value, ValueType, + WasmModule, +}; -const DEFAULT_MEMORY_PAGES: u32 = 1; -const DEFAULT_PROGRAM_COUNTER: usize = 0; - -// #[test] -// fn test_unreachable() {} - -// #[test] -// fn test_nop() {} +fn default_state(arena: &Bump) -> ExecutionState { + let pages = 1; + let program_counter = 0; + let globals = []; + ExecutionState::new(arena, pages, program_counter, globals) +} // #[test] // fn test_block() {} @@ -37,11 +40,100 @@ const DEFAULT_PROGRAM_COUNTER: usize = 0; // #[test] // fn test_brtable() {} -// #[test] -// fn test_return() {} +#[test] +fn test_call_return_no_args() { + let arena = Bump::new(); + let mut state = default_state(&arena); + let mut module = WasmModule::new(&arena); -// #[test] -// fn test_call() {} + // Function 0 + let func0_offset = module.code.bytes.len() as u32; + module.code.function_offsets.push(func0_offset); + module.add_function_signature(Signature { + param_types: Vec::new_in(&arena), + ret_type: Some(ValueType::I32), + }); + [ + 0, // no locals + OpCode::CALL as u8, + 1, // function 1 + OpCode::END as u8, + ] + .serialize(&mut module.code.bytes); + let func0_first_instruction = func0_offset + 2; // skip function length and locals length + + // Function 1 + let func1_offset = module.code.bytes.len() as u32; + module.code.function_offsets.push(func1_offset); + module.add_function_signature(Signature { + param_types: Vec::new_in(&arena), + ret_type: Some(ValueType::I32), + }); + [ + 0, // no locals + OpCode::I32CONST as u8, + 42, // constant value (<64 so that LEB-128 is just one byte) + OpCode::END as u8, + ] + .serialize(&mut module.code.bytes); + + state.program_counter = func0_first_instruction as usize; + + while let Action::Continue = state.execute_next_instruction(&module) {} + + assert_eq!(state.value_stack.peek(), Value::I32(42)); +} + +#[test] +fn test_call_return_with_args() { + let arena = Bump::new(); + let mut state = default_state(&arena); + let mut module = WasmModule::new(&arena); + + // Function 0: calculate 2+2 + let func0_offset = module.code.bytes.len() as u32; + module.code.function_offsets.push(func0_offset); + module.add_function_signature(Signature { + param_types: bumpalo::vec![in &arena;], + ret_type: Some(ValueType::I32), + }); + [ + 0, // no locals + OpCode::I32CONST as u8, + 2, + OpCode::I32CONST as u8, + 2, + OpCode::CALL as u8, + 1, + OpCode::END as u8, + ] + .serialize(&mut module.code.bytes); + let func0_first_instruction = func0_offset + 2; // skip function length and locals length + + // Function 1: add two numbers + let func1_offset = module.code.bytes.len() as u32; + module.code.function_offsets.push(func1_offset); + module.add_function_signature(Signature { + param_types: bumpalo::vec![in &arena; ValueType::I32, ValueType::I32], + ret_type: Some(ValueType::I32), + }); + [ + 0, // no locals + OpCode::GETLOCAL as u8, + 0, + OpCode::GETLOCAL as u8, + 1, + OpCode::I32ADD as u8, + OpCode::END as u8, + ] + .serialize(&mut module.code.bytes); + + state.program_counter = func0_first_instruction as usize; + + while let Action::Continue = state.execute_next_instruction(&module) {} + + assert_eq!(state.value_stack.peek(), Value::I32(4)); +} // #[test] // fn test_callindirect() {} @@ -52,100 +144,562 @@ const DEFAULT_PROGRAM_COUNTER: usize = 0; // #[test] // fn test_select() {} -// #[test] -// fn test_getlocal() {} +#[test] +fn test_set_get_local() { + let arena = Bump::new(); + let mut state = default_state(&arena); + let mut module = WasmModule::new(&arena); + let mut vs = ValueStack::new(&arena); -// #[test] -// fn test_setlocal() {} + let mut buffer = vec![]; + let mut cursor = 0; + [ + (1u32, ValueType::F32), + (1u32, ValueType::F64), + (1u32, ValueType::I32), + (1u32, ValueType::I64), + ] + .serialize(&mut buffer); + state + .call_stack + .push_frame(0x1234, 0, 0, &mut vs, &buffer, &mut cursor); -// #[test] -// fn test_teelocal() {} + module.code.bytes.push(OpCode::I32CONST as u8); + module.code.bytes.encode_i32(12345); + module.code.bytes.push(OpCode::SETLOCAL as u8); + module.code.bytes.encode_u32(2); -// #[test] -// fn test_getglobal() {} + module.code.bytes.push(OpCode::GETLOCAL as u8); + module.code.bytes.encode_u32(2); -// #[test] -// fn test_setglobal() {} + state.execute_next_instruction(&module); + state.execute_next_instruction(&module); + state.execute_next_instruction(&module); + assert_eq!(state.value_stack.len(), 1); + assert_eq!(state.value_stack.pop(), Value::I32(12345)); +} -// #[test] -// fn test_i32load() {} +#[test] +fn test_tee_get_local() { + let arena = Bump::new(); + let mut state = default_state(&arena); + let mut module = WasmModule::new(&arena); + let mut vs = ValueStack::new(&arena); -// #[test] -// fn test_i64load() {} + let mut buffer = vec![]; + let mut cursor = 0; + [ + (1u32, ValueType::F32), + (1u32, ValueType::F64), + (1u32, ValueType::I32), + (1u32, ValueType::I64), + ] + .serialize(&mut buffer); + state + .call_stack + .push_frame(0x1234, 0, 0, &mut vs, &buffer, &mut cursor); -// #[test] -// fn test_f32load() {} + module.code.bytes.push(OpCode::I32CONST as u8); + module.code.bytes.encode_i32(12345); + module.code.bytes.push(OpCode::TEELOCAL as u8); + module.code.bytes.encode_u32(2); -// #[test] -// fn test_f64load() {} + module.code.bytes.push(OpCode::GETLOCAL as u8); + module.code.bytes.encode_u32(2); -// #[test] -// fn test_i32load8s() {} + state.execute_next_instruction(&module); + state.execute_next_instruction(&module); + state.execute_next_instruction(&module); + assert_eq!(state.value_stack.len(), 2); + assert_eq!(state.value_stack.pop(), Value::I32(12345)); + assert_eq!(state.value_stack.pop(), Value::I32(12345)); +} -// #[test] -// fn test_i32load8u() {} +#[test] +fn test_global() { + let arena = Bump::new(); + let mut state = default_state(&arena); + state + .globals + .extend_from_slice(&[Value::F64(1.11), Value::I32(222), Value::F64(3.33)]); + let mut module = WasmModule::new(&arena); -// #[test] -// fn test_i32load16s() {} + module.code.bytes.push(OpCode::GETGLOBAL as u8); + module.code.bytes.encode_u32(1); + module.code.bytes.push(OpCode::I32CONST as u8); + module.code.bytes.encode_i32(555); + module.code.bytes.push(OpCode::SETGLOBAL as u8); + module.code.bytes.encode_u32(1); + module.code.bytes.push(OpCode::GETGLOBAL as u8); + module.code.bytes.encode_u32(1); -// #[test] -// fn test_i32load16u() {} + state.execute_next_instruction(&module); + state.execute_next_instruction(&module); + state.execute_next_instruction(&module); + state.execute_next_instruction(&module); + assert_eq!(state.value_stack.len(), 2); + assert_eq!(state.value_stack.pop(), Value::I32(555)); + assert_eq!(state.value_stack.pop(), Value::I32(222)); +} -// #[test] -// fn test_i64load8s() {} +fn create_exported_function_no_locals<'a, F>( + module: &mut WasmModule<'a>, + name: &'a str, + signature: Signature<'a>, + write_instructions: F, +) where + F: FnOnce(&mut Vec<'a, u8>), +{ + let internal_fn_index = module.code.function_offsets.len(); + let fn_index = module.import.function_count() + internal_fn_index; + module.export.exports.push(Export { + name, + ty: ExportType::Func, + index: fn_index as u32, + }); + module.add_function_signature(signature); -// #[test] -// fn test_i64load8u() {} + let offset = module.code.bytes.encode_padded_u32(0); + let start = module.code.bytes.len(); + module.code.bytes.push(0); // no locals + write_instructions(&mut module.code.bytes); + let len = module.code.bytes.len() - start; + module.code.bytes.overwrite_padded_u32(offset, len as u32); -// #[test] -// fn test_i64load16s() {} + module.code.function_count += 1; + module.code.function_offsets.push(offset as u32); +} -// #[test] -// fn test_i64load16u() {} +fn test_load(load_op: OpCode, ty: ValueType, data: &[u8], addr: u32, offset: u32) -> Value { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); -// #[test] -// fn test_i64load32s() {} + let is_debug_mode = false; + let start_fn_name = "test"; -// #[test] -// fn test_i64load32u() {} + module.memory = MemorySection::new(&arena, MemorySection::PAGE_SIZE); -// #[test] -// fn test_i32store() {} + module.data.append_segment(DataSegment { + mode: DataMode::Active { + offset: ConstExpr::I32(addr as i32), + }, + init: Vec::from_iter_in(data.iter().copied(), &arena), + }); -// #[test] -// fn test_i64store() {} + let signature = Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ty), + }; -// #[test] -// fn test_f32store() {} + create_exported_function_no_locals(&mut module, start_fn_name, signature, |buf| { + buf.append_u8(OpCode::I32CONST as u8); + buf.encode_u32(addr); + buf.append_u8(load_op as u8); + buf.encode_u32(0); // align + buf.encode_u32(offset); + buf.append_u8(OpCode::END as u8); + }); -// #[test] -// fn test_f64store() {} + if false { + let mut outfile_buf = Vec::new_in(&arena); + module.serialize(&mut outfile_buf); + std::fs::write("/tmp/roc/interp_load_test.wasm", outfile_buf).unwrap(); + } -// #[test] -// fn test_i32store8() {} + let mut state = + ExecutionState::for_module(&arena, &module, start_fn_name, is_debug_mode).unwrap(); -// #[test] -// fn test_i32store16() {} + while let Action::Continue = state.execute_next_instruction(&module) {} -// #[test] -// fn test_i64store8() {} + state.value_stack.pop() +} -// #[test] -// fn test_i64store16() {} +#[test] +fn test_i32load() { + let bytes = "abcdefgh".as_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD, ValueType::I32, bytes, 0x11, 0), + Value::I32(0x64636261) + ); + assert_eq!( + test_load(OpCode::I32LOAD, ValueType::I32, bytes, 0x11, 2), + Value::I32(0x66656463) + ); +} -// #[test] -// fn test_i64store32() {} +#[test] +fn test_i64load() { + let bytes = "abcdefghijkl".as_bytes(); + assert_eq!( + test_load(OpCode::I64LOAD, ValueType::I64, bytes, 0x11, 0), + Value::I64(0x6867666564636261) + ); + assert_eq!( + test_load(OpCode::I64LOAD, ValueType::I64, bytes, 0x11, 2), + Value::I64(0x6a69686766656463) + ); +} -// #[test] -// fn test_currentmemory() {} +#[test] +fn test_f32load() { + let value: f32 = 1.23456; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::F32LOAD, ValueType::F32, &bytes, 0x11, 0), + Value::F32(value) + ); +} -// #[test] -// fn test_growmemory() {} +#[test] +fn test_f64load() { + let value: f64 = 1.23456; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::F64LOAD, ValueType::F64, &bytes, 0x11, 0), + Value::F64(value) + ); +} + +#[test] +fn test_i32load8s() { + let value: i8 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8S, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i32load8u() { + let value: u8 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i32load16s() { + let value: i16 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD16S, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i32load16u() { + let value: u16 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD16U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i64load8s() { + let value: i8 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I64LOAD8S, ValueType::I64, &bytes, 0x11, 0), + Value::I64(value as i64) + ); +} + +#[test] +fn test_i64load8u() { + let value: u8 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i64load16s() { + let value: i16 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I64LOAD8S, ValueType::I64, &bytes, 0x11, 0), + Value::I64(value as i64) + ); +} + +#[test] +fn test_i64load16u() { + let value: u16 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i64load32s() { + let value: i32 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I64LOAD8S, ValueType::I64, &bytes, 0x11, 0), + Value::I64(value as i64) + ); +} + +#[test] +fn test_i64load32u() { + let value: u32 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +fn test_store<'a>( + arena: &'a Bump, + module: &mut WasmModule<'a>, + addr: u32, + store_op: OpCode, + offset: u32, + value: Value, +) -> Vec<'a, u8> { + let is_debug_mode = false; + let start_fn_name = "test"; + + module.memory = MemorySection::new(arena, MemorySection::PAGE_SIZE); + + let signature = Signature { + param_types: bumpalo::vec![in arena], + ret_type: None, + }; + + create_exported_function_no_locals(module, start_fn_name, signature, |buf| { + buf.append_u8(OpCode::I32CONST as u8); + buf.encode_u32(addr); + match value { + Value::I32(x) => { + buf.append_u8(OpCode::I32CONST as u8); + buf.encode_i32(x); + } + Value::I64(x) => { + buf.append_u8(OpCode::I64CONST as u8); + buf.encode_i64(x); + } + Value::F32(x) => { + buf.append_u8(OpCode::F32CONST as u8); + buf.encode_f32(x); + } + Value::F64(x) => { + buf.append_u8(OpCode::F64CONST as u8); + buf.encode_f64(x); + } + } + buf.append_u8(store_op as u8); + buf.encode_u32(0); // align + buf.encode_u32(offset); + buf.append_u8(OpCode::END as u8); + }); + + let mut state = + ExecutionState::for_module(arena, module, start_fn_name, is_debug_mode).unwrap(); + + while let Action::Continue = state.execute_next_instruction(module) {} + + state.memory +} + +#[test] +fn test_i32store() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let addr: u32 = 0x11; + let store_op = OpCode::I32STORE; + let offset = 1; + let value = Value::I32(0x12345678); + let memory = test_store(&arena, &mut module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x34, 0x12]); +} + +#[test] +fn test_i64store() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let addr: u32 = 0x11; + let store_op = OpCode::I64STORE; + let offset = 1; + let value = Value::I64(0x123456789abcdef0); + let memory = test_store(&arena, &mut module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!( + &memory[index..][..8], + &[0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12] + ); +} + +#[test] +fn test_f32store() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let addr: u32 = 0x11; + let store_op = OpCode::F32STORE; + let offset = 1; + let inner: f32 = 1.23456; + let value = Value::F32(inner); + let memory = test_store(&arena, &mut module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..4], &inner.to_le_bytes()); +} + +#[test] +fn test_f64store() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let addr: u32 = 0x11; + let store_op = OpCode::F64STORE; + let offset = 1; + let inner: f64 = 1.23456; + let value = Value::F64(inner); + let memory = test_store(&arena, &mut module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..8], &inner.to_le_bytes()); +} + +#[test] +fn test_i32store8() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let addr: u32 = 0x11; + let store_op = OpCode::I32STORE8; + let offset = 1; + let value = Value::I32(0x12345678); + let memory = test_store(&arena, &mut module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..4], &[0x78, 0x00, 0x00, 0x00]); +} + +#[test] +fn test_i32store16() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let addr: u32 = 0x11; + let store_op = OpCode::I32STORE16; + let offset = 1; + let value = Value::I32(0x12345678); + let memory = test_store(&arena, &mut module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x00, 0x00]); +} + +#[test] +fn test_i64store8() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let addr: u32 = 0x11; + let store_op = OpCode::I64STORE8; + let offset = 1; + let value = Value::I64(0x123456789abcdef0); + let memory = test_store(&arena, &mut module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!( + &memory[index..][..8], + &[0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ); +} + +#[test] +fn test_i64store16() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let addr: u32 = 0x11; + let store_op = OpCode::I64STORE16; + let offset = 1; + let value = Value::I64(0x123456789abcdef0); + let memory = test_store(&arena, &mut module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!( + &memory[index..][..8], + &[0xf0, 0xde, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ); +} + +#[test] +fn test_i64store32() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let addr: u32 = 0x11; + let store_op = OpCode::I64STORE32; + let offset = 1; + let value = Value::I64(0x123456789abcdef0); + let memory = test_store(&arena, &mut module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!( + &memory[index..][..8], + &[0xf0, 0xde, 0xbc, 0x9a, 0x00, 0x00, 0x00, 0x00] + ); +} + +#[test] +fn test_currentmemory() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let pages = 3; + let pc = 0; + module.memory = MemorySection::new(&arena, pages * MemorySection::PAGE_SIZE); + module.code.bytes.push(OpCode::CURRENTMEMORY as u8); + + let mut state = ExecutionState::new(&arena, pages, pc, []); + state.execute_next_instruction(&module); + assert_eq!(state.value_stack.pop(), Value::I32(3)) +} + +#[test] +fn test_growmemory() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let existing_pages = 3; + let grow_pages = 2; + let pc = 0; + module.memory = MemorySection::new(&arena, existing_pages * MemorySection::PAGE_SIZE); + module.code.bytes.push(OpCode::I32CONST as u8); + module.code.bytes.encode_i32(grow_pages); + module.code.bytes.push(OpCode::GROWMEMORY as u8); + + let mut state = ExecutionState::new(&arena, existing_pages, pc, []); + state.execute_next_instruction(&module); + state.execute_next_instruction(&module); + assert_eq!(state.memory.len(), 5 * MemorySection::PAGE_SIZE as usize); +} #[test] fn test_i32const() { let arena = Bump::new(); - let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER); + let mut state = default_state(&arena); let mut module = WasmModule::new(&arena); module.code.bytes.push(OpCode::I32CONST as u8); @@ -158,7 +712,7 @@ fn test_i32const() { #[test] fn test_i64const() { let arena = Bump::new(); - let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER); + let mut state = default_state(&arena); let mut module = WasmModule::new(&arena); module.code.bytes.push(OpCode::I64CONST as u8); @@ -171,7 +725,7 @@ fn test_i64const() { #[test] fn test_f32const() { let arena = Bump::new(); - let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER); + let mut state = default_state(&arena); let mut module = WasmModule::new(&arena); module.code.bytes.push(OpCode::F32CONST as u8); @@ -184,7 +738,7 @@ fn test_f32const() { #[test] fn test_f64const() { let arena = Bump::new(); - let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER); + let mut state = default_state(&arena); let mut module = WasmModule::new(&arena); module.code.bytes.push(OpCode::F64CONST as u8); @@ -308,7 +862,7 @@ fn test_f64const() { #[test] fn test_i32add() { let arena = Bump::new(); - let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER); + let mut state = default_state(&arena); let mut module = WasmModule::new(&arena); module.code.bytes.push(OpCode::I32CONST as u8); @@ -326,7 +880,7 @@ fn test_i32add() { #[test] fn test_i32sub() { let arena = Bump::new(); - let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER); + let mut state = default_state(&arena); let mut module = WasmModule::new(&arena); module.code.bytes.push(OpCode::I32CONST as u8); @@ -344,7 +898,7 @@ fn test_i32sub() { #[test] fn test_i32mul() { let arena = Bump::new(); - let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER); + let mut state = default_state(&arena); let mut module = WasmModule::new(&arena); module.code.bytes.push(OpCode::I32CONST as u8); diff --git a/crates/wasm_module/src/lib.rs b/crates/wasm_module/src/lib.rs index 85e730bc6a..8b55542ba1 100644 --- a/crates/wasm_module/src/lib.rs +++ b/crates/wasm_module/src/lib.rs @@ -115,7 +115,11 @@ impl<'a> WasmModule<'a> { + self.names.size() } - pub fn preload(arena: &'a Bump, bytes: &[u8]) -> Result { + pub fn preload( + arena: &'a Bump, + bytes: &[u8], + require_relocatable: bool, + ) -> Result { let is_valid_magic_number = &bytes[0..4] == "\0asm".as_bytes(); let is_valid_version = bytes[4..8] == Self::WASM_VERSION.to_le_bytes(); if !is_valid_magic_number || !is_valid_version { @@ -155,27 +159,36 @@ impl<'a> WasmModule<'a> { if code.bytes.is_empty() { module_errors.push_str("Missing Code section\n"); } - if linking.symbol_table.is_empty() { - module_errors.push_str("Missing \"linking\" Custom section\n"); - } - if reloc_code.entries.is_empty() { - module_errors.push_str("Missing \"reloc.CODE\" Custom section\n"); - } - if global.count != 0 { - let global_err_msg = + + if require_relocatable { + if linking.symbol_table.is_empty() { + module_errors.push_str("Missing \"linking\" Custom section\n"); + } + if reloc_code.entries.is_empty() { + module_errors.push_str("Missing \"reloc.CODE\" Custom section\n"); + } + if global.count != 0 { + let global_err_msg = format!("All globals in a relocatable Wasm module should be imported, but found {} internally defined", global.count); - module_errors.push_str(&global_err_msg); + module_errors.push_str(&global_err_msg); + } } if !module_errors.is_empty() { - return Err(ParseError { - offset: 0, - message: format!("{}\n{}\n{}", + let message = if require_relocatable { + format!( + "{}\n{}\n{}", "The host file has the wrong structure. I need a relocatable WebAssembly binary file.", "If you're using wasm-ld, try the --relocatable option.", module_errors, ) - }); + } else { + format!( + "I wasn't able to understand this WebAssembly file.\n{}", + module_errors, + ) + }; + return Err(ParseError { offset: 0, message }); } Ok(WasmModule { @@ -642,6 +655,57 @@ impl From for ValueType { } } +impl Parse<()> for ValueType { + fn parse(_: (), bytes: &[u8], cursor: &mut usize) -> Result { + let byte = u8::parse((), bytes, cursor)?; + Ok(ValueType::from(byte)) + } +} + +// A group of local variable declarations +impl Parse<()> for (u32, ValueType) { + fn parse(_: (), bytes: &[u8], cursor: &mut usize) -> Result { + let count = u32::parse((), bytes, cursor)?; + let ty = ValueType::parse((), bytes, cursor)?; + Ok((count, ty)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Value { + I32(i32), + I64(i64), + F32(f32), + F64(f64), +} + +impl Value { + pub fn unwrap_i32(&self) -> i32 { + match self { + Value::I32(x) => *x, + _ => panic!("Expected I32 but found {:?}", self), + } + } + pub fn unwrap_i64(&self) -> i64 { + match self { + Value::I64(x) => *x, + _ => panic!("Expected I64 but found {:?}", self), + } + } + pub fn unwrap_f32(&self) -> f32 { + match self { + Value::F32(x) => *x, + _ => panic!("Expected F32 but found {:?}", self), + } + } + pub fn unwrap_f64(&self) -> f64 { + match self { + Value::F64(x) => *x, + _ => panic!("Expected F64 but found {:?}", self), + } + } +} + /// Wasm memory alignment for load/store instructions. /// Rust representation matches Wasm encoding. /// It's an error to specify alignment higher than the "natural" alignment of the instruction diff --git a/crates/wasm_module/src/linking.rs b/crates/wasm_module/src/linking.rs index 851e1026dd..8be2fac257 100644 --- a/crates/wasm_module/src/linking.rs +++ b/crates/wasm_module/src/linking.rs @@ -214,7 +214,7 @@ impl<'a> Parse> for RelocationSection<'a> { fn parse(ctx: RelocCtx<'a>, bytes: &[u8], cursor: &mut usize) -> Result { let (arena, name) = ctx; - if *cursor > bytes.len() || bytes[*cursor] != SectionId::Custom as u8 { + if *cursor >= bytes.len() || bytes[*cursor] != SectionId::Custom as u8 { // The section we're looking for is missing, which is the same as being empty. return Ok(RelocationSection::new(arena, name)); } @@ -626,7 +626,7 @@ impl<'a> LinkingSection<'a> { impl<'a> Parse<&'a Bump> for LinkingSection<'a> { fn parse(arena: &'a Bump, bytes: &[u8], cursor: &mut usize) -> Result { - if *cursor > bytes.len() || bytes[*cursor] != SectionId::Custom as u8 { + if *cursor >= bytes.len() || bytes[*cursor] != SectionId::Custom as u8 { return Ok(LinkingSection::new(arena)); } *cursor += 1; diff --git a/crates/wasm_module/src/opcodes.rs b/crates/wasm_module/src/opcodes.rs index 7cddd6a7c7..47a21367af 100644 --- a/crates/wasm_module/src/opcodes.rs +++ b/crates/wasm_module/src/opcodes.rs @@ -1,3 +1,5 @@ +use crate::Serialize; + use super::parse::{Parse, ParseError, SkipBytes}; #[repr(u8)] @@ -495,3 +497,9 @@ impl SkipBytes for OpCode { Ok(()) } } + +impl Serialize for OpCode { + fn serialize(&self, buffer: &mut T) { + (*self as u8).serialize(buffer) + } +} diff --git a/crates/wasm_module/src/sections.rs b/crates/wasm_module/src/sections.rs index ef1249205c..3d587f6651 100644 --- a/crates/wasm_module/src/sections.rs +++ b/crates/wasm_module/src/sections.rs @@ -1,10 +1,11 @@ use std::fmt::{Debug, Formatter}; +use std::io::Write; use bumpalo::collections::vec::Vec; use bumpalo::Bump; use roc_error_macros::internal_error; -use crate::DUMMY_FUNCTION; +use crate::{Value, DUMMY_FUNCTION}; use super::linking::{LinkingSection, SymInfo, WasmObjectSymbol}; use super::opcodes::OpCode; @@ -256,6 +257,12 @@ impl<'a> TypeSection<'a> { pub fn is_empty(&self) -> bool { self.bytes.is_empty() } + + pub fn look_up_arg_count(&self, sig_index: u32) -> u32 { + let mut offset = self.offsets[sig_index as usize]; + offset += 1; // separator + u32::parse((), &self.bytes, &mut offset).unwrap() + } } impl<'a> Section<'a> for TypeSection<'a> { @@ -733,6 +740,9 @@ impl SkipBytes for Limits { impl Parse<()> for Limits { fn parse(_: (), bytes: &[u8], cursor: &mut usize) -> Result { + if *cursor >= bytes.len() { + return Ok(Limits::Min(0)); + } let variant_id = bytes[*cursor]; *cursor += 1; @@ -771,6 +781,25 @@ impl<'a> MemorySection<'a> { MemorySection { count: 1, bytes } } } + + pub fn min_bytes(&self) -> Result { + let mut cursor = 0; + let memory_limits = Limits::parse((), &self.bytes, &mut cursor)?; + let min_pages = match memory_limits { + Limits::Min(pages) | Limits::MinMax(pages, _) => pages, + }; + Ok(min_pages * MemorySection::PAGE_SIZE) + } + + pub fn max_bytes(&self) -> Result, ParseError> { + let mut cursor = 0; + let memory_limits = Limits::parse((), &self.bytes, &mut cursor)?; + let bytes = match memory_limits { + Limits::Min(_) => None, + Limits::MinMax(_, pages) => Some(pages * MemorySection::PAGE_SIZE), + }; + Ok(bytes) + } } section_impl!(MemorySection, SectionId::Memory); @@ -852,6 +881,59 @@ impl ConstExpr { _ => internal_error!("Expected ConstExpr to be I32"), } } + + // ConstExpr and Value are separate types in case we ever need to support + // arbitrary constant expressions, rather than just i32.const and friends. + fn as_value(&self) -> Value { + match self { + ConstExpr::I32(x) => Value::I32(*x), + ConstExpr::I64(x) => Value::I64(*x), + ConstExpr::F32(x) => Value::F32(*x), + ConstExpr::F64(x) => Value::F64(*x), + } + } +} + +impl Parse<()> for ConstExpr { + fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result { + let opcode = OpCode::from(bytes[*cursor]); + *cursor += 1; + + let result = match opcode { + OpCode::I32CONST => { + let x = i32::parse((), bytes, cursor)?; + Ok(ConstExpr::I32(x)) + } + OpCode::I64CONST => { + let x = i64::parse((), bytes, cursor)?; + Ok(ConstExpr::I64(x)) + } + OpCode::F32CONST => { + let mut b = [0; 4]; + b.copy_from_slice(&bytes[*cursor..][..4]); + Ok(ConstExpr::F32(f32::from_le_bytes(b))) + } + OpCode::F64CONST => { + let mut b = [0; 8]; + b.copy_from_slice(&bytes[*cursor..][..8]); + Ok(ConstExpr::F64(f64::from_le_bytes(b))) + } + _ => Err(ParseError { + offset: *cursor, + message: format!("Unsupported opcode {:?} in constant expression.", opcode), + }), + }; + + if bytes[*cursor] != OpCode::END as u8 { + return Err(ParseError { + offset: *cursor, + message: "Expected END opcode in constant expression.".into(), + }); + } + *cursor += 1; + + result + } } impl Serialize for ConstExpr { @@ -931,6 +1013,17 @@ impl<'a> GlobalSection<'a> { global.serialize(&mut self.bytes); self.count += 1; } + + pub fn initial_values<'b>(&self, arena: &'b Bump) -> Vec<'b, Value> { + let mut cursor = 0; + let iter = (0..self.count) + .map(|_| { + GlobalType::skip_bytes(&self.bytes, &mut cursor)?; + ConstExpr::parse((), &self.bytes, &mut cursor).map(|x| x.as_value()) + }) + .filter_map(|r| r.ok()); + Vec::from_iter_in(iter, arena) + } } section_impl!(GlobalSection, SectionId::Global); @@ -1220,8 +1313,9 @@ impl<'a> Serialize for ElementSection<'a> { #[derive(Debug)] pub struct CodeSection<'a> { pub function_count: u32, + pub section_offset: u32, pub bytes: Vec<'a, u8>, - /// The start of each preloaded function + /// The start of each function pub function_offsets: Vec<'a, u32>, /// Dead imports are replaced with dummy functions in CodeSection pub dead_import_dummy_count: u32, @@ -1231,6 +1325,7 @@ impl<'a> CodeSection<'a> { pub fn new(arena: &'a Bump) -> Self { CodeSection { function_count: 0, + section_offset: 0, bytes: Vec::new_in(arena), function_offsets: Vec::new_in(arena), dead_import_dummy_count: 0, @@ -1278,6 +1373,7 @@ impl<'a> CodeSection<'a> { Ok(CodeSection { function_count, + section_offset: section_body_start as u32, bytes, function_offsets, dead_import_dummy_count: 0, @@ -1405,10 +1501,40 @@ impl<'a> DataSection<'a> { segment.serialize(&mut self.bytes); index } + + pub fn load_into(&self, memory: &mut [u8]) -> Result<(), String> { + let mut cursor = 0; + for _ in 0..self.count { + let mode = + DataMode::parse((), &self.bytes, &mut cursor).map_err(|e| format!("{:?}", e))?; + let start = match mode { + DataMode::Active { + offset: ConstExpr::I32(addr), + } => addr as usize, + _ => { + continue; + } + }; + let len32 = u32::parse((), &self.bytes, &mut cursor).map_err(|e| format!("{:?}", e))?; + let len = len32 as usize; + let mut target_slice = &mut memory[start..][..len]; + target_slice + .write(&self.bytes[cursor..][..len]) + .map_err(|e| format!("{:?}", e))?; + } + Ok(()) + } } impl<'a> Parse<&'a Bump> for DataSection<'a> { fn parse(arena: &'a Bump, module_bytes: &[u8], cursor: &mut usize) -> Result { + if *cursor >= module_bytes.len() { + return Ok(DataSection { + end_addr: 0, + count: 0, + bytes: Vec::::new_in(arena), + }); + } let (count, range) = parse_section(Self::ID, module_bytes, cursor)?; let end = range.end; diff --git a/design/editor/design.md b/design/editor/design.md index 1f3d2b19e0..075dc9de80 100644 --- a/design/editor/design.md +++ b/design/editor/design.md @@ -1 +1,24 @@ -Should the editor organize all UI into a tree for easy altering/communication with plugins? \ No newline at end of file + +# Open Questions + +Should the editor organize all UI into a tree for easy altering/communication with plugins? + +# Why make a new editor? + +- Complete freedom and control to create a delightful roc editing, debugging, and execution monitoring experience. Everything can be optimized for this, there is no need to accommodate other use cases. +- So that plugins can be developed that can ship with roc packages. This allows all plugins to look and behave the same on all operating systems. + - Why plugins: + - To make it easier and more enjoyable to achieve your goal with the package. + - To provide transparency for how the package works. + - Opportunity to innovate on user experience with minimal friction install. +- Not limited by the language server protocol (LSP). +- To create a foundation that allows for easy experimentation. + +# Why make a projectional editor + + +- It requires a lot less work to communicate with the compiler because we have a valid AST at all time. +- Similarly, we never have to deal with partial expressions that have not been fully typed out yet. +- The user never has to fiddle with formatting. +- It allows plugins to work with typed values instead of: a string that is connected with a typed value and where any changes to the typed value would have to produce a string that is sensibly formatted similar the formatting of the original string. + diff --git a/design/editor/goals.md b/design/editor/goals.md new file mode 100644 index 0000000000..7a698f7b39 --- /dev/null +++ b/design/editor/goals.md @@ -0,0 +1,28 @@ +# Who is the editor for? + +Roc developers; beginners, experts, and everything in between. + +# What will the editor do? + +- Edit roc code. +- Debug roc programs. +- Allow everyone to make a plugin. Plugins can be standalone or come bundled with a package. +- Search and view documentation. +- Run tests with a nice UI. +- Make googling and stackoverflow redundant. You should be able to find the answer to your question with the editor. +- Accommodate those with disabilities. +- Provide traditional LSP functionality (without the actual language server); autocomplete, go to def, find references, rename, show type... + +# General goals + +- Follow UX best practices. Make the user feel comfortable, don't overload them with information or functionality. Offer a gentle learning curve. The editor should be usable without a tutorial. +- Maximal simplicity, strive for easy maintenance. +- Be respectful and transparent towards user privacy. Plugins should have permissions in accordance with this requirement. +- Editor code should be well-documented. Vital components should have live visualization to make it easy to understand and debug the editor. + +# Non-goals + +- Be a general-purpose editor for other programming languages. +- Take safe, familiar choices. +- Connect with LSP. +- Sell user data. diff --git a/design/editor/requirements.md b/design/editor/requirements.md new file mode 100644 index 0000000000..2d58c79fe8 --- /dev/null +++ b/design/editor/requirements.md @@ -0,0 +1,45 @@ +# The Editor Must +- Edit roc code +- Run plugins asynchronously. Plugins can be standalone or come with roc packages. + +# The Editor Should +- Support beginner-friendly projectional editing. +- Support typed holes. +- Support temporarily switching to free/legacy editing for operations that are difficult in projectional mode. +- Allow everyone to write, publish and test plugins. +- Disregard optional whitespace; there should be only one way to format roc. +- Provide useful debug UI and relevant data about the state of values. +- Allow running tests and viewing test results. +- Search and view documentation. +- Apply and render basic editing operations in 8.3 ms. +- Be the most popular roc editor. +- Be bundled with the roc CLI/compiler. +- Help find answers to all questions you would normally google. +- Accommodate those with disabilities better than other popular editors. +- Provide key familiar features: autocomplete, go to def, find references, rename, show type... +- Be able to execute core functionality with keyboard, with the exception of plugin functionality. +- Allow for output and errors that are nicer to read and search through compared to terminal output. +- Have an integrated REPL. +- Allow measuring performance of running roc apps. +- Show which code is covered by tests. +- Support several chart types, ability to transform data coming from logs, output, and intermediary values (from roc program execution). +- Support easily generating tests for a function. +- Indicate progress for slower operations. +- Provide assistance when upgrading roc/libs to new versions. +- Make it easy for the user to provide feedback/report problems. +- Provide tutorials for people with different amounts of programming experience. +- Provide useful cheat sheets. +- Accommodate those new to functional programming. +- Allow viewing editor, library, platform… release notes. +- Support roc notebooks similar to Jupyter. +- Support publishing and searching notebooks. +- Encourage making your code transparent and having a meaningful visual overview. + +# The Editor Might +- Warn the user for code patterns that can be written to run significantly faster. +- Support publishing of roc libraries from the editor. +- Support conversion for snippets of code from different languages to roc. +- Support running in the browser. +- Support code execution on a remote machine while being able to view UI locally. +- Allow detailed logging so you can see everything you were doing(including plugin actions) when you were for example editing a specific file 3 months ago. +- Support entering a parseable formatted piece of roc code character by character so that the rendered result in the editor looks exactly like the input. This could make working with a projectional editor more intuitive. diff --git a/examples/cli/.gitignore b/examples/cli/.gitignore index 8e260e400a..7df3b84374 100644 --- a/examples/cli/.gitignore +++ b/examples/cli/.gitignore @@ -7,3 +7,4 @@ tui http-get file-io env +out.txt \ No newline at end of file diff --git a/examples/cli/args.roc b/examples/cli/args.roc index 50f4ca08a3..9f9215840f 100644 --- a/examples/cli/args.roc +++ b/examples/cli/args.roc @@ -1,10 +1,11 @@ app "args" packages { pf: "cli-platform/main.roc" } - imports [pf.Stdout, pf.Arg, pf.Program.{ Program }] + imports [pf.Stdout, pf.Arg, pf.Task.{ Task }, pf.Process] provides [main] to pf -main : Program -main = Program.withArgs \args -> +main : Task {} [] +main = + args <- Arg.list |> Task.await parser = divCmd = Arg.succeed (\dividend -> \divisor -> Div (Num.toF64 dividend) (Num.toF64 divisor)) @@ -53,11 +54,10 @@ main = Program.withArgs \args -> runCmd cmd |> Num.toStr |> Stdout.line - |> Program.exit 0 Err helpMenu -> - Stdout.line helpMenu - |> Program.exit 1 + {} <- Stdout.line helpMenu |> Task.await + Process.exit 1 runCmd = \cmd -> when cmd is diff --git a/examples/cli/cli-platform/Arg.roc b/examples/cli/cli-platform/Arg.roc index 352f88ad85..34398bf315 100644 --- a/examples/cli/cli-platform/Arg.roc +++ b/examples/cli/cli-platform/Arg.roc @@ -14,8 +14,16 @@ interface Arg choice, withParser, program, + list, ] - imports [] + imports [Effect, InternalTask, Task.{ Task }] + +## Gives a list of the program's command-line arguments. +list : Task (List Str) * +list = + Effect.args + |> Effect.map Ok + |> InternalTask.fromEffect ## A parser for a command-line application. ## A [NamedParser] is usually built from a [Parser] using [program]. @@ -695,7 +703,7 @@ formatError = \err -> |> Str.joinWith ", " """ - The \(fmtFound) subcommand was found, but it's not expected in this context! + The \(fmtFound) subcommand was found, but it's not expected in this context! The available subcommands are: \t\(fmtChoices) """ @@ -1025,7 +1033,7 @@ expect err == """ - The "logs" subcommand was found, but it's not expected in this context! + The "logs" subcommand was found, but it's not expected in this context! The available subcommands are: \t"auth", "publish" """ diff --git a/examples/cli/cli-platform/Dir.roc b/examples/cli/cli-platform/Dir.roc index 73741ef035..a58d091ad8 100644 --- a/examples/cli/cli-platform/Dir.roc +++ b/examples/cli/cli-platform/Dir.roc @@ -9,7 +9,7 @@ DeleteErr : InternalDir.DeleteErr DirEntry : InternalDir.DirEntry ## Lists the files and directories inside the directory. -list : Path -> Task (List Path) [DirReadErr Path ReadErr] [Read [File]] +list : Path -> Task (List Path) [DirReadErr Path ReadErr] list = \path -> effect = Effect.map (Effect.dirList (InternalPath.toBytes path)) \result -> when result is @@ -19,7 +19,7 @@ list = \path -> InternalTask.fromEffect effect ## Deletes a directory if it's empty. -deleteEmptyDir : Path -> Task {} [DirDeleteErr Path DeleteErr] [Write [File]] +deleteEmptyDir : Path -> Task {} [DirDeleteErr Path DeleteErr] ## Recursively deletes the directory as well as all files and directories inside it. -deleteRecursive : Path -> Task {} [DirDeleteErr Path DeleteErr] [Write [File]] +deleteRecursive : Path -> Task {} [DirDeleteErr Path DeleteErr] diff --git a/examples/cli/cli-platform/Effect.roc b/examples/cli/cli-platform/Effect.roc index 9d20093613..20d88e59c7 100644 --- a/examples/cli/cli-platform/Effect.roc +++ b/examples/cli/cli-platform/Effect.roc @@ -23,6 +23,7 @@ hosted Effect fileDelete, fileWriteUtf8, fileWriteBytes, + processExit, ] imports [InternalHttp.{ Request, Response }, InternalFile, InternalDir] generates Effect with [after, map, always, forever, loop] @@ -43,6 +44,8 @@ envVar : Str -> Effect (Result Str {}) exePath : Effect (Result (List U8) {}) setCwd : List U8 -> Effect (Result {} {}) +processExit : U8 -> Effect {} + # If we encounter a Unicode error in any of the args, it will be replaced with # the Unicode replacement char where necessary. args : Effect (List Str) diff --git a/examples/cli/cli-platform/Env.roc b/examples/cli/cli-platform/Env.roc index c5dd393083..2826d34941 100644 --- a/examples/cli/cli-platform/Env.roc +++ b/examples/cli/cli-platform/Env.roc @@ -4,7 +4,7 @@ interface Env ## Reads the [current working directory](https://en.wikipedia.org/wiki/Working_directory) ## from the environment. File operations on relative [Path]s are relative to this directory. -cwd : Task Path [CwdUnavailable] [Read [Env]] +cwd : Task Path [CwdUnavailable] cwd = effect = Effect.map Effect.cwd \bytes -> if List.isEmpty bytes then @@ -17,14 +17,14 @@ cwd = ## Sets the [current working directory](https://en.wikipedia.org/wiki/Working_directory) ## in the environment. After changing it, file operations on relative [Path]s will be relative ## to this directory. -setCwd : Path -> Task {} [InvalidCwd] [Write [Env]] +setCwd : Path -> Task {} [InvalidCwd] setCwd = \path -> Effect.setCwd (InternalPath.toBytes path) |> Effect.map (\result -> Result.mapErr result \{} -> InvalidCwd) |> InternalTask.fromEffect ## Gets the path to the currently-running executable. -exePath : Task Path [ExePathUnavailable] [Read [Env]] +exePath : Task Path [ExePathUnavailable] exePath = effect = Effect.map Effect.exePath \result -> @@ -39,7 +39,7 @@ exePath = ## If the value is invalid Unicode, the invalid parts will be replaced with the ## [Unicode replacement character](https://unicode.org/glossary/#replacement_character) ## (`�`). -var : Str -> Task Str [VarNotFound] [Read [Env]] +var : Str -> Task Str [VarNotFound] var = \name -> Effect.envVar name |> Effect.map (\result -> Result.mapErr result \{} -> VarNotFound) @@ -65,7 +65,7 @@ var = \name -> ## - comma-separated lists (of either strings or numbers), as long as there are no spaces after the commas ## ## Trying to decode into any other types will always fail with a `DecodeErr`. -decode : Str -> Task val [VarNotFound, DecodeErr DecodeError] [Read [Env]] | val has Decoding +decode : Str -> Task val [VarNotFound, DecodeErr DecodeError] | val has Decoding decode = \name -> Effect.envVar name |> Effect.map @@ -83,7 +83,7 @@ decode = \name -> ## ## If any key or value contains invalid Unicode, the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) ## (`�`) will be used in place of any parts of keys or values that are invalid Unicode. -dict : Task (Dict Str Str) * [Read [Env]] +dict : Task (Dict Str Str) * dict = Effect.envDict |> Effect.map Ok diff --git a/examples/cli/cli-platform/File.roc b/examples/cli/cli-platform/File.roc index ee9136e34b..708fe1b7a6 100644 --- a/examples/cli/cli-platform/File.roc +++ b/examples/cli/cli-platform/File.roc @@ -26,7 +26,7 @@ WriteErr : InternalFile.WriteErr ## This opens the file first and closes it after writing to it. ## ## To write unformatted bytes to a file, you can use [File.writeBytes] instead. -write : Path, val, fmt -> Task {} [FileWriteErr Path WriteErr] [Write [File]] | val has Encode.Encoding, fmt has Encode.EncoderFormatting +write : Path, val, fmt -> Task {} [FileWriteErr Path WriteErr] | val has Encode.Encoding, fmt has Encode.EncoderFormatting write = \path, val, fmt -> bytes = Encode.toBytes val fmt @@ -41,7 +41,7 @@ write = \path, val, fmt -> ## This opens the file first and closes it after writing to it. ## ## To format data before writing it to a file, you can use [File.write] instead. -writeBytes : Path, List U8 -> Task {} [FileWriteErr Path WriteErr] [Write [File]] +writeBytes : Path, List U8 -> Task {} [FileWriteErr Path WriteErr] writeBytes = \path, bytes -> toWriteTask path \pathBytes -> Effect.fileWriteBytes pathBytes bytes @@ -53,7 +53,7 @@ writeBytes = \path, bytes -> ## This opens the file first and closes it after writing to it. ## ## To write unformatted bytes to a file, you can use [File.writeBytes] instead. -writeUtf8 : Path, Str -> Task {} [FileWriteErr Path WriteErr] [Write [File]] +writeUtf8 : Path, Str -> Task {} [FileWriteErr Path WriteErr] writeUtf8 = \path, str -> toWriteTask path \bytes -> Effect.fileWriteUtf8 bytes str @@ -73,7 +73,7 @@ writeUtf8 = \path, str -> ## ## On Windows, this will fail when attempting to delete a readonly file; the file's ## readonly permission must be disabled before it can be successfully deleted. -delete : Path -> Task {} [FileWriteErr Path WriteErr] [Write [File]] +delete : Path -> Task {} [FileWriteErr Path WriteErr] delete = \path -> toWriteTask path \bytes -> Effect.fileDelete bytes @@ -85,7 +85,7 @@ delete = \path -> ## This opens the file first and closes it after reading its contents. ## ## To read and decode data from a file, you can use `File.read` instead. -readBytes : Path -> Task (List U8) [FileReadErr Path ReadErr] [Read [File]] +readBytes : Path -> Task (List U8) [FileReadErr Path ReadErr] readBytes = \path -> toReadTask path \bytes -> Effect.fileReadBytes bytes @@ -100,10 +100,7 @@ readBytes = \path -> ## To read unformatted bytes from a file, you can use [File.readBytes] instead. readUtf8 : Path - -> Task - Str - [FileReadErr Path ReadErr, FileReadUtf8Err Path _] - [Read [File]] + -> Task Str [FileReadErr Path ReadErr, FileReadUtf8Err Path _] readUtf8 = \path -> effect = Effect.map (Effect.fileReadBytes (InternalPath.toBytes path)) \result -> when result is @@ -132,14 +129,14 @@ readUtf8 = \path -> # Err decodingErr -> Err (FileReadDecodeErr decodingErr) # Err readErr -> Err (FileReadErr readErr) # InternalTask.fromEffect effect -toWriteTask : Path, (List U8 -> Effect (Result ok err)) -> Task ok [FileWriteErr Path err] [Write [File]] +toWriteTask : Path, (List U8 -> Effect (Result ok err)) -> Task ok [FileWriteErr Path err] toWriteTask = \path, toEffect -> InternalPath.toBytes path |> toEffect |> InternalTask.fromEffect |> Task.mapFail \err -> FileWriteErr path err -toReadTask : Path, (List U8 -> Effect (Result ok err)) -> Task ok [FileReadErr Path err] [Read [File]] +toReadTask : Path, (List U8 -> Effect (Result ok err)) -> Task ok [FileReadErr Path err] toReadTask = \path, toEffect -> InternalPath.toBytes path |> toEffect diff --git a/examples/cli/cli-platform/Http.roc b/examples/cli/cli-platform/Http.roc index 78685ba131..4e39bad419 100644 --- a/examples/cli/cli-platform/Http.roc +++ b/examples/cli/cli-platform/Http.roc @@ -103,7 +103,7 @@ errorToString = \err -> BadStatus code -> Str.concat "Request failed with status " (Num.toStr code) BadBody details -> Str.concat "Request failed. Invalid body. " details -send : Request -> Task Str Error [Network [Http]] +send : Request -> Task Str Error send = \req -> # TODO: Fix our C ABI codegen so that we don't this Box.box heap allocation Effect.sendRequest (Box.box req) diff --git a/examples/cli/cli-platform/InternalProgram.roc b/examples/cli/cli-platform/InternalProgram.roc deleted file mode 100644 index 25ef2989b4..0000000000 --- a/examples/cli/cli-platform/InternalProgram.roc +++ /dev/null @@ -1,11 +0,0 @@ -interface InternalProgram - exposes [InternalProgram, fromEffect, toEffect] - imports [Effect.{ Effect }] - -InternalProgram := Effect U8 - -fromEffect : Effect U8 -> InternalProgram -fromEffect = @InternalProgram - -toEffect : InternalProgram -> Effect U8 -toEffect = \@InternalProgram effect -> effect diff --git a/examples/cli/cli-platform/InternalTask.roc b/examples/cli/cli-platform/InternalTask.roc index 3c25382b35..5fd5804003 100644 --- a/examples/cli/cli-platform/InternalTask.roc +++ b/examples/cli/cli-platform/InternalTask.roc @@ -2,16 +2,16 @@ interface InternalTask exposes [Task, fromEffect, toEffect, succeed, fail] imports [Effect.{ Effect }] -Task ok err fx := Effect (Result ok err) +Task ok err := Effect (Result ok err) -succeed : ok -> Task ok * * +succeed : ok -> Task ok * succeed = \ok -> @Task (Effect.always (Ok ok)) -fail : err -> Task * err * +fail : err -> Task * err fail = \err -> @Task (Effect.always (Err err)) -fromEffect : Effect (Result ok err) -> Task ok err * +fromEffect : Effect (Result ok err) -> Task ok err fromEffect = \effect -> @Task effect -toEffect : Task ok err * -> Effect (Result ok err) +toEffect : Task ok err -> Effect (Result ok err) toEffect = \@Task effect -> effect diff --git a/examples/cli/cli-platform/Process.roc b/examples/cli/cli-platform/Process.roc new file mode 100644 index 0000000000..b8d8a0551f --- /dev/null +++ b/examples/cli/cli-platform/Process.roc @@ -0,0 +1,13 @@ +interface Process + exposes [exit] + imports [Task.{ Task }, InternalTask, Effect] + +## Exit the process with +## +## {} <- Stderr.line "Exiting right now!" |> Task.await +## Process.exit 1 +exit : U8 -> Task {} * +exit = \code -> + Effect.processExit code + |> Effect.map \_ -> Ok {} + |> InternalTask.fromEffect diff --git a/examples/cli/cli-platform/Program.roc b/examples/cli/cli-platform/Program.roc deleted file mode 100644 index 7cefc46f0c..0000000000 --- a/examples/cli/cli-platform/Program.roc +++ /dev/null @@ -1,109 +0,0 @@ -interface Program - exposes [Program, ExitCode, noArgs, withArgs, quick, withEnv, exitCode, exit] - imports [Task.{ Task }, InternalProgram.{ InternalProgram }, InternalTask, Effect] - -## A [command-line interface](https://en.wikipedia.org/wiki/Command-line_interface) program. -Program : InternalProgram - -## An [exit status](https://en.wikipedia.org/wiki/Exit_status) code. -ExitCode := U8 - -## Converts a [U8] to an [ExitCode]. -## -## If you already have a [Task] and want to convert its success type -## from `{}` to [ExitCode], you may find [Program.exit] convenient. -exitCode : U8 -> ExitCode -exitCode = @ExitCode - -## Attach an [ExitCode] to a task. -## -## Stderr.line "I hit an error and couldn't continue." -## |> Program.exit 1 -## -## Note that this does not terminate the current process! By design, this platform does not have -## a [Task] which terminates the current process. Instead, error handling should be consistently -## done through task failures. -## -## To convert a [U8] directly into an [ExitCode], use [Program.exitCode]. -exit : Task {} [] fx, U8 -> Task ExitCode [] fx -exit = \task, code -> - Task.map task \{} -> @ExitCode code - -## A program which runs the given task and discards the values it produces on success or failure. -## One use for this is as an introductory [Program] when teaching someone how to use this platform. -## -## If the task succeeds, the program will exit with a [status](https://en.wikipedia.org/wiki/Exit_status) -## of 0. If the task fails, the program will exit with a status of 1. -## If the task crashes, the program will exit with a status of 2. -## -## For a similar program which specifies its exit status explicitly, see [Program.noArgs]. -quick : Task * * * -> Program -quick = \task -> - effect = - InternalTask.toEffect task - |> Effect.map \result -> - when result is - Ok _ -> 0 - Err _ -> 1 - - InternalProgram.fromEffect effect - -## A program which uses no [command-line arguments](https://en.wikipedia.org/wiki/Command-line_interface#Arguments) -## and specifies an [exit status](https://en.wikipedia.org/wiki/Exit_status) [U8]. -## -## Note that the task's failure type must be `[]`. You can satisfy that by handling all -## the task's potential failures using something like [Task.attempt]. -## -## For a similar program which does use command-line arguments, see [Program.withArgs]. -noArgs : Task ExitCode [] * -> Program -noArgs = \task -> - effect = - InternalTask.toEffect task - |> Effect.map \Ok (@ExitCode u8) -> u8 - - InternalProgram.fromEffect effect - -## A program which uses [command-line arguments](https://en.wikipedia.org/wiki/Command-line_interface#Arguments) -## and specifies an [exit status](https://en.wikipedia.org/wiki/Exit_status) [U8]. -## -## Note that the task's failure type must be `[]`. You can satisfy that by handling all -## the task's potential failures using something like [Task.attempt]. -## -## If any command-line arguments contain invalid Unicode, the invalid parts will be replaced with -## the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) -## (`�`). -withArgs : (List Str -> Task ExitCode [] *) -> Program -withArgs = \toTask -> - effect = Effect.after Effect.args \args -> - toTask args - |> InternalTask.toEffect - |> Effect.map \Ok (@ExitCode u8) -> u8 - - InternalProgram.fromEffect effect - -## A program which uses [command-line arguments](https://en.wikipedia.org/wiki/Command-line_interface#Arguments) -## and a dictionary of [environment variables](https://en.wikipedia.org/wiki/Environment_variable). -## -## This is a combination of [Program.withArgs] and `Env.dict`. Note that the task's failure type -## must be `[]`. You can satisfy that by handling all the task's potential failures using -## something like [Task.attempt]. -## -## If any command-line arguments contain invalid Unicode, the invalid parts will be replaced with -## the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) -## (`�`). -withEnv : (List Str, Dict Str Str -> Task ExitCode [] *) -> Program -withEnv = \toTask -> - effect = - args <- Effect.args |> Effect.after - dict <- Effect.envDict |> Effect.after - - toTask args dict - |> InternalTask.toEffect - |> Effect.map \Ok (@ExitCode u8) -> u8 - - InternalProgram.fromEffect effect - -# ## A combination of [Program.withArgs] and [Env.decodeAll], with the output of [Env.decodeAll] -# ## being passed after the command-line arguments. -# decodedEnv : (List Str, Result env [EnvDecodingFailed Str]* -> Task U8 [] *) -> Program -# | env has Decode diff --git a/examples/cli/cli-platform/Stderr.roc b/examples/cli/cli-platform/Stderr.roc index db7a75146b..557a0a21a9 100644 --- a/examples/cli/cli-platform/Stderr.roc +++ b/examples/cli/cli-platform/Stderr.roc @@ -2,13 +2,13 @@ interface Stderr exposes [line, write] imports [Effect, Task.{ Task }, InternalTask] -line : Str -> Task {} * [Write [Stderr]] +line : Str -> Task {} * line = \str -> Effect.stderrLine str |> Effect.map (\_ -> Ok {}) |> InternalTask.fromEffect -write : Str -> Task {} * [Write [Stderr]] +write : Str -> Task {} * write = \str -> Effect.stderrWrite str |> Effect.map (\_ -> Ok {}) diff --git a/examples/cli/cli-platform/Stdin.roc b/examples/cli/cli-platform/Stdin.roc index 961b578a11..f3a6124f53 100644 --- a/examples/cli/cli-platform/Stdin.roc +++ b/examples/cli/cli-platform/Stdin.roc @@ -2,7 +2,7 @@ interface Stdin exposes [line] imports [Effect, Task.{ Task }, InternalTask] -line : Task Str * [Read [Stdin]] +line : Task Str * line = Effect.stdinLine |> Effect.map Ok diff --git a/examples/cli/cli-platform/Stdout.roc b/examples/cli/cli-platform/Stdout.roc index c8793c776c..0b651563ec 100644 --- a/examples/cli/cli-platform/Stdout.roc +++ b/examples/cli/cli-platform/Stdout.roc @@ -2,13 +2,13 @@ interface Stdout exposes [line, write] imports [Effect, Task.{ Task }, InternalTask] -line : Str -> Task {} * [Write [Stdout]] +line : Str -> Task {} * line = \str -> Effect.stdoutLine str |> Effect.map (\_ -> Ok {}) |> InternalTask.fromEffect -write : Str -> Task {} * [Write [Stdout]] +write : Str -> Task {} * write = \str -> Effect.stdoutWrite str |> Effect.map (\_ -> Ok {}) diff --git a/examples/cli/cli-platform/Task.roc b/examples/cli/cli-platform/Task.roc index c5f439d60a..e350559665 100644 --- a/examples/cli/cli-platform/Task.roc +++ b/examples/cli/cli-platform/Task.roc @@ -2,9 +2,9 @@ interface Task exposes [Task, succeed, fail, await, map, mapFail, onFail, attempt, forever, loop, fromResult] imports [Effect, InternalTask] -Task ok err fx : InternalTask.Task ok err fx +Task ok err : InternalTask.Task ok err -forever : Task val err fx -> Task * err fx +forever : Task val err -> Task * err forever = \task -> looper = \{} -> task @@ -18,7 +18,7 @@ forever = \task -> Effect.loop {} looper |> InternalTask.fromEffect -loop : state, (state -> Task [Step state, Done done] err fx) -> Task done err fx +loop : state, (state -> Task [Step state, Done done] err) -> Task done err loop = \state, step -> looper = \current -> step current @@ -33,13 +33,13 @@ loop = \state, step -> Effect.loop state looper |> InternalTask.fromEffect -succeed : ok -> Task ok * * +succeed : ok -> Task ok * succeed = \ok -> InternalTask.succeed ok -fail : err -> Task * err * +fail : err -> Task * err fail = \err -> InternalTask.fail err -attempt : Task a b fx, (Result a b -> Task c d fx) -> Task c d fx +attempt : Task a b, (Result a b -> Task c d) -> Task c d attempt = \task, transform -> effect = Effect.after (InternalTask.toEffect task) @@ -50,7 +50,7 @@ attempt = \task, transform -> InternalTask.fromEffect effect -await : Task a err fx, (a -> Task b err fx) -> Task b err fx +await : Task a err, (a -> Task b err) -> Task b err await = \task, transform -> effect = Effect.after (InternalTask.toEffect task) @@ -61,7 +61,7 @@ await = \task, transform -> InternalTask.fromEffect effect -onFail : Task ok a fx, (a -> Task ok b fx) -> Task ok b fx +onFail : Task ok a, (a -> Task ok b) -> Task ok b onFail = \task, transform -> effect = Effect.after (InternalTask.toEffect task) @@ -72,7 +72,7 @@ onFail = \task, transform -> InternalTask.fromEffect effect -map : Task a err fx, (a -> b) -> Task b err fx +map : Task a err, (a -> b) -> Task b err map = \task, transform -> effect = Effect.after (InternalTask.toEffect task) @@ -83,7 +83,7 @@ map = \task, transform -> InternalTask.fromEffect effect -mapFail : Task ok a fx, (a -> b) -> Task ok b fx +mapFail : Task ok a, (a -> b) -> Task ok b mapFail = \task, transform -> effect = Effect.after (InternalTask.toEffect task) @@ -95,7 +95,7 @@ mapFail = \task, transform -> InternalTask.fromEffect effect ## Use a Result among other Tasks by converting it into a Task. -fromResult : Result ok err -> Task ok err * +fromResult : Result ok err -> Task ok err fromResult = \result -> when result is Ok ok -> succeed ok diff --git a/examples/cli/cli-platform/main.roc b/examples/cli/cli-platform/main.roc index 9877ce9ac5..8e185a6db1 100644 --- a/examples/cli/cli-platform/main.roc +++ b/examples/cli/cli-platform/main.roc @@ -1,9 +1,9 @@ platform "cli" - requires {} { main : InternalProgram } + requires {} { main : Task {} [] } exposes [] packages {} - imports [Effect.{ Effect }, InternalProgram.{ InternalProgram }] + imports [Task.{ Task }] provides [mainForHost] -mainForHost : Effect U8 as Fx -mainForHost = InternalProgram.toEffect main +mainForHost : Task {} [] as Fx +mainForHost = main diff --git a/examples/cli/cli-platform/src/lib.rs b/examples/cli/cli-platform/src/lib.rs index acb98743e3..b56e7179a1 100644 --- a/examples/cli/cli-platform/src/lib.rs +++ b/examples/cli/cli-platform/src/lib.rs @@ -58,12 +58,16 @@ pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { } #[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { +pub unsafe extern "C" fn roc_panic(msg: &RocStr, tag_id: u32) { match tag_id { 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc crashed with:\n\n\t{}\n", string); + eprintln!("Roc crashed with:\n\n\t{}\n", msg.as_str()); + + print_backtrace(); + std::process::exit(1); + } + 1 => { + eprintln!("The program crashed with:\n\n\t{}\n", msg.as_str()); print_backtrace(); std::process::exit(1); @@ -213,7 +217,7 @@ pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut } #[no_mangle] -pub extern "C" fn rust_main() -> u8 { +pub extern "C" fn rust_main() { let size = unsafe { roc_main_size() } as usize; let layout = Layout::array::(size).unwrap(); @@ -223,11 +227,9 @@ pub extern "C" fn rust_main() -> u8 { roc_main(buffer); - let exit_code = call_the_closure(buffer); + call_the_closure(buffer); std::alloc::dealloc(buffer, layout); - - exit_code } } @@ -287,6 +289,11 @@ pub extern "C" fn roc_fx_setCwd(roc_path: &RocList) -> RocResult<(), ()> { } } +#[no_mangle] +pub extern "C" fn roc_fx_processExit(exit_code: u8) { + std::process::exit(exit_code as i32); +} + #[no_mangle] pub extern "C" fn roc_fx_exePath(_roc_str: &RocStr) -> RocResult, ()> { match std::env::current_exe() { diff --git a/examples/cli/cli-platform/src/main.rs b/examples/cli/cli-platform/src/main.rs index 0765384f29..57692d3619 100644 --- a/examples/cli/cli-platform/src/main.rs +++ b/examples/cli/cli-platform/src/main.rs @@ -1,3 +1,3 @@ fn main() { - std::process::exit(host::rust_main() as _); + host::rust_main(); } diff --git a/examples/cli/countdown.roc b/examples/cli/countdown.roc index 473fdc18a8..710ee83486 100644 --- a/examples/cli/countdown.roc +++ b/examples/cli/countdown.roc @@ -3,7 +3,7 @@ app "countdown" imports [pf.Stdin, pf.Stdout, pf.Task.{ await, loop, succeed }] provides [main] to pf -main = \_args -> +main = _ <- await (Stdout.line "\nLet's count down from 10 together - all you have to do is press .") _ <- await Stdin.line loop 10 tick diff --git a/examples/cli/echo.roc b/examples/cli/echo.roc index 6066ea2bd7..e380d095bf 100644 --- a/examples/cli/echo.roc +++ b/examples/cli/echo.roc @@ -1,18 +1,14 @@ app "echo" packages { pf: "cli-platform/main.roc" } - imports [pf.Stdin, pf.Stdout, pf.Task.{ Task }, pf.Program.{ Program, ExitCode }] + imports [pf.Stdin, pf.Stdout, pf.Task.{ Task }] provides [main] to pf -main : Program -main = Program.noArgs mainTask - -mainTask : Task ExitCode [] [Read [Stdin], Write [Stdout]] -mainTask = +main : Task {} [] +main = _ <- Task.await (Stdout.line "🗣 Shout into this cave and hear the echo! 👂👂👂") - Task.loop {} (\_ -> Task.map tick Step) - |> Program.exit 0 + Task.loop {} \_ -> Task.map tick Step -tick : Task.Task {} [] [Read [Stdin], Write [Stdout]] +tick : Task.Task {} [] tick = shout <- Task.await Stdin.line Stdout.line (echo shout) diff --git a/examples/cli/env.roc b/examples/cli/env.roc index c53f7d7b26..981c7f49c8 100644 --- a/examples/cli/env.roc +++ b/examples/cli/env.roc @@ -1,25 +1,30 @@ app "env" packages { pf: "cli-platform/main.roc" } - imports [pf.Stdout, pf.Env, pf.Task, pf.Program.{ Program }] + imports [pf.Stdout, pf.Stderr, pf.Env, pf.Task.{ Task }] provides [main] to pf -main : Program +main : Task {} [] main = - Env.decode "EDITOR" - |> Task.await (\editor -> Stdout.line "Your favorite editor is \(editor)!") - |> Task.await (\{} -> Env.decode "SHLVL") - |> Task.await - (\lvl -> - when lvl is - 1u8 -> Stdout.line "You're running this in a root shell!" - n -> - lvlStr = Num.toStr n + task = + Env.decode "EDITOR" + |> Task.await (\editor -> Stdout.line "Your favorite editor is \(editor)!") + |> Task.await (\{} -> Env.decode "SHLVL") + |> Task.await + (\lvl -> + when lvl is + 1u8 -> Stdout.line "You're running this in a root shell!" + n -> + lvlStr = Num.toStr n - Stdout.line "Your current shell level is \(lvlStr)!") - |> Task.await (\{} -> Env.decode "LETTERS") - |> Task.await - (\letters -> - joinedLetters = Str.joinWith letters " " + Stdout.line "Your current shell level is \(lvlStr)!") + |> Task.await \{} -> Env.decode "LETTERS" - Stdout.line "Your favorite letters are: \(joinedLetters)") - |> Program.quick + Task.attempt task \result -> + when result is + Ok letters -> + joinedLetters = Str.joinWith letters " " + + Stdout.line "Your favorite letters are: \(joinedLetters)" + + Err _ -> + Stderr.line "I couldn't find your favorite letters in the environment variables!" diff --git a/examples/cli/file.roc b/examples/cli/file.roc index 8c71317a32..e7803d4a6b 100644 --- a/examples/cli/file.roc +++ b/examples/cli/file.roc @@ -1,7 +1,7 @@ app "file-io" packages { pf: "cli-platform/main.roc" } imports [ - pf.Program.{ Program, ExitCode }, + pf.Process, pf.Stdout, pf.Stderr, pf.Task.{ Task }, @@ -12,11 +12,8 @@ app "file-io" ] provides [main] to pf -main : Program -main = Program.noArgs mainTask - -mainTask : Task ExitCode [] [Write [File, Stdout, Stderr], Read [File, Env]] -mainTask = +main : Task {} [] +main = path = Path.fromStr "out.txt" task = cwd <- Env.cwd |> Task.await @@ -34,10 +31,7 @@ mainTask = Task.attempt task \result -> when result is - Ok {} -> - Stdout.line "Successfully wrote a string to out.txt" - |> Program.exit 0 - + Ok {} -> Stdout.line "Successfully wrote a string to out.txt" Err err -> msg = when err is @@ -47,5 +41,5 @@ mainTask = FileReadErr _ _ -> "Error reading file" _ -> "Uh oh, there was an error!" - Stderr.line msg - |> Program.exit 1 + {} <- Stderr.line msg |> Task.await + Process.exit 1 diff --git a/examples/cli/form.roc b/examples/cli/form.roc index 5df943c514..9cd6e34817 100644 --- a/examples/cli/form.roc +++ b/examples/cli/form.roc @@ -1,16 +1,12 @@ app "form" packages { pf: "cli-platform/main.roc" } - imports [pf.Stdin, pf.Stdout, pf.Task.{ await, Task }, pf.Program.{ Program, ExitCode }] + imports [pf.Stdin, pf.Stdout, pf.Task.{ await, Task }] provides [main] to pf -main : Program -main = Program.noArgs mainTask - -mainTask : Task ExitCode [] [Read [Stdin], Write [Stdout]] -mainTask = +main : Task {} [] +main = _ <- await (Stdout.line "What's your first name?") firstName <- await Stdin.line _ <- await (Stdout.line "What's your last name?") lastName <- await Stdin.line Stdout.line "Hi, \(firstName) \(lastName)! 👋" - |> Program.exit 0 diff --git a/examples/cli/http-get.roc b/examples/cli/http-get.roc index f6e5b4059f..0333aba08f 100644 --- a/examples/cli/http-get.roc +++ b/examples/cli/http-get.roc @@ -1,13 +1,10 @@ app "http-get" packages { pf: "cli-platform/main.roc" } - imports [pf.Http, pf.Task, pf.Stdin, pf.Stdout, pf.Program.{ Program, ExitCode }] + imports [pf.Http, pf.Task.{ Task }, pf.Stdin, pf.Stdout] provides [main] to pf -main : Program -main = Program.noArgs mainTask - -mainTask : Task.Task ExitCode [] [Read [Stdin], Write [Stdout], Network [Http]] -mainTask = +main : Task {} [] +main = _ <- Task.await (Stdout.line "Please enter a URL to fetch") url <- Task.await Stdin.line @@ -25,4 +22,3 @@ mainTask = |> Task.await Stdout.line output - |> Program.exit 0 diff --git a/examples/helloWorld.roc b/examples/helloWorld.roc index 08c5230d8d..7eef5d17a1 100644 --- a/examples/helloWorld.roc +++ b/examples/helloWorld.roc @@ -1,11 +1,7 @@ app "helloWorld" packages { pf: "cli/cli-platform/main.roc" } - imports [pf.Stdout, pf.Program.{ Program }] + imports [pf.Stdout] provides [main] to pf -main = Program.noArgs mainTask - -mainTask = +main = Stdout.line "Hello, World!" - |> Program.exit 0 - diff --git a/getting_started/README.md b/getting_started/README.md index f05c36a90e..0f928167ab 100644 --- a/getting_started/README.md +++ b/getting_started/README.md @@ -4,7 +4,7 @@ Roc is a language for making delightful software. It does not have an 0.1 releas certainly don't recommend using it in production in its current state! However, it can be fun to play around with as long as you have a high tolerance for missing features and compiler bugs. :) -The [tutorial](../TUTORIAL.md) is the best place to learn about how to use the language - it assumes no prior knowledge of Roc or similar languages. (If you already know [Elm](https://elm-lang.org/), then [Roc for Elm Programmers](https://github.com/roc-lang/roc/blob/main/roc-for-elm-programmers.md) may be of interest.) +The [tutorial](https://roc-lang.org/tutorial) is the best place to learn about how to use the language - it assumes no prior knowledge of Roc or similar languages. (If you already know [Elm](https://elm-lang.org/), then [Roc for Elm Programmers](https://github.com/roc-lang/roc/blob/main/roc-for-elm-programmers.md) may be of interest.) There's also a folder of [examples](https://github.com/roc-lang/roc/tree/main/examples) - the [CLI form example](https://github.com/roc-lang/roc/tree/main/examples/cli/form.roc) in particular is a reasonable starting point to build on. diff --git a/roc-for-elm-programmers.md b/roc-for-elm-programmers.md index 84946ebbb2..e5b0123e0c 100644 --- a/roc-for-elm-programmers.md +++ b/roc-for-elm-programmers.md @@ -1249,7 +1249,7 @@ If you put these into a hypothetical Roc REPL, here's what you'd see: - `comparable` is used in Elm for comparison operators (like `<` and such), plus `List.sort`, `Dict`, and `Set`. Roc's comparison operators (like `<`) only accept numbers; `"foo" < "bar"` is valid Elm, but will not compile in Roc. Roc's dictionaries and sets are hashmaps behind the scenes (rather than ordered trees), so their keys need to be hashable but not necessarily comparable. That said, Roc's `Dict` and `Set` do have a restriction on their keys, just not `comparable`. -See the section on Abilities in [the tutorial](TUTORIAL.md) for details. +See the section on Abilities in [the tutorial](https://roc-lang.org/tutorial) for details. ## Standard library diff --git a/www/public/index.html b/www/public/index.html index 0686c63138..e5a18adae7 100644 --- a/www/public/index.html +++ b/www/public/index.html @@ -39,7 +39,7 @@

With all that context in mind, if you'd like to try it out or to get involved with contributing, the source code repository has nightly builds you can download, - and a tutorial.

+ and a tutorial.

If you'd like to learn more about Roc, you can continue reading here, or check out one of these videos:

    diff --git a/www/public/site.css b/www/public/site.css index a7901c0950..09eacf4b10 100644 --- a/www/public/site.css +++ b/www/public/site.css @@ -331,12 +331,18 @@ h1 a, h2 a, h3 a, h4 a, h5 a { color: var(--header-link-color); } - h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover, h5 a:hover { text-decoration: none; color: var(--header-link-hover); } +h1 code, h2 code, h3 code, h4 code, h5 code { + color: inherit; + background-color: inherit; + padding: 0; + margin: 0; +} + h1 { font-size: 7rem; line-height: 7rem; diff --git a/www/public/tutorial/index.html b/www/public/tutorial/index.html index 463c997cfb..033a7f0bd5 100644 --- a/www/public/tutorial/index.html +++ b/www/public/tutorial/index.html @@ -36,16 +36,17 @@
  • Defining Functions
  • if-then-else
  • Records
  • +
  • Debugging
  • Tags & Pattern Matching
  • Booleans
  • Lists
  • Types
  • +
  • Crashing
  • Tests and Expectations
  • Modules
  • Platforms and Packages
  • Tasks
  • Abilities
  • -
  • Next Steps
  • Advanced Concepts
  • Operator Desugaring Table
  • @@ -294,6 +295,34 @@ the else branch contains another if/else. on the same line, although the convention is to use the original version's style.

    +

    Debugging

    +

    Print debugging is the most +common debugging technique in the history of programming, and Roc has a dbg +keyword to facilitate it. Here's an example of how to use dbg:

    +pluralize = \singular, plural, count -> + dbg count + + if count == 1 then + singular + else + plural + +

    Whenever this dbg line of code is reached, the value of count +will be printed to stderr, +along with the source code file and line number where the dbg itself was written:

    +[pluralize.roc 6:8] 5 + +

    You can give dbg any expression you like, for example:

    +dbg Str.concat singular plural +

    An easy way to print multiple values at a time is to wrap them in a tag, for example +a concise tag like T: +

    +dbg T "the value of `count` is:" count +
    +

    Note that dbg is a debugging tool, and is only available when running +your program via roc dev, roc run, or roc test. When you +build a standalone application with roc build, any uses of dbg won't +be included!

    Records

    Currently our addAndStringify function takes two arguments. We can instead make it take one argument like so:

    @@ -504,6 +533,25 @@ inside a when, we would write Custom r g b - Custom description -> description branch, Custom description would be a pattern. In programming, using patterns in branching conditionals like when is known as pattern matching. You may hear people say things like "let's pattern match on Custom here" as a way to suggest making a when branch that begins with something like Custom description ->.

    +

    Pattern Matching on Lists

    +

    You can also pattern match on lists, like so:

    +when myList is + [] -> 0 # the list is empty + [Foo, ..] -> 1 # it starts with a Foo tag + [_, ..] -> 2 # it contains at least one element, which we ignore + [Foo, Bar, ..] -> 3 # it starts with a Foo tag followed by a Bar tag + [Foo, Bar, Baz] -> 4 # it has exactly 3 elements: Foo, Bar, and Baz + [Foo, a, ..] -> 5 # its first element is Foo, and its second we name `a` + [Ok a, ..] -> 6 # it starts with an Ok containing a payload named `a` + [.., Foo] -> 7 # it ends with a Foo tag + [A, B, .., C, D] -> 8 # it has certain elements at the beginning and end + +

    This can be both more concise and more efficient (at runtime) than calling List.get +multiple times, since each call to get requires a separate conditional to handle the different +Results they return.

    +
    +

    Note: Each list pattern can only have one .., which is known as the "rest pattern" because it's where the rest of the list goes.

    +

    Booleans

    In many programming languages, true and false are special language keywords that refer to the two boolean values. In Roc, booleans do not get special keywords; instead, they are exposed @@ -739,7 +787,7 @@ fullName = \firstName, lastName

    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.

    -

    Type annotations

    +

    Type Annotations

    Here's another way to document this function's type, which doesn't have that problem:

    fullName : Str, Str -> Str fullName = \firstName, lastName -> @@ -765,6 +813,7 @@ amy = { firstName: jen : { firstName : Str, lastName : Str } jen = { firstName: "Jen", lastName: "Majura" } +

    Type Aliases

    When we have a recurring type annotation like this, it can be nice to give it its own name. We do this like so:

    Musician : { firstName : Str, lastName : Str } @@ -779,16 +828,8 @@ simone = { firstName: name : Str
    as "name has the type Str," you can also read Musician : { firstName : Str, lastName : Str } as "Musician has the type { firstName : Str, lastName : Str }."

    -

    We can also give type annotations to tag unions:

    -colorFromStr : Str -> [Red, Green, Yellow] -colorFromStr = \string -></span> - when string is - "red" -> Red - "green" -> Green - _ -> Yellow - -

    You can read the type [Red, Green, Yellow] as "a tag union of the tags Red, Green, and Yellow."

    -

    When we annotate a list type, we have to specify the type of its elements:

    +

    Type Parameters

    +

    Annotations for lists must specify what type the list's elements have:

    names : List Str names = ["Amy", "Simone", "Tarja"] @@ -796,6 +837,7 @@ names = ["Amy", Wildcard Types (*)

    There are some functions that work on any list, regardless of its type parameter. For example, List.isEmpty has this type:

    isEmpty : List * -> Bool @@ -807,6 +849,7 @@ with any type of List - so, List Str, List Bool< function that takes a List Bool. We might reasonably expect to be able to pass an empty list (that is, []) to either of these functions. And so we can! This is because a [] value has the type List * - that is, "a list with a wildcard type parameter," or "a list whose element type could be anything."

    +

    Type Variables

    List.reverse works similarly to List.isEmpty, but with an important distinction. As with isEmpty, we can call List.reverse on any list, regardless of its type parameter. However, consider these calls:

    strings : List Str @@ -840,6 +883,46 @@ of the type annotation, or even the function's implementation! The only way to h List * is if it returns an empty list.

    Similarly, the only way to have a function whose type is a -> a is if the function's implementation returns its argument without modifying it in any way. This is known as the identity function.

    +

    Tag Union Types

    +

    We can also annotate types that include tags:

    +colorFromStr : Str -> [Red, Green, Yellow] +colorFromStr = \string -> + when string is + "red" -> Red + "green" -> Green + _ -> Yellow + +

    You can read the type [Red, Green, Yellow] as "a tag union of the tags Red, Green, and Yellow."

    +

    Some tag unions have only one tag in them. For example:

    +redTag : [Red] + +redTag = Red + +

    Accumulating Tag Types

    +

    Tag union types can accumulate more tags based on how they're used. Consider this if expression:

    +\str -> + if Str.isEmpty str then + Ok "it was empty" + else + Err ["it was not empty"] + +

    Here, Roc sees that the first branch has the type [Ok Str] and that the else branch has +the type [Err (List Str)], so it concludes that the whole if expression evaluates to the +combination of those two tag unions: [Ok Str, Err (List Str)].

    +

    This means this entire \str -> … function has the type Str -> [Ok Str, Err (List Str)]. +However, it would be most common to annotate it as Result Str (List Str) instead, because +the Result type (for operations like Result.withDefault, which we saw earlier) is a type +alias for a tag union with Ok and Err tags that each have one payload:

    +Result ok err : [Ok ok, Err err] + +

    We just saw how tag unions get combined when different branches of a conditional return different tags. Another way tag unions can get combined is through pattern matching. For example:

    +when color is + Red -> "red" + Yellow -> "yellow" + Green -> "green" + +

    Here, Roc's compiler will infer that color's type is [Red, Yellow, Green], because +those are the three possibilities this when handles.

    Numeric types

    Roc has different numeric types that each have different tradeoffs. They can all be broken down into two categories: fractions, @@ -993,21 +1076,96 @@ compatible with fractions. For example:

    and also Num.cos 1 and have them all work as expected; the number literal 1 has the type Num *, which is compatible with the more constrained types Int and Frac. For the same reason, you can pass number literals to functions expecting even more constrained types, like I32 or F64.

    -

    Typed Number Literals

    -

    When writing a number literal in Roc you can specify the numeric type as a suffix of the literal. -1u8 specifies 1 as an unsigned 8-bit integer, 5i32 specifies 5 as a signed 32-bit integer, etc. -The full list of possible suffixes includes: -i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, nat, f32, f64, dec

    -

    Hexadecimal Integer Literals

    -

    Integer literals can be written in hexadecimal form by prefixing with 0x followed by hexadecimal characters. -0xFE evaluates to decimal 254 -The integer type can be specified as a suffix to the hexadecimal literal, -so 0xC8u8 evaluates to decimal 200 as an unsigned 8-bit integer.

    -

    Binary Integer Literals

    -

    Integer literals can be written in binary form by prefixing with 0b followed by the 1's and 0's representing -each bit. 0b0000_1000 evaluates to decimal 8 -The integer type can be specified as a suffix to the binary literal, -so 0b0100u8 evaluates to decimal 4 as an unsigned 8-bit integer.

    +

    Number Literals

    +

    By default, a number literal with no decimal point has the type Num *—that is, +we know it's "a number" but nothing more specific. (Number literals with decimal points have the type Frac * instead.) +

    +

    You can give a number literal a more specific type by adding the type you want as a lowercase suffix. +For example, 1u8 specifies 1 with the type U8, +and 5dec specifies 5 with the type Dec.

    +

    The full list of possible suffixes includes:

    + u8, + i8, + u16, + i16, + u32, + i32, + u64, + i64, + u128, + i128, + nat, + f32, + f64, + dec +

    +

    Integer literals can be written in hexadecimal +form by prefixing with 0x followed by hexadecimal characters +(a - f in addition to 0 - 9). +For example, writing 0xfe is the same as writing 254. Similarly, +the prefix 0b specifies binary integers. Writing 0b0000_1000 +is the same as writing 8. +

    +

    Crashing

    +

    Ideally, Roc programs would never crash. However, there are some situations where they may. For example:

    +
      +
    1. When doing normal integer arithmetic (e.g. x + y) that overflows.
    2. +
    3. When the system runs out of memory.
    4. +
    5. When a variable-length collection (like a List or Str) gets too + long to be representible in the operating system's address space. + (A 64-bit operating system's address space can represent several + exibytes + of data, so this case should not come up often.) +
    6. +
    +

    Crashes in Roc are not like + try/catch exceptions +found in some other programming languages. There is no way to "catch" a crash. It immediately +ends the program, and what happens next is defined by the platform. (For example, a command-line +interface platform might exit with a nonzero exit code, +whereas a web server platform might have the current request respond with a +HTTP 500 error.)

    +

    Crashing in unreachable branches

    +

    You can intentionally crash a Roc program, for example inside a conditional branch that +you believe is unreachable. Suppose you're certain that a particular List U8 +contains valid UTF-8 bytes, which means when you call Str.fromUtf8 on it, the +Result it returns will always be Ok. In that scenario, you can use +the crash keyword to handle the Err case like so:

    +answer : Str +answer = + when Str.fromUtf8 definitelyValidUtf8 is + Ok str -> str + Err _ -> crash "This should never happen!" + +

    If the unthinkable happens, and somehow the program reaches this Err branch even though that +was thought to be impossible, then it will crash - just like if the system had run out of memory. +The string passed to crash will be provided to the platform as context; each platform may do +something different with it.

    +
    +

    Note: crash is a language keyword and not a function; you can't assign crash to a variable +or pass it to a function.

    +
    +

    Crashing for TODOs

    +

    Another use for crash is as a TODO marker when you're in the middle of building something:

    +if x > y then + transmogrify (x * 2) +else + crash "TODO handle the x <= y case" + +

    This lets you do things like write tests for the non-crash branch, and then come back and finish +the other branch later.

    +
    +

    Crashing for error handling

    +

    crash is not for error handling.

    +

    The reason Roc has a crash keyword is for scenarios where it's expected that no error + will ever happen (like in unreachable branches), + or where graceful error handling is infeasible (like running out of memory). +

    +

    Errors that are recoverable should be represented using normal Roc types ( + like Result) + and then handled without crashing—for example, by having the application + report that something went wrong, and then continue running from there. +

    Tests and expectations

    You can write automated tests for your Roc code like so:

    pluralize = \singular, plural, count -> @@ -1024,7 +1182,7 @@ expect pluralize "cactus" "cacti"

    If you put this in a file named main.roc and run roc test, Roc will execute the two expect expressions (that is, the two pluralize calls) and report any that returned false.

    -

    Inline expects

    +

    Inline Expectations

    For example:

    if count == 1 then "\(countStr) \(singular)" @@ -1045,19 +1203,6 @@ control flow. In fact, if you do roc build, they are not even inclu

    If you try this code out, you may note that when an expect fails (either a top-level or inline one), the failure message includes the values of any named variables - such as count here. This leads to a useful technique, which we will see next.

    -

    Quick debugging with inline expects

    -

    An age-old debugging technique is printing out a variable to the terminal. In Roc you can use -expect to do this. Here's an example:

    -\arg -> - x = arg - 1 - - # Reports the value of `x` without stopping the program - expect x != x - - Num.abs x - -

    The failure output will include both the value of x as well as the comment immediately above it, -which lets you use that comment for extra context in your output.

    Interface modules

    [This part of the tutorial has not been written yet. Coming soon!]

    Builtin modules