Ship

Book · Format, Display, and Inspect

Format, Display, and Inspect

Draft

Build human and diagnostic representations that adapt to available width.

On this page · 5 sections

Format.Doc is an immutable structured document. Text is fixed, while soft breaks record places where a renderer may choose a newline. group keeps every soft break flat when the complete group fits; nest controls indentation when it does not.

let deploymentdeployment: Format.Doc = Format.groupgroup: pure fn!(document: Format.Doc) -> Format.Doc(
  Format.concatconcat: pure fn!(documents: Vector(Format.Doc)) -> Format.Doc(
    [
      Format.texttext: pure fn!(value: String) -> Format.Doc("deploy(""deploy(": String),
      Format.nestnest: pure fn!(document: Format.Doc, by: Int) -> Format.Doc(
        Format.concatconcat: pure fn!(documents: Vector(Format.Doc)) -> Format.Doc(
          [
            Format.soft_breaksoft_break: pure fn!(~flat: String = " ") -> Format.Doc(flat = """": String),
            Format.joinjoin: pure fn!(documents: Vector(Format.Doc), separator: Format.Doc) -> Format.Doc(
              [Format.texttext: pure fn!(value: String) -> Format.Doc("alpha""alpha": String), Format.texttext: pure fn!(value: String) -> Format.Doc("beta""beta": String)],
              Format.concatconcat: pure fn!(documents: Vector(Format.Doc)) -> Format.Doc([Format.texttext: pure fn!(value: String) -> Format.Doc(","",": String), Format.soft_breaksoft_break: pure fn!(~flat: String = " ") -> Format.Doc()]),
            ),
          ],
        ),
        22: Int,
      ),
      Format.soft_breaksoft_break: pure fn!(~flat: String = " ") -> Format.Doc(flat = """": String),
      Format.texttext: pure fn!(value: String) -> Format.Doc(")"")": String),
    ],
  ),
);

Width Aware Layout

test✓
  expect ✓Format.renderrender: pure fn!(document: Format.Doc, width: Int, ~ribbon: Option(Int) = None) -> String(deploymentdeployment: Format.Doc, 8080: Int) == "deploy(alpha, beta)""deploy(alpha, beta)": String;
  expect ✓Format.renderrender: pure fn!(document: Format.Doc, width: Int, ~ribbon: Option(Int) = None) -> String(deploymentdeployment: Format.Doc, 1212: Int) == "deploy(\n  alpha,\n  beta\n)""deploy(\n  alpha,\n  beta\n)": String;
  expect ✓Format.compactcompact: pure fn!(document: Format.Doc) -> String(deploymentdeployment: Format.Doc) == "deploy(alpha, beta)""deploy(alpha, beta)": String;

Semantic annotations

An annotation describes meaning rather than a backend style. String, ANSI, HTML, and editor renderers can interpret the same role differently without putting escape codes or markup into the document.

let annotated_nameannotated_name: Format.Doc = Format.annotateannotate: pure fn!(document: Format.Doc, role: Format.Role) -> Format.Doc(Format.texttext: pure fn!(value: String) -> Format.Doc("Invoice""Invoice": String), TypeName);

Annotations Preserve Text

test✓
  expect ✓Format.renderrender: pure fn!(document: Format.Doc, width: Int, ~ribbon: Option(Int) = None) -> String(annotated_nameannotated_name: Format.Doc, 8080: Int) == "Invoice""Invoice": String;

Display and Inspect

Display is concise, human-facing presentation. Inspect is quoted, deterministic diagnostic structure. Both produce Format.Doc; layout is a separate decision made by the destination.

type Currency =
  | USD
  | EUR

type! Money = {
  cents: Int;
  currency: Currency;
}

let money_textmoney_text: fn(money: Book.LanguageReference.Formatting.Money) -> String = fn(moneymoney: Book.LanguageReference.Formatting.Money: Money) {
  let wholewhole: Int = moneymoney: Book.LanguageReference.Formatting.Money.centscents: Int / 100100: Int;
  let remainderremainder: Int = moneymoney: Book.LanguageReference.Formatting.Money.centscents: Int - wholewhole: Int * 100100: Int;
  let fractionalfractional: String = if remainderremainder: Int < 1010: Int {
    | "0""0": String ++ Int.to_stringto_string: pure fn!(value: Int) -> String(remainderremainder: Int)
    | Int.to_stringto_string: pure fn!(value: Int) -> String(remainderremainder: Int)
  };
  return "\$""\$": String ++ Int.to_stringto_string: pure fn!(value: Int) -> String(wholewhole: Int) ++ "."".": String ++ fractionalfractional: String;
};

module behavior Display(Money) {
  documentdocument: fn(money: Book.LanguageReference.Formatting.Money, _context: Display.Context) -> Format.Doc = (moneymoney: Book.LanguageReference.Formatting.Money: Money, _context_context: Display.Context: Display.Context) =>
    Format.annotateannotate: pure fn!(document: Format.Doc, role: Format.Role) -> Format.Doc(Format.texttext: pure fn!(value: String) -> Format.Doc(money_textmoney_text: fn(money: Book.LanguageReference.Formatting.Money) -> String(moneymoney: Book.LanguageReference.Formatting.Money)), NumberValue);
}

module behavior Inspect(Money) {
  documentdocument: fn(money: Book.LanguageReference.Formatting.Money, context: Inspect.Context) -> Format.Doc = (moneymoney: Book.LanguageReference.Formatting.Money: Money, contextcontext: Inspect.Context: Inspect.Context) =>
    Inspect.productproduct: pure fn!(name: String, fields: Vector(Format.Doc)) -> Format.Doc(
      "Money""Money": String,
      [
        Inspect.fieldfield: pure fn!(name: String, value: Format.Doc) -> Format.Doc(
          "cents""cents": String,
          Inspect.document(moneymoney: Book.LanguageReference.Formatting.Money.centscents: Int, contextcontext: Inspect.Context),
        ),
        Inspect.fieldfield: pure fn!(name: String, value: Format.Doc) -> Format.Doc(
          "currency""currency": String,
          Inspect.document(moneymoney: Book.LanguageReference.Formatting.Money.currencycurrency: Book.LanguageReference.Formatting.Currency, contextcontext: Inspect.Context),
        ),
      ],
    );
}

Display And Inspect

test✓
  let amountamount: Book.LanguageReference.Formatting.Money = Money { cents = 12501250: Int; currency = USD; };
  expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.LanguageReference.Formatting.Money) -> String(amountamount: Book.LanguageReference.Formatting.Money) == "\$12.50""\$12.50": String;
  expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: Book.LanguageReference.Formatting.Money) -> String(amountamount: Book.LanguageReference.Formatting.Money) == "Money { cents = 1250; currency = USD }""Money { cents = 1250; currency = USD }": String;
  expect ✓Format.renderrender: pure fn!(document: Format.Doc, width: Int, ~ribbon: Option(Int) = None) -> String(Display.to_docto_doc: pure fn!('a: Display)(value: Book.LanguageReference.Formatting.Money) -> Format.Doc(amountamount: Book.LanguageReference.Formatting.Money), 88: Int) == "\$12.50""\$12.50": String;

Primitive and collection behavior modules are ordinary checked Core modules. Collection constraints carry the element module through nested traversal, so a custom behavior is never replaced by a runtime fallback.

Nested Behaviors

test✓
  let amountamount: Book.LanguageReference.Formatting.Money = Money { cents = 12501250: Int; currency = USD; };
  expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: Option(Book.LanguageReference.Formatting.Money)) -> String(SomeSome: fn(Book.LanguageReference.Formatting.Money) -> Option(Book.LanguageReference.Formatting.Money)(amountamount: Book.LanguageReference.Formatting.Money)) == "Some(Money { cents = 1250; currency = USD })""Some(Money { cents = 1250; currency = USD })": String;
  expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: List(Book.LanguageReference.Formatting.Money)) -> String(listlist: pure fn!('a)(...Book.LanguageReference.Formatting.Money) -> List(Book.LanguageReference.Formatting.Money)(amountamount: Book.LanguageReference.Formatting.Money)) == "list [Money { cents = 1250; currency = USD }]""list [Money { cents = 1250; currency = USD }]": String;
  expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: (Book.LanguageReference.Formatting.Money, String)) -> String((amountamount: Book.LanguageReference.Formatting.Money, "paid""paid": String)) == "(Money { cents = 1250; currency = USD }, \"paid\")""(Money { cents = 1250; currency = USD }, \"paid\")": String;

Tuples, open variants, and query projections have no module of their own. They format by their checked shape: each element gets its own behavior at compile time, so generic code and nested collections see the same document.

let describeddescribed: fn('a: Display)(value: 'a) -> String = ('a: Display)(valuevalue: 'a: 'a) => "value ${valuevalue: 'a}";

Structural Values

test✓
  let amountamount: Book.LanguageReference.Formatting.Money = Money { cents = 12501250: Int; currency = USD; };
  let paymentpayment: [ :paid(Book.LanguageReference.Formatting.Money) | :pending ]: [ :paid(Money) | :pending ] = :paid(amountamount: Book.LanguageReference.Formatting.Money);
  expect ✓Display.displaydisplay: pure fn!('a: Display)(value: (Book.LanguageReference.Formatting.Money, [ :paid(Book.LanguageReference.Formatting.Money) | :pending ])) -> String((amountamount: Book.LanguageReference.Formatting.Money, paymentpayment: [ :paid(Book.LanguageReference.Formatting.Money) | :pending ])) == "(\$12.50, :paid(\$12.50))""(\$12.50, :paid(\$12.50))": String;
  expect ✓describeddescribed: fn('a: Display)(value: Vector((Int, Option([> :pending ])))) -> String([(11: Int, SomeSome: fn([> :pending ]) -> Option([> :pending ])(:pending))]) == "value vector [(1, Some(:pending))]""value vector [(1, Some(:pending))]": String;

Ordinary interpolation remains an eager compact String; document-template syntax is deliberately not part of this API.

Compact String Boundaries

test✓
  let amountamount: Book.LanguageReference.Formatting.Money = Money { cents = 12501250: Int; currency = USD; };
  expect ✓"Total: ${amountamount: Book.LanguageReference.Formatting.Money}" == "Total: \$12.50""Total: \$12.50": String;
  expect ✓Format.renderrender: pure fn!(document: Format.Doc, width: Int, ~ribbon: Option(Int) = None) -> String(Display.to_docto_doc: pure fn!('a: Display)(value: Format.Doc) -> Format.Doc(deploymentdeployment: Format.Doc), 8080: Int) == Format.renderrender: pure fn!(document: Format.Doc, width: Int, ~ribbon: Option(Int) = None) -> String(deploymentdeployment: Format.Doc, 8080: Int);

Derived documents

Generated product and variant behaviors preserve declaration order and put legal breaks between fields. Collections retain their nested documents, so a narrow renderer can reflow the value without rebuilding it.

type Receipt !Encode !Decode = {
  customer: String;
  total: Money;
  tags: Vector(String);
}

Derived Documents

test✓
  let receiptreceipt: Book.LanguageReference.Formatting.Receipt = Receipt {
    customer = "Ada""Ada": String;
    total = Money { cents = 12501250: Int; currency = USD; };
    tags = ["language""language": String, "tools""tools": String];
  };
  expect ✓Format.renderrender: pure fn!(document: Format.Doc, width: Int, ~ribbon: Option(Int) = None) -> String(Inspect.to_docto_doc: pure fn!('a: Inspect)(value: Book.LanguageReference.Formatting.Receipt) -> Format.Doc(receiptreceipt: Book.LanguageReference.Formatting.Receipt), 8080: Int) == "Receipt {\n  customer = \"Ada\";\n  total = Money { cents = 1250; currency = USD };\n  tags = vector [\"language\", \"tools\"]\n}""Receipt {\n  customer = \"Ada\";\n  total = Money { cents = 1250; currency = USD };\n  tags = vector [\"language\", \"tools\"]\n}": String;
  expect ✓Format.renderrender: pure fn!(document: Format.Doc, width: Int, ~ribbon: Option(Int) = None) -> String(Inspect.to_docto_doc: pure fn!('a: Inspect)(value: Book.LanguageReference.Formatting.Receipt) -> Format.Doc(receiptreceipt: Book.LanguageReference.Formatting.Receipt), 120120: Int) == "Receipt { customer = \"Ada\"; total = Money { cents = 1250; currency = USD }; tags = vector [\"language\", \"tools\"] }""Receipt { customer = \"Ada\"; total = Money { cents = 1250; currency = USD }; tags = vector [\"language\", \"tools\"] }": String;

A field and the beginning of its value stay together. Compound values own their internal break points; the field boundary itself is not one. An empty option needs context for its element type, so this note is explicitly an optional String.

Field Values Start Inline

test✓
  let notenote: Option(String): Option(String) = None;
  let documentdocument: Format.Doc = Inspect.productproduct: pure fn!(name: String, fields: Vector(Format.Doc)) -> Format.Doc(
    "Row""Row": String,
    [
      Inspect.fieldfield: pure fn!(name: String, value: Format.Doc) -> Format.Doc("title""title": String, Inspect.to_docto_doc: pure fn!('a: Inspect)(value: String) -> Format.Doc("Keep this todo""Keep this todo": String)),
      Inspect.fieldfield: pure fn!(name: String, value: Format.Doc) -> Format.Doc("note""note": String, Inspect.to_docto_doc: pure fn!('a: Inspect)(value: Option(String)) -> Format.Doc(notenote: Option(String))),
      Inspect.fieldfield: pure fn!(name: String, value: Format.Doc) -> Format.Doc("completed""completed": String, Inspect.to_docto_doc: pure fn!('a: Inspect)(value: Bool) -> Format.Doc(FalseFalse: Bool)),
    ],
  );
  expect ✓Format.renderrender: pure fn!(document: Format.Doc, width: Int, ~ribbon: Option(Int) = None) -> String(documentdocument: Format.Doc, 2020: Int)
    == "Row {\n  title = \"Keep this todo\";\n  note = None;\n  completed = False\n}""Row {\n  title = \"Keep this todo\";\n  note = None;\n  completed = False\n}": String;
type Marker =
  | Cleared {}
  | Placed { at: Int; }

let marker_positionmarker_position: fn(Book.LanguageReference.Formatting.Marker) -> Int = function {
  | Cleared {} -> 00: Int
  | Placed { atat: Int; } -> atat: Int
};

Empty Products

test✓
  let clearedcleared: [ :cleared {  } | :placed { at: Int } ]: [ :cleared {} | :placed { at: Int } ] = :cleared {};
  expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.LanguageReference.Formatting.Settings) -> String(Settings {}) == "Settings {}""Settings {}": String;
  expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: Book.LanguageReference.Formatting.Marker) -> String(ClearedCleared: fn(sparse named fields ()) -> Book.LanguageReference.Formatting.Marker {}) == "Cleared {}""Cleared {}": String;
  expect ✓Display.displaydisplay: pure fn!('a: Display)(value: [ :cleared {  } | :placed { at: Int } ]) -> String(clearedcleared: [ :cleared {  } | :placed { at: Int } ]) == ":cleared {}"":cleared {}": String;
  expect ✓marker_positionmarker_position: fn(Book.LanguageReference.Formatting.Marker) -> Int(ClearedCleared: fn(sparse named fields ()) -> Book.LanguageReference.Formatting.Marker {}) == 00: Int;

Redaction

Secret fields are replaced before their child behavior can run. The hidden text therefore never enters the document arena or a captured report.

type Credentials !Encode !Decode = {
  username: String;
  password_hash: String #{ visibility = Secret; };
}

Secrets Stay Out Of Documents

test✓
  let credentialscredentials: Book.LanguageReference.Formatting.Credentials = Credentials {
    username = "Ada""Ada": String;
    password_hash = "must-never-appear""must-never-appear": String;
  };
  let renderedrendered: String = Inspect.inspectinspect: pure fn!('a: Inspect)(value: Book.LanguageReference.Formatting.Credentials) -> String(credentialscredentials: Book.LanguageReference.Formatting.Credentials);
  expect ✓renderedrendered: String == "Credentials { username = \"Ada\"; password_hash = <redacted> }""Credentials { username = \"Ada\"; password_hash = <redacted> }": String;

Traversal limits

Inspect threads one opaque context through generated and hand-written behavior calls. It bounds depth, visited items, string bytes, document nodes, and final rendered bytes; repeated values are allowed, while an active-path cycle becomes <cycle>. These traversal limits are independent from the layout width.

print builds one Display document from its values, separator, and ending, then renders once at the destination width. A function value displays as <function>; a type that opts out with !Display cannot be printed.

Functions Display As A Marker

test✓
  let answeranswer: fn() -> Int = () => 4242: Int;
  expect ✓"${answeranswer: fn() -> Int}" == "<function>""<function>": String;
crates/ship-types/tests/golden/check-types-print-display/run.t

Print requires every variadic value to support Display.

✓
type Sealed !Display = {
  key: String;
}

let main = fn() -> Unit {
  print({ key = "k"; } :> Sealed);
};
$ship-types-test input.shp
error: type `Sealed` does not support `Display`
  --> input.shp:6:9
    |
  6 |   print({ key = "k"; } :> Sealed);
    |         ^^^^^^^^^^^^^^^^^^^^^^^^ expected a value that supports `Display`