Skip to content
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ fn main() {
match io.read_file("settings.json") {
Ok(content) -> match json.parse(content, Config) {
Ok(cfg) -> println("loaded: {cfg.name}")
Err(e) -> println("parse error: {e.message()}")
Err(e) -> println("parse error: {Error.message(e)}")
}
Err(io.NotFound(path)) -> println("no config at {path} — run `silt init` first")
Err(e) -> println("read error: {e.message()}")
Err(e) -> println("read error: {Error.message(e)}")
}
}
```

Stdlib errors are typed enums — `IoError`, `JsonError`, `HttpError`, and so on — so `match` can fork on specific failures like `NotFound(path)` and still fall back to `.message()` for the long tail.
Stdlib errors are typed enums — `IoError`, `JsonError`, `HttpError`, and so on — so `match` can fork on specific failures like `NotFound(path)` and still fall back to `Error.message` for the long tail.

## HTTP

Expand Down Expand Up @@ -129,9 +129,9 @@ fn main() {
(http.POST, ["todos"]) -> match bytes.to_string(req.body) {
Ok(text) -> match json.parse(text, Todo) {
Ok(todo) -> reply(201, todo)
Err(e) -> reply(400, #{ "error": e.message() })
Err(e) -> reply(400, #{ "error": Error.message(e) })
}
Err(e) -> reply(400, #{ "error": e.message() })
Err(e) -> reply(400, #{ "error": Error.message(e) })
}
_ -> reply(404, #{ "error": "Not found" })
}
Expand Down
2 changes: 1 addition & 1 deletion docs/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,7 @@ fn main() {
Ok(Ok(contents)) -> println(contents)
Ok(Err(e)) -> println("the read failed: {e}")
Err(task.Timeout) -> println("the read took too long")
Err(e) -> println(e.message())
Err(e) -> println(Error.message(e))
}
}
```
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ fn read_head(path) {
```

Use `match` to handle the result. Every stdlib error enum (here `IoError`) implements the built-in
`Error` trait, so you can pattern-match on specific variants and fall back to `.message()` for
`Error` trait, so you can pattern-match on specific variants and fall back to `Error.message` for
anything else:

```silt
Expand All @@ -177,7 +177,7 @@ fn main() {
match io.read_file("app.json") {
Ok(content) -> println("loaded {string.length(content)} characters")
Err(io.NotFound(path)) -> println("file does not exist: {path}")
Err(e) -> println("error: {e.message()}")
Err(e) -> println("error: {Error.message(e)}")
}
}
```
Expand Down
10 changes: 5 additions & 5 deletions docs/language/bindings-and-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ fn main() {
let _ = task.spawn { -> println("in the background") }
match io.write_file("out.txt", "data") {
Ok(_) -> println("saved")
Err(e) -> println("not saved: {e.message()}")
Err(e) -> println("not saved: {Error.message(e)}")
}
}
```
Expand Down Expand Up @@ -185,9 +185,9 @@ What a `let`'s value can reach is decided by four rules:
module's impls give that module's traits (and the builtin traits):
a `let` that mentions another module's function is initialised after
every `let` those methods read.
- **A method call on a value whose type is a bounded type variable
(`fn f(x: a) where a: Label { x.label() }`) reaches every impl of the
method in the module**, whichever value the call is given.
- **A call through a trait with a value whose type is a bounded type
variable (`fn f(x: a) where a: Label { Label.label(x) }`) reaches every
impl of the method in the module**, whichever value the call is given.
- **A `let` that is a plain value runs nothing.** A plain value is a
literal, a name, a closure, or a list, tuple, record, map, set or
variant made of plain values. Such a `let` needs only the top-level
Expand Down Expand Up @@ -216,7 +216,7 @@ A closure handed to a function is not a plain value: `let f = wrap({ n
-> ... f(n - 1) ... })` runs `wrap`, which may call the closure.

- **Showing a value reaches the `Display` impls of its parts.**
Interpolation, `println`, `string.from` and `.display()` call the
Interpolation, `println` and `string.from` call the
`Display` impl written for each type the value is made of: its own,
its fields', its type arguments'. A value of an unknown type (a type
variable, an associated type such as `Self::Item`) may be of any
Expand Down
8 changes: 4 additions & 4 deletions docs/language/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ catalog is in [`docs/stdlib/errors.md`](../stdlib/errors.md); hovering
over an error type in your editor shows the same table.

You can either pattern-match on a specific variant, or fall back to
`.message()` for a rendered string:
`Error.message` for a rendered string:

```silt
import io
Expand All @@ -47,15 +47,15 @@ fn main() {
Ok(content) -> println("loaded {string.length(content)} bytes")
Err(io.NotFound(path)) -> println("file does not exist: {path}")
Err(io.PermissionDenied(path)) -> println("denied: {path}")
Err(e) -> println("error: {e.message()}")
Err(e) -> println("error: {Error.message(e)}")
}
}
```

Every stdlib error enum implements the built-in `Error` trait, which has
`Display` as a supertrait and provides `message(self) -> String`. String
interpolation `"{e}"` and `e.message()` produce the same text — silt routes
the `Display` impl through `Error::message` for stdlib error enums so the two
interpolation `"{e}"` and `Error.message(e)` produce the same text — silt routes
the `Display` impl through `Error.message` for stdlib error enums so the two
shapes never drift.

## The `?` Operator
Expand Down
59 changes: 31 additions & 28 deletions docs/language/generics.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ point to.
Inside the function a type variable stands for a type the function does
not know: the body must work for every type the caller may choose. It
cannot treat an `a` as an `Int`, return a `b` where it promised an `a`,
or call a method no `where` clause promises:
or ask of it a trait no `where` clause promises:

```silt
fn one(x: a) -> a { 1 } -- ERROR: expected a, got Int
fn second(x: a, y: b) -> a { y } -- ERROR: expected a, got b
fn show(x: a) -> String { x.display() } -- ERROR: `a` has no trait bound
fn show(x: a) -> String where a: Display { x.display() } -- ok
fn show(x: a) -> String { "{x}" } -- ERROR: `a` is not known to implement Display
fn show(x: a) -> String where a: Display { "{x}" } -- ok
```

An annotation in the body that writes one of the signature's variables
Expand Down Expand Up @@ -225,12 +225,12 @@ fn parse(body: String, type a) -> Result(a, DecodeError) where a: Parse {
}

fn try_from(x: a, type b) -> b where a: Convertible(b) {
x.convert()
Convertible.convert(x)
}
```

Inside the body, the `type a` parameter doubles as a dispatch target for
trait methods. `a.default()` invokes the `default` method of whichever
the trait methods that make a value. `a.default()` invokes the `default` method of whichever
`Default` impl matches the concrete type passed in — see
[Trait methods on types](#trait-methods-on-types) below.
`try_from` shows the companion pattern: a parameterized trait
Expand Down Expand Up @@ -381,7 +381,7 @@ import list
import string

fn shout(items: List(a)) -> List(String) where a: Display {
items |> list.map { x -> x.display() |> string.to_upper }
items |> list.map { x -> "{x}" |> string.to_upper }
}
```

Expand All @@ -399,7 +399,7 @@ type Box(a) {
trait Display for Box(a) where a: Display {
fn display(self) -> String {
match self {
Box(inner) -> "Box({inner.display()})"
Box(inner) -> "Box({inner})"
}
}
}
Expand Down Expand Up @@ -442,9 +442,9 @@ trait HashTable(Fn(Int) -> Int) for OtherStore { ... }
```

Supertraits can also reference the enclosing trait's params. The args
flow automatically — a `where x: Child(Int)` constraint makes
`Parent`'s methods callable on `x` with Parent's own params bound to
the same `Int`:
flow automatically — a `where x: Child(Int)` constraint lets `x` be
given to `Parent`'s methods with Parent's own params bound to the same
`Int`:

```silt
trait Parent(a) {
Expand All @@ -456,7 +456,7 @@ trait Child(a): Parent(a) {
}

fn use_parent(x: b, type a) -> a where b: Child(a) {
x.parent_method() -- returns a, bound by Child's arg
Parent.parent_method(x) -- returns a, bound by Child's arg
}
```

Expand All @@ -474,7 +474,7 @@ lowercase type variables bound elsewhere in the signature:

```silt
fn try_from(x: a, type b) -> b where a: Convertible(b) {
x.convert()
Convertible.convert(x)
}
```

Expand All @@ -494,16 +494,16 @@ supplied arguments when resolving method types at the call site.
Constraints transitively include supertraits. Given a user-defined
`Ordered` trait whose declaration names `Equal` as a supertrait
(hypothetical user-defined trait, not built-in), a `where a: Ordered`
bound makes methods from `Equal` callable on `a` as well:
bound gives `a` what `Equal` gives as well:

```silt
trait Ordered: Equal {
fn less(self, other: Self) -> Bool
}

fn sorted_unique(xs: List(a)) -> List(a) where a: Ordered {
-- Both a.less(b) (from Ordered) and a.equal(b) (from the
-- transitively-included Equal supertrait) are callable here.
-- Both Ordered.less(a, b) and a == b (from the transitively-
-- included Equal supertrait) work here.
...
}
```
Expand Down Expand Up @@ -532,9 +532,10 @@ error. See [Built-in Traits](traits.md#built-in-traits).

### Trait methods on types

Some trait methods take no `self` and only return `Self` — constructors
like `Default::default()` or `Monoid::empty()`. To invoke these without
an instance, call them on a **type descriptor**: either a bare type
Some trait methods take no value of the type and only make one —
constructors like `default()` or `empty()`. A call through the trait
could not say which impl it means, so these are called on a **type
descriptor**: either a bare type
name (`Int.default()`) or a `type a` parameter (`a.default()`):

```silt
Expand Down Expand Up @@ -563,19 +564,21 @@ The rules:
- **Dispatch is by the descriptor's carried type name.** At runtime the
descriptor `Int` resolves to the `Default` impl for `Int`; `Todo`
resolves to `Default for Todo`; etc.
- **The descriptor is a dispatch key, not an argument.** It doesn't
occupy a `self` slot. Method signatures that declare `Self` as a
parameter (e.g. `fn combine(a: Self, b: Self)`) receive only the
caller's explicit arguments.
- **The descriptor is a dispatch key, not an argument.** It occupies no
parameter of the method: `a.empty()` passes nothing.
- **A method that takes a value of the type is called through its
trait.** `Monoid.combine(x, y)` reaches the impl for the type of its
arguments; `a.combine(x, y)` and `Int.combine(1, 2)` are errors whose
help names that call.
- **`where` constraints are required** for the generic path. Writing
`fn f(type a) -> a { a.default() }` without `where a: Default`
rejects at the call to `a.default()` — the compiler can't prove an
impl exists.
- **A method without `self` is called on a type, never on a value.**
`x.default()` on a value `x: a` is an error that names the call to
write: `a.default()`, with a `type a` parameter. Nor is it a candidate
for a call on a value: with `where a: Foo, a: Bar`, where only `Bar`'s
`build` takes `self`, `x.build()` is `Bar`'s.
- **A method that makes a value is called on a type, never on a value
or through its trait.** `x.default()` on a value `x: a` is an error (a
dot after a value names a field) whose help names the call to write:
`a.default()`, with a `type a` parameter. `Default.default()` is an
error that says to call it on a type.
- **Ambiguity across traits is rejected.** If both `Foo` and `Bar`
declare a method `build` and `a` is constrained to `Foo + Bar`,
calling `a.build()` errors with "ambiguous method 'build' on `type a`:
Expand Down Expand Up @@ -800,7 +803,7 @@ trait Convert(Int) for String {
}

fn into(x: a, type b) -> b where a: Convert(b) {
x.convert()
Convert.convert(x)
}
```

Expand Down
9 changes: 4 additions & 5 deletions docs/language/loops-and-pipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ let a = list.filter(xs, { x -> x > 0 })
let b = xs |> list.filter { x -> x > 0 }
```

The right side is a call like any other, so the rule is the same when it
is a method call: the piped value is the first argument the call writes,
after the receiver. `5 |> acc.add()` is `acc.add(5)`.
The right side is a call like any other, so the rule is the same for a
call through a trait: `5 |> Greet.greet` is `Greet.greet(5)`, and
`5 |> Adder.add(3)` is `Adder.add(5, 3)`.

Without pipes, function composition nests inside-out. With pipes, it reads
top-to-bottom:
Expand Down Expand Up @@ -92,8 +92,7 @@ println("{user.name} is {user.age}") -- field access in interpolation
String interpolation automatically invokes the `Display` trait. User-defined
types and the displayable builtins implement `Display` automatically;
channels and function values are the exception — they do not implement
`Display`, so interpolating them is a compile error. No need to call
`.display()` explicitly.
`Display`, so interpolating them is a compile error.

Escape literal braces with backslash: `"\{not interpolation}"`.

Expand Down
17 changes: 7 additions & 10 deletions docs/language/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,11 @@ module `util`. A type whose name no other type of the program has
prints bare: `Pt {x: 1}`.

Two modules may also each declare a trait of one name, say `Show` with
a method `show`, and implement it for one type. A method call means the
method of the trait its module sees: a trait it declares, imports by
name, or reaches through a module it imports. A call that sees both
traits is an error at the call. The builtin traits (`Display`,
`Compare`, `Equal`, `Hash`, `Error`) are always seen: a trait of the
program with a method named `display` implemented for `Int` makes
`5.display()` ambiguous; call it through a `where` bound for the
trait. The structural traits of a type (`Equal`, `Hash`, `Compare`,
a method `show`, and implement it for one type. A call names the trait
it means, as it names any item: `a.Show.show(5)` and `b.Show.show(5)`
are two calls of two methods. A trait of the program with a method named
`display` implemented for `Int` is no `Display`: `Pretty.display(5)` calls
it, and interpolation still shows `5` structurally. The structural traits of a type (`Equal`, `Hash`, `Compare`,
`Display` without a written impl) go by the builtin traits of its
fields, whatever trait of the program has a method of the same name.

Expand Down Expand Up @@ -208,8 +205,8 @@ fn caller() -> Int {
```

A builtin module follows the same rule: after `import list`, a binding named
`list` hides the module inside its scope: `list.map` there is a method call
on the binding, which a `List` value does not have. No warning is given;
`list` hides the module inside its scope: `list.map` there is a field of
the binding, which a `List` value does not have. No warning is given;
rename the binding, or import the module under another name
(`import list as l`), to reach both.

Expand Down
Loading
Loading