Ship

Book · Behavior Synthesis

Behavior Synthesis

Draft

Control Ship's compiler-generated value behaviors.

On this page · 3 sections

Concrete product and variant types receive canonical Display, Inspect, Encode, and Decode behavior modules when their shapes support them. These are ordinary checked behavior modules: constrained functions and direct behavior calls use them in exactly the same way as hand-written modules. Core follows the same rule: scalars and collections have checked canonical modules, while concrete Core products and variants receive automatic Display and Inspect derives during toolchain assembly. A project may replace one Core default with its own canonical module; two project definitions for the same slot remain a coherence error.

Every slot exists from the start, but a program checks and compiles a synthesized module only once it uses it, directly or through another synthesized module. A type that is never printed or sent over the wire costs nothing beyond its declaration; ship check --generated still writes every module for review.

Synthesis stops at these four. Every other behavior, such as Order, HashKey, or a project's own Rank, is a hand-written canonical module (Behaviors), so a type never gains an API its author did not write.

type Account = {
  name: String;
  active: Bool;
}

Default Synthesis

test✓
  let accountaccount: Book.LanguageReference.BehaviorSynthesis.Account = Account { name = "Ada""Ada": String; active = TrueTrue: Bool; };
  expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.LanguageReference.BehaviorSynthesis.Account) -> String(accountaccount: Book.LanguageReference.BehaviorSynthesis.Account) == "Account { name = Ada; active = True }""Account { name = Ada; active = True }": String;
  expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: Book.LanguageReference.BehaviorSynthesis.Account) -> String(accountaccount: Book.LanguageReference.BehaviorSynthesis.Account) == "Account { name = \"Ada\"; active = True }""Account { name = \"Ada\"; active = True }": String;

Generated codecs use the same nominal value on both sides of the wire.

Generated Product Codecs

test✓
  let accountaccount: Book.LanguageReference.BehaviorSynthesis.Account = Account { name = "Ada""Ada": String; active = TrueTrue: Bool; };
  let encodedencoded: Bytes = must Json.encodeencode: pure fn('a: Encode)(value: Book.LanguageReference.BehaviorSynthesis.Account) -> Bytes, exn [ :encode(Wire.EncodeError) ](accountaccount: Book.LanguageReference.BehaviorSynthesis.Account);
  expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](encodedencoded: Bytes) == "{\"name\":\"Ada\",\"active\":true}""{\"name\":\"Ada\",\"active\":true}": String;

  let readerreader: Json.Reader = Json.Reader.newnew: pure fn!(body: Bytes, ~max_depth: Int = 128) -> Json.Reader(encodedencoded: Bytes);
  let decodeddecoded: Book.LanguageReference.BehaviorSynthesis.Account: Account = must Decode.decode(readerreader: Json.Reader);
  expect ✓decodeddecoded: Book.LanguageReference.BehaviorSynthesis.Account == accountaccount: Book.LanguageReference.BehaviorSynthesis.Account;

Closed and structural variants receive codecs when every payload does.

type LoadState =
  | Ready(String)
  | Progress(Int, String)
  | Idle

type JobState = [
  | :idle
  | :done(String)
  | :failed { reason: String; retry: Bool }
]

Generated Variant Codecs

test✓
  let readyready: Book.LanguageReference.BehaviorSynthesis.LoadState: LoadState = Ready("ok""ok": String);
  expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.LanguageReference.BehaviorSynthesis.LoadState) -> Bytes, exn [ :encode(Wire.EncodeError) ](readyready: Book.LanguageReference.BehaviorSynthesis.LoadState)) == "{\"Ready\":\"ok\"}""{\"Ready\":\"ok\"}": String;

  let ready_readerready_reader: Json.Reader = Json.Reader.newnew: pure fn!(body: Bytes, ~max_depth: Int = 128) -> Json.Reader(Bytes.of_stringof_string: pure fn!(string: String) -> Bytes("{\"Ready\":\"decoded\"}""{\"Ready\":\"decoded\"}": String));
  let decoded_readydecoded_ready: Book.LanguageReference.BehaviorSynthesis.LoadState: LoadState = must Decode.decode(ready_readerready_reader: Json.Reader);
  expect ✓decoded_readydecoded_ready: Book.LanguageReference.BehaviorSynthesis.LoadState == ReadyReady: fn(String) -> Book.LanguageReference.BehaviorSynthesis.LoadState("decoded""decoded": String);

  let progressprogress: Book.LanguageReference.BehaviorSynthesis.LoadState: LoadState = Progress(22: Int, "loading""loading": String);
  let encoded_progressencoded_progress: Bytes = must Json.encodeencode: pure fn('a: Encode)(value: Book.LanguageReference.BehaviorSynthesis.LoadState) -> Bytes, exn [ :encode(Wire.EncodeError) ](progressprogress: Book.LanguageReference.BehaviorSynthesis.LoadState);
  expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](encoded_progressencoded_progress: Bytes) == "{\"Progress\":[2,\"loading\"]}""{\"Progress\":[2,\"loading\"]}": String;

  let progress_readerprogress_reader: Json.Reader = Json.Reader.newnew: pure fn!(body: Bytes, ~max_depth: Int = 128) -> Json.Reader(encoded_progressencoded_progress: Bytes);
  let decoded_progressdecoded_progress: Book.LanguageReference.BehaviorSynthesis.LoadState: LoadState = must Decode.decode(progress_readerprogress_reader: Json.Reader);
  expect ✓decoded_progressdecoded_progress: Book.LanguageReference.BehaviorSynthesis.LoadState == progressprogress: Book.LanguageReference.BehaviorSynthesis.LoadState;

  let encoded_progress_xmlencoded_progress_xml: Bytes = must Xml.encodeencode: pure fn('a: Encode)(value: Book.LanguageReference.BehaviorSynthesis.LoadState) -> Bytes, exn [ :encode(Wire.EncodeError) ](progressprogress: Book.LanguageReference.BehaviorSynthesis.LoadState);
  expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](encoded_progress_xmlencoded_progress_xml: Bytes)
    == "<Progress><item>2</item><item>loading</item></Progress>""<Progress><item>2</item><item>loading</item></Progress>": String;

  let decoded_progress_xmldecoded_progress_xml: Book.LanguageReference.BehaviorSynthesis.LoadState: LoadState = must Decode.decode(Xml.Reader.newnew: pure fn!(body: Bytes, ~max_depth: Int = 128) -> Xml.Reader(encoded_progress_xmlencoded_progress_xml: Bytes));
  expect ✓decoded_progress_xmldecoded_progress_xml: Book.LanguageReference.BehaviorSynthesis.LoadState == progressprogress: Book.LanguageReference.BehaviorSynthesis.LoadState;

  let failedfailed: Book.LanguageReference.BehaviorSynthesis.JobState: JobState = :failed { reason = "boom""boom": String; retry = TrueTrue: Bool; };
  let encoded_failedencoded_failed: Bytes = must Json.encodeencode: pure fn('a: Encode)(value: Book.LanguageReference.BehaviorSynthesis.JobState) -> Bytes, exn [ :encode(Wire.EncodeError) ](failedfailed: Book.LanguageReference.BehaviorSynthesis.JobState);
  expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](encoded_failedencoded_failed: Bytes)
    == "{\":failed\":{\"reason\":\"boom\",\"retry\":true}}""{\":failed\":{\"reason\":\"boom\",\"retry\":true}}": String;

  let failed_readerfailed_reader: Json.Reader = Json.Reader.newnew: pure fn!(body: Bytes, ~max_depth: Int = 128) -> Json.Reader(encoded_failedencoded_failed: Bytes);
  let decoded_faileddecoded_failed: Book.LanguageReference.BehaviorSynthesis.JobState: JobState = must Decode.decode(failed_readerfailed_reader: Json.Reader);
  expect ✓decoded_faileddecoded_failed: Book.LanguageReference.BehaviorSynthesis.JobState == failedfailed: Book.LanguageReference.BehaviorSynthesis.JobState;

Primitive canonical defaults use the same generic behavior lookup.

Primitive Defaults

test✓
  let inferredinferred: Int: Int = default();
  expect ✓inferredinferred: Int == 00: Int;
  expect ✓default(Float) == 0.00.0: Float;
  expect ✓default(Bool) == FalseFalse: Bool;
  expect ✓default(String) == """": String;
  expect ✓default(Unit) == ()(): Unit;
  expect ✓default(Byte) == '\x00''\x00': Byte;
  expect ✓default(UInt64) == 00: Int;

Selective opt-outs

A capability after the type head disables only that automatic behavior. This is useful when a value should remain human-readable but must not cross a machine-format boundary automatically.

type HumanMessage !Encode = {
  text: String;
}

Selective Opt Out

test✓
  let messagemessage: Book.LanguageReference.BehaviorSynthesis.HumanMessage = HumanMessage { text = "hello""hello": String; };
  expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.LanguageReference.BehaviorSynthesis.HumanMessage) -> String(messagemessage: Book.LanguageReference.BehaviorSynthesis.HumanMessage) == "HumanMessage { text = hello }""HumanMessage { text = hello }": String;

No automatic behaviors

type! disables automatic behavior synthesis as a category. Unlike spelling today's four selective opt-outs, it also covers any default behaviors Ship adds in the future. It does not remove the type's runtime shape or schema metadata.

type! ManualLabel = {
  value: String;
}

module behavior Display(ManualLabel) {
  documentdocument: fn(label: Book.LanguageReference.BehaviorSynthesis.ManualLabel, _context: Display.Context) -> Format.Doc = (labellabel: Book.LanguageReference.BehaviorSynthesis.ManualLabel: ManualLabel, _context_context: Display.Context: Stdlib.Display.Context) =>
    Stdlib.Format.texttext: pure fn!(value: String) -> Format.Doc("label:""label:": String ++ labellabel: Book.LanguageReference.BehaviorSynthesis.ManualLabel.valuevalue: String);
}

Manual Behavior

test✓
  expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.LanguageReference.BehaviorSynthesis.ManualLabel) -> String(ManualLabel { value = "stable""stable": String; }) == "label:stable""label:stable": String;

Explicit late derive

type! prevents automatic synthesis; it does not forbid an explicit request. derive runs the named compiler-known generator at that point.

type! Coordinate = {
  x: Int;
  y: Int;
}

derive Display(Coordinate);

Late Derive

test✓
  expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.LanguageReference.BehaviorSynthesis.Coordinate) -> String(Coordinate { x = 33: Int; y = 44: Int; }) == "Coordinate { x = 3; y = 4 }""Coordinate { x = 3; y = 4 }": String;