Type Presentation
This chapter describes how sigil-stitch renders TypeName variants across
different languages using a data-driven presentation layer.
This is a lower-level seam than declaration lowering. It converts one semantic type algebra into a width-aware document; declaration specs do not inspect its patterns to assemble functions, fields, or types. The decision to keep complete declaration grammar in language adapters is documented in Declaration Specs and Language Lowering.
The Problem
TypeName is a semantic type algebra — Array(T) means “array of T” regardless of target language. But the surface syntax varies widely:
| TypeName | TypeScript | Rust | Go | Python | C++ |
|---|---|---|---|---|---|
Array(T) | T[] | Vec<T> | []T | list[T] | std::vector<T> |
Optional(T) | T | null | Option<T> | *T | T | None | std::optional<T> |
Map(K, V) | Record<K, V> | HashMap<K, V> | map[K]V | dict[K, V] | std::map<K, V> |
Pointer(T) | n/a | *const T | *T | n/a | T* |
Tuple(A, B) | [A, B] | (A, B) | n/a | tuple[A, B] | std::tuple<A, B> |
Reference(T) | (identity) | &T | (identity) | (identity) | const T& |
Reference(T, mut) | (identity) | &mut T | *T | (identity) | T& |
Each variant needs language-specific rendering, but the rendering follows a small set of structural patterns. Rather than writing per-language rendering code for every variant, we identify these patterns and let languages declare which pattern to use.
Architecture
┌──────────────┐
│ TypeName │ Semantic type algebra
│ (unchanged) │ Array, Optional, Map, ...
└──────┬───────┘
│ to_doc_with_lang(resolve, lang)
▼
┌──────────────────────────────┐
│ lang.type_presentation() │ RendererLang returns TypePresentationConfig (DATA)
└──────────────┬───────────────┘
▼
┌─────────────────────┐
│ Rendering engine │ Single function: (TypePresentation, inner docs) → BoxDoc
│ (one place) │ Lives in type_name_render.rs, not in adapters
└──────────┬──────────┘
▼
BoxDoc output
The key invariant: BoxDoc never appears in the RendererLang trait.
Languages declare data describing which type pattern to use. The rendering
engine in type_name_render.rs interprets that data into BoxDoc output.
This separates three concerns that were previously tangled:
- What a type means —
TypeNamevariants (semantic, language-independent) - How a language spells it —
TypePresentationdata (per-language, no rendering logic) - How to assemble output — rendering engine (one place, all patterns)
TypePresentation
TypePresentation is an enum of syntactic patterns. Each variant describes a structural template for assembling already-rendered inner type docs:
#![allow(unused)]
fn main() {
pub enum TypePresentation<'a> {
/// `name<P1, P2>` — delimiters from generic_syntax().open/.close.
/// Vec<T>, Option<T>, HashMap<K,V>, List<T>.
GenericWrap { name: &'a str },
/// `prefix inner` — *T, &T, []T, &mut T.
Prefix { prefix: &'a str },
/// `inner suffix` — T[], T?, T*.
Postfix { suffix: &'a str },
/// `prefix inner suffix` — const T&, const T*.
Surround { prefix: &'a str, suffix: &'a str },
/// `open P1 sep P2 sep ... close` — (A, B), [T], [K: V], dict[K, V].
Delimited {
open: &'a str,
sep: &'a str,
close: &'a str,
},
/// `P1 sep P2 sep ... Pn` — A | B, A & B, A + B.
Infix { sep: &'a str },
}
}
Six patterns cover every type rendering need across all supported languages. A language implementation never builds BoxDoc — it returns one of these variants with the appropriate strings filled in.
FunctionPresentation
Function types are too complex for a single TypePresentation variant — they have parameter lists, return types, arrows, optional keywords, and wrappers that combine in language-specific ways. They get their own struct:
#![allow(unused)]
fn main() {
pub struct FunctionPresentation<'a> {
pub keyword: &'a str, // "fn", "func", ""
pub params_open: &'a str, // "(", "Callable[["
pub params_sep: &'a str, // ", "
pub params_close: &'a str, // ")", "]]"
pub arrow: &'a str, // " -> ", " => ", ", "
pub return_first: bool, // Dart: R Function(A, B)
pub curried: bool, // Haskell: A -> B -> R
pub wrapper_open: &'a str, // C++: "std::function<"
pub wrapper_close: &'a str, // C++: ">"
}
}
This declaratively covers TypeScript (A, B) => R, Rust fn(A, B) -> R, Python Callable[[A, B], R], C++ std::function<R(A, B)>, Dart R Function(A, B), and Haskell A -> B -> R — all from a single rendering engine interpreting the data.
RendererLang Trait Method
Languages declare their type syntax by returning a TypePresentationConfig
from a single method on the RendererLang trait:
trait RendererLang {
fn type_presentation(&self) -> TypePresentationConfig<'_>;
}
TypePresentationConfig bundles every type-rendering decision into one struct — never BoxDoc:
pub struct TypePresentationConfig<'a> {
pub array: TypePresentation<'a>,
pub readonly_array: Option<TypePresentation<'a>>,
pub optional: TypePresentation<'a>,
pub optional_absent_literal: &'a str,
pub map: TypePresentation<'a>,
pub union: TypePresentation<'a>,
pub intersection: TypePresentation<'a>,
pub pointer: TypePresentation<'a>,
pub slice: TypePresentation<'a>,
pub tuple: TypePresentation<'a>,
pub reference: TypePresentation<'a>,
pub reference_mut: TypePresentation<'a>,
pub function: FunctionPresentation<'a>,
pub associated_type: AssociatedTypeStyle<'a>,
pub impl_trait: BoundsPresentation<'a>,
pub dyn_trait: BoundsPresentation<'a>,
pub wildcard: WildcardPresentation<'a>,
}
Every field has a sensible default via Default::default(). TypeScript needs almost no overrides. Most languages override 3–5 fields with struct-update syntax (..Default::default()).
Rendering Engine
A single private function in type_name.rs interprets presentations:
fn render_presentation(
pres: &TypePresentation<'_>,
inner_docs: Vec<BoxDoc<'static, ()>>,
gs: &GenericSyntaxConfig<'_>,
) -> BoxDoc<'static, ()> {
match pres {
TypePresentation::GenericWrap { name } => {
// name<P1, P2> using lang.generic_syntax().open / .close
}
TypePresentation::Prefix { prefix } => {
// prefix inner
}
TypePresentation::Postfix { suffix } => {
// inner suffix
}
TypePresentation::Surround { prefix, suffix } => {
// prefix inner suffix
}
TypePresentation::Delimited { open, sep, close } => {
// open P1 sep P2 close
}
TypePresentation::Infix { sep } => {
// P1 sep P2 sep P3
}
}
}
Each TypeName variant in to_doc_with_lang becomes a three-step process:
- Recursively render inner types to
BoxDoc - Ask the language for a
TypePresentation - Pass both to
render_presentation
TypeName::Array(inner) => {
let inner_doc = inner.to_doc_with_lang(resolve, lang);
let tp = lang.type_presentation();
let gs = lang.generic_syntax();
render_presentation(&tp.array, vec![inner_doc], &gs)
}
Per-Language Examples
TypeScript
TypeScript overrides five fields from the defaults:
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
map: TypePresentation::GenericWrap { name: "Record" },
tuple: TypePresentation::Delimited { open: "[", sep: ", ", close: "]" },
associated_type: AssociatedTypeStyle::IndexAccess { open: "[\"", close: "\"]" },
impl_trait: BoundsPresentation { keyword: "", separator: " & " },
wildcard: WildcardPresentation { unbounded: "unknown", .. },
..Default::default()
}
}
The remaining fields use defaults: Array → Postfix { suffix: "[]" }, Optional → Infix { sep: " | " } with optional_absent_literal set to "null".
Rust
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
array: TypePresentation::GenericWrap { name: "Vec" },
optional: TypePresentation::GenericWrap { name: "Option" },
map: TypePresentation::GenericWrap { name: "HashMap" },
intersection: TypePresentation::Infix { sep: " + " },
pointer: TypePresentation::Prefix { prefix: "*const " },
slice: TypePresentation::Delimited { open: "&[", sep: "", close: "]" },
reference: TypePresentation::Prefix { prefix: "&" },
reference_mut: TypePresentation::Prefix { prefix: "&mut " },
..Default::default()
}
}
C++
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
array: TypePresentation::GenericWrap { name: "std::vector" },
optional: TypePresentation::GenericWrap { name: "std::optional" },
pointer: TypePresentation::Postfix { suffix: "*" },
reference: TypePresentation::Surround { prefix: "const ", suffix: "&" },
reference_mut: TypePresentation::Postfix { suffix: "&" },
tuple: TypePresentation::GenericWrap { name: "std::tuple" },
..Default::default()
}
}
The Surround variant was introduced specifically for C++’s const T& pattern, where a type needs both a prefix and a suffix. C uses it similarly for const T*.
Go
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
array: TypePresentation::Prefix { prefix: "[]" },
map: TypePresentation::Delimited { open: "map[", sep: "]", close: "" },
..Default::default()
}
}
Note that GenericWrap reuses generic_syntax().open/.close, so Go’s List[T] works automatically because Go already sets generic_syntax().open to "[".
Swift
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
array: TypePresentation::Delimited { open: "[", sep: "", close: "]" },
optional: TypePresentation::Postfix { suffix: "?" },
map: TypePresentation::Delimited { open: "[", sep: ": ", close: "]" },
..Default::default()
}
}
Design Properties
BoxDocnever appears inRendererLang— languages declare data, the engine renders.- Adding a
TypeNamevariant requires one new field onTypePresentationConfig. No per-language render code needed. - 17 fields on
TypePresentationConfigreplace what would otherwise be ~20+ render methods. Each override is a single struct field. - One rendering engine in
type_name.rshandles all patterns uniformly. - Semantic types are preserved —
Array(T)staysArray(T). The language says “render Array as GenericWrap(Vec)” not “rewrite Array to Generic(‘Vec’, [T])”. GenericWrapreusesgeneric_syntax().open/.close— languages that already configure these delimiters get correct rendering automatically.