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
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
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
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
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
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
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
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
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
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
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
let answeranswer: fn() -> Int = () => 4242: Int;
expect ✓"${answeranswer: fn() -> Int}" == "<function>""<function>": String;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`