Introduction
sigil-stitch is a Rust library for type-safe, import-aware, width-aware code generation across multiple languages. It combines two ideas: JavaPoet’s builder model for constructing structured code, and the Wadler-Lindig algorithm for width-aware formatting. You describe code with builders and format specifiers, and the library handles imports, name conflicts, indentation, and line breaking.
Where the ideas come from
JavaPoet’s builder model. JavaPoet (by Square) introduced the idea of building code
with CodeBlock format strings and structural Spec types (TypeSpec, FunSpec, etc.).
You write a format string like "const user: %T = getUser()", pass a TypeName for
the %T slot, and the library renders the type reference and tracks the import.
sigil-stitch adopts this model directly, extending it from Java-only to multiple languages.
Wadler-Lindig pretty printing. The pretty crate implements the Wadler-Lindig
algorithm, which decides where to break lines based on a target width. sigil-stitch
uses this via the %W (soft line break) specifier – you mark where breaks can
happen, and the algorithm decides where they should happen. Without %W, output
is rendered with direct string concatenation (no pretty-printer overhead).
Four key properties
Ergonomic multi-language. CodeBlock, TypeName, and all spec types have no
language generic parameter. The language enters when FileSpec materializes a
declaration or when a renderer receives &dyn RendererLang. Semantic
TypeName and spec values can be reused across targets that support their
intent. Literal text inside a CodeBlock is already target syntax and is only
portable where that syntax is shared.
Import-aware. When you use %T with a TypeName::Importable, the library records
that import. FileSpec rewrites and validates each materialized source tree,
lowers every complete type name, then collects and resolves imports through a
fallible complete-set resolver. Its default policy uses encounter order only as
a deterministic tie-break; callers can supply a different borrowed policy for
one render. You never write ordinary import statements by hand.
Width-aware. Place %W in a format string to mark a soft line break. When the
output fits within the target width, %W produces a space. When it doesn’t fit, %W
produces a newline with proper indentation. This is the Wadler-Lindig algorithm at
work, via the pretty crate. You pass the target width to FileSpec::render(width),
and the same code blocks produce different layouts for different widths.
Multi-language. RendererLang owns final-rendering policy, while each
CodeLang adapter validates declaration intent and owns its target grammar.
sigil-stitch ships with adapters for TypeScript, JavaScript, Rust, Go, Python,
Java, Kotlin, Swift, Dart, Scala, Haskell, OCaml, C, C++, C#, Lua, Bash, and Zsh.
The shared container types work with every adapter; each value must still be
representable by its selected target.
Design philosophy
Specs lower to structured blocks. Specs record target-independent
declaration intent. Their .emit() facade performs validation and delegates
target grammar to the selected language adapter, producing CodeBlock trees
rather than type-bearing strings. The renderer and import collector therefore
remain independent of declaration kinds while retaining structured type
references.
Minimal dependencies. The runtime dependencies are pretty (v0.12) for
Wadler-Lindig formatting, serde (v1, with derive) so every spec can round-trip
to JSON or YAML, and snafu for structured errors. Everything else – parsing
format strings, collecting imports, resolving conflicts, rendering output – is
implemented in sigil-stitch itself.
Two builder flavours. Spec builders (TypeSpec, FunSpec, FieldSpec,
FileSpec, EnumVariantSpec, PropertySpec, AnnotationSpec, ProjectSpec) use an
owning chain pattern – every setter takes mut self and returns Self, so you
chain calls fluently:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("todo!()", ()).unwrap();
let fun = FunSpec::builder("greet")
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap();
}
CodeBlockBuilder is different: its methods take &mut self and return
&mut Self, so you keep the builder in a let mut binding and call methods
on it:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add_statement("return user", ());
let block = cb.build().unwrap();
}
Quick orientation
There are three levels of abstraction, and you can use whichever fits:
- CodeBlock for code fragments. Use format specifiers (
%T,%S,%L,%W) to interpolate values. Good for function bodies, one-off statements, and anything that doesn’t need structural metadata. - Specs (
FunSpec,TypeSpec,FieldSpec,ParameterSpec, etc.) for declaration intent. They carry semantic facts such as visibility, annotations, type parameters, and modifiers; the selected adapter validates and lowers them to target syntax. - FileSpec to render a complete file. It lowers specs, rewrites and validates
each source tree, lowers type references, resolves the imports in the prepared
blocks, and then renders with no further rewrite or type lowering. See
Architecture for the complete pipeline. Pass a target width
to
file.render(80)and get aStringback.
For multi-file output, ProjectSpec collects multiple FileSpecs and can render
them all at once or write them to disk.
What’s next
Continue to Getting Started for a hands-on walkthrough, or jump to Architecture for the full technical picture.