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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,54 @@ before touching `logs`"), check the high-priority channel first with
`channel.try_receive` and fall back to `channel.select` only when it is
empty. `select` itself gives you fairness, not ordering.

### Receiving from channels of different types

The operations of one `channel.select` are of one type, and so are the
channels of one `stream.merge`. To receive from channels that carry
different types, give them one: declare an enum with a variant for each,
turn each channel into a channel of the enum with `stream.map`, and
merge those. One `match` then handles every kind of message, and the
merged channel closes when all of its inputs have.

```silt
import channel
import stream
import task

type Event {
Key(String),
Tick(Int),
}

fn main() {
let keys = channel.new(4)
let ticks = channel.new(4)
let _ = task.spawn { ->
channel.send(keys, "q")
channel.close(keys)
}
let _ = task.spawn { ->
channel.send(ticks, 1)
channel.send(ticks, 2)
channel.close(ticks)
}
let from_keys = stream.map(keys) { k -> Key(k) }
let from_ticks = stream.map(ticks) { n -> Tick(n) }
let events = stream.merge([from_keys, from_ticks])
let count = stream.fold(events, 0) { seen, event ->
match event {
Key(k) -> println("key {k}")
Tick(n) -> println("tick {n}")
}
seen + 1
}
println("{count} events") -- 3 events
}
```

The order of messages from different channels is the order they arrive
in; the messages of one channel keep theirs.

### Graceful shutdown via channel.close

Use `channel.close` to signal that a pipeline stage is done. Downstream stages
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ fn main() {

```sh
silt run <file.silt> -- run a program
silt run <file.silt> -- a b -- run it with the arguments a and b (io.args())
silt run -w <file.silt> -- run and re-run on file changes
silt check <file.silt> -- type-check without running
silt check --format json <file.silt> -- type-check with JSON output (for CI/editors)
Expand Down
82 changes: 81 additions & 1 deletion docs/language/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -727,14 +727,18 @@ and `==`, do what they would. A record is ordered by its fields
in the order the type declares them, an enum by its variants in the
order it declares them and then by their payloads. A
hand-written `trait Equal for T`, `trait Compare for T` or
`trait Hash for T` is an error:
`trait Hash for T` is an error (what to write instead is in
[Equality and order idioms](#equality-and-order-idioms)):

```silt
type Version { major: Int, minor: Int }
trait Compare for Version { ... }
-- error: trait 'Compare' cannot be implemented by hand: it is derived
-- structurally for every type whose fields support it — remove this
-- impl; Equal, Compare and Hash are derived
-- help: to compare, order or hash by something else (a key, a
-- normalised value), see "Equality and order idioms" in
-- docs/language/traits.md
```

What "every field supports them" means, for `Equal`, `Hash`, `Compare`
Expand Down Expand Up @@ -786,6 +790,82 @@ The `Error` trait has supertrait `Display` and one method,
`JsonError`, `HttpError`, …) implements it explicitly, and user code
can implement it on its own error types.

### Equality and order idioms

Since `==`, `<` and map keys always use a type's structure, a type
compares by something else through what it holds or how it is used,
not through an impl:

- **Normalise once, in a constructor function.** A value that has
several spellings of one meaning (an e-mail address in any case, a
path with a trailing slash) is made by one function that writes the
one spelling. Its `==`, its hash and its order are then the meaning's.
- **Key a map by an id**, not by the record: `map.set(users, u.id, u)`.
Two versions of one record (an old and a new name) are one entry, and
the record may hold what has no `Hash`.
- **Key a record that holds a function by its name.** A function has no
`Equal` or `Hash`, so neither has a record with one: keep such records
in a map from their names, and compare the names.
- **Sort by a key** with `list.sort_by`: a tuple key sorts by its parts
in turn, `|> list.reverse` sorts descending, and `list.sort_with`
takes a comparison of two elements (`Compare.compare(b, a)` for
descending order).
- **Find duplicates by a key** with `list.unique_by`, which keeps the
first element of each key.
- **Order versions as records.** A record compares field by field in the
order the type declares them, so `Version { major, minor, patch }`
orders versions as versions are ordered, with `<`, `list.sort` and
map keys, and needs no impl.

```silt
import list
import map
import string

type Email {
address: String,
}

fn email(text: String) -> Email {
Email { address: string.to_lower(string.trim(text)) }
}

type Version {
major: Int,
minor: Int,
patch: Int,
}

type Command {
name: String,
run: Fn(Int) -> Int,
}

fn main() {
println(email(" Ann@Example.com") == email("ann@example.com")) -- true

let v1 = Version { major: 1, minor: 10, patch: 0 }
let v2 = Version { major: 1, minor: 9, patch: 3 }
println(v2 < v1) -- true

let commands = [
Command { name: "double", run: { n -> n * 2 } },
Command { name: "negate", run: { n -> -n } },
]
let by_name = map.from_entries(list.map(commands) { c -> (c.name, c) })
match map.get(by_name, "double") {
Some(c) -> println(c.run(21)) -- 42
None -> println("no such command")
}

let words = ["Apple", "avocado", "Banana", "apple"]
let firsts = list.unique_by(words) { w -> string.to_lower(string.slice(w, 0, 1)) }
println(firsts) -- [Apple, Banana]
let ranked = list.sort_by(words) { w -> (string.length(w), w) } |> list.reverse
println(ranked) -- [avocado, Banana, apple, Apple]
}
```

## Where an Impl Is Written

An impl is written in the module that declares its trait, or in the
Expand Down
4 changes: 0 additions & 4 deletions docs/stdlib/bytes.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,3 @@ indices to the length, as `string.slice` does, and returns `Bytes` (empty if
and `bytes.ends_with` return `true` for an empty prefix / suffix.
- `bytes.split` with an empty `sep` is a runtime error (ambiguous). Splitting an empty
`b` yields `[empty_bytes]` — one element — mirroring `string.split`.
- A future silt release may promote `Bytes` to a language-level type with
literal syntax (e.g. `b"hello"`) and method-form access. Today's API
is forward-compatible: programs written against the current `bytes`
module surface will continue to behave identically.
5 changes: 5 additions & 0 deletions docs/stdlib/channel-task.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ whose channel is closed while it waits to send.

Receive and send arms can be mixed freely in the same call.

The arms of one call are of one type, `ChannelOp(a)`. To receive from
channels of different types, map each to a channel of one enum and merge
them: see [Receiving from channels of different
types](../concurrency.md#receiving-from-channels-of-different-types).

Receive-only form:

```silt
Expand Down
26 changes: 24 additions & 2 deletions docs/stdlib/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,13 +691,15 @@ fn main() {
list.sort(xs: List(a)) -> List(a) where a: Compare
```

Returns a new list sorted in natural (ascending) order.
Returns a new list sorted in natural (ascending) order. For the
descending order, reverse the sorted list:

```silt
import list

fn main() {
println(list.sort([3, 1, 2])) -- [1, 2, 3]
println(list.sort([3, 1, 2]) |> list.reverse) -- [3, 2, 1]
}
```

Expand Down Expand Up @@ -733,16 +735,35 @@ list.sort_by(xs: List(a), key: Fn(a) -> b) -> List(a) where b: Compare
```

Returns a new list sorted by the result of applying the key function to each
element.
element. The sort is stable: elements with equal keys keep their order.

A tuple key sorts by its first part, then by the next where the first
parts are equal, and so on. A part of an `Int` or `Float` key sorts
descending when it is negated; for the descending order of a key that
cannot be negated, use `list.sort_with` (or `|> list.reverse` on the
sorted list, which also reverses the order of equal keys).

```silt
import list
import string

type Player {
name: String,
score: Int,
}

fn main() {
let words = ["banana", "fig", "apple"]
let sorted = words |> list.sort_by { w -> string.length(w) }
println(sorted) -- [fig, apple, banana]
let players = [
Player { name: "Bo", score: 7 },
Player { name: "Al", score: 9 },
Player { name: "Cy", score: 7 },
]
-- By score, highest first, then by name.
let ranked = players |> list.sort_by { p -> (-p.score, p.name) }
println(list.map(ranked) { p -> p.name }) -- [Al, Bo, Cy]
}
```

Expand All @@ -756,6 +777,7 @@ list.sort_with(xs: List(a), cmp: Fn(a, a) -> Int) -> List(a)
Returns a new list sorted by the comparison function: `cmp(a, b)` is below
0 when `a` comes before `b`, 0 when the two are equal, and above 0 when `a`
comes after `b`. The sort is stable: equal elements keep their order.
`Compare.compare(b, a)` sorts descending, by any key that has `Compare`.

```silt
import list
Expand Down
6 changes: 0 additions & 6 deletions docs/stdlib/stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,3 @@ fn main() {
- **`stream.repeat` is infinite.** Always pair it with `take`,
`take_while`, or another bounded sink — `collect` on an unbounded
stream will hang.

## Forward compatibility

Function names mirror what method-form dispatch (`s.map(f)`) would look
like once silt grows a `Stream` trait. Existing silt programs will
continue to compile and behave identically when that trait lands.
4 changes: 2 additions & 2 deletions src/cli/disasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ use silt::disassemble::disassemble_function;
use silt::intern::intern;
use silt::session::{ENTRY_POINT, Entry, LockPolicy};

use crate::cli::help::disasm_usage_banner;
use crate::cli::help::{asks_for_help, disasm_usage_banner};
use crate::cli::package::resolve_package_entry_point;
use crate::cli::paths::{ProgramFiles, door_diagnostics, open_entry_or_exit};

/// Dispatch `silt disasm [<file>]`.
pub(crate) fn dispatch(args: &[String]) {
if args[2..].iter().any(|a| a == "--help" || a == "-h") {
if asks_for_help(&args[2..]) {
println!("Usage: {}", disasm_usage_banner());
println!();
println!("Prints the compiled bytecode disassembly for <file.silt>.");
Expand Down
13 changes: 12 additions & 1 deletion src/cli/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ pub(crate) fn usage_text() -> String {
/// noise. Keep in sync with the match arms in `run_main`.
pub(crate) const GLOBAL_FLAGS: &[&str] = &["--version", "-V", "-v", "--help", "-h", "help"];

/// Whether `--help` or `-h` is one of silt's own arguments in `args`:
/// the ones before `--`, after which every argument is the program's
/// (`silt run main.silt -- --help` runs the program with `--help`).
pub(crate) fn asks_for_help(args: &[String]) -> bool {
(args.iter())
.take_while(|a| *a != "--")
.any(|a| a == "--help" || a == "-h")
}

/// Single source of truth for the `silt check` usage banner line.
/// Both the `--help` path and the "no arguments given" path render
/// from this so they can't drift apart. A regression test in
Expand All @@ -131,7 +140,7 @@ pub(crate) fn check_usage_banner() -> &'static str {
/// also accepts no file argument when invoked inside a package — the
/// optional-no-arg behavior is documented separately in the help text.
pub(crate) fn run_usage_banner() -> &'static str {
"silt run [--watch] [--disassemble] <file.silt>"
"silt run [--watch] [--disassemble] <file.silt> [-- <args>...]"
}

/// Single source of truth for the `silt test` usage banner line.
Expand Down Expand Up @@ -172,10 +181,12 @@ pub(crate) fn run_help_text() -> String {
s.push_str("Options:\n");
s.push_str(" --watch, -w Re-run on file changes\n");
s.push_str(" --disassemble Show bytecode disassembly instead of running\n");
s.push_str(" -- <args>... Pass the arguments after it to the program (io.args())\n");
s.push('\n');
s.push_str("Examples:\n");
s.push_str(" silt run (inside a package, runs src/main.silt)\n");
s.push_str(" silt run main.silt\n");
s.push_str(" silt run main.silt -- input.txt\n");
s.push_str(" silt run --watch main.silt\n");
s.push_str(" silt run --disassemble main.silt\n");
s
Expand Down
4 changes: 2 additions & 2 deletions src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ use silt::session::{Entry, LockPolicy};
use silt::source::{FileId, SourceMap, Span};
use silt::vm::{Vm, VmError};

use crate::cli::help::{run_help_text, run_usage_banner};
use crate::cli::help::{asks_for_help, run_help_text, run_usage_banner};
use crate::cli::package::resolve_package_entry_point;
use crate::cli::paths::{ProgramFiles, door_diagnostics, open_entry_or_exit};

/// Dispatch `silt run [--disassemble] [<file>] [-- <program-args>...]`.
pub(crate) fn dispatch(args: &[String]) {
if args[2..].iter().any(|a| a == "--help" || a == "-h") {
if asks_for_help(&args[2..]) {
print!("{}", run_help_text());
process::exit(0);
}
Expand Down
8 changes: 8 additions & 0 deletions src/cli/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ pub(crate) fn dispatch(args: &[String]) {
}
filter = Some(value.to_string());
i += 1;
} else if args[i] == "--" {
// The tests are functions of the files, called with no
// arguments: nothing could read what would follow.
eprintln!(
"silt test takes no program arguments: '--' passes arguments to a program run by 'silt run'"
);
eprintln!("Run 'silt test --help' for usage.");
process::exit(1);
} else if args[i] == "--help" || args[i] == "-h" {
println!("Usage: {}", test_usage_banner());
println!();
Expand Down
2 changes: 1 addition & 1 deletion src/cli/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ fn handle_watch(args: &[String]) {
// otherwise hang silently forever, because the initial rerun
// prints a 1-line usage banner and the loop just sits there
// waiting for saves).
let wants_help = filtered.iter().any(|a| a == "--help" || a == "-h");
let wants_help = crate::cli::help::asks_for_help(&filtered);
if wants_help {
// Run the subcommand once so its own help handler fires, then
// return without entering the watch loop.
Expand Down
18 changes: 11 additions & 7 deletions src/typechecker/declare_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,18 +809,22 @@ impl TypeChecker {
_ => true,
});
for (trait_name, span) in errors {
let message = if resolve(trait_name) == "Number" {
"trait 'Number' cannot be implemented by hand: it is the types arithmetic \
is on, Int and Float"
.to_string()
let text = if resolve(trait_name) == "Number" {
let message = "trait 'Number' cannot be implemented by hand: it is the \
types arithmetic is on, Int and Float";
(message.to_string(), None)
} else {
format!(
let message = format!(
"trait '{trait_name}' cannot be implemented by hand: it is derived \
structurally for every type whose fields support it — remove this \
impl; Equal, Compare and Hash are derived"
)
);
let help = "to compare, order or hash by something else (a key, a \
normalised value), see \"Equality and order idioms\" in \
docs/language/traits.md";
(message, Some(help.to_string()))
};
self.error(Code::InvalidTraitImpl, message, span);
self.error_help(Code::InvalidTraitImpl, text, span);
}
}

Expand Down
Loading
Loading