Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add guidelines for writing sensible needs reasons #245

Merged
merged 18 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion compiler/src/compiler/ast_to_hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,20 @@ impl<'a> Context<'a> {
condition: Box::new(condition.clone()),
reason: Box::new(self.push(
None,
Expression::Text("needs not satisfied".to_string()),
Expression::Text(
match self.db.ast_id_to_span(call.arguments[0].id.clone()) {
Some(span) => format!(
"`{}` was not satisfied",
&self
.db
.get_module_content_as_string(
call.arguments[0].id.module.clone()
)
.unwrap()[span],
),
None => "the needs of a function were not met".to_string(),
},
),
None,
)),
},
Expand Down
16 changes: 9 additions & 7 deletions compiler/src/vm/builtin_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,9 @@ macro_rules! unpack_and_later_drop {
impl Heap {
fn channel_create(&mut self, args: &[Pointer]) -> BuiltinResult {
unpack_and_later_drop!(self, args, |capacity: Int| {
CreateChannel {
capacity: capacity.value.clone().try_into().expect(
"you tried to create a channel with a capacity bigger than the maximum usize",
),
match capacity.value.clone().try_into() {
Ok(capacity) => CreateChannel { capacity },
Err(_) => return Err("you tried to create a channel with a capacity that is either negative or bigger than the maximum usize".to_string()),
}
})
}
Expand Down Expand Up @@ -295,7 +294,7 @@ impl Heap {
fn parallel(&mut self, args: &[Pointer]) -> BuiltinResult {
unpack!(self, args, |body_taking_nursery: Closure| {
if body_taking_nursery.num_args != 1 {
return Err("Parallel expects a closure that takes a nursery.".to_string());
return Err("parallel expects a closure taking a nursery".to_string());
}
Parallel {
body: body_taking_nursery.address,
Expand All @@ -317,7 +316,10 @@ impl Heap {
self.dup(value);
Ok(Return(value))
}
None => Err(format!("Struct does not contain key {}.", key.format(self))),
None => Err(format!(
"the struct does not contain the key {}",
key.format(self)
)),
}
})
}
Expand Down Expand Up @@ -503,7 +505,7 @@ impl TryInto<bool> for Data {
match symbol.value.as_str() {
"True" => Ok(true),
"False" => Ok(false),
_ => Err("a builtin function expected `True` or `False`".to_string()),
_ => Err("a builtin function expected True or False".to_string()),
}
}
}
8 changes: 4 additions & 4 deletions compiler/src/vm/fiber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ impl Fiber {
..
}) => {
if num_args != expected_num_args {
self.panic(format!("Closure expects {expected_num_args} parameters, but you called it with {num_args} arguments."));
self.panic(format!("a closure expected {expected_num_args} parameters, but you called it with {num_args} arguments"));
return;
}

Expand Down Expand Up @@ -477,11 +477,11 @@ impl Fiber {
}
"False" => self.panic(reason),
_ => {
self.panic("Needs expects True or False as a symbol.".to_string());
self.panic("needs expect True or False as the condition".to_string());
}
},
_ => {
self.panic("Needs expects a boolean symbol.".to_string());
self.panic("needs expect a boolean symbol as the condition".to_string());
}
}
}
Expand Down Expand Up @@ -553,7 +553,7 @@ impl Fiber {
}
Instruction::Error { id, errors } => {
self.panic(format!(
"The fiber crashed because there {} at {id}: {errors:?}",
"there {} at {id}: {errors:?}",
if errors.len() == 1 {
"was an error"
} else {
Expand Down
101 changes: 61 additions & 40 deletions language.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ In particular, this means the following:
That's mainly achieved by providing good tooling so that the feedback loop is fast.
- **Candy is minimalistic.**
The language itself is simple and can be explained in a few minutes.
Candy intentionally leaves out advanced concepts like types in favor of good editor tooling.
Candy intentionally leaves out advanced concepts like types, offsetting that with good editor tooling.
- **Candy is expressive.**
You can be flexible with how you model your data in Candy.
We aim to have a reasonable concise syntax to express common patterns.
Expand All @@ -25,7 +25,7 @@ Note that not all of the features described here are implemented or even finaliz
- [Symbols](#symbols)
- [Structs](#structs)
- [Closures](#closures)
- [Channel Ends](#channel-ends)
- [Ports](#ports)
- [More?](#more)
- [Variables](#variables)
- [Functions](#functions)
Expand All @@ -49,12 +49,14 @@ Candy's syntax is inspired by Elm and Haskell.

Source code is stored in plain text files with a `.candy` file extension.

[Comments](#comments) start with `#` and end at the end of the line:
[Comments](#comments) start with `##` and end at the end of the line:

```candy
# This is a comment.
## This is a comment.
```

> One `#` is also a comment, but a doc comment for the item above it. See [Comments](#comments) for more info on that.

Naming rules are similar to other programming languages.
Identifiers start with a lowercase letter and may contain letters or digits.
Case is sensitive.
Expand Down Expand Up @@ -174,16 +176,15 @@ longClosure = { foo ->
}
```

### Channel Ends

TODO: Or are they called ports?
### Ports

Channels ends allow you to interact with a [channel](#concurrency).
There are receive ends and send ends to receive and send data from a channel, respectively.
Ports allow you to interact with a [channel](#concurrency).
There are receive ports and send ports to receive and send data from a channel, respectively.
See the [Concurrency](#concurrency) section for more information.

### More?

TODO: Sets?
TODO: Tuples? Tags? Sets?

- sets: Clojure has `%{ value }`
- or like Toit? `{hey, you, there}` for set, empty map is `{:}`
Expand Down Expand Up @@ -251,6 +252,17 @@ five = identity (identity 5)
error = identity identity 5 # error because the first `identity` is called with two arguments
```

You can also split arguments across multiple lines using indentation.
This allows you to omit parentheses for nested calls.

```
foo = add
subtract 5 3
multiply
logarithm 5
divide 8 4
```

TODO: Piping

## Modules
Expand All @@ -259,18 +271,18 @@ For bigger project it becomes necessary to split code into multiple files.
In Candy, *modules* are a unit of composition.
Modules are self-contained units of code that choose what to expose to the outside world.

Modules correspond either to single candy files or directories containing a single file that is named just `.candy`.
Modules correspond either to single candy files or directories containing a single file that is named just `_.candy`.
For example, a Candy project might look like this:

```
main.candy
green/
.candy
_.candy
brown.candy
red/
.candy
_.candy
yellow/
.candy
_.candy
purple.candy
blue.candy
```
Expand All @@ -279,10 +291,10 @@ This directory structure corresponds to the following module hierarchy:

```
main # from main.candy
green # from green/.candy
green # from green/_.candy
brown # from green/brown.candy
red # from red/.candy
yellow # from red/yellow/.candy
red # from red/_.candy
yellow # from red/yellow/_.candy
purple # from red/yellow/purple.candy
blue # from red/blue.candy
```
Expand All @@ -292,7 +304,7 @@ In each module, there automatically exists a `use` function that will import oth
You pass it a text that describes what module to import.

```
# inside red/yellow/.candy
# inside red/yellow/_.candy

foo = use ".purple" # imports the purple child module
foo = use "..blue" # imports the blue sibling module
Expand All @@ -314,7 +326,7 @@ baz a := a
```

```candy
# inside green/.candy
# inside green/_.candy

brown = use ".brown"

Expand Down Expand Up @@ -395,7 +407,7 @@ Consequently, closures can't reject inputs, but they also don't promise that the
foo a =
needs (core.int.is a)

# `product` is a parameterized variable, so it needs to handle every input
# `product` is a parameterized variable, so it needs to handle every input.
product b =
needs (core.int.is b)
core.int.multiply a b
Expand All @@ -415,13 +427,24 @@ foo a =
foo Hey # Calling `foo Hey` panics because life's not fair.
```

Here are some recommended guidelines for writing reasons:

- For `needs` that only check the type, you typically don't need a reason.
- Try to keep the reason short and simple.
- Phrase the reason as a self-contained sentence, including a period at the end.
- Write concrete references such as function or parameter names in backticks.
- Prefer concepts over concrete functions. For example, write "This function needs a non-negative int." rather than "This function needs an int that `isNonNegative`." – after all, users can always jump to the `needs` itself.
- Consider also highlighting what is wrong with the input rather than just spelling out the needs.
- Consider starting new sentences in long reasons.
- Consider special-casing typical erroneous inputs with custom reasons.

The editor tooling will analyze your functions and try them out with different values.
If an input crashes in a way that your code is at fault, you will see a hint.

```candy
mySqrt a = # If you pass `a = -1`,
needs (core.int.is a) # this needs succeeds because `core.int.is -1 = True`,
core.int.sqrt a # but calling `core.int.sqrt -1` panics because sqrt only works on non-negative integers. If you think this should be different, check out the `ComplexNumbers` package.
core.int.sqrt a # but calling `core.int.sqrt -1` panics: If you want to take the square root of a negative integer, check out the `ComplexNumbers` package.
```

## Pattern Matching
Expand Down Expand Up @@ -462,8 +485,8 @@ Note: The actual `print` works differently, using [capabilities](#environment-an

```candy
core.parallel { nursery ->
core.fiber.spawn nursery { print "Banana" }
core.fiber.spawn nursery { print "Kiwi" }
core.async nursery { print "Banana" }
core.async nursery { print "Kiwi" }
# Banana and Kiwi may print in any order
}
print "Peach" # Always prints after the others
Expand All @@ -480,14 +503,14 @@ It has a *send end*, which you can use to put messages into it, and it has a *re
A channel also has a capacity, which indicates how many messages it can hold simultaneously.

```candy
channel = core.channel.new 5 # creates a new channel with capacity 5
sender = channel.sendEnd
receiver = channel.receiveEnd
channel = core.channel.create 5 # creates a new channel with capacity 5
sendPort = channel.sendPort
receivePort = channel.receivePort

core.channel.send sender Foo
core.channel.send sender Bar
core.channel.receive receiver # Foo
core.channel.receive receiver # Bar
core.channel.send sendPort Foo
core.channel.send sendPort Bar
core.channel.receive receivePort # Foo
core.channel.receive receivePort # Bar
```

There is no guaranteed ordering between messages sent by multiple fibers, but messages coming from the same fiber are guaranteed to arrive in the same order they were sent.
Expand All @@ -497,15 +520,15 @@ Thus, channels also function as a synchronization primitive and can generate *ba

```candy
core.parallel { nursery ->
channel = core.channel.new 3
core.fiber.spawn nursery {
channel = core.channel.create 3
core.async nursery {
loop {
core.channel.send channel.sendEnd "Hi!"
core.channel.send channel.sendPort "Hi!"
}
}
core.fiber.spawn nursery {
core.async nursery {
loop {
print (core.channel.receive channel.receiveEnd)
print (core.channel.receive channel.receivePort)
}
}
}
Expand All @@ -514,8 +537,6 @@ core.parallel { nursery ->
In this example, if the printing takes longer than the generating of new texts to print, the generator will wait for the printing to happen.
At most 3 texts will exist before being printed.

TODO: Write about async await as soon as they are in the Core package

## Packages

TODO: Write something
Expand All @@ -529,8 +550,8 @@ For example, on desktop platforms, the environment looks something like this:

```candy
[
Stdin: <channel receive end>,
Stdout: <channel send end>,
Stdin: <channel receive port>,
Stdout: <channel send port>,
WorkingDirectory: ...,
Variables: [
...
Expand Down Expand Up @@ -559,7 +580,7 @@ Depending on the use case, we offer two alternative options:
If you develop for a new platform or want to enable more functionality in the native platform, we will have some way of plugging a new part of native code into the runtime that can make its own capabilities available on the environment passed to `main`.

For example, on a microcontroller, the stdout capability doesn't make sense.
Instead, you might wave a pin capability that allows you to modify the voltage of the hardware pins.
Instead, you might have a pin capability that allows you to modify the voltage of the hardware pins.

### Contain Pure Code

Expand Down
Loading