Each card is one command from a committed golden transcript: the claim it proves, the command, and the diagnostics Ship reported. A HOLE: note names extra diagnostics that are known noise, with the issue that removes them.
Blocks
A trailing semicolon, a return written directly in a value block, a break with no loop or block to leave, and a label that is unknown, reused, or unused are errors. So is an exit that would leave a callback, a spawn task, or defer cleanup. Blocks and control flow shows the accepted forms.
A semicolon-terminated expression is a statement, not a block value.
let value = {
42;
};
$ship-types-test value-trailing-expr-reject.shp
error: expression statement must have type Unit
--> value-trailing-expr-reject.shp:2:3
|
2 | 42;
| ^^ use a trailing expression or `break expr;` to produce a block value, or `ignore(expr)` to discardA semicolon after the last expression discards it; omit ; for a tail value. HOLE: the block must produce a value and expected Int, found Unit errors after the first are follow-on noise (SHIP-256).
let value = fn() -> Int {
return {
let x = 1;
x;
};
};
$ship-types-test value-multi-stmt-no-break-reject.shp
error: expression statement must have type Unit
--> value-multi-stmt-no-break-reject.shp:4:5
|
4 | x;
| ^ use a trailing expression or `break expr;` to produce a block value, or `ignore(expr)` to discard
error: block must produce a value
--> value-multi-stmt-no-break-reject.shp:4:5
|
4 | x;
| ^ add a trailing expression or `break expr;` before the block ends
error: type mismatch
--> value-multi-stmt-no-break-reject.shp:2:10
|
2 | return {
| ^ expected Int, found UnitValue blocks use break, not return.
let value = fn() -> Int {
return {
return 42;
};
};
$ship-types-test value-return-reject.shp
error: `return` is not valid in value-position blocks
--> value-return-reject.shp:3:12
|
3 | return 42;
| ^^ use `break` to produce a block valueA semicolon after a function tail makes it a Unit statement. HOLE: both expected Int, found Unit mismatches are follow-on noise (SHIP-256).
let value = fn() -> Int {
7;
};
$ship-types-test function-trailing-expr-reject.shp
error: expression statement must have type Unit
--> function-trailing-expr-reject.shp:2:3
|
2 | 7;
| ^ use a trailing expression or `return expr;` to produce a function value, or `ignore(expr)` to discard
error: type mismatch
--> function-trailing-expr-reject.shp:1:25
|
1 | let value = fn() -> Int {
| ^ expected Int, found Unit
error: type mismatch
--> function-trailing-expr-reject.shp:1:21
|
1 | let value = fn() -> Int {
| ^^^ expected Int, found UnitA break needs an enclosing loop or value block; a function body is neither.
let value = fn() -> Int {
break 7;
};
$ship-types-test function-break-reject.shp
error: `break` has no enclosing loop or value block
--> function-break-reject.shp:2:3
|
2 | break 7;
| ^^^^^^^ use `return` to leave a functionAn arm or fallback exit leaves the block around its match, so an arm break in an arrow body has nothing to leave.
let pick = (flag: Bool) -> Int =>
match flag {
| True -> 3
| False -> break 4
};
$ship-types-test arm-break-no-target-reject.shp
error: `break` has no enclosing loop or value block
--> arm-break-no-target-reject.shp:4:16
|
4 | | False -> break 4
| ^^^^^^^ use `return` to leave a functionA loop exit carries no value; label the block to leave it with one.
let first = fn(items: Vector(Int)) -> Int {
let mut index = 0;
while index < Vector.length(items) {
break items[index];
}
0
};
$ship-types-test loop-break-value-reject.shp
error: a loop `break` carries no value
--> loop-break-value-reject.shp:4:11
|
4 | break items[index];
| ^^^^^^^^^^^^ this loop leaves with `break;`
help: to leave a block with this value, label the block (`'name: { ... }`) and write `break 'name value;`break 'name must name an enclosing block or loop.
let value = fn() -> Int {
let x = {
break 'missing 1;
};
x
};
$ship-types-test label-unknown-reject.shp
error: unknown label `'missing`
--> label-unknown-reject.shp:3:11
|
3 | break 'missing 1;
| ^^^^^^^^ no enclosing block or loop has this labelA label may not reuse the name of an enclosing label.
let scan = fn(limit: Int) -> Int {
'scan: {
let mut index = 0;
'scan: while index < limit {
break 'scan;
}
break 'scan index;
}
};
$ship-types-test label-shadow-reject.shp
error: label `'scan` is already in scope
--> label-shadow-reject.shp:4:5
|
4 | 'scan: while index < limit {
| ^^^^^ rename this label
--> label-shadow-reject.shp:2:3
|
2 | 'scan: {
| ----- the enclosing labelA label that no break names is an error.
let value = fn() -> Int {
let x = 'value: {
1
};
x
};
$ship-types-test label-unused-reject.shp
error: label `'value` is never used
--> label-unused-reject.shp:2:11
|
2 | let x = 'value: {
| ^^^^^^ remove it, or leave with `break 'value`Exits stay in their function, so a callback cannot leave its caller's block.
let first_big = fn(items: Vector(Int)) -> Int {
'search: {
Vector.iter(
items,
f = fn(item) {
if item > 10 {
break 'search item;
}
ignore(item);
},
);
0
}
};
$ship-types-test label-callback-reject.shp
error: `break 'search` cannot leave this function
--> label-callback-reject.shp:7:11
|
7 | break 'search item;
| ^^^^^^^^^^^^^^^^^^ a callback ends with `return`, not its caller's labels
--> label-callback-reject.shp:2:3
|
2 | 'search: {
| ------- label declared hereA spawn body is its own task and leaves with return.
let start = fn() -> Int {
'outer: {
let task = spawn {
break 'outer 1;
};
await task
}
};
$ship-types-test label-spawn-reject.shp
error: `break 'outer` cannot leave a `spawn` body
--> label-spawn-reject.shp:4:7
|
4 | break 'outer 1;
| ^^^^^^^^^^^^^^ a task ends with `return`
--> label-spawn-reject.shp:2:3
|
2 | 'outer: {
| ------ label declared heredefer cleanup always runs to its end.
let cleanup = fn() -> Int {
'outer: {
defer {
break 'outer 1;
}
2
}
};
$ship-types-test label-defer-reject.shp
error: `break 'outer` cannot leave `defer` cleanup
--> label-defer-reject.shp:4:7
|
4 | break 'outer 1;
| ^^^^^^^^^^^^^^ cleanup always runs to its end
--> label-defer-reject.shp:2:3
|
2 | 'outer: {
| ------ label declared hereFunction bodies reject discarded non-Unit statements.
type Model = {
title: String;
}
let smell = fn(model: Model) -> Model {
{ model with title = "x"; };
return model;
};
$ship-types-test function-discard-record-reject.shp
error: expression statement must have type Unit
--> function-discard-record-reject.shp:6:3
|
6 | { model with title = "x"; };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ use a trailing expression or `return expr;` to produce a function value, or `ignore(expr)` to discardConcrete non-Unit callbacks still fail against the inferred statement contract.
let call = fn(value, ~f) {
f(value);
};
let bad = () => call(1, f = (value) => value);
$ship-types-test statement-callback-non-unit-reject.shp
error: type mismatch
--> statement-callback-non-unit-reject.shp:5:40
|
5 | let bad = () => call(1, f = (value) => value);
| ^^^^^ expected Unit, found IntStatement-position match arms report one clean Unit diagnostic.
let bad = fn(flag: Bool) {
match flag {
| True -> 1
| False -> ()
};
};
$ship-types-test statement-match-non-unit-reject.shp
error: expression statement must have type Unit
--> statement-match-non-unit-reject.shp:3:15
|
3 | | True -> 1
| ^ expression statements must have type Unit; use `ignore(expr)` to discardMulti-statement match arms reject return (value-block rule).
let classify = (flag: Bool) -> Int =>
match flag {
| True -> {
let x = 1;
return x;
}
| False -> 2
};
$ship-types-test match-arm-block-return-reject.shp
error: `return` is not valid in value-position blocks
--> match-arm-block-return-reject.shp:5:14
|
5 | return x;
| ^ use `break` to produce a block valueResource scopes
A return or break that would leave a with body, a try arm that names the region's binding, a With.execute that may skip its body, one that raises an expected error of its own, and a region written before a manager whose execute uses services are all errors. Resource scopes shows the accepted forms.
A with body ends only with its value or its failure: return and break never leave it.
type! TestResource = {
name: String;
}
let open_resource = (name: String) -> TestResource =>
TestResource { name = name; };
module behavior With(TestResource) {
execute = fn(manager: TestResource, body: (fn(TestResource) -> ScopeExit('error)) @ required) -> Unit {
print("enter ${manager.name}");
ignore(body(manager));
print("exit ${manager.name}");
};
}
let run = fn() -> String {
with handle = open_resource("tx") {
return "early";
};
return "after";
};
$ship check return-input.shp
error: `return` cannot leave a `with` body
--> return-input.shp:18:12
|
18 | return "early";
| ^^^^^^^ a `with` body ends with its value
help: make the body's value the result of `with`, then return it after the `with`A with body ends only with its value or its failure: return and break never leave it.
type! TestResource = {
name: String;
}
let open_resource = (name: String) -> TestResource =>
TestResource { name = name; };
module behavior With(TestResource) {
execute = (manager: TestResource, body: (fn(TestResource) -> ScopeExit('error)) @ required) -> Unit =>
ignore(body(manager));
}
let run = fn(items: Vector(Int)) -> Unit {
let mut index = 0;
while index < Vector.length(items) {
with handle = open_resource("tx") {
if items[index] > 10 {
break;
}
};
index := index + 1;
}
};
$ship check break-input.shp
error: `break` cannot leave a `with` body
--> break-input.shp:18:9
|
18 | break;
| ^^^^^ a `with` body ends with its value
help: make the body's value the result of `with`, then leave after the `with`The arms of try name = manager { body } { arms } sit outside the scope of name.
type! Savepoint = {
name: String;
}
let savepoint = (name: String) -> Savepoint =>
Savepoint { name = name; };
module behavior With(Savepoint) {
execute = (manager: Savepoint, body: (fn(Savepoint) -> ScopeExit('error)) @ required) -> Unit =>
ignore(body(manager));
}
let insert = fn() -> String {
try sp = savepoint("sp") {
error :unique;
} {
| :unique -> sp.name
}
};
$ship check try-arms-input.shp
error: unknown name `sp`
--> try-arms-input.shp:17:18
|
17 | | :unique -> sp.name
| ^^^^^^^ not found in this scope
note: `sp` is bound only inside the `try` body; the arms run after its scope endsWith.execute calls body exactly once on every path, and cannot raise an expected error of its own: a manager decides cleanup but never the outcome.
type! Gate = {
enabled: Bool;
}
module behavior With(Gate) {
execute = fn(gate: Gate, body: (fn(Gate) -> ScopeExit('error)) @ required) -> Unit {
if gate.enabled {
ignore(body(gate));
}
};
}
$ship check skip-input.shp
error: callback `body` is `@ required` but is not called exactly once on this path
--> skip-input.shp:6:86
|
6 | execute = fn(gate: Gate, body: (fn(Gate) -> ScopeExit('error)) @ required) -> Unit {
| ^ path exits here
--> skip-input.shp:6:34
|
6 | execute = fn(gate: Gate, body: (fn(Gate) -> ScopeExit('error)) @ required) -> Unit {
| ------------------------------------------ required exactly once hereWith.execute calls body exactly once on every path, and cannot raise an expected error of its own: a manager decides cleanup but never the outcome.
type! Commit = {
name: String;
}
module behavior With(Commit) {
execute = fn(commit: Commit, body: (fn(Commit) -> ScopeExit('error)) @ required) -> Unit {
ignore(body(commit));
error :commit_failed;
};
}
$ship check cleanup-error-input.shp
error: module `With(Commit)` does not satisfy `With`
--> cleanup-error-input.shp:6:13
|
6 | execute = fn(commit: Commit, body: (fn(Commit) -> ScopeExit('error)) @ required) -> Unit {
| ^ `With.execute` requires `fn!('error, 'effects)(manager: Commit, body: (fn(resource: Commit) -> ScopeExit('error)) @ required) -> Unit, eff 'effects`, but `With(Commit).execute` is `fn(commit: Commit, body: (fn(Commit) -> ScopeExit('a)) @ required) -> Unit, exn [ :commit_failed ]`
note: type mismatch: expected [ ], found [ :commit_failed ]A region adds what its manager's execute performs to the enclosing function, so the manager's With must be declared before the regions that use it.
effect service Journal {
write: fn(entry: String) -> Unit;
}
type Gate = {
name: String;
}
let guarded = fn(gate: Gate) {
with g = gate {
g.name
}
};
module behavior With(Gate) {
execute = fn(gate: Gate, body: (fn(Gate) -> ScopeExit('error)) @ required) -> Unit {
Journal.write("open " ++ gate.name);
ignore(body(gate));
};
}
$ship check late-manager-input.shp
error: `With(Gate)` is declared after this region
--> late-manager-input.shp:10:12
|
10 | with g = gate {
| ^^^^ this region cannot see that `With(Gate).execute` performs `Journal`
--> late-manager-input.shp:15:17
|
15 | module behavior With(Gate) {
| ---- declared here
help: declare `With(Gate)` before the regions that use itOutcome pipelines
A wrapped function that declares its own result, a non-function on the left of >>, a stage that is none of the five kinds, >> mixed with |> or ?> without parentheses, an impossible handler arm, success arms and residual binders in a handler, and a handler used as a value are errors. Outcome pipelines shows the accepted forms.
The left side of >> is a function whose stages decide its result, so it cannot declare one, and it must be a function at all.
let to_store = function {
| error :db(_) -> error :store("database")
};
let wrapped = fn(id: Int) -> Int { id } >> to_store;
$ship check declared-input.shp
error: a function wrapped by `>>` cannot declare its result
--> declared-input.shp:5:15
|
5 | let wrapped = fn(id: Int) -> Int { id } >> to_store;
| ^^^^^^^^^^^^^^^^^^^^^^^^^ its stages decide the result and `exn` row
help: annotate the binding instead: `let f: fn(...) -> T = fn(...) { ... } >> stage;`The left side of >> is a function whose stages decide its result, so it cannot declare one, and it must be a function at all.
let to_store = function {
| error :db(_) -> error :store("database")
};
let wrapped = 5 >> to_store;
$ship check not-function-input.shp
error: the left side of `>>` must be a function
--> not-function-input.shp:5:15
|
5 | let wrapped = 5 >> to_store;
| ^ found `Int`
note: `>>` on other expressions is open question 094A stage is a function, an outcome handler, a With manager, or a Drive driver.
let wrapped = fn(id: Int) { id } >> 5;
$ship check not-stage-input.shp
error: this is not a `>>` stage
--> not-stage-input.shp:1:37
|
1 | let wrapped = fn(id: Int) { id } >> 5;
| ^ found `Int`
help: a stage is a function, an outcome handler `| error ... -> ...`, a `With` manager, or a `Drive` driver>> and the value pipes |> and ?> do not mix without parentheses.
// fmt: ignore
let wrapped = fn(id: Int) { id } >> Int.to_string |> String.char_count;
$ship check mixed-input.shp
error: cannot mix `>>` with `|>` or `?>` without parentheses
--> mixed-input.shp:2:15
|
2 | let wrapped = fn(id: Int) { id } >> Int.to_string |> String.char_count;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ parenthesize the pipeline: `(f >> stage) |> g`A handler arm for a tag the function cannot raise is impossible.
let to_store = function {
| error :db(_) -> error :store("database")
};
let safe = fn(id: Int) { id + 1 } >> to_store;
$ship check impossible-input.shp
error: impossible error arm
--> impossible-input.shp:2:11
|
2 | | error :db(_) -> error :store("database")
| ^^^^^^ error variant cannot produce `:db`
--> impossible-input.shp:5:12
|
5 | let safe = fn(id: Int) { id + 1 } >> to_store;
| ---------------------------------- in this `>>` pipelineStage handlers hold concrete error :tag arms only.
let load = fn(id: Int) {
error :db(id);
} >> | 0 -> 1
| error :db(_) -> 2
;
$ship check success-arm-input.shp
error: a `>>` handler cannot match the success value
--> success-arm-input.shp:3:8
|
3 | } >> | 0 -> 1
| ^ not a concrete `error :tag` arm
--> success-arm-input.shp:3:6
|
3 | } >> | 0 -> 1
| - in this outcome handler
note: success arms and residual binders in stages are open question 033Stage handlers hold concrete error :tag arms only.
let load = fn(id: Int) {
error :db(id);
} >> | error rest -> 2;
$ship check residual-input.shp
error: a `>>` handler arm cannot bind the remaining errors
--> residual-input.shp:3:8
|
3 | } >> | error rest -> 2;
| ^^^^^^^^^^ not a concrete `error :tag` arm
--> residual-input.shp:3:6
|
3 | } >> | error rest -> 2;
| ----------------- in this outcome handler
note: success arms and residual binders in stages are open question 033Handlers exist only as stages.
let to_store = function {
| error :db(_) -> error :store("database")
};
let leaked = to_store;
$ship check value-input.shp
error: outcome handler `to_store` is only valid as a `>>` stage
--> value-input.shp:5:14
|
5 | let leaked = to_store;
| ^^^^^^^^ used as a value here
note: outcome handlers as values are open question 033
help: attach it to a function: `fn(...) { ... } >> to_store`Behavior witnesses
Opting out, missing or duplicate implementations, a message outside an implementation's associated row, and contract heads that do not match their companion file are errors. Behavior modules and witnesses shows the accepted forms.
A canonical implementation covers every instance of its constructor, so its operations cannot fix or repeat the constructor's type arguments.
type Pair('a, 'b) = {
left: 'a;
right: 'b;
}
type Twin('a, 'b) = {
first: 'a;
second: 'b;
}
module behavior Describe(struct 'a) {
describe: fn(value: 'a) -> String;
}
module behavior Describe(Pair) {
describe = ('b)(value: Pair(String, 'b)) -> String => value.left;
}
module behavior Describe(Twin) {
describe = ('a)(value: Twin('a, 'a)) -> String => "twin";
}
$ship-types-test canonical-family-narrow.shp
error: `Describe(Pair)` must accept every `Pair`, but `describe` only accepts `Pair(String, 'a)`
--> canonical-family-narrow.shp:16:3
|
16 | describe = ('b)(value: Pair(String, 'b)) -> String => value.left;
| ^^^^^^^^ narrower than the canonical target
note: keep `Pair`'s type arguments generic and require behaviors on them instead
error: `Describe(Twin)` must accept every `Twin`, but `describe` only accepts `Twin('a, 'a)`
--> canonical-family-narrow.shp:20:3
|
20 | describe = ('a)(value: Twin('a, 'a)) -> String => "twin";
| ^^^^^^^^ narrower than the canonical target
note: keep `Twin`'s type arguments generic and require behaviors on them insteadOpting out of Display is a missing canonical.
type! Secret = {
value: String;
}
test "#display requires Display" {
#display(Secret { value = "hidden"; });
}
$ship-types-test display-opt-out.shp
error: type `Secret` does not support `Display`
--> display-opt-out.shp:6:12
|
6 | #display(Secret { value = "hidden"; });
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected a value that supports `Display`An omitted canonical default is resolved at each call, so a subject without the canonical module is a call-site error. Only a parameter default may name a generic canonical module.
module behavior Rank(struct 'a) {
compare: pure fn!(left: 'a, right: 'a) -> Int;
}
module behavior Rank(Int) {
compare = (left: Int, right: Int) => left - right;
}
let pick = (struct 'a)(left: 'a, right: 'a, ~rank = Rank(struct 'a)) =>
rank.compare(left, right);
let supported = pick(1, 2);
let unsupported = pick("a", "b");
let body_reference = fn(struct 'b)(left: 'b, right: 'b) {
let rank = Rank(struct 'b);
return rank.compare(left, right);
};
$ship-types-test canonical-default-witness.shp
error: type `String` does not support `Rank`
--> canonical-default-witness.shp:13:19
|
13 | let unsupported = pick("a", "b");
| ^^^^^^^^^^^^^^ expected a value that supports `Rank`
error: `Rank('b)` names a generic canonical module outside a parameter default
--> canonical-default-witness.shp:16:14
|
16 | let rank = Rank(struct 'b);
| ^^^^^^^^^^^^^^^ only a parameter default can, because each call supplies it
note: constrain `'b: Rank` insteadCall-site operation errors share one fixture.
let missing_argument = Display.display();
let too_many_arguments = Display.display(1, 2);
let named_argument = Display.display(value = 1);
let unknown_operation = Display.render(1);
let unknown_behavior = Missing.display(1);
type Sealed !Display = {
key: String;
}
let invalid_target = Display.display({ key = "k"; } :> Sealed);
let missing_evidence = ('a)(value: 'a) -> String => Display.display(value);
$ship-types-test operation-errors.shp
error: missing argument `value`
--> operation-errors.shp:1:24
|
1 | let missing_argument = Display.display();
| ^^^^^^^^^^^^^^^^^ call is missing this argument
error: too many positional arguments
--> operation-errors.shp:2:45
|
2 | let too_many_arguments = Display.display(1, 2);
| ^ unexpected argument
error: unknown argument `value`
--> operation-errors.shp:3:46
|
3 | let named_argument = Display.display(value = 1);
| ^ not a parameter label of `Display.display`
error: behavior `Display` has no operation `render`
--> operation-errors.shp:4:25
|
4 | let unknown_operation = Display.render(1);
| ^^^^^^^^^^^^^^ operation not found on behavior
error: unknown name `Missing`
--> operation-errors.shp:5:24
|
5 | let unknown_behavior = Missing.display(1);
| ^^^^^^^^^^^^^^^ not found in this scope
error: type `Sealed` does not support `Display`
--> operation-errors.shp:11:22
|
11 | let invalid_target = Display.display({ key = "k"; } :> Sealed);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected a value that supports `Display`
error: type `'a` does not support `Display`
--> operation-errors.shp:12:53
|
12 | let missing_evidence = ('a)(value: 'a) -> String => Display.display(value);
| ^^^^^^^^^^^^^^^^^^^^^^ expected a value that supports `Display`Missing operations on named, canonical, HashKey, and anonymous implementations.
type! Score = {
value: Int;
}
module behavior NamedOrder: Order(Score) {
}
module behavior Order(Score) {
}
type! Address = {
host: String;
}
module behavior HostKey: HashKey(Address) {
}
let anonymous_encoder = module: Encode(String) #{
};
$ship-types-test missing-operations.shp
error: module `NamedOrder` is missing operation `compare` required by `Order`
--> missing-operations.shp:5:17
|
5 | module behavior NamedOrder: Order(Score) {
| ^^^^^^^^^^ module declared here
error: module `Order(Score)` is missing operation `compare` required by `Order`
--> missing-operations.shp:8:17
|
8 | module behavior Order(Score) {
| ^^^^^ module declared here
error: module `HostKey` is missing operation `hash` required by `HashKey`
--> missing-operations.shp:15:17
|
15 | module behavior HostKey: HashKey(Address) {
| ^^^^^^^ module declared here
error: module `HostKey` is missing operation `equal` required by `HashKey`
--> missing-operations.shp:15:17
|
15 | module behavior HostKey: HashKey(Address) {
| ^^^^^^^ module declared here
error: anonymous module is missing operation `encode` required by `Encode`
--> missing-operations.shp:18:33
|
18 | let anonymous_encoder = module: Encode(String) #{
| ^^^^^^^^^^^^^^ anonymous module declared hereNamed, canonical, anonymous, and adapter implementations must satisfy each interface operation's type.
type! Task = {
title: String;
priority: Int;
}
module behavior ByTitle: Order(Task) {
compare = (left: Task, right: Task) => left.title;
}
module behavior Order(Task) {
compare = pure fn!(_left: Task, _right: Task, _extra: Int) -> Ordering {
todo;
};
}
let anonymous = module: Display(Task) #{
document = (value, _context) => value.title;
};
module behavior Reverse(struct 'a)(Base: Order(struct 'a)): Order(struct 'a) {
compare = (left: 'a, right: 'a) => True;
}
type! Gate = {
name: String;
}
module behavior With(Gate) {
execute = fn!(gate: Gate, body: (fn(resource: Gate) -> ScopeExit('error)) @ once) -> Unit {
ignore(body(gate));
};
}
$ship-types-test operation-mismatch.shp
error: module `ByTitle` does not satisfy `Order`
--> operation-mismatch.shp:7:13
|
7 | compare = (left: Task, right: Task) => left.title;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Order.compare` requires `pure fn!(left: Task, right: Task) -> Ordering`, but `ByTitle.compare` is `fn(left: Task, right: Task) -> String`
note: type mismatch: expected Ordering, found String
error: module `Order(Task)` does not satisfy `Order`
--> operation-mismatch.shp:11:13
|
11 | compare = pure fn!(_left: Task, _right: Task, _extra: Int) -> Ordering {
| ^ `Order.compare` requires `pure fn!(left: Task, right: Task) -> Ordering`, but `Order(Task).compare` is `pure fn!(_left: Task, _right: Task, _extra: Int) -> Ordering`
note: the operation takes 2 parameter(s), but the member takes 3
error: anonymous module does not satisfy `Display`
--> operation-mismatch.shp:17:14
|
17 | document = (value, _context) => value.title;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Display.document` requires `pure fn!(value: Task, context: Display.Context) -> Format.Doc`, but `document` is `fn(value: Task, _context: 'a) -> String`
note: type mismatch: expected Format.Doc, found String
error: module `Reverse` does not satisfy `Order`
--> operation-mismatch.shp:21:13
|
21 | compare = (left: 'a, right: 'a) => True;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Order.compare` requires `pure fn!(left: 'a, right: 'a) -> Ordering`, but `Reverse.compare` is `fn(left: 'a, right: 'a) -> Bool`
note: type mismatch: expected Ordering, found Bool
error: module `With(Gate)` does not satisfy `With`
--> operation-mismatch.shp:29:13
|
29 | execute = fn!(gate: Gate, body: (fn(resource: Gate) -> ScopeExit('error)) @ once) -> Unit {
| ^ `With.execute` requires `fn!('error, 'effects)(manager: Gate, body: (fn(resource: Gate) -> ScopeExit('error)) @ required) -> Unit, eff 'effects`, but `With(Gate).execute` is `fn!(gate: Gate, body: (fn(resource: Gate) -> ScopeExit('a)) @ once) -> Unit`
note: parameter `body` is `@ required` in the operation, but `@ once` in the memberAn implementation's associated row is the message type at every call through its witness, so a tag outside the row is rejected.
type Next('state) =
| Continue('state)
| Stop
module behavior Handler(struct 'state) {
type Message;
handle: pure fn!(state: 'state, message: Message) -> Next('state);
}
type Counter = {
count: Int;
}
module behavior Handler(Counter) {
type Message = [
| :increment(Int)
| :stop
]
handle = (state, message) =>
match message {
| :increment(by) -> Continue({ state with count = state.count + by; })
| :stop -> Stop
};
}
let deliver = fn(struct 'state)(state: 'state, message, ~handler = Handler(struct 'state)) {
handler.handle(state, message)
};
let canonical = Handler.handle(Counter { count = 1; }, :reset);
let witnessed = deliver(Counter { count = 1; }, :reset);
$ship-types-test associated-row.shp
error: type mismatch
--> associated-row.shp:32:56
|
32 | let canonical = Handler.handle(Counter { count = 1; }, :reset);
| ^^^^^^ expected [ :increment(Int) | :stop ], found [> :reset ]
error: type mismatch
--> associated-row.shp:33:49
|
33 | let witnessed = deliver(Counter { count = 1; }, :reset);
| ^^^^^^ expected [ :increment(Int) | :stop ], found [> :reset ]Duplicate canonicals are rejected.
type! Score = {
value: Int;
}
module behavior Order(Score) {
}
module behavior Order(Score) {
}
$ship-types-test duplicate-canonical.shp
error: module `Order(Score)` is missing operation `compare` required by `Order`
--> duplicate-canonical.shp:5:17
|
5 | module behavior Order(Score) {
| ^^^^^ module declared here
error: canonical module `Order(Score)` is already defined
--> duplicate-canonical.shp:8:17
|
8 | module behavior Order(Score) {
| ^^^^^ duplicate canonical module
--> duplicate-canonical.shp:5:17
|
5 | module behavior Order(Score) {
| ----- first definition is here
error: module `Order(Score)` is missing operation `compare` required by `Order`
--> duplicate-canonical.shp:8:17
|
8 | module behavior Order(Score) {
| ^^^^^ module declared hereUnknown behavior interfaces.
type! Score = {
value: Int;
}
module behavior Named: MissingOrder(Score) {
}
module behavior MissingBehavior(Score) {
}
let anonymous = module: MissingBehavior(String) #{
};
let missing_context = module #{
};
$ship-types-test unknown-interface.shp
error: unknown module type `MissingOrder`
--> unknown-interface.shp:5:24
|
5 | module behavior Named: MissingOrder(Score) {
| ^^^^^^^^^^^^^^^^^^^ not found in this scope
error: unknown module type `MissingBehavior`
--> unknown-interface.shp:8:17
|
8 | module behavior MissingBehavior(Score) {
| ^^^^^^^^^^^^^^^ not found in this scope
error: unknown module type `MissingBehavior`
--> unknown-interface.shp:11:25
|
11 | let anonymous = module: MissingBehavior(String) #{
| ^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope
error: anonymous module signature cannot be inferred
--> unknown-interface.shp:14:23
|
14 | let missing_context = module #{
| ^ write `module: Signature #{ ... }` or provide an expected module typeMalformed signatures are parse errors.
// fmt: ignore
module behavior UserByName: {
}
$ship-types-test malformed-signature.shp
error: expected type
--> malformed-signature.shp:2:29
|
2 | module behavior UserByName: {
| ^ found {
error: expected module body or `;` contract declaration
--> malformed-signature.shp:3:1
|
3 | }
| ^ found }
error: expected expression
--> malformed-signature.shp:3:1
|
3 | }
| ^ found }Behavior implementation contracts use body-less complete heads.
contract-behavior-missing.shc
#infer type Task;
module behavior T(struct 'a) {
compare: pure fn!(left: 'a, right: 'a) -> Ordering;
}
module behavior ByTitle: Ranking(Task);
contract-behavior-missing.shp
type! Task = {
title: String;
}
$ship-types-test --stdlib-pair Ranking contract-behavior-missing.shc contract-behavior-missing.shp
error: missing implementation for contract behavior module `ByTitle`
--> contract-behavior-missing.shc:7:17
|
7 | module behavior ByTitle: Ranking(Task);
| ^^^^^^^ declared in the contract but not defined with this complete head in the companion `.shp` fileBehavior implementation contracts use body-less complete heads.
contract-behavior-mismatch.shc
#infer type Task;
module behavior T(struct 'a) {
compare: pure fn!(left: 'a, right: 'a) -> Ordering;
}
module behavior ByTitle: Ranking(Task);
contract-behavior-mismatch.shp
type! Task = {
title: String;
}
module behavior ByTitle: Ranking(Int) {
compare = pure fn!(_left: Int, _right: Int) -> Ordering {
todo;
};
}
$ship-types-test --stdlib-pair Ranking contract-behavior-mismatch.shc contract-behavior-mismatch.shp
error: missing implementation for contract behavior module `ByTitle`
--> contract-behavior-mismatch.shc:7:17
|
7 | module behavior ByTitle: Ranking(Task);
| ^^^^^^^ declared in the contract but not defined with this complete head in the companion `.shp` file
--> contract-behavior-mismatch.shp:5:17
|
5 | module behavior ByTitle: Ranking(Int) {
| ------- an implementation with a different head is defined hereBehavior implementation contracts use body-less complete heads.
contract-behavior-body.shc
#infer type Task;
module behavior T(struct 'a) {
compare: pure fn!(left: 'a, right: 'a) -> Ordering;
}
module behavior ByTitle: Ranking(Task) {
compare = (left: Task, right: Task) =>
String.compare(left.title, right.title);
}
contract-behavior-body.shp
type! Task = {
title: String;
}
module behavior ByTitle: Ranking(Task) {
compare = pure fn!(_left: Task, _right: Task) -> Ordering {
todo;
};
}
$ship-types-test --stdlib-pair Ranking contract-behavior-body.shc contract-behavior-body.shp
error: behavior implementation bodies belong in the companion `.shp` file
--> contract-behavior-body.shc:7:17
|
7 | module behavior ByTitle: Ranking(Task) {
| ^^^^^^^ replace this contract body with `;`Body-less behavior declarations are not implementation syntax.
module behavior ByValue: Order(Int);
$ship-types-test source-behavior-declaration.shp
error: body-less behavior declarations are only valid in `.shc` contracts
--> source-behavior-declaration.shp:1:17
|
1 | module behavior ByValue: Order(Int);
| ^^^^^^^ define the behavior implementation body hereCanonical shorthand must not repeat the inferred signature.
type! Score = {
value: Int;
}
module behavior Order(Score): Order(Score) {
compare = pure fn!(_left: Score, _right: Score) -> Ordering {
todo;
};
}
$ship-types-test redundant-canonical-signature.shp
error: canonical module `Order(Score)` repeats its inferred signature
--> redundant-canonical-signature.shp:5:31
|
5 | module behavior Order(Score): Order(Score) {
| ^^^^^^^^^^^^ remove this signature and write the canonical shorthandDefault operation bodies are checked against their signatures.
module behavior Ranked(struct 'a) {
rank: pure fn(value: 'a) -> Int;
label: pure fn(value: 'a) -> String = (value) => rank(value);
}
$ship-types-test default-body-mismatch.shp
error: type mismatch
--> default-body-mismatch.shp:3:41
|
3 | label: pure fn(value: 'a) -> String = (value) => rank(value);
| ^^^^^^^^^^^^^^^^^^^^^^ expected String, found IntA default must work for every witness, so it cannot fix the interface parameter to one type.
module behavior Ranked(struct 'a) {
rank: pure fn(value: 'a) -> Int;
let bumped = (value) => value + rank(value);
}
$ship-types-test default-body-too-specific.shp
error: default operation is less general than its interface
--> default-body-too-specific.shp:3:27
|
3 | let bumped = (value) => value + rank(value);
| ^^^^^ `'a` must stay generic, found IntConstrained witness calls may use the behavior's qualified operation name even inside a module of the same name; ordinary self-paths still error.
witness-self-qualified.shc
module behavior T(struct 'a) {
echo: pure fn!(value: 'a) -> 'a;
}
witness-self-qualified.shp
module behavior Echo(Int) {
echo = (value: Int) -> Int => value;
}
module behavior Echo(Option) {
echo = ('a: Echo)(value: Option('a)) -> Option('a) =>
match value {
| Some(inner) -> Some(Echo.echo(inner))
| None -> None
};
}
let rejects_ordinary_self_path = () -> Int => Echo.missing();
let rejects_unconstrained_behavior_self_path = (value: Int) -> Int => Echo.echo(value);
$ship-types-test --stdlib-pair Echo witness-self-qualified.shc witness-self-qualified.shp
error: `Echo.missing` refers to the current module
--> witness-self-qualified.shp:13:47
|
13 | let rejects_ordinary_self_path = () -> Int => Echo.missing();
| ^^^^^^^^^^^^ a module's own name is not in scope inside itself
note: use `missing` directly
error: `Echo.echo` refers to the current module
--> witness-self-qualified.shp:14:71
|
14 | let rejects_unconstrained_behavior_self_path = (value: Int) -> Int => Echo.echo(value);
| ^^^^^^^^^ a module's own name is not in scope inside itself
note: use `echo` directlyModules and functors
Declaring type t outside module struct, omitting type t inside module struct, unknown signatures or functors, wrong functor arity, unsatisfied or mismatched parameter members, and canonical behavior declarations for a functor parameter's type are errors. Episode 2: Modules and Module Signatures show the accepted forms.
Only module struct declares a primary type t. Plain modules and module behavior implementations cannot declare type t, and a module struct cannot omit it.
// fmt: ignore
module Slug {
type t = String;
}
module struct MissingPrimary {
let empty = "";
}
type! Ticket = {
id: Int;
}
module behavior Inspect(Ticket) {
type t = Int;
document = (ticket: Ticket, _context: Inspect.Context) -> Format.Doc =>
Format.text(Display.display(ticket.id));
}
$ship-types-test primary-type-rules.shp
error: plain modules cannot declare `type t`
--> primary-type-rules.shp:2:8
|
2 | module Slug {
| ^^^^ use `module struct` for a primary-type module
error: struct modules must declare `type t`
--> primary-type-rules.shp:6:15
|
6 | module struct MissingPrimary {
| ^^^^^^^^^^^^^^ missing primary type `t`
error: behavior implementations cannot declare `type t`
--> primary-type-rules.shp:14:17
|
14 | module behavior Inspect(Ticket) {
| ^^^^^^^ behavior implementations only declare operations and member typesA functor parameter signature must name a known module type.
module MakeLookup(S: MissingSignature) {
let render = () -> String => "never";
}
$ship-types-test unknown-module-type.shp
error: unknown module type `MissingSignature`
--> unknown-module-type.shp:1:22
|
1 | module MakeLookup(S: MissingSignature) {
| ^^^^^^^^^^^^^^^^ not found in this scopeApplying a functor to the wrong number of module arguments is rejected at the call site.
module type Store {
describe: fn(value: Int) -> String;
}
module MakeLookup(S: Store) {
let render = (value: Int) -> String => S.describe(value);
}
module IntStore {
let describe = (value: Int) -> String => Display.display(value);
}
module Broken = MakeLookup(IntStore, IntStore);
$ship-types-test arity.shp
error: functor `MakeLookup` expects 1 module argument(s), found 2
--> arity.shp:13:17
|
13 | module Broken = MakeLookup(IntStore, IntStore);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ applied hereA functor argument must define every operation required by the parameter signature.
module type Store {
describe: fn(value: Int) -> String;
forget: fn(value: Int) -> Unit;
}
module MakeLookup(S: Store) {
let render = (value: Int) -> String => S.describe(value);
}
module PartialStore {
let describe = (value: Int) -> String => Display.display(value);
}
module Broken = MakeLookup(PartialStore);
$ship-types-test unsatisfied-argument.shp
error: `PartialStore` does not satisfy `Store`
--> unsatisfied-argument.shp:14:17
|
14 | module Broken = MakeLookup(PartialStore);
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Store` requires `forget`, but `PartialStore.forget` is not defined
--> unsatisfied-argument.shp:6:8
|
6 | module MakeLookup(S: Store) {
| ---------- required by functor parameter `S`When a signature pins a member module (module struct Key = String;), an argument that binds it to a different type is rejected.
module type Store {
module struct Key = String;
describe: fn(key: Key) -> String;
}
module MakeLookup(S: Store) {
let render = (key: S.Key) -> String => S.describe(key);
}
module IntStore {
module Key = Int;
let describe = (key: Int) -> String => Display.display(key);
}
module Broken = MakeLookup(IntStore);
$ship-types-test pinned-mismatch.shp
error: `IntStore` does not satisfy `Store`
--> pinned-mismatch.shp:17:17
|
17 | module Broken = MakeLookup(IntStore);
| ^^^^^^^^^^^^^^^^^^^^ `Store` pins `Key` to `String`, but `IntStore.Key` is `Int`
--> pinned-mismatch.shp:7:8
|
7 | module MakeLookup(S: Store) {
| ---------- required by functor parameter `S`Operations pulled into a signature via include are checked alongside direct members.
module type Resettable {
reset: fn() -> Unit;
}
module type Store {
include Resettable;
describe: fn(value: Int) -> String;
}
module MakeLookup(S: Store) {
let render = (value: Int) -> String => S.describe(value);
}
module ForgetfulStore {
let describe = (value: Int) -> String => Display.display(value);
}
module Broken = MakeLookup(ForgetfulStore);
$ship-types-test include-signature.shp
error: `ForgetfulStore` does not satisfy `Store`
--> include-signature.shp:18:17
|
18 | module Broken = MakeLookup(ForgetfulStore);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ `Store` requires `reset`, but `ForgetfulStore.reset` is not defined
--> include-signature.shp:10:8
|
10 | module MakeLookup(S: Store) {
| ---------- required by functor parameter `S`Nested functor applications check the inner result surface against the outer functor's parameter signature.
module type Describer {
module struct Value;
describe: fn(value: Value) -> String;
}
module Wrong(S: Describer) {
module Value = S.Value;
let describe = (value: Int) -> Int => value;
}
module Render(S: Describer) {
let render = (value: S.Value) -> String => S.describe(value);
}
module Numbers {
module Value = Int;
let describe = (value: Int) -> String => "ok";
}
module Broken = Render(Wrong(Numbers));
$ship-types-test nested-mismatch.shp
error: `Wrong(Numbers)` does not satisfy `Describer`
--> nested-mismatch.shp:23:17
|
23 | module Broken = Render(Wrong(Numbers));
| ^^^^^^^^^^^^^^^^^^^^^^ `Describer.describe` requires `fn(value: Int) -> String`, but `Wrong(Numbers).describe` is `fn(value: Int) -> Int`
--> nested-mismatch.shp:13:8
|
13 | module Render(S: Describer) {
| ------ required by functor parameter `S`A functor body cannot declare a canonical behavior for a parameter's type, because two applications of the functor to the same argument type could install conflicting canonical witnesses.
module type Store {
module struct Key;
describe: fn(key: Key) -> String;
}
module MakeLookup(S: Store) {
module behavior Inspect(S.Key) {
document = (key: S.Key, _context: Inspect.Context) -> Format.Doc =>
Format.text(S.describe(key));
}
let render = (key: S.Key) -> String => S.describe(key);
}
$ship-types-test coherence.shp
error: functor body declares canonical behavior for `S.Key`, which it does not define
--> coherence.shp:8:27
|
8 | module behavior Inspect(S.Key) {
| ^^^^^ canonical behavior in a functor body may only target struct modules defined by the body
--> coherence.shp:7:8
|
7 | module MakeLookup(S: Store) {
| ---------- functor declared hereApplying a name that is not a declared functor in scope is an error.
module Broken = MissingFunctor(String);
$ship-types-test unknown-functor.shp
error: unknown functor `MissingFunctor`
--> unknown-functor.shp:1:17
|
1 | module Broken = MissingFunctor(String);
| ^^^^^^^^^^^^^^^^^^^^^^ no functor with this name is in scopeConstructor names
Two equally close constructors with one name, and an unknown qualified constructor, are errors. Pattern matching shows how an expected type or a module qualifier picks one.
Equal-distance Shared constructors are rejected.
// Two same-scope types declaring one constructor name is a hard ambiguity
// without expected-type context — never an arbitrary pick.
type First =
| Shared(Int)
| OnlyFirst
type Second =
| Shared(Int)
| OnlySecond
let which = Shared(1);
$ship-types-test ambiguous.shp
error: ambiguous constructor `Shared`
--> ambiguous.shp:11:13
|
11 | let which = Shared(1);
| ^^^^^^ declared by `First` and `Second`
note: annotate the expected type or qualify the constructor with its module pathThe same collision is rejected in a pattern. HOLE: unknown name n is follow-on noise from the unbound pattern (SHIP-257).
// Pattern position with an uninferred scrutinee hits the same ambiguity.
type First =
| Shared(Int)
| OnlyFirst
type Second =
| Shared(Int)
| OnlySecond
let pick = (value) =>
match value {
| Shared(n) -> n
| _ -> 0
};
$ship-types-test ambiguous-pattern.shp
error: ambiguous constructor `Shared`
--> ambiguous-pattern.shp:12:7
|
12 | | Shared(n) -> n
| ^^^^^^^^^ declared by `First` and `Second`
note: annotate the expected type or qualify the constructor with its module path
error: unknown name `n`
--> ambiguous-pattern.shp:12:20
|
12 | | Shared(n) -> n
| ^ not found in this scopeAn unknown qualified constructor is rejected. HOLE: unknown name n is follow-on noise from the unbound pattern (SHIP-257).
// Qualified constructor paths must name a visible constructor tag, and the
// resolved constructor must still match the scrutinee type.
module struct First {
type t = {
marker: Int;
}
type Value =
| Shared(Int)
| OnlyFirst
}
module struct Second {
type t = {
marker: Int;
}
type Value =
| Shared(Int)
| OnlySecond
}
let unknown_path = (value: First.Value) -> Int =>
match value {
| Third.Shared(n) -> n
| First.OnlyFirst -> 0
};
let wrong_owner = (value: First.Value) -> Int =>
match value {
| Second.Shared(n) -> n
| First.OnlyFirst -> 0
};
$ship-types-test qualified-pattern-errors.shp
error: unknown constructor `Third.Shared`
--> qualified-pattern-errors.shp:25:7
|
25 | | Third.Shared(n) -> n
| ^^^^^^^^^^^^^^^ not found in this scope
error: unknown name `n`
--> qualified-pattern-errors.shp:25:26
|
25 | | Third.Shared(n) -> n
| ^ not found in this scope
error: type mismatch
--> qualified-pattern-errors.shp:31:7
|
31 | | Second.Shared(n) -> n
| ^^^^^^^^^^^^^^^^ expected Second.Value, found First.ValueTag families
Coverage checks each level of a family, so a match missing a member at any depth is non-exhaustive. Members of one family share their tag, so a family member beside a plain payload, or one member with two payloads, is an error wherever the row is built. Diagnostics spell families in dotted form. Pattern matching shows the accepted forms.
Coverage checks each level of a family, so a missing member is reported in dotted form at any depth. An at-least inner row needs a wildcard at its level.
type Event = [
| :window([ :focused | :blurred | :minimized ])
| :key.pressed(String)
]
let describe = (event: Event) -> String =>
match event {
| :window.focused -> "focused"
| :window.blurred -> "blurred"
| :key.pressed(key) -> key
};
type Path = [
| :a([ :b([ :c | :d ]) | :e ])
]
let walk = (path: Path) -> Int =>
match path {
| :a.b.c -> 1
| :a.e -> 2
};
exception DbError = [
| :db.unique(String)
| :db.not_found(Int)
]
let describe_failure = (failure: DbError) -> String =>
match failure {
| :db.unique(slug) -> slug
};
type Loose = [
| :window([ > :focused ])
]
let describe_loose = (event: Loose) -> String =>
match event {
| :window.focused -> "focused"
};
$ship-types-test missing.shp
error: non-exhaustive match
--> missing.shp:7:3
|
7 | match event {
| ^ missing case: :window.minimized
error: non-exhaustive match
--> missing.shp:18:3
|
18 | match path {
| ^ missing case: :a.b.d
error: non-exhaustive match
--> missing.shp:29:3
|
29 | match failure {
| ^ missing case: :db.not_found(_)
error: non-exhaustive match
--> missing.shp:38:3
|
38 | match event {
| ^ missing case: :window(_)Type mismatches print family rows in dotted form.
let claim = fn(slug: String) -> String, exn [ :db.unique(String) ] {
slug
};
test "Claim" {
#type claim: fn(String) -> String, exn [ :db.not_found(Int) ];
}
$ship-types-test mismatch.shp
error: type assertion failed
--> mismatch.shp:6:16
|
6 | #type claim: fn(String) -> String, exn [ :db.not_found(Int) ];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `fn(String) -> String, exn [ :db.not_found(Int) ]`
--> mismatch.shp:6:9
|
6 | #type claim: fn(String) -> String, exn [ :db.not_found(Int) ];
| ----- found `fn(slug: String) -> String, exn [ :db.unique(String) ]`
note: function exception row: expected `[ :db.not_found(Int) ]`; found `[ :db.unique(String) ]`Members of one family share their tag, so payloads that cannot share it are rejected wherever the row is built: written, composed, or inferred from raises.
let written = fn(slug: String) -> String, exn [ :db(String) | :db.unique(String) ] {
slug
};
let members = fn(slug: String) -> String, exn [ :db.unique(String) | :db.unique(Int) ] {
slug
};
exception Plain = [
| :db(String)
]
exception Family = [
| :db.unique(String)
]
let composed = fn(slug: String) -> String, exn [ Plain | Family ] { slug };
let raised = fn(slug: String) {
if slug == "plain" {
error :db(slug);
}
error :db.unique(slug);
};
$ship-types-test conflict.shp
error: tag `:db` has conflicting payloads
--> conflict.shp:1:63
|
1 | let written = fn(slug: String) -> String, exn [ :db(String) | :db.unique(String) ] {
| ^^^ `:db.unique(String)` conflicts with `:db(String)`
error: tag `:db.unique` has conflicting payloads
--> conflict.shp:5:70
|
5 | let members = fn(slug: String) -> String, exn [ :db.unique(String) | :db.unique(Int) ] {
| ^^^ `:db.unique(Int)` conflicts with `:db.unique(String)`
error: tag `:db` has conflicting payloads
--> conflict.shp:17:58
|
17 | let composed = fn(slug: String) -> String, exn [ Plain | Family ] { slug };
| ^^^^^^ `:db.unique(String)` conflicts with `:db(String)`
error: tag `:db` has conflicting payloads
--> conflict.shp:23:9
|
23 | error :db.unique(slug);
| ^^^^^^^^^^^^^^^^ `:db.unique(String)` conflicts with `:db(String)`Rest parameters and spreads
Unknown, repeated, and mistyped labels, spread fields the call does not accept, spreads outside a complete call with known labels, and malformed rest parameters are errors. Rest parameters and spreads shows the accepted forms.
A rest parameter collects call labels that are not other parameters. A label that is neither, a label written twice, and a label of the wrong field type are rejected at the label.
type Attrs = sparse {
class: String;
disabled: Bool;
}
let button = fn(label: String, ..attrs: Attrs) -> String { label };
let unknown = fn() -> String { button("a", colour = "red") };
let twice = fn() -> String { button("a", class = "x", class = "y") };
let wrong = fn() -> String { button("a", disabled = "yes") };
$ship-types-test labels.shp
error: unknown argument `colour`
--> labels.shp:7:53
|
7 | let unknown = fn() -> String { button("a", colour = "red") };
| ^^^^^ not a parameter label of `button` or a field of `Attrs`
error: argument `class` was provided twice
--> labels.shp:8:63
|
8 | let twice = fn() -> String { button("a", class = "x", class = "y") };
| ^^^ duplicate argument
error: type mismatch
--> labels.shp:9:53
|
9 | let wrong = fn() -> String { button("a", disabled = "yes") };
| ^^^^^ expected Bool, found StringA spread forwards every field of a record. Fields the call does not accept, fields of a different type, values that are not records, and sparse fields with no fallback for a required or defaulted label are rejected at the spread.
type Attrs = sparse {
class: String;
disabled: Bool;
}
type Wide = sparse {
class: String;
width: Int;
height: Int;
}
type Loose = {
class: String;
disabled: Option(Bool);
}
type Flags = sparse {
verbose: Bool;
}
let button = fn(label: String, ..attrs: Attrs) -> String { label };
let configure = fn(~verbose: Bool) -> Bool { verbose };
let quiet = fn(~verbose: Bool = False) -> Bool { verbose };
let extra = fn(wide: Wide) -> String { button("a", ..wide) };
let mismatch = fn(loose: Loose) -> String { button("a", ..loose) };
let scalar = fn(count: Int) -> String { button("a", ..count) };
let required = fn(flags: Flags) -> Bool { configure(..flags) };
let defaulted = fn(flags: Flags) -> Bool { quiet(..flags) };
$ship-types-test spreads.shp
error: this call does not accept fields `width`, `height` from `..wide`
--> spreads.shp:24:52
|
24 | let extra = fn(wide: Wide) -> String { button("a", ..wide) };
| ^^^^^^ `Wide` has fields this call does not accept
help: a spread forwards every field; spread a record whose fields are all labels of this call
error: field `disabled` has a different type in `..loose`
--> spreads.shp:25:57
|
25 | let mismatch = fn(loose: Loose) -> String { button("a", ..loose) };
| ^^^^^^^ `..loose` supplies `disabled: Option(Bool)`
note: this call expects `disabled: Bool`
help: a spread field must have exactly the target's type
error: `..count` is not a record
--> spreads.shp:26:53
|
26 | let scalar = fn(count: Int) -> String { button("a", ..count) };
| ^^^^^^^ found `Int`
help: a spread forwards the fields of a record value
error: `..flags` may omit `verbose`, which this call requires
--> spreads.shp:27:53
|
27 | let required = fn(flags: Flags) -> Bool { configure(..flags) };
| ^^^^^^^ `verbose` is optional in `Flags`
help: write `verbose = ...` before the spread to give it a fallback
error: `..flags` may omit `verbose`, and a spread cannot fall back to its default
--> spreads.shp:28:50
|
28 | let defaulted = fn(flags: Flags) -> Bool { quiet(..flags) };
| ^^^^^^^ `verbose` is optional in `Flags`
help: write `verbose = ...` before the spread to give it a fallbackA spread needs a complete call whose labels are known: not a constructor, partial application, a variadic callee, an unannotated callee, or a value whose record type is not known yet.
type Attrs = sparse {
class: String;
disabled: Bool;
}
let button = fn(label: String, ..attrs: Attrs) -> String { label };
let join = fn(...parts: Vector(String)) -> String { "joined" };
let wrapped = fn(attrs: Attrs) -> Option(Attrs) { Some(..attrs) };
let partial = fn(attrs: Attrs) -> String {
let pending = button(..attrs, ...);
pending("a")
};
let variadic = fn(attrs: Attrs) -> String { join("a", ..attrs) };
let unknown = fn() {
let render = (value) => button("a", ..value);
render
};
let opaque = fn(attrs: Attrs) -> Int {
let apply = (f) => f(..attrs);
1
};
$ship-types-test positions.shp
error: a spread is not allowed here
--> positions.shp:8:56
|
8 | let wrapped = fn(attrs: Attrs) -> Option(Attrs) { Some(..attrs) };
| ^^^^^^^ `..value` only forwards record fields as call labels
error: partial application does not accept spreads
--> positions.shp:11:24
|
11 | let pending = button(..attrs, ...);
| ^^^^^^^ spread into a complete call instead
error: `..attrs` needs a call with known labels
--> positions.shp:15:55
|
15 | let variadic = fn(attrs: Attrs) -> String { join("a", ..attrs) };
| ^^^^^^^ this callable collects variadic arguments
error: cannot spread a value whose record type is not known yet
--> positions.shp:18:39
|
18 | let render = (value) => button("a", ..value);
| ^^^^^^^ annotate this value's record type
error: `..attrs` needs a call with known labels
--> positions.shp:23:24
|
23 | let apply = (f) => f(..attrs);
| ^^^^^^^ annotate this function so its labels are knownA call whose callee cannot be typed still checks its arguments, and a spread argument reports nothing beyond the callee's own error.
type Attrs = sparse {
class: String;
disabled: Bool;
}
let missing = fn(attrs: Attrs) -> String { paint_label("a", ..attrs) };
let unreachable = fn(attrs: Attrs, stop: Never) -> String { stop(..attrs) };
$ship-types-test callees.shp
error: unknown name `paint_label`
--> callees.shp:6:44
|
6 | let missing = fn(attrs: Attrs) -> String { paint_label("a", ..attrs) };
| ^^^^^^^^^^^ not found in this scopeA rest parameter is last, has a sparse record type, and shares no field name with the function's labels.
type Attrs = sparse {
class: String;
disabled: Bool;
}
type Plain = {
class: String;
}
let early = fn(..attrs: Attrs, label: String) -> String { label };
let dense = fn(label: String, ..attrs: Plain) -> String { label };
let scalar = fn(label: String, ..count: Int) -> String { label };
let doubled = fn(label: String, ~class: String, ..attrs: Attrs) -> String {
label
};
$ship-types-test definitions.shp
error: rest parameter `attrs` must be the last parameter
--> definitions.shp:10:18
|
10 | let early = fn(..attrs: Attrs, label: String) -> String { label };
| ^^^^^ move it after every other parameter
error: rest parameter `attrs` must have a sparse record type
--> definitions.shp:11:33
|
11 | let dense = fn(label: String, ..attrs: Plain) -> String { label };
| ^^^^^ found `Plain`
help: declare its type with `type Name = sparse { ... }`
error: rest parameter `count` must have a sparse record type
--> definitions.shp:12:34
|
12 | let scalar = fn(label: String, ..count: Int) -> String { label };
| ^^^^^ found `Int`
help: declare its type with `type Name = sparse { ... }`
error: `Attrs` field `class` is also a label of this function
--> definitions.shp:14:51
|
14 | let doubled = fn(label: String, ~class: String, ..attrs: Attrs) -> String {
| ^^^^^ `class = ...` would fill both `~class` and this record
help: remove `~class` or the `class` field so each label has one targetA rest parameter is written ..name: Record, with no label and no default.
// fmt: ignore
type Attrs = sparse {
class: String;
}
let labeled = fn(~..attrs: Attrs) -> Int {
1
};
let defaulted = fn(..attrs: Attrs = Attrs {}) -> Int {
1
};
let untyped = fn(..attrs) -> Int {
1
};
$ship-types-test syntax.shp
error: a rest parameter is written `..name: Record`
--> syntax.shp:6:21
|
6 | let labeled = fn(~..attrs: Attrs) -> Int {
| ^^^^^ give it a sparse record type, with no label or default
error: a rest parameter is written `..name: Record`
--> syntax.shp:10:22
|
10 | let defaulted = fn(..attrs: Attrs = Attrs {}) -> Int {
| ^^^^^ give it a sparse record type, with no label or default
error: a rest parameter is written `..name: Record`
--> syntax.shp:14:20
|
14 | let untyped = fn(..attrs) -> Int {
| ^^^^^ give it a sparse record type, with no label or defaultSignature generality and type arity
A body less general than its written signature, a local let of mutable state used at two types, and a type applied to too few arguments are errors. Functions shows the accepted forms.
Type variables written in a function signature are rigid: the body must work for every type the caller chooses, so a body that fixes 'a to Int is less general than its signature.
let bump = fn('a)(x: 'a) -> 'a { x + 1 };
let annotated = fn(x: 'a) {
let y: 'a = 1;
x
};
$ship-types-test rigid.shp
error: function body is less general than its signature
--> rigid.shp:1:34
|
1 | let bump = fn('a)(x: 'a) -> 'a { x + 1 };
| ^ `'a` must stay generic, found Int
error: function body is less general than its signature
--> rigid.shp:4:10
|
4 | let y: 'a = 1;
| ^^ `'a` must stay generic, found IntLocal let bindings whose value can hold mutable state stay monomorphic: the first assignment fixes the element type for every later use.
type Slot('a) = {
mut value: Option('a);
}
let make = fn() { Slot { value = None; } };
let literal = fn() {
let slot = Slot { value = None; };
slot.value := Some("x");
match slot.value {
| Some(n) -> n + 1
| None -> 0
}
};
let called = fn() {
let slot = make();
slot.value := Some("x");
match slot.value {
| Some(n) -> n + 1
| None -> 0
}
};
let cell = fn() {
let mut x = None;
x := Some("a");
match x {
| Some(n) -> n + 1
| None -> 0
}
};
let reassigned = fn() {
let mut n = 1;
n := "s";
n
};
$ship-types-test value-restriction.shp
error: type mismatch
--> value-restriction.shp:11:18
|
11 | | Some(n) -> n + 1
| ^ expected Int, found String
error: type mismatch
--> value-restriction.shp:20:18
|
20 | | Some(n) -> n + 1
| ^ expected Int, found String
error: type mismatch
--> value-restriction.shp:29:18
|
29 | | Some(n) -> n + 1
| ^ expected Int, found String
error: type mismatch
--> value-restriction.shp:36:8
|
36 | n := "s";
| ^^^ expected Int, found StringEvery written type application supplies every parameter, in field types, parameter annotations, and bare names alike.
type Box('a, 'b) = {
left: 'a;
right: 'b;
}
type Holder = {
box: Box(Int);
}
let partial = fn(b: Box(Int)) { b };
let bare = fn(b: Box) { b };
let full = fn(b: Box(Int, String)) { b.right };
let over = fn(b: Box(Int, String, Bool)) { b };
$ship-types-test arity.shp
error: type `Box` expects 2 arguments, got 1
--> arity.shp:7:8
|
7 | box: Box(Int);
| ^^^^^^^^ type argument arity mismatch
note: type parameters cannot be elided yet; write every type argument
error: type `Box` expects 2 arguments, got 1
--> arity.shp:10:21
|
10 | let partial = fn(b: Box(Int)) { b };
| ^^^^^^^^ type argument arity mismatch
note: type parameters cannot be elided yet; write every type argument
error: type `Box` expects 2 arguments, got 0
--> arity.shp:11:18
|
11 | let bare = fn(b: Box) { b };
| ^^^ type argument arity mismatch
note: type parameters cannot be elided yet; write every type argument
error: type `Box` expects 2 arguments, got 3
--> arity.shp:13:18
|
13 | let over = fn(b: Box(Int, String, Bool)) { b };
| ^^^^^^^^^^^^^^^^^^^^^^ type argument arity mismatchIndexed variants
An arm that returns a type its case does not allow, a type a case binds leaving its arm, a case result outside its own type, a match whose indexes are still being inferred, and matching a bare type variable are errors. Indexed variants shows the accepted forms.
A case that declares its result fixes its type's parameters inside the arm that matches it, so an arm that returns another type is a mismatch.
type Expr('t) =
| IntLit(Int): Expr(Int)
| BoolLit(Bool): Expr(Bool)
let eval = fn('t)(expr: Expr('t)) -> 't {
match expr {
| IntLit(_) -> True
| BoolLit(value) -> value
}
};
$ship-types-test wrong-arm.shp
error: type mismatch
--> wrong-arm.shp:7:20
|
7 | | IntLit(_) -> True
| ^^^^ expected Int, found BoolA variable that appears only in a case's payload stays abstract and cannot leave the arm that matched it.
type Shown =
| Shown('a, fn(value: 'a) -> String): Shown
let unwrap = (shown: Shown) =>
match shown {
| Shown(value, _) -> value
};
$ship-types-test escape.shp
error: type `'a` from `Shown` escapes its match arm
--> escape.shp:6:7
|
6 | | Shown(value, _) -> value
| ^^^^^^^^^^^^^^^ `'a` exists only inside this arm
help: give the match a written type, such as the enclosing function's signatureA case's declared result must build the type being declared, with one argument per parameter.
type Other('a) =
| Other('a)
type Expr('t) =
| IntLit(Int): Other(Int)
| Pair('a, 'b): Expr('a, 'b)
$ship-types-test result-type.shp
error: type `Expr` expects 1 argument, got 2
--> result-type.shp:6:19
|
6 | | Pair('a, 'b): Expr('a, 'b)
| ^^^^^^^^^^^^ type argument arity mismatch
error: case `IntLit` must build `Expr`
--> result-type.shp:5:18
|
5 | | IntLit(Int): Other(Int)
| ^^^^^^^^^^ `IntLit` is declared in `Expr`Refinement needs the matched value's type written down. Without a signature, the first arm would fix the type and contradict the next.
type Expr('t) =
| IntLit(Int): Expr(Int)
| BoolLit(Bool): Expr(Bool)
let eval = (expr) =>
match expr {
| IntLit(value) -> value
| BoolLit(value) -> value
};
$ship-types-test unannotated.shp
error: matching `IntLit` needs the type of the matched value
--> unannotated.shp:7:7
|
7 | | IntLit(value) -> value
| ^^^^^^^^^^^^^ this case refines `'a`, which is still being inferred
help: give the matched value a written type, such as a function signature
error: type mismatch
--> unannotated.shp:8:25
|
8 | | BoolLit(value) -> value
| ^^^^^ expected Int, found BoolMatching a case refines only a value already typed as that indexed variant; a value of a bare type variable does not match it.
type Expr('t) =
| IntLit(Int): Expr(Int)
let peek = fn('a)(value: 'a) -> Int {
match value {
| IntLit(number) -> number
}
};
$ship-types-test bare-variable.shp
error: function body is less general than its signature
--> bare-variable.shp:6:7
|
6 | | IntLit(number) -> number
| ^^^^^^^^^^^^^^ `'a` must stay generic, found Expr(Int)
error: non-exhaustive match
--> bare-variable.shp:5:3
|
5 | match value {
| ^ missing case: _Actors
Notifying with a request case, requesting with a notification case, and a handler arm that answers with the wrong builder are errors. Actors shows the accepted forms.
Actor.notify takes only notification cases and Actor.request only request cases: a message's kind is part of its type.
type Counter = {
count: Int;
}
type CounterMessage('kind) =
| Increment(Int): CounterMessage(Actor.Notify)
| Get: CounterMessage(Actor.Request(Int))
module behavior Actor(Counter) {
type Message('kind) = CounterMessage('kind)
handle = (state, message, from) =>
match message {
| Increment(by) -> Actor.continue({ state with count = state.count + by; })
| Get -> Actor.respond(state.count, state)
};
}
let notify_request = fn(counter: Actor.Ref(Counter)) {
Actor.notify(counter, Get);
};
let request_notification = fn(counter: Actor.Ref(Counter)) {
Actor.request(
counter,
Increment(1),
within = Duration.seconds(1),
)
};
$ship-types-test counter.shp
error: type mismatch
--> counter.shp:20:25
|
20 | Actor.notify(counter, Get);
| ^^^ expected Actor.Notify, found Actor.Request(Int)
error: type mismatch
--> counter.shp:26:5
|
26 | Increment(1),
| ^^^^^^^^^^^^ expected Actor.Request('a), found Actor.NotifyA handler arm answers with the builder its case allows: a notification cannot respond, and a request must.
type Counter = {
count: Int;
}
type CounterMessage('kind) =
| Increment(Int): CounterMessage(Actor.Notify)
| Get: CounterMessage(Actor.Request(Int))
module behavior Actor(Counter) {
type Message('kind) = CounterMessage('kind)
handle = (state, message, from) =>
match message {
| Increment(by) -> Actor.respond(by, state)
| Get -> Actor.continue(state)
};
}
$ship-types-test builders.shp
error: type mismatch
--> builders.shp:14:26
|
14 | | Increment(by) -> Actor.respond(by, state)
| ^^^^^^^^^^^^^^^^^^^^^^^^ expected Actor.Notify, found Actor.Request('a)
error: type mismatch
--> builders.shp:15:16
|
15 | | Get -> Actor.continue(state)
| ^^^^^^^^^^^^^^^^^^^^^ expected Actor.Request(Int), found Actor.NotifyTerminal failures
A handler inside tx's scope that absorbs a failure ending tx, exn! or error! in a fn!, a target that is not a parameter, with binding, or service, an ended argument that is not such a name, a body that ends a binding its signature does not declare, and a callable that ends something passed to a sig binder are errors. Terminal failures shows the accepted forms.
A failure that ends tx cannot be handled inside tx's scope. A named arm, a wildcard, error rest, and an arm that raises an ordinary failure all absorb it. The rule follows the scope: a handler inside the with that introduced tx is still inside it.
type! Tx = {
mut writes: Int;
}
let begin = () -> Tx =>
Tx { writes = 0; };
module behavior With(Tx) {
execute = fn(manager: Tx, body: (fn(Tx) -> ScopeExit('error)) @ required) -> Unit {
let _exit = body(manager);
};
}
let insert = fn(tx: Tx, slug: String) -> Unit, exn! tx [ :db.unique(String) ] {
if slug == "home" {
error! tx, :db.unique(slug);
}
tx.writes := tx.writes + 1;
};
let named = fn(tx: Tx, slug: String) {
try insert(tx, slug) {
| :db.unique(_) -> ()
}
};
let wildcard = fn(tx: Tx, slug: String) {
try insert(tx, slug) {
| _ -> ()
}
};
let rest = fn(tx: Tx, slug: String) {
match insert(tx, slug) {
| _ -> ()
| error rest -> print(rest)
}
};
let ordinary = fn(tx: Tx, slug: String) {
try insert(tx, slug) {
| :db(_) -> error :conflict
}
};
let in_with = fn(slug: String) {
with tx = begin() {
try insert(tx, slug) {
| :db(_) -> ()
};
};
};
$ship-types-test scope.shp
error: `:db.unique(...)` ends `tx`, so it can't be handled inside `tx`'s scope
--> scope.shp:23:7
|
23 | | :db.unique(_) -> ()
| ^^^^^^^^^^^^^ handles a failure that ends `tx`
help: handle it outside, run it in a savepoint of `tx`, or re-raise with `error! tx, ...`
error: `:db.unique(...)` ends `tx`, so it can't be handled inside `tx`'s scope
--> scope.shp:29:7
|
29 | | _ -> ()
| ^ handles a failure that ends `tx`
help: handle it outside, run it in a savepoint of `tx`, or re-raise with `error! tx, ...`
error: `:db.unique(...)` ends `tx`, so it can't be handled inside `tx`'s scope
--> scope.shp:36:13
|
36 | | error rest -> print(rest)
| ^^^^ handles a failure that ends `tx`
help: handle it outside, run it in a savepoint of `tx`, or re-raise with `error! tx, ...`
error: `:db(...)` ends `tx`, so it can't be handled inside `tx`'s scope
--> scope.shp:42:7
|
42 | | :db(_) -> error :conflict
| ^^^^^^ handles a failure that ends `tx`
help: handle it outside, run it in a savepoint of `tx`, or re-raise with `error! tx, ...`
error: `:db(...)` ends `tx`, so it can't be handled inside `tx`'s scope
--> scope.shp:49:9
|
49 | | :db(_) -> ()
| ^^^^^^ handles a failure that ends `tx`
help: handle it outside, run it in a savepoint of `tx`, or re-raise with `error! tx, ...`A nested tag pattern catches only the nested tags it names. An ordinary :db.not_found(_) can be handled inside tx's scope even though :db.unique(_) ends tx.
type! Tx = {
mut writes: Int;
}
let update = fn(tx: Tx, slug: String) -> Int, exn [ :db.not_found(String) ], exn! tx [ :db.unique(String) ] {
if slug == "" {
error :db.not_found(slug);
}
if slug == "home" {
error! tx, :db.unique(slug);
}
tx.writes := tx.writes + 1;
tx.writes
};
let missing = fn(tx: Tx, slug: String) {
try update(tx, slug) {
| :db.not_found(_) -> 0
}
};
let taken = fn(tx: Tx, slug: String) {
try update(tx, slug) {
| :db.not_found(_) -> 0
| :db.unique(_) -> 0
}
};
$ship-types-test nested.shp
error: `:db.unique(...)` ends `tx`, so it can't be handled inside `tx`'s scope
--> nested.shp:25:7
|
25 | | :db.unique(_) -> 0
| ^^^^^^^^^^^^^ handles a failure that ends `tx`
help: handle it outside, run it in a savepoint of `tx`, or re-raise with `error! tx, ...`fn! cannot fail, so it can neither declare exn! nor end anything with error!.
type! Tx = {
mut writes: Int;
}
let declared = fn!(tx: Tx) -> Unit, exn! tx [ :closed ] {
tx.writes := 0;
};
let raised = fn!(tx: Tx) -> Unit {
error! tx, :closed;
};
$ship-types-test strict.shp
error: `fn!` cannot also declare an `exn!` clause
--> strict.shp:5:37
|
5 | let declared = fn!(tx: Tx) -> Unit, exn! tx [ :closed ] {
| ^^^^^^^^^^^^^^^^^^^ `fn!` cannot fail, so it cannot end anything
error: `fn!` cannot end `tx`
--> strict.shp:10:10
|
10 | error! tx, :closed;
| ^^ `fn!` cannot fail, so it cannot end anythingThe target of error! is a parameter, a with binding, or a service. The target of a signature's exn! is one of its parameters or a service.
type! Tx = {
name: String;
}
let field = fn(tx: Tx) {
error! tx.name, :closed;
};
let local = fn(tx: Tx) {
let count = 1;
error! count, :closed;
};
let missing = fn(tx: Tx) {
error! nope, :closed;
};
let signature = fn(tx: Tx) -> Unit, exn! other [ :closed ] {
print(tx.name);
};
$ship-types-test targets.shp
error: `tx.name` is a field of `tx`, not a name
--> targets.shp:6:10
|
6 | error! tx.name, :closed;
| ^^^^^^^ only a parameter, a `with` binding, or a service can be ended
error: `count` is not a parameter or `with` binding, so it can't be ended
--> targets.shp:11:10
|
11 | error! count, :closed;
| ^^^^^ only a parameter, a `with` binding, or a service can be ended
error: cannot find `nope` to end
--> targets.shp:15:10
|
15 | error! nope, :closed;
| ^^^^ not a parameter, `with` binding, or service in scope
error: `exn!` cannot end `other`
--> targets.shp:18:42
|
18 | let signature = fn(tx: Tx) -> Unit, exn! other [ :closed ] {
| ^^^^^ not a parameter of this function or a serviceA call that ends its argument needs a name it can end: a parameter or a with binding. Partial application would hide which binding ends.
type! Tx = {
mut writes: Int;
}
let begin = () -> Tx =>
Tx { writes = 0; };
let insert = fn(tx: Tx, slug: String) -> Unit, exn! tx [ :db.unique(String) ] {
if slug == "home" {
error! tx, :db.unique(slug);
}
tx.writes := tx.writes + 1;
};
let temporary = fn() {
insert(begin(), "post");
};
let alias = fn(tx: Tx) {
let other = tx;
insert(other, "post");
};
let partial = fn(tx: Tx) {
let later = insert(tx, ...);
later("post");
};
$ship-types-test arguments.shp
error: `insert` ends its `tx` argument, so it needs a name
--> arguments.shp:16:10
|
16 | insert(begin(), "post");
| ^^^^^^^ pass a parameter or `with` binding here
error: `insert` ends its `tx` argument, but `other` can't be ended
--> arguments.shp:21:10
|
21 | insert(other, "post");
| ^^^^^ not a parameter or `with` binding
help: bind the value with `with` and pass that name
error: `insert` ends its `tx` argument, so it can't be partially applied
--> arguments.shp:25:15
|
25 | let later = insert(tx, ...);
| ^^^^^^ call it with every argument insteadA written signature must declare every binding the body ends. A callback that ends its own parameter needs a callee that declares it.
type! Tx = {
mut writes: Int;
}
let undeclared = fn(tx: Tx) -> Unit, exn [ :io ] {
error! tx, :closed;
};
let each = fn(item: Tx, body: fn(Tx) -> Unit) {
body(item);
};
let close = fn(tx: Tx) {
error! tx, :closed;
};
let callback = fn(item: Tx) {
each(item, close);
};
$ship-types-test signatures.shp
error: this function ends `tx`, but its signature has no `exn! tx` clause
--> signatures.shp:5:42
|
5 | let undeclared = fn(tx: Tx) -> Unit, exn [ :io ] {
| ^^^^^^^ ends `tx` with [ :closed ]
error: this callback ends its own parameter `tx`, which the callee cannot see
--> signatures.shp:18:14
|
18 | each(item, close);
| ^^^^^ ends `tx`
help: declare `exn! tx [ ... ]` on the callback parameter, or end a `with` binding inside the callbackA sig binder such as retry's 'f cannot take a callable that ends a binding, a service, or its own parameter: the callee could catch that failure, or retry the callable, inside the scope it ended. Retrying a whole region, with the with inside the callable, is accepted.
type! Tx = {
mut balance: Int;
}
module behavior With(Tx) {
execute = fn(tx: Tx, body: (fn(Tx) -> ScopeExit('error)) @ required) -> Unit {
let _exit = body(tx);
};
}
let withdraw = fn(tx: Tx, amount: Int) {
if amount > tx.balance {
error! tx, :insufficient_funds;
}
tx.balance := tx.balance - amount;
};
effect service Audit {
write: fn(entry: String) -> Unit;
}
let record = fn(entry: String) {
if entry == "" {
error! Audit, :empty_entry;
}
Audit.write(entry);
};
let retry: pure fn!(sig 'f)(body: 'f) -> 'f = (body) => body;
let binding = fn(tx: Tx) {
let charge = retry(() => withdraw(tx, 5));
charge();
};
let service = fn() {
let audited = retry(record);
audited("signed up");
};
let parameter = fn(tx: Tx) {
let charge = retry(withdraw);
charge(tx, 5);
};
let region = fn(ledger: Tx) {
let charge = retry(
fn() {
with tx = ledger {
withdraw(tx, 5)
}
},
);
charge();
};
$ship-types-test retry.shp
error: `retry` cannot take a callable that ends `tx`
--> retry.shp:32:22
|
32 | let charge = retry(() => withdraw(tx, 5));
| ^^^^^^^^^^^^^^^^^^^^^ this callable ends `tx`, and `sig 'f` hides that from `retry`
help: `retry` may catch the failure and keep running inside the scope of `tx`, for example to retry it. Retry the whole region instead: open `tx` with `with` inside the callable
error: `retry` cannot take a callable that ends `Audit`
--> retry.shp:37:23
|
37 | let audited = retry(record);
| ^^^^^^ this callable ends `Audit`, and `sig 'f` hides that from `retry`
help: `retry` may catch the failure and keep running inside the scope of `Audit`, for example to retry it. Retry the whole region instead: `provide` `Audit` inside the callable
error: `retry` cannot take a callable that ends `tx`
--> retry.shp:42:22
|
42 | let charge = retry(withdraw);
| ^^^^^^^^ this callable ends `tx`, and `sig 'f` hides that from `retry`
help: `retry` may catch the failure and keep running inside the scope of `tx`, for example to retry it. Retry the whole region instead: open a `with` inside the callable instead of taking `tx`Compile-time validation
A constant that fails its ensure, an ensure! that reaches a step which cannot run while checking, an ensure! constant whose evaluation cannot finish, and two validations on one declaration are errors. Compile-time code shows the accepted forms.
Total functions should reject construction of ensured types.
$ship-types-test totality.shp
error: fallible expression in total function
--> totality.shp:12:10
|
12 | return { age = age; };
| ^^^^^^^^^^^^^^ produces [ :negative_age(Int) ]A constant that fails its ensure is a compile error. The rendered payload must match the runtime must defect for the same value in crates/ship-cli/tests/golden/run-ensure-construction — both sides run the same interpreter, and this pairing guards against drift.
$ship-types-test constant-failure.shp
error: construction fails validation with :negative_age(-1)
--> constant-failure.shp:12:11
|
12 | { age = 0 - 1; };
| ^^^^^ field `age` fails its `ensure` at compile timeField defaults are validated against their ensures at compile time; a failing default can never construct, so it is a compile error.
$ship-types-test default-validation.shp
error: default fails validation with :negative_balance(-50)
--> default-validation.shp:3:15
|
3 | default = 0 - 50;
| ^^^^^^ this default fails the field's `ensure` at compile timeA module's own types enter compile-time scope before its values check, so an ensure body can construct them at constant sites in the same module.
$ship-types-test current-module-types.shp
error: construction fails validation with :too_low(1)
--> current-module-types.shp:24:32
|
24 | let low = () -> Score => Score(1);
| ^ value fails `Score` `ensure` at compile timeBare product literals must resolve to one scoped product type.
$ship-types-test ambiguous-product-literal.shp
error: fields { id } match multiple product types
--> ambiguous-product-literal.shp:10:3
|
10 | { id = id; };
| ^^^^^^^^^^^^ qualify the literal, for example `Team`.{ ... }
--> ambiguous-product-literal.shp:10:3
|
10 | { id = id; };
| ------------ candidates: `Team`, `User`Bare product literals stay ambiguous when labels and values match multiple products.
$ship-types-test product-literal-value-ambiguity.shp
error: fields { id } match multiple product types
--> product-literal-value-ambiguity.shp:10:3
|
10 | { id = id; };
| ^^^^^^^^^^^^ qualify the literal, for example `TeamId`.{ ... }
--> product-literal-value-ambiguity.shp:10:3
|
10 | { id = id; };
| ------------ candidates: `TeamId`, `UserId`Bare product literals cannot introduce anonymous product types.
$ship-types-test unknown-product-literal.shp
error: no product type in scope matches fields { name }
--> unknown-product-literal.shp:6:3
|
6 | { name = name; };
| ^^^^^^^^^^^^^^^^ declare a product type or use qualified constructionCall-style product construction is rejected.
$ship-types-test call-style-construction.shp
error: call-style product construction is not supported
--> call-style-construction.shp:5:43
|
5 | let make_user = (name: String) -> User => User({ name = name; });
| ^^^^^^^^^^^^^^^^^^^^^^ use `Type { ... }` for direct construction or `Type.{ ... }` for namespace-qualified inferenceDirect product construction reports exact shape errors.
$ship-types-test product-construction-errors.shp
error: missing field `name`
--> product-construction-errors.shp:7:8
|
7 | User { id = id; };
| ^^^^^^^^^^^^ required by product type `User`
error: unknown field `nickname`
--> product-construction-errors.shp:10:8
|
10 | User { id = id; name = name; nickname = name; };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not a field on product type `User`
error: duplicate field `id`
--> product-construction-errors.shp:13:8
|
13 | User { id = id; id = id; name = name; };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ field is specified more than once
error: type mismatch
--> product-construction-errors.shp:16:15
|
16 | User { id = name; name = name; };
| ^^^^ expected Int, found StringProduct updates reject unknown fields, non-product bases, duplicate fields, and wrong values.
$ship-types-test product-update-errors.shp
error: unknown field `nickname`
--> product-update-errors.shp:7:3
|
7 | { user with nickname = "Ada"; };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not a field on product type `User`
error: product update base is not a product value
--> product-update-errors.shp:10:5
|
10 | { value with name = "Ada"; };
| ^^^^^ expected a product value, found Int
error: type mismatch
--> product-update-errors.shp:13:20
|
13 | { user with id = "wrong"; };
| ^^^^^^^ expected Int, found String
error: duplicate field `name`
--> product-update-errors.shp:16:3
|
16 | { user with name = "Ada"; name = "Grace"; };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ field is specified more than onceGeneric product candidates stay ambiguous when labels alone are insufficient.
$ship-types-test generic-product-ambiguity.shp
error: fields { value } match multiple product types
--> generic-product-ambiguity.shp:10:3
|
10 | { value = value; };
| ^^^^^^^^^^^^^^^^^^ qualify the literal, for example `Alpha`.{ ... }
--> generic-product-ambiguity.shp:10:3
|
10 | { value = value; };
| ------------------ candidates: `Alpha`, `Beta`ensure! must be compile-time compatible: every helper it reaches is followed, so an effectful builtin behind a helper is rejected at the declaration.
$ship-types-test required-effectful.shp
error: `ensure!` cannot run at compile time
--> required-effectful.shp:5:8
|
5 | if digest(value) == "" {
| ^^^^^^ this cannot run at compile time
note: `Password.hash` is not a pure builtin
help: use `ensure` to validate at runtime instead
error: `ensure!` cannot run at compile time
--> required-effectful.shp:14:10
|
14 | if Password.hash(value) == "" {
| ^^^^^^^^^^^^^ this cannot run at compile time
note: `Password.hash` is not a pure builtin
help: use `ensure` to validate at runtime insteadUnder ensure!, a constant input that compile-time evaluation cannot finish is a compile error naming the reason; runtime inputs still validate at runtime.
$ship-types-test required-constant.shp
error: `ensure!` could not validate this constant at compile time
--> required-constant.shp:23:18
|
23 | return Endless(1);
| ^ evaluation stopped because it exceeds the compile-time step budget
error: `ensure!` could not validate this constant at compile time
--> required-constant.shp:38:10
|
38 | return { low = 0; high = 1; };
| ^^^^^^^^^^^^^^^^^^^^^^ evaluation stopped because it exceeds the compile-time step budgetA declaration has at most one validation, whichever spelling it uses.
$ship-types-test required-duplicate.shp
error: a declaration has at most one `ensure`
--> required-duplicate.shp:7:3
|
7 | ensure! = fn(value) {
| ^^^^^^^ second validation here
help: combine the checks into one functionType descriptors
Type.of over a generic witness is an error: there is no statically named type to describe. Compile-time code shows the accepted form.
A descriptor needs a statically named type; a generic witness has none.
$ship check errors.shp
error: `Type.of` needs a statically named type
--> errors.shp:1:66
|
1 | let describe = fn('t)(target: Struct('t)) -> Meta.Type { Type.of(target) };
| ^^^^^^ `Struct('a)` is not a named type known at compile time
help: name a declared type directly, as in `Type.of(User)`Program targets
A browser program must provide its contract, and each program target may use only what that target provides: browser APIs stay in browser programs, and a builtin whose provider runs only on Native is rejected in a browser program before it is built.
Missing child contract members are reported together.
$(cd cases/embed-missing-contract && ship check .)
error: program(Web) is missing `type message`
--> client/Counters/Counter.shp:1:9
|
1 | program(Web) {
| ^^^ in this client program
error: program(Web) is missing `let update`
--> client/Counters/Counter.shp:1:9
|
1 | program(Web) {
| ^^^ in this client program
error: program(Web) is missing `let view`
--> client/Counters/Counter.shp:1:9
|
1 | program(Web) {
| ^^^ in this client program
error: embedded browser program `Counter` is missing `type message`
--> client/Counters/init.shp:2:9
|
2 | embed Counter :left;
| ^^^^^^^ embedded child contract checked here
error: embedded browser program `Counter` is missing `let update`
--> client/Counters/init.shp:2:9
|
2 | embed Counter :left;
| ^^^^^^^ embedded child contract checked here
error: embedded browser program `Counter` is missing `let view`
--> client/Counters/init.shp:2:9
|
2 | embed Counter :left;
| ^^^^^^^ embedded child contract checked hereUnknown embedded child modules point at the embed.
$(cd cases/embed-unknown-module && ship check .)
error: unknown embedded browser program `MissingCounter`
--> client/Counters/init.shp:2:9
|
2 | embed MissingCounter :left;
| ^^^^^^^^^^^^^^ no target-local client module with this name
help: add `client/<App>/MissingCounter.shp` with a `program(Web) { ... }` or `program(Android) { ... }` declaration matching the appNon-program child modules cannot be embedded.
$(cd cases/embed-not-program && ship check .)
error: `embed Counter` can only embed a browser child program
--> client/Counters/init.shp:2:9
|
2 | embed Counter :left;
| ^^^^^^^ this module does not define a program
help: use portable `program { ... }` under `client/src/`, or a `program(...) { ... }` declaration matching the app in `Counter.shp`Duplicate static embed tags are rejected.
$(cd cases/embed-duplicate-tag && ship check .)
error: duplicate program embed tag `:left`
--> client/Counters/init.shp:2:17
|
2 | embed Counter :left;
| ^^^^^ first embed with this tag
--> client/Counters/init.shp:3:17
|
3 | embed Counter :left;
| ----- duplicate embed tagStatic embed delegation requires a direct message match.
$(cd cases/embed-update-shape && ship check .)
error: static browser embed delegation requires `update` to match on the message
--> client/Counters/init.shp:16:7
|
16 | let update = fn(model, message) {
| ^^^^^^ expected `let update = (model, message) => match message { ... };`Keyed embeds require all routing settings.
$(cd cases/keyed-missing-setting && ship check .)
error: keyed embed `:counter` is missing `model`
--> client/Counters/init.shp:2:3
|
2 | embed Counter :counter {
| ^ keyed embed configured hereKeyed row projections must name an existing child model field.
$(cd cases/keyed-missing-field && ship check .)
error: keyed row type is missing child model field `counter`
--> client/Counters/init.shp:5:13
|
5 | model = (row) => row.counter;
| ^^^^^^^^^^^^^^^^^^^^ `model` projects this fieldSTUB: keyed embeds reject child subscriptions until lifecycle support exists (SHIP-258).
$(cd cases/keyed-child-subscribe && ship check .)
error: keyed browser embeds do not support child subscriptions yet
--> client/Counters/init.shp:2:9
|
2 | embed Counter :counter {
| ^^^^^^^ this child program defines `subscribe`
help: remove the child `subscribe` or use a static embed until keyed subscription lifecycle support existsMalformed browser program contract functions are rejected.
$(cd cases/contract-shapes && ship check .)
error: program(Web) requires `let init` to be `fn()`
--> client/Bad/init.shp:10:7
|
10 | let init = fn(seed) {
| ^^^^ expected `fn()`
error: program(Web) requires `let update` to be `fn(model, message)`
--> client/Bad/init.shp:14:7
|
14 | let update = (model) => model;
| ^^^^^^ expected `fn(model, message)`
error: program(Web) requires `let view` to be `fn(model)`
--> client/Bad/init.shp:15:7
|
15 | let view = (model, on_pressed) => Html.div([]);
| ^^^^ expected `fn(model)`Wrong init return type is rejected.
$(cd cases/wrong-init-return && ship check .)
error: type mismatch
--> client/Bad/init.shp:11:12
|
11 | return 0;
| ^ expected __program, found IntWrong update return type is rejected.
$(cd cases/wrong-update-return && ship check .)
error: type mismatch
--> client/Bad/init.shp:14:36
|
14 | let update = (model, message) => 0;
| ^ expected __program, found IntWrong view return type is rejected.
$(cd cases/wrong-view-return && ship check .)
error: type mismatch
--> client/Bad/init.shp:15:25
|
15 | let view = (model) => "not html";
| ^^^^^^^^^^ expected Html(__program.message), found StringSibling client modules are checked with the browser target.
$(cd cases/sibling-type-error && ship check .)
error: type mismatch
--> client/Counters/Helper.shp:1:13
|
1 | let broken: Int = "not int";
| ^^^ expected Int, found StringDuplicate subscription prefixes are rejected.
$(cd cases/duplicate-subscription && ship check .)
error: duplicate program subscription prefix `:pointer`
--> client/Bad/init.shp:2:32
|
2 | subscription Browser.Pointer :pointer { };
| ^^^^^^^^ first subscription with this prefix
--> client/Bad/init.shp:3:30
|
3 | subscription Browser.Mouse :pointer { };
| -------- duplicate subscription prefixGenerated program clients require on_error.
$(cd cases/missing-client-on-error && ship check .)
error: program client `TodoClient` must configure `on_error`
--> client/Bad/init.shp:2:10
|
2 | client TodoClient : TodoApi;
| ^^^^^^^^^^ success-only route calls need a default error callback
help: write `client TodoClient : TodoApi { on_error = (fetch_error: Browser.Fetch.Error) => :failed(fetch_error); }`Browser-only APIs are rejected outside browser program targets.
$(cd cases/native-browser-api && ship check .)
error: browser APIs are only available in browser program targets
--> bin/main.shp:2:3
|
2 | Browser.Fetch.get(
| ^^^^^^^^^^^^^^^^^ browser-only API used outside `client/<App>`
help: move this code under a browser target such as `client/App/init.shp`; use `HttpClient` for portable outbound HTTP on native serversNative-only builtins are rejected in browser programs at check time.
$(cd cases/browser-native-provider && ship check .)
error: `Password.hash` is not available in Browser programs
--> client/Bad/init.shp:14:40
|
14 | | :hash(text) -> { digest = must Password.hash(text); }
| ^^^^^^^^^^^^^^^^^^^ its provider `password_hash` runs only on Native
help: call it from a Native target and send its result to this programType descriptors are not available in browser programs yet.
$(cd cases/browser-type-descriptor && ship check .)
error: `Type.of` is not available in browser programs yet
--> client/Bad/init.shp:14:45
|
14 | | :describe -> { schema = Json.schema(Type.of(t)); }
| ^^^^^^^^^^ descriptors have no JavaScript form
help: compute what you need from the descriptor on a Native targetNot supported yet
Named type arguments, record literal spreads, spreads in tag payloads, Html outside browser programs, and codecs for tag records are decided or planned but not built; each reports that in place of a generic error. Updating records shows the accepted way to copy a record.
Type arguments are positional; naming one waits on question 087.
// fmt: ignore
type Box('a) = {
value: 'a;
}
let unbox = fn(b: Box('a = Int)) -> Int { b.value };
$ship-types-test named-type-arguments.shp
error: named type arguments are not supported yet
--> named-type-arguments.shp:6:23
|
6 | let unbox = fn(b: Box('a = Int)) -> Int { b.value };
| ^^ type argument named here
help: pass every type argument by position, in declaration orderRecord literals cannot spread a record value yet, with or without a type name; question 063 owns the design.
// fmt: ignore
type Aria = {
label: String;
expanded: Bool;
}
let expand = fn(aria: Aria) -> Aria { { ..aria; expanded = True; } };
let expand_typed = fn(aria: Aria) -> Aria { Aria { ..aria; expanded = True; } };
$ship-types-test record-literal-spread.shp
error: record literal spreads are not supported yet
--> record-literal-spread.shp:7:41
|
7 | let expand = fn(aria: Aria) -> Aria { { ..aria; expanded = True; } };
| ^^^^^^ spread of a record value
help: copy the record with `{ base with field = value }`
error: record literal spreads are not supported yet
--> record-literal-spread.shp:9:52
|
9 | let expand_typed = fn(aria: Aria) -> Aria { Aria { ..aria; expanded = True; } };
| ^^^^^^ spread of a record value
help: copy the record with `{ base with field = value }`Tag payloads cannot spread a record type yet.
// fmt: ignore
type Common = {
id: String;
}
type Attr = [
| :link sparse { ..Common; href: String }
| :plain
]
$ship-types-test tag-payload-spread.shp
error: spreads in tag payloads are not supported yet
--> tag-payload-spread.shp:7:20
|
7 | | :link sparse { ..Common; href: String }
| ^^^^^^^^ record spread in a tag payload
help: write the copied fields out in the payloadHtml views run only in browser programs until one Html tree runs on every target (question 185).
let view = fn() { Html.div([Html.text("hi")]) };
$ship-types-test native-html.shp
error: `Html` is browser-only for now
--> native-html.shp:1:19
|
1 | let view = fn() { Html.div([Html.text("hi")]) };
| ^^^^^^^^ `Html` views run only in browser programs
help: move this view under a browser target such as `client/App/init.shp`; native servers render pages with `ServerHTML`
error: `Html` is browser-only for now
--> native-html.shp:1:29
|
1 | let view = fn() { Html.div([Html.text("hi")]) };
| ^^^^^^^^^ `Html` views run only in browser programs
help: move this view under a browser target such as `client/App/init.shp`; native servers render pages with `ServerHTML`A sparse field declared not_implemented tells a caller that supplies it why, instead of reporting an unknown argument; the reason must be a string literal, and only a sparse field can carry it.
type Attrs = sparse {
class: String;
data: Never #{ not_implemented = "data tags wait on question 087"; };
}
let button = (label: String, ..attrs: Attrs) => label;
let tagged = fn() -> String { button("a", data = "x") };
type Required = {
data: Never #{ not_implemented = "a required field"; };
}
type Unquoted = sparse {
data: Never #{ not_implemented = reason; };
}
$ship-types-test not-implemented-field.shp
error: `data` is not implemented yet
--> not-implemented-field.shp:7:50
|
7 | let tagged = fn() -> String { button("a", data = "x") };
| ^^^ data tags wait on question 087
error: only a sparse field can be not implemented, and `data` is required
--> not-implemented-field.shp:10:18
|
10 | data: Never #{ not_implemented = "a required field"; };
| ^^^^^^^^^^^^^^^ a required field nobody can supply makes the record unconstructible
error: `not_implemented` must be a string literal
--> not-implemented-field.shp:14:36
|
14 | data: Never #{ not_implemented = reason; };
| ^^^^^^ callers read this reason as writtenTag records have no JSON or Wire codec yet, so a record with a Tags field derives no Decode.
type Flag = [
| :bold
| :italic
]
type Style = {
flags: Tags(Flag);
}
let decode = fn(bytes: Bytes) { Json.decode(bytes, Style) };
$ship-types-test tags-codec.shp
error: type `Style` does not support `Decode`
--> tags-codec.shp:10:33
|
10 | let decode = fn(bytes: Bytes) { Json.decode(bytes, Style) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^ expected a value that supports `Decode`