Add Block Results

Glen Whitney 2024-08-30 22:23:09 +00:00
parent 1c2af0bbe4
commit bb67aec303

102
Block-Results.md Normal file

@ -0,0 +1,102 @@
In Rust, a block that ends with a semicolon evaluates to `()`, and a block that doesn't end with a semicolon evaluates to the value of its last expression. In Husht, we want semicolons to be unnecessary, so how do we make this distinction? Here are some ideas.
### Explicitly mark results
To indicate that a block evaluates to its last expression, you mark the last expression with a special token. This inverts the Rust convention, in the sense that you indicate a block result by the presence of a token rather than the absence of one.
Pros
- Simple rule
- Doesn't require type-checking in the transpiler
- Tight correspondence with Rust syntax
Cons
- Adds a little noise—but block results usually make up a small fraction of expressions, so noise should still decrease overall
- Prevents some Rust code from being valid Husht
Here are some ideas for result markers.
#### Arrow away from result
"Ship out." For function blocks, can be seen as the input end of the return type arrow in the signature.
```
let q = x*x - y*y
if q >= 0
<- x
else
<- y
```
#### Arrow toward result
"Out of the pipe." For function blocks, can be seen as standing in for the return type in the signature.
```
let q = x*x - y*y
if q >= 0
-> x
else
-> y
```
#### Dollar sign
"Cash out."
```
let q = x*x - y*y
if q >= 0
$ x
else
$ y
```
### Allow implicit coercion to `Unit`
There are few or no circumstances where Rust allows implicit coercion to `Unit`, but Husht could be more permissive about this—perhaps only in particular [coercion sites](https://doc.rust-lang.org/reference/type-coercions.html). This might allow us to infer most of the time whether a block should be treated as though the last expression ends with a semicolon. However, it would probably also lead to ambiguity in weird but not totally unreasonable code like the following, where we could infer either `i32` or `Unit` for the type of `result`.
```
fn say_hello
println! "Hello"
0
fn say_goodbye
println! "Goodbye"
1
fn say_something(arriving: bool)
let result = if arriving
say_hello()
else
say_goodbye()
/* function body continues... */
```
Ideas for resolving the ambiguity:
- Only do implicit coercion as a "last resort" (no idea how to formalize that)?
- Storing or using the result of a block forces it to take the value of its final expressions?
- Require explicit coercion in cases like this?
Pros
- Might reduce verbosity in many different situations.
Cons
- Requires type-checking in the transpiler
- Messing with such a basic feature of Rust could have far-reaching effects with hard-to-foresee consequences
- I'm worried that this could collapse lots of different type errors into errors involving `()`, and obscure many errors' locations. That can lead to frustrating, uninformative error messages, akin to "... ended by `\end{document}` in LaTeX. For example, the Rust code
```
let x = if true {
2.0
} else {
3
};
let x_sq = x*x;
```
gives the error "`if` and `else` have incompatible types" error. Allowing implicit coercion to `()` might mean we instead get the error "cannot multiply `()` by `()`" in the expression `x*x`, making it harder to figure out what and where the problem is.