diff --git a/docs/concurrency.md b/docs/concurrency.md index c463dbd3d..42f26e015 100644 --- a/docs/concurrency.md +++ b/docs/concurrency.md @@ -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 diff --git a/docs/getting-started.md b/docs/getting-started.md index c543b185c..2c62714f7 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -219,6 +219,7 @@ fn main() { ```sh silt run -- run a program +silt run -- a b -- run it with the arguments a and b (io.args()) silt run -w -- run and re-run on file changes silt check -- type-check without running silt check --format json -- type-check with JSON output (for CI/editors) diff --git a/docs/language/traits.md b/docs/language/traits.md index 08e252b66..27b074db6 100644 --- a/docs/language/traits.md +++ b/docs/language/traits.md @@ -727,7 +727,8 @@ 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 } @@ -735,6 +736,9 @@ 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` @@ -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 diff --git a/docs/stdlib/bytes.md b/docs/stdlib/bytes.md index b8a13888c..51d0b2cbc 100644 --- a/docs/stdlib/bytes.md +++ b/docs/stdlib/bytes.md @@ -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. diff --git a/docs/stdlib/channel-task.md b/docs/stdlib/channel-task.md index c04d242c3..7d1e858e5 100644 --- a/docs/stdlib/channel-task.md +++ b/docs/stdlib/channel-task.md @@ -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 diff --git a/docs/stdlib/list.md b/docs/stdlib/list.md index 30e23e051..d63117257 100644 --- a/docs/stdlib/list.md +++ b/docs/stdlib/list.md @@ -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] } ``` @@ -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] } ``` @@ -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 diff --git a/docs/stdlib/stream.md b/docs/stdlib/stream.md index 50bace2da..ee92673bc 100644 --- a/docs/stdlib/stream.md +++ b/docs/stdlib/stream.md @@ -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. diff --git a/src/cli/disasm.rs b/src/cli/disasm.rs index aaad8b095..5c3a8c3f7 100644 --- a/src/cli/disasm.rs +++ b/src/cli/disasm.rs @@ -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 []`. 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 ."); diff --git a/src/cli/help.rs b/src/cli/help.rs index 87604fcc2..c86b86429 100644 --- a/src/cli/help.rs +++ b/src/cli/help.rs @@ -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 @@ -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] " + "silt run [--watch] [--disassemble] [-- ...]" } /// Single source of truth for the `silt test` usage banner line. @@ -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(" -- ... 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 diff --git a/src/cli/run.rs b/src/cli/run.rs index f3fd51730..60e5c7c2d 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -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] [] [-- ...]`. 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); } diff --git a/src/cli/test.rs b/src/cli/test.rs index 0a546ce12..21519e5ce 100644 --- a/src/cli/test.rs +++ b/src/cli/test.rs @@ -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!(); diff --git a/src/cli/watch.rs b/src/cli/watch.rs index f0e668c0b..c3d2b9eca 100644 --- a/src/cli/watch.rs +++ b/src/cli/watch.rs @@ -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. diff --git a/src/typechecker/declare_traits.rs b/src/typechecker/declare_traits.rs index 36a5703c2..2c9079c99 100644 --- a/src/typechecker/declare_traits.rs +++ b/src/typechecker/declare_traits.rs @@ -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); } } diff --git a/tests/cli/watch_double_dash_separator_tests.rs b/tests/cli/watch_double_dash_separator_tests.rs index 8bcc93033..78244b461 100644 --- a/tests/cli/watch_double_dash_separator_tests.rs +++ b/tests/cli/watch_double_dash_separator_tests.rs @@ -160,3 +160,66 @@ fn check_dash_w_after_separator_does_one_shot_check() { "`silt check -- -w` must one-shot check and exit 0, not enter watch mode" ); } + +/// `silt run -w -- --help`: the `--help` is the program's, so the +/// watcher starts and runs the program, which sees it, and goes on +/// watching; silt prints no help. (The watcher took any `--help` as a +/// request for help, one after `--` too, ran the command once and +/// stopped.) +#[cfg(feature = "watch")] +#[test] +fn run_watch_with_help_after_separator_watches_and_forwards_arg() { + use std::io::{BufRead, BufReader}; + use std::process::Stdio; + + let dir = fresh_dir("run_w_help"); + let script = dir.join("dump_args.silt"); + write_args_dumper(&script); + let mut child = Command::new(env!("CARGO_BIN_EXE_silt")) + .args(["run", "-w"]) + .arg(&script) + .args(["--", "--help"]) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .expect("failed to spawn silt"); + let stdout = child.stdout.take().expect("stdout"); + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + for line in BufReader::new(stdout).lines() { + let Ok(line) = line else { break }; + if tx.send(line).is_err() { + break; + } + } + }); + // The first line of the program's own output, past what the watcher + // writes before each run. + let mut seen = Vec::new(); + let found = loop { + match rx.recv_timeout(Duration::from_secs(15)) { + Ok(line) if line.trim() == "--help" => break true, + Ok(line) if line.starts_with("Usage:") => break false, + Ok(line) => seen.push(line), + Err(_) => break false, + } + }; + // And it goes on watching: its output does not end. (One that + // ends within the wait is a watcher that ran the program once and + // stopped.) + let watching = found + && !matches!( + rx.recv_timeout(Duration::from_secs(3)), + Err(mpsc::RecvTimeoutError::Disconnected) + ); + let _ = child.kill(); + let _ = child.wait(); + assert!( + found, + "`silt run -w -- --help` must run the program with `--help`; got: {seen:?}" + ); + assert!( + watching, + "`silt run -w -- --help` must go on watching" + ); +} diff --git a/tests/golden/cli/disasm/program_args__a_help_flag_after_the_separator_is_the_program_s/silt.toml b/tests/golden/cli/disasm/program_args__a_help_flag_after_the_separator_is_the_program_s/silt.toml new file mode 100644 index 000000000..345933707 --- /dev/null +++ b/tests/golden/cli/disasm/program_args__a_help_flag_after_the_separator_is_the_program_s/silt.toml @@ -0,0 +1,3 @@ +[package] +name = "app" +version = "0.1.0" diff --git a/tests/golden/cli/disasm/program_args__a_help_flag_after_the_separator_is_the_program_s/src/main.silt b/tests/golden/cli/disasm/program_args__a_help_flag_after_the_separator_is_the_program_s/src/main.silt new file mode 100644 index 000000000..52d91cabc --- /dev/null +++ b/tests/golden/cli/disasm/program_args__a_help_flag_after_the_separator_is_the_program_s/src/main.silt @@ -0,0 +1,7 @@ +-- cmd: disasm -- --help +-- What follows `--` is the program's: silt disassembles the program +-- and prints no help. +-- stdout-contains: "disassembled" +fn main() { + println("disassembled") +} diff --git a/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/case.stderr b/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/case.stderr new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/case.stdout b/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/case.stdout new file mode 100644 index 000000000..753984fb7 --- /dev/null +++ b/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/case.stdout @@ -0,0 +1,2 @@ +--help +-h diff --git a/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/silt.toml b/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/silt.toml new file mode 100644 index 000000000..345933707 --- /dev/null +++ b/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/silt.toml @@ -0,0 +1,3 @@ +[package] +name = "app" +version = "0.1.0" diff --git a/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/src/main.silt b/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/src/main.silt new file mode 100644 index 000000000..1032db620 --- /dev/null +++ b/tests/golden/cli/run/program_args__a_help_flag_after_the_separator_is_the_program_s/src/main.silt @@ -0,0 +1,9 @@ +-- cmd: run -- --help -h +-- What follows `--` is the program's, a help flag too: the program +-- runs and sees it in io.args(), and silt prints no help. +import io +import list + +fn main() { + io.args() |> list.each { arg -> println(arg) } +} diff --git a/tests/golden/cli/run/run_help__names_the_program_arguments.silt b/tests/golden/cli/run/run_help__names_the_program_arguments.silt new file mode 100644 index 000000000..fc527498c --- /dev/null +++ b/tests/golden/cli/run/run_help__names_the_program_arguments.silt @@ -0,0 +1,4 @@ +-- cmd: run --help +fn main() { + println("not run") +} diff --git a/tests/golden/cli/run/run_help__names_the_program_arguments.stderr b/tests/golden/cli/run/run_help__names_the_program_arguments.stderr new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/cli/run/run_help__names_the_program_arguments.stdout b/tests/golden/cli/run/run_help__names_the_program_arguments.stdout new file mode 100644 index 000000000..7397d3965 --- /dev/null +++ b/tests/golden/cli/run/run_help__names_the_program_arguments.stdout @@ -0,0 +1,13 @@ +Usage: silt run [--watch] [--disassemble] [-- ...] + +Options: + --watch, -w Re-run on file changes + --disassemble Show bytecode disassembly instead of running + -- ... Pass the arguments after it to the program (io.args()) + +Examples: + silt run (inside a package, runs src/main.silt) + silt run main.silt + silt run main.silt -- input.txt + silt run --watch main.silt + silt run --disassemble main.silt diff --git a/tests/golden/cli/test/program_args__the_separator_is_refused.silt b/tests/golden/cli/test/program_args__the_separator_is_refused.silt new file mode 100644 index 000000000..990eccf3d --- /dev/null +++ b/tests/golden/cli/test/program_args__the_separator_is_refused.silt @@ -0,0 +1,8 @@ +-- cmd: test -- +-- exit: 1 +-- `silt test` runs no program that could read arguments after `--`. +import test + +fn test_one() { + test.assert(true) +} diff --git a/tests/golden/cli/test/program_args__the_separator_is_refused.stderr b/tests/golden/cli/test/program_args__the_separator_is_refused.stderr new file mode 100644 index 000000000..f021ac11a --- /dev/null +++ b/tests/golden/cli/test/program_args__the_separator_is_refused.stderr @@ -0,0 +1,2 @@ +silt test takes no program arguments: '--' passes arguments to a program run by 'silt run' +Run 'silt test --help' for usage. diff --git a/tests/golden/cli/test/program_args__the_separator_is_refused.stdout b/tests/golden/cli/test/program_args__the_separator_is_refused.stdout new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/typecheck/operators/number__sealed.silt b/tests/golden/typecheck/operators/number__sealed.silt index 94e5eb5e5..3ebe69320 100644 --- a/tests/golden/typecheck/operators/number__sealed.silt +++ b/tests/golden/typecheck/operators/number__sealed.silt @@ -1,5 +1,6 @@ -- cmd: check -- exit: 1 +-- stderr-not-contains: Equality and order idioms -- stderr-contains: error[type]: trait 'Number' cannot be implemented by hand: it is the types arithmetic is on, Int and Float trait Number for String {} fn main() { println(1) } diff --git a/tests/golden/typecheck/traits/sealed_traits__the_help_points_at_the_idioms.silt b/tests/golden/typecheck/traits/sealed_traits__the_help_points_at_the_idioms.silt new file mode 100644 index 000000000..13e830cc2 --- /dev/null +++ b/tests/golden/typecheck/traits/sealed_traits__the_help_points_at_the_idioms.silt @@ -0,0 +1,15 @@ +-- The error of a hand-written impl of a sealed trait points at what to +-- write instead: the idioms section of the traits page. +-- cmd: check +-- exit: 1 +-- stderr-contains: trait 'Equal' cannot be implemented by hand +-- stderr-contains: = help: to compare, order or hash by something else (a key, a normalised value), see "Equality and order idioms" in docs/language/traits.md +type Email { address: String } + +trait Equal for Email { + fn equal(self, other: Email) -> Bool { + true + } +} + +fn main() {}