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.
Getting Started
Installation
Add sigil-stitch to your project:
cargo add sigil-stitch
Or add it directly to your Cargo.toml:
[dependencies]
sigil-stitch = "0.6"
sigil-stitch requires Rust edition 2024 and MSRV 1.88.0. Runtime dependencies (pretty, serde with derive, and snafu) are pulled in automatically. No feature flags are needed – all spec types implement serde::Serialize and serde::Deserialize out of the box.
Your First CodeBlock
A CodeBlock is a composable code fragment built from format strings and typed arguments. Here’s a complete example that generates a TypeScript file with an automatic import:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::code_block::StringLitArg;
fn main() {
let user_type = TypeName::importable_type("./models", "User");
let mut cb = CodeBlock::builder();
cb.add_statement(
"const user: %T = await getUser(%S)",
(user_type.clone(), StringLitArg("id".into())),
);
cb.add_statement("return user", ());
let body = cb.build().unwrap();
let file = FileSpec::builder("user.ts")
.add_code(body)
.build()
.unwrap();
let output = file.render(80).unwrap();
println!("{output}");
}
This produces:
import type { User } from './models'
const user: User = await getUser('id');
return user;
Two things happened automatically:
%Twithuser_typerendered asUserin the code and addedimport type { User } from './models'at the top of the file.%SwithStringLitArgrendered the string"id"as a single-quoted TypeScript string literal'id'.
The () in cb.add_statement("return user", ()) means “no arguments” – the format string has no specifiers, so none are needed.
The Macro Alternative
The sigil_quote! macro lets you write target-language code inline, with less ceremony than the builder API. Here’s the same example:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let user_type = TypeName::importable_type("./models", "User");
let body = sigil_quote!(TypeScript {
const user: $T(user_type) = await getUser($S("id"));
return user;
}).unwrap();
}
This produces the same CodeBlock as the builder version above. The macro uses $T instead of %T and $S instead of %S, but the result is identical – same import tracking, same rendering, same output when passed to FileSpec.
The macro is a good fit when you’re writing a block of target-language code with a few interpolations. The builder is better when you’re constructing code programmatically (loops, conditionals on what to emit).
Building Structured Declarations
For functions, types, and other declarations, use the spec layer. Specs carry
declaration intent such as name, return type, visibility, and modifiers. The
selected language validates that intent and lowers it to a structured
CodeBlock.
Here’s a function declaration:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let user_type = TypeName::importable_type("./models", "User");
let fun = FunSpec::builder("getActiveUsers")
.returns(TypeName::array(user_type.clone()))
.is_async()
.body(sigil_quote!(TypeScript {
const users = await fetchAll();
return users.filter(u => u.active);
}).unwrap())
.build()
.unwrap();
let file = FileSpec::builder("users.ts")
.add_function(fun)
.build()
.unwrap();
let output = file.render(80).unwrap();
println!("{output}");
}
This produces a complete TypeScript file with the function declaration, including the async keyword, the User[] return type annotation, and the import for User.
Notice the builder pattern: spec builders like FunSpec::builder() and FileSpec::builder() use an owning chain pattern – setter methods like .returns(), .is_async(), and .body() take mut self and return Self, so you chain them fluently. The .build() call at the end consumes the builder and returns Result<FunSpec>. (CodeBlockBuilder is different: it uses &mut self, so you keep it in a let mut binding.)
Specs Lower to CodeBlocks
Every spec type follows the same pattern: configure it with a builder and call
.build(). During file rendering, its .emit() facade validates the intent and
delegates concrete grammar to the selected language adapter. The result is a
structured CodeBlock. This means:
- You never write raw import statements.
%Thandles it. - You describe a function declaration once; the language adapter owns its concrete grammar.
- You can mix specs and raw CodeBlocks freely in a
FileSpec.
The renderer and import collector only see CodeBlock trees. They don’t know or care whether a block came from a FunSpec, a TypeSpec, or a hand-written CodeBlock::builder() call.
Configuring a Language
Each language type (TypeScript, JavaScript, Python, Java, and so on)
is a struct with public fields. The ones you usually want to tweak are exposed
as fluent with_* builders:
extern crate sigil_stitch;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::prelude::*;
fn main() {
// Four-space indentation, no semicolons, .tsx extension.
let ts = TypeScript::new()
.with_semicolons(false)
.with_extension("tsx")
.with_indent(" ");
}
| Language | with_indent | with_semicolons | with_extension |
|---|---|---|---|
TypeScript | yes | yes | yes |
JavaScript | yes | yes | yes |
Python | yes | n/a | yes (e.g. pyi) |
Java | yes | n/a | yes |
Rust | yes | n/a | yes |
Go | yes | n/a | yes |
Kotlin | yes | n/a | yes (e.g. kts) |
Swift | yes | n/a | yes |
Dart | yes | n/a | yes |
CSharp | yes | n/a | yes |
Lua | yes | n/a | yes |
C | yes | n/a | yes (e.g. h) |
Cpp | yes | n/a | yes (e.g. hpp, cxx) |
Bash | yes | n/a | yes (e.g. sh) |
Zsh | yes | n/a | yes |
The shared pre-0.6.8 QuoteStyle enum and with_quote_style(...) setters are
compatibility APIs, not the model for new language configuration. The accepted
0.7 target gives TypeScript, JavaScript, and Python separate language-local
with_single_quotes() and with_double_quotes() conveniences while preserving
the old field for source compatibility. See the legacy appendix.
Language configuration is per-instance, not global: pass the configured language
into the FileSpec / ProjectSpec you want rendered with those settings.
What’s Next
Now that you’ve seen the basics:
- Format Specifiers explains every
%specifier in depth. - TypeName covers semantic type references, import tracking, and language-owned lowering.
- Building Functions & Fields covers ParameterSpec, FieldSpec, and FunSpec.
- Building Types & Enums covers TypeSpec, PropertySpec, AnnotationSpec, and EnumVariantSpec.
- Files & Projects covers ImportSpec, FileSpec, and ProjectSpec.
- sigil_quote! Macro has the full guide for the macro syntax.
- Code Templates covers reusable named-parameter templates.
- Language Cookbook has idiomatic recipes for each supported language.
Format Specifiers
CodeBlock format strings use %-prefixed specifiers to interpolate arguments. Each specifier consumes one argument from the args list (except %W, %>, %<, %[, %], and %%, which consume none).
Quick Reference
| Specifier | Name | Argument | Purpose |
|---|---|---|---|
%T | Type | TypeName | Emit type reference, track import |
%N | Name | NameArg | Emit identifier name |
%S | String | StringLitArg | Emit escaped string literal |
%V | Verbatim | VerbatimStrArg | Emit string with interpolation preserved |
%R | Remark | CommentArg | Emit inline comment |
%L | Literal | &str, String, CodeBlock, CodeFragment | Emit raw value or nested block/fragment |
%W | Wrap | (none) | Soft line break point |
%> | Indent | (none) | Increase indent level |
%< | Dedent | (none) | Decrease indent level |
%[ | Begin | (none) | Start of statement |
%] | End | (none) | End of statement |
%% | Escape | (none) | Literal % character |
%T – Type Reference
The most powerful specifier. Takes a TypeName and does two things: emits the type name in the output AND registers the import so FileSpec::render() can collect, deduplicate, and emit import headers automatically.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::type_name::TypeName;
fn main() {
let user = TypeName::importable("./models", "User");
let block = CodeBlock::of("const u: %T = getUser()", (user,)).unwrap();
// Value import (not `import type`):
// import { User } from './models';
// const u: User = getUser();
}
For type-only imports (TypeScript’s import type), use importable_type:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let user = TypeName::importable_type("./models", "User");
// import type { User } from './models';
}
Generic types track imports recursively. Every TypeName nested inside the generic’s parameters is collected:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let promise = TypeName::application(TypeName::primitive("Promise"), vec![TypeArgument::Single(TypeName::importable("./models", "User"))]);
let block = CodeBlock::of("function load(): %T", (promise,)).unwrap();
// Promise<User> -- the User import is still tracked
}
%N – Name
Emits an identifier with automatic keyword escaping. If the name collides with a reserved word in the target language, it is escaped using the language’s convention (Rust: r#type, Go/Python: type_). Bare &str and String values map to Arg::Literal (for %L) by default, so you must use the NameArg wrapper when your format string contains %N.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, NameArg};
use sigil_stitch::prelude::*;
fn main() {
let method_name = "getData";
let mut cb = CodeBlock::builder();
cb.add_statement("this.%N()", (NameArg(method_name.to_string()),));
let block = cb.build().unwrap();
// Output: this.getData();
}
Reserved-word escaping happens at render time based on the target language:
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, NameArg};
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::spec::file_spec::FileSpec;
use sigil_stitch::prelude::*;
fn main() {
let field_name = "type"; // reserved in Rust
let block = CodeBlock::of("let %N = value", NameArg(field_name.into())).unwrap();
let file = FileSpec::builder_with("test.rs", Rust::new())
.add_code(block)
.build()
.unwrap();
let output = file.render(80).unwrap();
// Output: let r#type = value
}
%S – String Literal
Emits a language-aware quoted string. The
RendererLang::render_string_literal() method on each language controls the
quoting style and escape rules. TypeScript and JavaScript default to single
quotes; Rust, Java, Go, C, C++, Swift, and Kotlin use double quotes; Dart uses
single quotes; Python uses single quotes.
Requires the StringLitArg wrapper.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, StringLitArg};
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add_statement("const msg = %S", (StringLitArg("hello world".to_string()),));
let block = cb.build().unwrap();
// TypeScript output: const msg = 'hello world';
// Java output: const msg = "hello world";
}
Special characters are escaped according to each language’s rules. For example, Kotlin and Dart escape $ to prevent string interpolation.
%V – Verbatim String Literal
Emits a string with minimal escaping — only characters that would structurally break the string delimiter are escaped, while interpolation sigils ($, `, {, etc.) are preserved as-is. This is useful for generating code that uses the target language’s string interpolation.
Requires the VerbatimStrArg wrapper.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, VerbatimStrArg};
use sigil_stitch::lang::bash::Bash;
use sigil_stitch::spec::file_spec::FileSpec;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add("local config=%V", (VerbatimStrArg("\"${XDG_CONFIG_HOME:-$HOME/.config}\"".to_string()),));
cb.add_line();
cb.add("local version=%V", (VerbatimStrArg("\"$(git describe --tags 2>/dev/null || echo dev)\"".to_string()),));
cb.add_line();
cb.add("echo %V", (VerbatimStrArg("Deploying ${APP_NAME} v${version} (PID=$)".to_string()),));
let block = cb.build().unwrap();
let file = FileSpec::builder_with("test.bash", Bash::new())
.add_code(block)
.build()
.unwrap();
let output = file.render(80).unwrap();
assert!(output.contains(r#""${XDG_CONFIG_HOME:-$HOME/.config}""#));
assert!(output.contains(r#""$(git describe --tags 2>/dev/null || echo dev)""#));
assert!(output.contains("Deploying ${APP_NAME} v${version} (PID=$)"));
// Output (Bash $V is pure passthrough — users include their own quotes):
// local config="${XDG_CONFIG_HOME:-$HOME/.config}"
// local version="$(git describe --tags 2>/dev/null || echo dev)"
// echo Deploying ${APP_NAME} v${version} (PID=$)
}
Per-language behavior:
| Language | %V output for "$x" | Delimiter | Escapes only |
|---|---|---|---|
| Bash/Zsh | $x | (passthrough) | (none) |
| JavaScript/TS | `$x` | `...` | \ ` |
| Python | f"$x" | f"..." | \ " |
| Kotlin/Swift | "$x" | "..." | \ " |
| Dart | '$x' | '...' | \ ' |
| C# | $"$x" | $"..." | \ " |
| Scala | s"$x" | s"..." | \ " |
| Others | Same as %S | (full escaping) | All |
For Bash/Zsh, %V is pure passthrough — the string is emitted as-is with no wrapping quotes and no escaping. Shell interpolates by default, and users control quoting in the %V content itself (include "..." in the string when quoting is desired in the output).
For languages without string interpolation (C, C++, Go, Rust, Java, Haskell, OCaml, Lua), %V falls back to %S behavior (full escaping).
%R – Inline Comment
Emits a language-specific inline comment. Requires the CommentArg wrapper.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, CommentArg};
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add_statement("const x = 42; %R", (CommentArg("TODO: validate".to_string()),));
let block = cb.build().unwrap();
// TypeScript: const x = 42; // TODO: validate
// Python: const x = 42; # TODO: validate
}
The comment prefix (//, #, --, etc.) is determined by the target language’s
comment_syntax(). The comment text is emitted verbatim after the prefix with a
single space separator.
In sigil_quote!, inline $comment(expr) after a statement expands to %R:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
sigil_quote!(TypeScript {
doStuff() $comment("cleanup")
}).unwrap();
// Equivalent builder call:
// cb.add("doStuff() %R", (CommentArg("cleanup".to_string()),));
}
@{expr} interpolation in $V and $L
When using $V or $L with a string literal in sigil_quote!, you can embed Rust expressions with @{expr}. These are evaluated at compile time and spliced into the output:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let registry = "ghcr.io";
let tag = "latest";
let block = sigil_quote!(Bash {
docker push $V("@{registry}/myapp:@{tag}")
}).unwrap();
// Output: docker push ghcr.io/myapp:latest
}
Use $V when you want the result wrapped in the target language’s string delimiter (backticks for JS/TS, f"..." for Python, etc.). Use $L when you need plain unwrapped output — type expressions, switch headers, return statements, and other non-string-literal contexts:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let disc = "foo.bar";
let block = sigil_quote!(TypeScript {
switch ($L("@{disc}")) {
$L("case 1:") {
break;
}
}
}).unwrap();
// Output: switch (foo.bar) {
// (No backticks — $L emits plain text, $V would wrap in `...`)
}
This is syntactic sugar — the macro transforms the string into a format!() call. Shell variables like $HOME pass through unchanged while @{expr} parts are resolved at Rust compile time.
Escape @@ to emit a literal @:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let block = sigil_quote!(Bash {
echo $V("admin@@localhost")
}).unwrap();
// Output: echo admin@localhost
}
Arbitrary Rust expressions work inside @{...}, including method calls:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let items = vec!["a", "b", "c"];
let block = sigil_quote!(Bash {
echo $V("count=@{items.len()}")
}).unwrap();
// Output: echo count=3
}
If the expression is not a string literal (e.g. $V(my_var) or $L(format!(...))), @{...} processing is skipped and the expression is used as-is.
%L – Literal and Nested Code
Emits raw literal text or structured nested code. Bare &str and String
arguments map to raw Arg::Literal, so no wrapper is needed for ordinary
language text. %L also accepts CodeBlock and CodeFragment for nested code
that should keep imports, indentation, and other structure. Supports @{expr}
interpolation inside string literals in sigil_quote! (see above).
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, CodeFragment};
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
// Bare string -> Arg::Literal -> used by %L
cb.add_statement("const count = %L", "42");
// Nested CodeBlock -> Arg::Code -> also used by %L
let inner = CodeBlock::of("getValue()", ()).unwrap();
cb.add_statement("const x = %L", inner);
// Parsed CodeFragment -> Arg::Code -> structural markers compose
let branch = CodeFragment::of("if (ready) {\n%>return true;%<\n}", ()).unwrap();
cb.add("%L", branch);
let block = cb.build().unwrap();
// const count = 42;
// const x = getValue();
// if (ready) {
// return true;
// }
}
Raw literal strings are intentionally not reparsed as format strings. If a raw
&str / String passed through %L contains %> or %<, build() returns an
UnresolvedIndentMarker error instead of rendering those markers literally. Use
CodeFragment::of(...) for snippets that contain structural markers.
CodeFragment snippets must balance their own %> / %< markers. A fragment
with %> and no matching %< is rejected because it would leak indentation into
whatever code is rendered after it. A balanced fragment may temporarily borrow
its caller’s indentation, such as %<private:\n%> inside a class body; rendering
that fragment by itself still fails because the complete tree would dedent below
zero. If you need indentation to span multiple builder calls, use
CodeBlock::builder() and balance the markers before build().
%W – Soft Line Break
No argument consumed. Marks a point where the Wadler-Lindig pretty printer (via the pretty crate) MAY insert a line break if the line exceeds the target width passed to FileSpec::render(width). If the line fits within the width, %W renders as a space.
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add_statement("const result = someFunction(arg1,%Warg2,%Warg3,%Warg4)", ());
let block = cb.build().unwrap();
// At width 80 (fits on one line):
// const result = someFunction(arg1, arg2, arg3, arg4);
//
// At width 40 (wraps):
// const result = someFunction(arg1,
// arg2,
// arg3,
// arg4);
}
Without any %W in a CodeBlock tree, the renderer’s semantic walker writes
through a direct string adapter. When %W is present anywhere in the tree, the
same walker writes the full tree through a pretty::BoxDoc adapter. A broken
%W emits the exact indentation configured by the language; tabs and other
indent strings are not converted to spaces. Width calculations use terminal
display width, with ASCII control characters such as tabs counting as one
column.
%> and %< – Indent / Dedent
No argument consumed. Manually increase (%>) or decrease (%<) the indent level. Rarely needed directly because begin_control_flow(), next_control_flow(), and end_control_flow() manage indentation automatically. Useful when building custom block structures.
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add("items: [%>\n", ());
cb.add("'first',\n", ());
cb.add("'second',\n", ());
cb.add("%<]", ());
let block = cb.build().unwrap();
// items: [
// 'first',
// 'second',
// ]
}
Indent depth must balance to zero by the time build() is called. An unbalanced depth produces an UnbalancedIndent error.
%[ and %] – Statement Boundaries
No argument consumed. %[ marks the start of a statement. %] marks the end and appends the language’s statement terminator – ; for TypeScript, Rust, Java, C, C++, Dart; nothing for Python, Go, Kotlin, Swift.
You almost never write these directly. add_statement() wraps your format string in %[...%] and appends a newline automatically:
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
// These produce the same output:
cb.add_statement("const x = 1", ());
cb.add("%[const x = 1%]\n", ());
let block = cb.build().unwrap();
// const x = 1;
// const x = 1;
}
%% – Literal Percent
Emits a literal % character in the output. No argument consumed.
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let block = CodeBlock::of("progress: 100%%", ()).unwrap();
// progress: 100%
}
Arguments and the IntoArgs Trait
Every method that accepts a format string (add, add_statement, begin_control_flow, next_control_flow, CodeBlock::of, CodeFragment::of) takes args: impl IntoArgs. This trait converts Rust values into Vec<Arg> for the format engine.
The critical rule: bare strings map to Arg::Literal (consumed by %L), not to Arg::Name or Arg::StringLit. To target %N or %S, use the NameArg and StringLitArg wrappers from sigil_stitch::code_block.
Type-to-Arg Mapping
| Rust Type | Maps To | Consumed By |
|---|---|---|
() | empty vec | (no specifiers) |
TypeName | Arg::TypeName | %T |
&str | Arg::Literal | %L |
String | Arg::Literal | %L |
CodeBlock | Arg::Code | %L |
CodeFragment | Arg::Code | %L |
NameArg(String) | Arg::Name | %N |
StringLitArg(String) | Arg::StringLit | %S |
VerbatimStrArg(String) | Arg::VerbatimStr | %V |
CommentArg(String) | Arg::Comment | %R |
Vec<Arg> | passthrough | any |
Single Argument
When a format string has exactly one specifier, pass the value directly (no tuple needed):
extern crate sigil_stitch;
use sigil_stitch::type_name::TypeName;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let user = TypeName::importable("./models", "User");
let block = CodeBlock::of("let u: %T", user).unwrap();
}
Multiple Arguments with Tuples
For two or more specifiers, use a tuple. Tuples are supported up to 8 elements. Each element must implement Into<Arg>.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, StringLitArg};
use sigil_stitch::type_name::TypeName;
use sigil_stitch::prelude::*;
fn main() {
let user_type = TypeName::importable("./models", "User");
// Two args: a TypeName and a StringLitArg
let mut cb = CodeBlock::builder();
cb.add_statement("const u: %T = getUser(%S)", (user_type, StringLitArg("admin".into())));
let block = cb.build().unwrap();
// const u: User = getUser('admin');
}
No Arguments
Pass () when the format string has no specifiers:
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add_statement("return null", ());
let block = cb.build().unwrap();
}
Format Validation
The builder checks that the number of argument-consuming specifiers (%T, %N,
%S, %V, %L, %R) matches the number of arguments provided. A mismatch
records a FormatArgCount error, surfaced when build() is called. The error
carries the expected specifier list and the actual argument kinds so you can see
exactly which slot is wrong.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// This will fail: format has 2 specifiers but only 1 argument
let mut cb = CodeBlock::builder();
cb.add_statement("const %N: %T = null", "x"); // &str gives one Arg::Literal
let result = cb.build();
// Err(FormatArgCount {
// format: "const %N: %T = null",
// expected_specifiers: vec!["%N", "%T"],
// actual_arg_kinds: vec!["Literal"],
// })
}
If an argument has the wrong kind for its slot, conversion returns
FormatArgKind with the zero-based argument index, expected specifier and kind,
and actual argument kind. The mismatch never renders as empty text.
A format string ending in a bare % returns TrailingFormatMarker, including
the byte offset of that marker. An unrecognised specifier character (anything
after % that isn’t T, N, S, V, L, R, W, >, <, [, ], or
%) returns InvalidFormatSpecifier instead.
TypeName
This chapter describes the implemented 0.7 type-name-lowering contract.
TypeName is the type reference enum at the heart of sigil-stitch’s import tracking. When you use a TypeName with the %T format specifier in a CodeBlock, the library renders the type name in the output and records the import. At render time, FileSpec collects all recorded imports, deduplicates them, resolves naming conflicts, and emits the import header automatically.
TypeName carries semantic type structure and has no language generic
parameter. At FileSpec::render() time, the selected RendererLang lowers one
complete type name into structured target-language output or rejects it.
Primitive, Qualified, and especially Raw values may still contain
target-specific names or syntax.
Public type rendering is always language-aware. For normal generation, place a
TypeName in a CodeBlock %T slot. FileSpec first applies the selected
adapter’s source-tree rewrite, then lowers every type name, collects imports
from the lowered blocks, resolves aliases, and renders the target syntax with no
further rewrite or type lowering. Language-neutral rendering shortcuts are not
exposed because they would flatten type references before representability
checks and import resolution.
Import tracking
The two Importable constructors are the primary way to create types that generate import statements:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
// Value import: import { User } from './models'
let user = TypeName::importable("./models", "User");
// Type-only import: import type { User } from './models'
let user = TypeName::importable_type("./models", "User");
}
When these types appear in a CodeBlock via %T, the import is tracked
automatically. At file render time, all imports are collected, deduplicated,
and emitted. Imports requesting the same local name form a peer conflict set.
The default resolver uses encounter order only as a deterministic compatibility
tie-break; a custom fallible resolver can assign a different complete set.
You can also set an explicit alias:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let user = TypeName::importable("./other", "User")
.with_alias("OtherUser");
// import { User as OtherUser } from './other'
// Rendered as: OtherUser
}
Primitives
Types that don’t need imports – built-in language types, type parameters, or any name that’s already in scope:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let s = TypeName::primitive("string");
let n = TypeName::primitive("number");
let t = TypeName::primitive("T"); // type parameter
}
Qualified types
For types that should render with their full module path inline without generating an import statement:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Rust: serde_json::Value (no `use serde_json::Value;`)
let val = TypeName::qualified("serde_json", "Value");
// Rust: super::Foo
let foo = TypeName::qualified("super", "Foo");
// Java: java.util.HashMap
let map = TypeName::qualified("java.util", "HashMap");
}
The selected language lowerer owns the separator between module and name:
"::" for targets such as Rust and C++, and "." for targets such as Go,
Python, Java, Kotlin, Scala, Swift, Dart, Haskell, and OCaml. A language that
cannot preserve a qualified reference rejects it instead of silently dropping
the module.
Qualified types work anywhere a TypeName is accepted, including inside generics:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Rust: std::collections::HashMap<String, serde_json::Value>
let map = TypeName::application(TypeName::qualified("std::collections", "HashMap"), vec![TypeArgument::Single(TypeName::primitive("String")), TypeArgument::Single(TypeName::qualified("serde_json", "Value"))]);
}
You can also convert an existing importable type to qualified rendering with .qualify():
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Equivalent to TypeName::qualified("serde_json", "Value")
let val = TypeName::importable("serde_json", "Value").qualify();
}
Collections
Arrays
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// TypeScript: string[]
// Rust: Vec<String>
// Go: []string
let arr = TypeName::array(TypeName::primitive("string"));
// TypeScript: readonly number[]
let ro = TypeName::readonly_array(TypeName::primitive("number"));
}
Maps
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Go: map[string]User
// TypeScript: Record<string, User>
let m = TypeName::map(
TypeName::primitive("string"),
TypeName::importable("./models", "User"),
);
}
Tuples
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Rust: (String, i32)
// TS: [string, number]
// Python: tuple[str, int]
// C++: std::tuple<string, int>
let t = TypeName::tuple(vec![
TypeName::primitive("string"),
TypeName::primitive("number"),
]);
// Unit type (empty tuple): Rust ()
let unit = TypeName::unit();
}
Slices
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Go: []User
let s = TypeName::slice(TypeName::primitive("User"));
}
Generics
For structured applications, use TypeName::application. A declaration’s
bindings are separate GenericParamSpec values; a use refers to one by
TypeName::parameter. Expansions preserve a complete pattern, not just a
special final argument:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
let tuple = TypeName::application(
TypeName::primitive("std::tuple"),
vec![TypeArgument::Expansion { pattern: TypeName::parameter("Ts") }],
);
// C++: std::tuple<Ts...>
}
An empty application argument sequence is valid shared data. A language may
express it, as C++ does with Bundle<>, or reject it. The library performs no
arity inference or pack evaluation. The older Generic representation remains
a compatibility input with its existing fields.
Wrap a base type with type parameters:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// TypeScript: Promise<User>
let promise = TypeName::application(TypeName::primitive("Promise"), vec![TypeArgument::Single(TypeName::importable("./models", "User"))]);
// Rust: HashMap<String, Vec<User>>
let map = TypeName::application(TypeName::primitive("HashMap"), vec![TypeArgument::Single(TypeName::primitive("String")), TypeArgument::Single(TypeName::application(TypeName::primitive("Vec"), vec![TypeArgument::Single(TypeName::primitive("User"))]))]);
}
Nesting works to any depth. Imports are collected recursively – every Importable type anywhere in the tree gets tracked.
Union and intersection types
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// TypeScript: string | number | boolean
let u = TypeName::union(vec![
TypeName::primitive("string"),
TypeName::primitive("number"),
TypeName::primitive("boolean"),
]);
// TypeScript: Serializable & Loggable
let i = TypeName::intersection(vec![
TypeName::primitive("Serializable"),
TypeName::primitive("Loggable"),
]);
}
These are primarily useful for languages with union or intersection type syntax. Each adapter must preserve the requested meaning exactly or reject the complete type; it cannot substitute a merely similar construct.
Optional types
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// TypeScript: string | null
// Rust: Option<String>
// Go: *string
// Kotlin: String?
// Swift: String?
let opt = TypeName::optional(TypeName::primitive("string"));
}
The selected language lowerer owns the complete optional-type grammar and rejects the variant when the target has no exact representation.
Pointer and reference types
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Go: *User
let ptr = TypeName::pointer(TypeName::primitive("User"));
// Rust: &str
let r = TypeName::reference(TypeName::primitive("str"));
// Rust: &mut Vec<i32>
let rm = TypeName::reference_mut(TypeName::primitive("Vec<i32>"));
}
Reference rendering is language-aware:
- Rust:
&T/&mut T - C++:
const T&/T& - C:
const T*/T* - Go: shared reference is a no-op, mutable reference renders as
*T - TypeScript: references are a no-op (everything is by reference)
Function types
TypeName::callable carries an ordered sequence whose scalar slots can be
required or optional. Repetition supplies an element type; expansion supplies
a complete pattern. Labels and ordering rules belong to the target adapter:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
let callback = TypeName::callable(
vec![CallableParam::Single {
name: Some("value".into()),
type_name: TypeName::primitive("string"),
presence: CallableParamPresence::Optional,
}],
TypeName::primitive("void"),
);
// TypeScript: (value?: string) => void
}
Optional presence differs from TypeName::Optional, which describes the
value in a supplied slot. A target must preserve all supplied labels and
segment intent or reject the complete expression; it cannot silently drop
them. The older Function representation remains a compatibility input.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// TypeScript: (string, number) => boolean
// Rust: fn(String, i32) -> bool
// Python: Callable[[str, int], bool]
// C++: std::function<bool(string, int)>
// Dart: bool Function(String, int)
let f = TypeName::callable(vec![CallableParam::Single { name: None, type_name: TypeName::primitive("string"), presence: CallableParamPresence::Required }, CallableParam::Single { name: None, type_name: TypeName::primitive("number"), presence: CallableParamPresence::Required }], TypeName::primitive("boolean"));
}
Function type grammar varies significantly across languages. The selected adapter owns the complete construct, including parameter order, delimiters, arrows or keywords, wrapping, and any target-derived imports.
String literal types
0.7 adds one focused singleton type:
TypeName::StringLiteral("active".to_owned())
The stored string is the decoded semantic value, not source text with quotes
or escapes. Use TypeName::string_literal(...) when constructing one.
TypeScript lowers it to a string literal type, Python lowers it through
structured typing.Literal, and targets without an exact string singleton
type reject it.
Python lowers one singleton as typing.Literal["active"]. A non-empty direct
union containing only string singletons becomes one typing.Literal[...] in
the original order, including duplicate members. A mixed union or a union
nested inside another type lowers recursively through ordinary Python union
grammar; this special case does not flatten nested unions.
Several accepted values use ordinary union composition:
TypeName::Union([
TypeName::StringLiteral("active".to_owned()),
TypeName::StringLiteral("inactive".to_owned()),
])
There is no separate string-enum or literal-set type. Numeric literal types are not part of this extension.
Raw escape hatch
For type expressions not covered by the built-in variants:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let t = TypeName::raw("keyof User");
}
Raw emits the string verbatim with no import tracking. Use it sparingly – prefer the structured variants when possible.
Language-owned lowering across targets
The same TypeName variant lowers differently per language. Each adapter
constructs a non-empty CodeBlock for the complete accepted type expression;
the core validates that block, collects its imports, resolves aliases, and then
uses the ordinary direct or pretty renderer. Type blocks are produced after
source rewrite and are not rewritten a second time.
| TypeName | TypeScript | Rust | Go | C++ |
|---|---|---|---|---|
array(T) | T[] | Vec<T> | []T | std::vector<T> |
optional(T) | T | null | Option<T> | *T | std::optional<T> |
tuple(A, B) | [A, B] | (A, B) | n/a | std::tuple<A, B> |
reference(T) | T | &T | T | const T& |
reference_mut(T) | T | &mut T | *T | T& |
map(K, V) | Record<K, V> | HashMap<K, V> | map[K]V | std::map<K, V> |
function(A) -> R | (A) => R | fn(A) -> R | func(A) R | std::function<R(A)> |
See TypeName Validation and Lowering for ownership, output validation, compatibility, and import behavior.
Inspection methods
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Check if a type renders to empty string (used internally by ParameterSpec)
let empty = TypeName::primitive("");
assert!(empty.is_empty());
// Get the simple name (for import resolution lookups)
let t = TypeName::importable("./models", "User");
assert_eq!(t.simple_name(), Some("User"));
}
Building Functions & Fields
Specs are builders for declaration intent. They let you work with semantic
concepts such as functions, parameters, fields, and modifiers instead of
assembling declaration grammar from raw format strings. At emit time the
selected CodeLang validates whether the target can represent that intent and
lowers it to structured CodeBlocks. The same builder can be reused across
targets that support the requested semantics; unsupported combinations fail
closed.
All spec types live in src/spec/. They follow a consistent builder pattern:
mut selffor setters – owning chainable configuration methods that returnSelfselffor.build()– consumes the builder and returnsResult<Spec, SigilStitchError>- Chain calls fluently –
Builder::new(...).method().method().build()
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("todo!()", ()).unwrap();
// Correct:
let fun = FunSpec::builder("greet")
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap();
}
(CodeBlockBuilder is different: it uses &mut self, so you keep it in a let mut binding and call methods on it.)
Every spec type (including CodeBlock, TypeName, FileSpec, and ProjectSpec) derives serde::Serialize and serde::Deserialize, so you can round-trip specs through JSON, YAML, or any other serde format. This is useful for caching materialized specs, shipping them across process boundaries, or diffing them in tests.
ParameterSpec
A single function parameter: name, type, optional default value, variadic flag, and property mode.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
// Simple parameter
let p = ParameterSpec::new("name", TypeName::primitive("string")).unwrap();
// Parameter with default value
let p = ParameterSpec::builder("count", TypeName::primitive("number"))
.default_value(CodeBlock::of("0", ()).unwrap())
.build()
.unwrap();
// Output: count: number = 0
// Variadic parameter
let p = ParameterSpec::builder("args", TypeName::primitive("string"))
.variadic()
.build()
.unwrap();
// Output: ...args: string
// Readonly property parameter (Kotlin: val name: String)
let p = ParameterSpec::builder("name", TypeName::primitive("String"))
.is_property()
.build()
.unwrap();
// Mutable property parameter (Kotlin: var name: String)
let p = ParameterSpec::builder("name", TypeName::primitive("String"))
.is_mutable_property()
.build()
.unwrap();
}
ParameterSpec records parameter intent. The selected adapter may lower it as
name: type in TypeScript, type name in C, or without an annotation in Python
when the type is empty. Likewise, is_property() and
is_mutable_property() record constructor-property intent; the adapter chooses
spellings such as val/var in Kotlin or readonly in C#.
FieldSpec
A struct field or class property: name, type, visibility, static/readonly flags, initializer, annotations, and doc comments.
Fields are validated and lowered as a complete ordered sequence. The selected adapter receives a semantic context for direct emission, ordinary type members, an ordinary variant record payload, or a closed-sum case record payload. The payload contexts stay separate so supporting a generated closed-sum case does not grant that shape to an ordinary enum. The adapter can therefore validate sibling name collisions and own sequence-level grammar such as access sections and separators. Every built-in declares explicit field capability profiles; an unsupported context, modifier, annotation form, tag, or type requirement returns an error before lowering.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::lang::rust::Rust;
fn main() {
let field = FieldSpec::builder("name", TypeName::primitive("string"))
.visibility(Visibility::Private)
.is_readonly()
.build()
.unwrap();
// TypeScript: private readonly name: string;
let field = FieldSpec::builder("name", TypeName::primitive("String"))
.visibility(Visibility::Public)
.build()
.unwrap();
// Rust: pub name: String,
}
Fields support initializers for default values:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let field = FieldSpec::builder("count", TypeName::primitive("number"))
.initializer(CodeBlock::of("0", ()).unwrap())
.build()
.unwrap();
// TypeScript: count: number = 0;
}
For Go, use .tag() to attach struct tags:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let field = FieldSpec::builder("Name", TypeName::primitive("string"))
.tag("json:\"name\" db:\"name\"")
.build()
.unwrap();
// Go: Name string `json:"name" db:"name"`
}
Optional fields
is_optional() marks a field whose key may be absent. This requests
FieldCapability::OptionalPresence, which is distinct from a present value
that can be null or option-like. The selected adapter must explicitly support
the capability in the current field context:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let field = FieldSpec::builder("email", TypeName::primitive("string"))
.is_optional()
.build()
.unwrap();
// TypeScript: email?: string;
// Other built-in adapters currently reject OptionalPresence rather than
// silently changing its meaning.
}
Use is_optional() for “the key might not be there” (e.g., an OpenAPI property
not listed in required). Use TypeName::optional(...) for “the field is
present, but its value might be absent or null” at the type level:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let field = FieldSpec::builder(
"email",
TypeName::optional(TypeName::primitive("String")),
)
.build()
.unwrap();
// Rust: email: Option<String>,
// Swift: var email: String?
// Python: email: String | None
}
The deprecated OptionalFieldStyle and CodeLang::optional_field_style() API
exists only so adapters written against 0.6.8 keep their frozen output through
the default compatibility lowerer. New adapters must use field capabilities,
TypeName::Optional, and complete lower_fields() implementations instead.
See 0.6.8 Legacy Compatibility and Migration
for the compatibility boundary and adapter migration sequence.
FunSpec
A function or method: parameters, return type, body, modifiers (async, static, abstract, constructor, override), type parameters, annotations, and doc comments.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let body = CodeBlock::of("return this.name", ()).unwrap();
let fun = FunSpec::builder("getName")
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap();
// function getName(): string {
// return this.name
// }
}
Async methods
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("return await db.find(id)", ()).unwrap();
let fun = FunSpec::builder("fetchUser")
.is_async()
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::application(TypeName::primitive("Promise"), vec![TypeArgument::Single(TypeName::primitive("User"))]))
.body(body)
.build()
.unwrap();
// public async fetchUser(id: string): Promise<User> {
// return await db.find(id)
// }
}
Type parameters
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let tp = GenericParamSpec::single("T").unwrap()
.with_bound(TypeName::primitive("Serializable")).unwrap();
let body = CodeBlock::of("return JSON.stringify(value)", ()).unwrap();
let fun = FunSpec::builder("serialize")
.add_generic_param(tp)
.add_param(ParameterSpec::new("value", TypeName::primitive("T")).unwrap())
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap();
// function serialize<T extends Serializable>(value: T): string {
// return JSON.stringify(value)
// }
}
Abstract methods
When no body is provided, the function renders as a declaration. Combined with is_abstract(), this produces abstract method signatures:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let fun = FunSpec::builder("validate")
.is_abstract()
.returns(TypeName::primitive("boolean"))
.build()
.unwrap();
// abstract validate(): boolean;
}
Constructor delegation
Use .delegation() to provide a super(...) or this(...) delegation payload.
The selected adapter owns its placement: TypeScript, Java, Dart, and Swift put
it first in the body, while Kotlin places it after the parameter list.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("this.name = name", ()).unwrap();
let fun = FunSpec::builder("constructor")
.is_constructor()
.add_param(ParameterSpec::new("name", TypeName::primitive("string")).unwrap())
.delegation(CodeBlock::of("super(name)", ()).unwrap())
.body(body)
.build()
.unwrap();
// constructor(name: string) {
// super(name);
// this.name = name
// }
}
Building Types & Enums
This chapter covers type declarations (classes, structs, interfaces, enums, type aliases, newtypes), computed properties, annotations, and enum variants. These specs follow the same builder pattern described in Building Functions & Fields: mut self for setters that return Self, self for .build(), and fluent chaining: Builder::new(...).method().method().build().
TypeSpec
The largest spec. Models type declarations: struct, class, interface, trait,
enum, type alias, or newtype wrapper. Takes a TypeKind to select the semantic
declaration kind. At emission, sigil-stitch validates the complete type and its
children, constructs ValidatedType, and delegates the entire declaration to
the selected adapter’s lower_type() implementation.
.build() returns Err(SigilStitchError::DuplicateFieldName { type_name, field_name }) when two fields in the same type share a name.
Single-block output (TypeScript class)
The TypeScript adapter lowers a class and its members into one CodeBlock:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let body = CodeBlock::of("return this.name", ()).unwrap();
let type_spec = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("name", TypeName::primitive("string"))
.visibility(Visibility::Private)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("getName")
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
let blocks = type_spec.emit(&TypeScript::new()).unwrap();
// blocks.len() == 1
//
// export class UserService {
// private name: string;
//
// getName(): string {
// return this.name
// }
// }
}
Two-block output (Rust struct + impl)
The Rust adapter lowers a struct with methods into two CodeBlocks: one for the
data definition and one for the impl block:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::rust::Rust;
fn main() {
let body = CodeBlock::of("Self { name: name.to_string() }", ()).unwrap();
let type_spec = TypeSpec::builder("Config", TypeKind::Struct)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("name", TypeName::primitive("String"))
.visibility(Visibility::Public)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("new")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("name", TypeName::primitive("&str")).unwrap())
.returns(TypeName::primitive("Self"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
let blocks = type_spec.emit(&Rust::new()).unwrap();
// blocks.len() == 2
//
// Block 0:
// pub struct Config {
// pub name: String,
// }
//
// Block 1:
// impl Config {
// pub fn new(name: &str) -> Self {
// Self { name: name.to_string() }
// }
// }
}
The split is target grammar owned by the adapter. The TypeSpec records the
same declaration intent without describing whether members are nested in the
type or emitted in a separate implementation block.
Extends and implements
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("AdminService", TypeKind::Class)
.visibility(Visibility::Public)
.extends(TypeName::primitive("BaseService"))
.implements(TypeName::primitive("Serializable"))
.build()
.unwrap();
// export class AdminService extends BaseService implements Serializable {
// }
}
Keep nominal inheritance in .extends() and implemented contracts in
.implements() even when the target writes both in one punctuation-delimited
list. Single-inheritance adapters reject a second nominal superclass instead
of silently reinterpreting or dropping it.
Kotlin initializes a superclass in the type header with a zero-argument call
when the declaration has an implicit or explicit primary constructor, so
.extends(BaseService) becomes : BaseService(). A class with only secondary
constructors keeps the bare superclass in the header and each secondary
constructor must provide a this(...) or super(...) delegation. Superclass
constructor arguments for a primary constructor are not part of the current
semantic vocabulary; use a target-local declaration when a nonzero-argument
header call is required.
Embedded types (Go struct composition)
Use add_embedded(TypeName) for unnamed type references inside a struct body. This models Go’s embedded field pattern where a type is included by name without a field identifier:
extern crate sigil_stitch;
use sigil_stitch::lang::go::Go;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("UserAdmin", TypeKind::Struct)
.add_embedded(TypeName::primitive("User"))
.add_embedded(TypeName::primitive("Admin"))
.add_field(
FieldSpec::builder("Role", TypeName::primitive("string"))
.build()
.unwrap(),
)
.build()
.unwrap();
// type UserAdmin struct {
// User
// Admin
// Role string
// }
}
The Go adapter renders embedded types before regular fields. If an embedded
type is TypeName::importable(...), its import is tracked automatically via
%T. Go interfaces use the same semantic input for interface composition:
extern crate sigil_stitch;
use sigil_stitch::lang::go::Go;
use sigil_stitch::prelude::*;
fn main() {
let io_reader = TypeName::importable("io", "Reader");
let io_writer = TypeName::importable("io", "Writer");
let type_spec = TypeSpec::builder("ReadWriter", TypeKind::Interface)
.add_embedded(io_reader)
.add_embedded(io_writer)
.build()
.unwrap();
// type ReadWriter interface {
// io.Reader
// io.Writer
// }
}
Go is currently the built-in adapter that advertises structural embedding. Python, Rust, and TypeScript reject this capability because their previous generic output was invalid or did not preserve composition semantics. Use a nominal supertype, implemented contract, named field, or explicit target-local member instead.
Type aliases
TypeKind::TypeAlias emits a single-line type alias declaration with no body. The aliased target is set via .extends() (exactly one required). No fields, methods, or variants are allowed.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::lang::rust::Rust;
fn main() {
// TypeScript: export type UserId = string;
let type_spec = TypeSpec::builder("UserId", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.extends(TypeName::primitive("string"))
.build()
.unwrap();
// Rust: pub type Meters = f64;
let type_spec = TypeSpec::builder("Meters", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.extends(TypeName::primitive("f64"))
.build()
.unwrap();
}
Each language adapter owns the complete type-alias form:
- TypeScript/Rust:
type Foo = Bar; - C++:
using Foo = Bar; - C:
typedef Bar Foo; - Go:
type Foo = Bar - Kotlin:
typealias Foo = Bar - Python:
type Foo = Bar
Type aliases support type parameters:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// Rust: pub type Result<T> = std::result::Result<T, MyError>;
let type_spec = TypeSpec::builder("Result", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.add_generic_param(GenericParamSpec::single("T").unwrap())
.extends(TypeName::application(TypeName::primitive("std::result::Result"), vec![TypeArgument::Single(TypeName::primitive("T")), TypeArgument::Single(TypeName::primitive("MyError"))]))
.build()
.unwrap();
}
Newtype wrappers
TypeKind::Newtype emits a single-line newtype wrapper. Like type aliases, the inner type is set via .extends() (exactly one required).
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::lang::go::Go;
fn main() {
// Rust: pub struct Meters(f64);
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.visibility(Visibility::Public)
.extends(TypeName::primitive("f64"))
.build()
.unwrap();
// Go: type Meters float64
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.extends(TypeName::primitive("float64"))
.build()
.unwrap();
}
Newtype syntax varies across languages and is owned by each adapter’s
declaration lowering. Lowering preserves the inner TypeName as a structured
reference, so imports and aliases work inside newtype declarations just as they
do in ordinary %T slots:
- Rust:
struct Meters(f64);(tuple struct) - Go:
type Meters float64(distinct type) - Kotlin:
value class Meters(val value: f64)(inline class) - Python:
Meters = NewType("Meters", float)(typing.NewType)
Rust, Go, Haskell, Kotlin, and Scala adapters emit supported type parameters and bounds. C, PHP, and Python reject generic newtype intent because their supported wrapper forms do not preserve declaration-site generic parameters.
Primary constructors
Kotlin and Scala accept primary-constructor parameters on the type declaration. Pass the identifier as the parameter name and use semantic promotion flags:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("User", TypeKind::Struct)
.add_primary_constructor_param(
ParameterSpec::builder("name", TypeName::primitive("String"))
.is_property()
.build()
.unwrap(),
)
.add_primary_constructor_param(
ParameterSpec::builder("age", TypeName::primitive("Int"))
.is_mutable_property()
.build()
.unwrap(),
)
.build()
.unwrap();
// Kotlin: data class User(val name: String, var age: Int) { ... }
}
Do not put val or var in the name. Strict adapters reject such syntax in an
identifier. A Kotlin TypeKind::Struct is a data class, so it requires at
least one primary-constructor parameter and every such parameter must request
an immutable or mutable property. Haskell and OCaml algebraic constructor data
uses variant positional or record payloads instead; it is not modeled as a
primary constructor.
Enums with EnumVariantSpec
TypeSpec with TypeKind::Enum uses add_variant() instead of add_field(). See the EnumVariantSpec section below for variant forms.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let type_spec = TypeSpec::builder("Direction", TypeKind::Enum)
.add_variant(
EnumVariantSpec::builder("Up")
.discriminant(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Down")
.discriminant(CodeBlock::of("'DOWN'", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
// enum Direction {
// Up = 'UP',
// Down = 'DOWN',
// }
}
Closed sums
Use ClosedSumSpec when the declaration carries a complete set of cases rather
than value-enum entries. Cases may be unit-shaped, carry positional types, or
carry named record fields. This is declaration intent: each adapter chooses
native enum, algebraic-data-type, nested sealed-hierarchy, or sibling case
syntax locally.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::spec::closed_sum_case_spec::ClosedSumCaseSpec;
use sigil_stitch::spec::field_spec::FieldSpec;
fn main() {
let outcome = ClosedSumSpec::builder("Outcome")
.add_case(ClosedSumCaseSpec::unit("Empty").unwrap())
.add_case(ClosedSumCaseSpec::positional(
"Value",
vec![TypeName::primitive("Payload")],
).unwrap())
.add_case(ClosedSumCaseSpec::record(
"Failure",
vec![FieldSpec::of("code", TypeName::primitive("FailureCode"))],
).unwrap())
.build()
.unwrap();
}
TypeSpec::builder(name, TypeKind::Enum) remains the ordinary value-enum
entry point. Closed-sum cases intentionally have no discriminant, legacy
variant value, or enum constructor-argument fields: those concepts belong to
ordinary enum entries rather than named sum cases. Wire discriminator values
and serialization tags remain caller data or annotations; they do not change
which case declaration is generated.
The built-in support matrix is:
| Target | Representation | Empty sum |
|---|---|---|
| Rust | Native enum | Native empty enum |
| Swift | Native enum | Native empty enum |
| Haskell | Data declaration | Rejected without an EmptyDataDecls file contract |
| OCaml | Native variant | Native `type name = |
| Scala | Scala 3 enum | Rejected |
| Java | Sealed interface with nested singleton and record cases | Rejected |
| Kotlin | Private-constructor sealed class with nested data cases | Supported |
| Dart | Sealed root with root-qualified final sibling cases | Rejected |
Other built-ins reject closed-sum intent instead of widening it to Object,
Any, an open hierarchy, or an ordinary value enum. Root annotations, type
parameters, and constraints require the selected target’s
ClosedSumCapabilityProfile; case annotations use the same declaration
capability. Rust, Haskell, and OCaml preserve the supported generic forms;
Scala rejects generic closed sums until every case can preserve the root type
arguments, and the other targets reject generic forms not present in their
closed-sum profile.
Calling ClosedSumSpec::builder(name).build() with no cases requests a named
empty sum. It is not the unit type and does not add a TypeName::Never
reference. A target accepts this form only when it can emit that named
uninhabited declaration exactly.
PropertySpec
PropertySpec describes a computed value with read and/or write behavior. It
records the value type, accessor bodies, visibility, static intent,
documentation, and annotations without choosing a target syntax. At emission,
the selected adapter validates PropertyIntent against its context-specific
profile and completely lowers the accepted declaration:
- TypeScript and JavaScript emit native accessor declarations.
- Swift emits a
varcomputed property, including getter-only properties. - Kotlin emits a
valorvarfollowed directly by its indented accessors; there is no outer property brace. - PHP emits
getName()andsetName()methods. - Scala emits
def nameanddef name_=methods.
Other built-ins reject the unsupported property context instead of falling
back to plausible target text. Swift and Kotlin require an explicit value type
and read accessor. Kotlin and Scala reject static property intent.
TypeScript interfaces and Swift protocols also reject PropertySpec: those
targets support bodyless property requirements, while this spec carries
concrete accessor bodies and never discards them.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::spec::property_spec::PropertySpec;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let getter_body = CodeBlock::of("return this._name", ()).unwrap();
let setter_body = CodeBlock::of("this._name = value", ()).unwrap();
let prop = PropertySpec::builder("name", TypeName::primitive("string"))
.getter(getter_body)
.setter("value", setter_body)
.build()
.unwrap();
// TypeScript:
// get name(): string {
// return this._name
// }
// set name(value: string) {
// this._name = value
// }
}
PropertySpec::emit() retains its pre-0.6.8 direct facade and accepts a
DeclarationContext. Adding the property to TypeSpec supplies the owning
TypeKind, which lets the adapter reject invalid contract or declaration
contexts before lowering. External adapters written against 0.6.8 retain the
deprecated PropertyStyle compatibility behavior; new adapters implement
validate_property() and lower_property() instead. The complete deprecated
surface and migration paths are listed in 0.6.8 Legacy Compatibility and
Migration.
When a target lowers properties into a namespace shared with other members,
TypeSpec also supplies one validation-only TypeMembersIntent after all
per-family checks. Exact duplicate property names are rejected by the crate;
the adapter rejects names that collide only after its own lowering. PHP uses
this pass because method names are case-insensitive and generated getName()
or setName() accessors can collide with accessors from another property or
with an explicit method. TypeScript, Kotlin, Swift, and Scala use the same seam
for their own field/property namespaces; only declarations in the same
target-local namespace collide, so TypeScript private names and TypeScript and
Swift static members stay distinct from their instance counterparts.
TypeScript, Swift, and Scala also reject explicit methods that occupy the same
emitted member name in that namespace. These are separate adapter rules, not
one general namespace abstraction. This owner-wide view does not change the
per-property PropertyIntent -> ValidatedProperty -> lower_property() path.
AnnotationSpec
Structured annotations that render with language-appropriate syntax. The prefix and suffix adapt automatically:
| Language | Syntax |
|---|---|
| Java, Kotlin, TS | @Name(args) |
| Rust | #[name(args)] |
| C++ | [[name(args)]] |
| C | __attribute__((name(args))) |
Attribute support is declaration-kind specific. For example, TypeScript decorators are accepted on class-backed declarations but rejected on interfaces, where decorator syntax cannot be emitted.
extern crate sigil_stitch;
use sigil_stitch::spec::annotation_spec::AnnotationSpec;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() {
// Simple annotation: #[allow(dead_code)]
let ann = AnnotationSpec::new("allow").arg("dead_code");
// Multiple arguments: #[cfg(test, feature = "nightly")]
let ann = AnnotationSpec::new("cfg")
.arg("test")
.arg("feature = \"nightly\"");
// Bulk arguments from an iterator: #[derive(Debug, Clone, Serialize)]
let ann = AnnotationSpec::new("derive")
.args(["Debug", "Clone", "Serialize"]);
}
For import-tracked annotations, use importable() with a TypeName:
extern crate sigil_stitch;
use sigil_stitch::spec::annotation_spec::AnnotationSpec;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::type_name::TypeName;
use sigil_stitch::prelude::*;
fn main() {
let type_name = TypeName::importable("./decorators", "Component");
let ann = AnnotationSpec::importable(type_name);
// TS: @Component (with import { Component } from './decorators')
}
If AnnotationSpec does not cover your annotation format, every builder also has an .annotation(CodeBlock) escape hatch that accepts a raw CodeBlock.
EnumVariantSpec
Variants are validated and lowered as one owner-aware sequence through
TypeSpec. This lets the selected language derive first/last position, choose
valid separators, and terminate the variant section when fields or methods
follow. Direct positional emission with VariantContext is deprecated and is
rejected by strict built-in adapters. See 0.6.8 Legacy Compatibility and
Migration for direct-facade and builder
replacements.
Individual enum variants. Five forms are supported:
Simple variant
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() {
let v = EnumVariantSpec::new("Red").unwrap();
// Rust: Red,
}
Discriminated variant
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::prelude::*;
fn main() {
let variant = EnumVariantSpec::builder("Up")
.discriminant(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap();
// TypeScript: Up = 'UP',
}
Use .constructor_argument(...) instead when an enum entry invokes its
declaring enum’s constructor, as in Java or Kotlin. Discriminants, constructor
arguments, positional payload types, and record payload fields are distinct
semantic forms and cannot be combined on one variant. The deprecated
.value(...) builder remains only for 0.6.8 compatibility and is rejected when
the selected language cannot give it one validity-preserving meaning.
Enum-entry constructor arguments (Java, Kotlin)
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::prelude::*;
fn main() {
let variant = EnumVariantSpec::builder("ACTIVE")
.constructor_argument(CodeBlock::of("\"active\"", ()).unwrap())
.build()
.unwrap();
// Java/Kotlin: ACTIVE("active")
}
The owning enum must also declare a compatible structured constructor (or Kotlin primary constructor). sigil-stitch checks every enum entry against the accepted argument-count ranges of structured constructors, including overloads, defaulted parameters, and variadic parameters. Opaque extra members remain an escape hatch whose target-language constructor signatures cannot be inferred.
Structured variant annotations are accepted only when the adapter can preserve
declaration-metadata semantics. Ruby therefore rejects AnnotationSpec on enum
constants instead of rendering it as a comment; .annotation(CodeBlock) remains
an explicit escape hatch for target-specific Ruby code.
Positional payload (Rust, Swift)
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() {
let variant = EnumVariantSpec::builder("Literal")
.positional_payload(TypeName::primitive("i64"))
.build()
.unwrap();
// Rust: Literal(i64),
// Multi-element tuple
let variant = EnumVariantSpec::builder("Pair")
.positional_payload(TypeName::primitive("String"))
.positional_payload(TypeName::primitive("i32"))
.build()
.unwrap();
// Rust: Pair(String, i32),
}
Record payload (Rust)
extern crate sigil_stitch;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::spec::field_spec::FieldSpec;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() {
let variant = EnumVariantSpec::builder("Move")
.record_payload_field(
FieldSpec::builder("x", TypeName::primitive("i32")).build().unwrap(),
)
.record_payload_field(
FieldSpec::builder("y", TypeName::primitive("i32")).build().unwrap(),
)
.build()
.unwrap();
// Rust:
// Move {
// x: i32,
// y: i32,
// },
}
Variants are added to a TypeSpec via add_variant(). The language adapter
owns their complete grammar, including separators, trailing punctuation, and
prefixes such as Swift’s case. The pre-0.6.8 builder names
.associated_type(...) and .add_field(...) remain as deprecated aliases for
.positional_payload(...) and .record_payload_field(...), respectively.
Files & Projects
This chapter covers the import system, file rendering, and multi-file project generation. These specs follow the same builder pattern described in Building Functions & Fields.
ImportSpec
Explicit import control for cases where %T / TypeName::Importable is not sufficient. Add to a FileSpec via add_import().
extern crate sigil_stitch;
use sigil_stitch::spec::import_spec::ImportSpec;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::prelude::*;
fn main() {
// Forced named import (even without %T usage in code)
let spec = ImportSpec::named("./models", "User");
// Aliased import: import { User as MyUser } from './models'
let spec = ImportSpec::named_as("./models", "User", "MyUser");
// Type-only import: import type { User } from './models'
let spec = ImportSpec::named_type("./models", "User");
// Side-effect import: import './polyfill'
let spec = ImportSpec::side_effect("./polyfill");
// Wildcard import: import * from './utils'
let spec = ImportSpec::wildcard("./utils");
}
Most of the time you do not need ImportSpec – imports driven by %T and TypeName::importable() handle the common case. Use ImportSpec for forced imports, side-effect imports, and wildcard imports.
FileSpec
The top-level file orchestrator combines code blocks and declaration specs.
FileSpec::render() owns the complete render-preparation pipeline:
- Lower declarations – Validate declaration specs and ask the language
adapter to lower them to source
CodeBlocks. - Prepare blocks – Rewrite each source block exactly once, validate its
structure, lower every
%Ttype, and validate the lowered type blocks. - Resolve imports – Collect imports only from the prepared blocks, merge explicit imports, then deduplicate them and assign every peer conflict set atomically.
- Render – Emit the import header and prepared body with no further rewrite or type lowering.
No import header or body text is returned until every preparation and
resolution operation succeeds. FileSpec::validate() remains model-only
validation; rewrite, type-name lowering, and import resolution run only during
render preparation.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let user = TypeName::importable_type("./models", "User");
let mut cb = CodeBlock::builder();
cb.add_statement("const u: %T = getUser()", (user,));
let block = cb.build().unwrap();
let file = FileSpec::builder("user.ts")
.add_code(block)
.build()
.unwrap();
let output = file.render(80).unwrap();
// import type { User } from './models'
//
// const u: User = getUser();
}
Custom import conflict resolution
render() uses the built-in deterministic module-prefix policy. For
project-specific naming, implement ImportAliasConflictResolver and pass a
borrowed value to render_with_import_alias_resolver(). One call receives all
ambiguous peer classes in that file. It must return exactly one assignment for
every claim; exact ImportSpec bindings cannot change. Missing, duplicate,
unknown, unsafe, globally colliding, or target-invalid assignments abort the
render before source is returned.
ProjectSpec::render_with_import_alias_resolver() applies the same borrowed
policy independently to each file. Its matching
write_to_with_import_alias_resolver() renders every file successfully before
creating output, so a resolution failure cannot leave a partially written
project. The resolver is an execution dependency and is never stored or
serialized in a file or project spec.
Direct ImportGroup::try_resolve() and try_resolve_with() callers must invoke
the selected adapter’s CodeLang::validate_resolved_imports() before passing
the group to render_imports(). FileSpec and ProjectSpec perform this
target-local validation automatically.
You can mix member types freely: add_code() for raw CodeBlocks, add_type() for TypeSpec, add_function() for FunSpec, add_raw() for escape-hatch strings with no import tracking.
A file header (license comment, package declaration) can be set with .header():
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let service_type = TypeSpec::builder("Service", TypeKind::Class).build().unwrap();
let mut header_b = CodeBlock::builder();
header_b.add("// License: MIT", ());
let header = header_b.build().unwrap();
let file = FileSpec::builder("service.ts")
.header(header)
.add_type(service_type)
.build()
.unwrap();
}
ProjectSpec
Multi-file generation. Wraps multiple FileSpecs, renders them all, and can optionally write to the filesystem.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
// Build individual files
let models = FileSpec::builder("src/models.ts")
.add_type(
TypeSpec::builder("User", TypeKind::Interface).build().unwrap(),
)
.build()
.unwrap();
let index = FileSpec::builder("src/index.ts")
.add_code(CodeBlock::of("export {}", ()).unwrap())
.build()
.unwrap();
// Combine into a project
let project = ProjectSpec::builder()
.add_file(models)
.add_file(index)
.build()
.unwrap();
// Render all files in memory
let rendered = project.render(80).unwrap();
for file in &rendered {
println!("--- {} ---\n{}", file.path, file.content);
}
// Or write directly to disk
// project.write_to(Path::new("./output"), 80).unwrap();
}
ProjectSpec::validate() checks every file in project order and returns one
ProjectSpecValidation error containing each invalid file’s complete
FileSpec::validate() failure. Member errors remain grouped inside their
FileSpecValidation error. render() performs this complete validation before
rendering any file, and write_to() renders the whole project in memory before
creating directories or files. A validation failure therefore returns all
known file diagnostics and performs no writes.
After validation, each file resolves imports independently. render() returns
Vec<RenderedFile> with path and content fields. write_to() creates
parent directories as needed only after every file renders successfully.
End-to-End Example
A complete TypeScript class with imports, fields, a constructor, and a method – from builder calls to rendered output.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
// Define an imported type
let repo_type = TypeName::importable_type("./repository", "UserRepository");
// Build the class
let user_type = TypeName::importable_type("./models", "User");
let ctor_body = CodeBlock::of("this.repo = repo", ()).unwrap();
let method_body = CodeBlock::of("return this.repo.findById(id)", ()).unwrap();
let type_spec = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
// Field: private readonly repo: UserRepository;
.add_field(
FieldSpec::builder("repo", repo_type.clone())
.visibility(Visibility::Private)
.is_readonly()
.build()
.unwrap(),
)
// Constructor
.add_method(
FunSpec::builder("constructor")
.is_constructor()
.add_param(ParameterSpec::new("repo", repo_type.clone()).unwrap())
.body(ctor_body)
.build()
.unwrap(),
)
// Method: async getUser(id: string): Promise<User>
.add_method(
FunSpec::builder("getUser")
.is_async()
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::application(TypeName::primitive("Promise"), vec![TypeArgument::Single(user_type)]))
.body(method_body)
.build()
.unwrap(),
)
.build()
.unwrap();
// Build the file
let file = FileSpec::builder("user_service.ts")
.add_type(type_spec)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
Rendered output:
import type { User } from './models'
import { UserRepository } from './repository'
export class UserService {
private readonly repo: UserRepository;
constructor(repo: UserRepository) {
this.repo = repo
}
async getUser(id: string): Promise<User> {
return this.repo.findById(id)
}
}
The import header is fully automatic. UserRepository and User are collected from the %T references inside the emitted CodeBlocks, deduplicated, and rendered as import statements. No manual import management required.
sigil_quote! Macro
sigil_quote! lets you write target-language code inline and have it expand to
CodeBlockBuilder method calls at compile time. It’s the recommended way to build
CodeBlocks when the structure is known ahead of time.
For background on the % format specifiers that sigil_quote! expands to, see
Format Specifiers. For a hands-on introduction, see
Getting Started.
Basic Usage
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let user_type = TypeName::importable_type("./models", "User");
let block = sigil_quote!(TypeScript {
const user: $T(user_type) = await getUser($S("id"));
if (!user) {
throw new Error($S("not found"));
}
return user;
}).unwrap();
}
The macro takes a language type followed by a braced body of target-language code.
It returns Result<CodeBlock, SigilStitchError>.
Testing Quoted Fragments
Use assert_quote! for small exact snapshots of inline quoted code:
extern crate sigil_stitch;
use sigil_stitch::{assert_quote, prelude::*};
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
assert_quote!(TypeScript, {
const x = 1;
}, "const x = 1;\n");
}
Use assert_rendered! when the block is built separately, needs imports, or uses
a configured language instance:
extern crate sigil_stitch;
use sigil_stitch::{assert_rendered, prelude::*};
use sigil_stitch::lang::python::Python;
fn main() {
let block = sigil_quote!(Python {
print($S("hi"))
}).unwrap();
assert_rendered!(
Python::new().with_indent(" "),
block,
"print('hi')\n",
);
}
Both helpers render through FileSpec, so import collection and language-specific
rendering match real files. Comparisons are exact: indentation, whitespace, and
final newlines are significant.
Interpolation Markers
| Syntax | Specifier | Argument Type | Purpose |
|---|---|---|---|
$T(expr) | %T | TypeName | Type reference, tracks imports |
$N(expr) | %N | impl ToString | Name identifier |
$S(expr) | %S | impl ToString | String literal (quoted in output) |
$V(expr) | %V | impl ToString | Verbatim string (interpolation preserved) |
$L(expr) | %L | impl Into<Arg> | Literal value, nested code, or parsed fragment |
$C(expr) | %L | CodeBlock | Nested code block |
$W | %W | (none) | Soft line-break point |
$> | %> | (none) | Increase indent level |
$< | %< | (none) | Decrease indent level |
$$ | $ | (none) | Literal dollar sign |
$C_each(expr) | — | impl IntoIterator<Item: Into<CodeBlock>> | Splice each code block from iterable |
$attr("text") | — | impl ToString | Structural annotation (language-specific prefix/suffix) |
$T_join(sep, iter) | %T | separator + impl IntoIterator<Item: TypeName> | Type name join with per-item import tracking |
$if(cond) { ... } | — | Rust expression | Meta-conditional (runtime codegen control) |
$for(pat in expr) { ... } | — | Rust pattern + iterable | Meta-loop (emit body per iteration) |
$for(pat in expr; separator = expr, trailing = bool) { ... } | — | Rust pattern + iterable + options | Meta-loop with separator control |
$let(binding); | — | Rust let binding | Rust-level variable binding inside macro body |
$join(sep, iter) | %L | separator + impl IntoIterator<Item: ToString> | Separator-joined list |
$+ | — | (none) | Line continuation (suppress line-break split) |
Types ($T)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let user_type = TypeName::importable_type("./models", "User");
let block = sigil_quote!(TypeScript {
const user: $T(user_type) = getUser();
}).unwrap();
// Expands to: __sigil_builder.add_statement("const user: %T = getUser()", (user_type,));
// The import collector picks up User and generates: import type { User } from './models'
}
$T accepts a complete TypeName, including Parameter, Application, and
Callable. It preserves the value as a structured type reference; it does not
render it to a string inside the macro. The selected language lowers that value
before import collection, so nested and language-derived imports remain visible.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), SigilStitchError> {
let input = TypeName::application(
TypeName::importable_type("./models", "Box"),
vec![TypeArgument::Single(TypeName::parameter("T"))],
);
let handler = TypeName::callable(
vec![CallableParam::Single {
name: Some("value".into()),
type_name: input,
presence: CallableParamPresence::Optional,
}],
TypeName::importable_type("./results", "Result"),
);
let block = sigil_quote!(TypeScript {
type Handler<T> = $T(handler);
})?;
let output = FileSpec::builder("handler.ts").add_code(block).build()?.render(80)?;
assert!(output.contains("type Handler<T> = (value?: Box<T>) => Result;"));
assert!(output.contains("import type { Box } from './models'"));
assert!(output.contains("import type { Result } from './results'"));
Ok(())
}
The same entry point accepts complete C++ application expansion patterns and
Haskell indexed applications. Their spelling and representability belong to
the selected language, not the macro. A successful sigil_quote! call builds a
source block; unsupported type intent still returns SigilStitchError when
that block is prepared for rendering. See TypeName.
Names ($N)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let var_name = "myVariable";
let block = sigil_quote!(TypeScript {
const $N(var_name) = 42;
}).unwrap();
// Output: const myVariable = 42;
}
String Literals ($S)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let block = sigil_quote!(TypeScript {
console.log($S("hello world"));
}).unwrap();
// Output: console.log('hello world'); (TypeScript uses single quotes)
}
Verbatim Strings ($V)
Emits a string with minimal escaping — interpolation sigils are preserved. Use this when generating code that uses the target language’s string interpolation.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let block = sigil_quote!(Bash {
echo $V("$HOME/.config")
}).unwrap();
// Output: echo "$HOME/.config"
// (Compare with $S which would produce: echo "\$HOME/.config")
}
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let block = sigil_quote!(TypeScript {
const greeting = $V("Hello, ${name}!");
}).unwrap();
// Output: const greeting = `Hello, ${name}!`;
}
Complex shell patterns — braced defaults, command substitution, arithmetic:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let block = sigil_quote!(Bash {
local config_dir = $V("${XDG_CONFIG_HOME:-$HOME/.config}")
local version = $V("$(cat ${PROJECT_ROOT}/VERSION)")
local next_port = $V("$((BASE_PORT + ${#services[@]}))")
echo $V("Deploying ${APP_NAME} v${version} (PID=$$)")
}).unwrap();
// Output:
// local config_dir = "${XDG_CONFIG_HOME:-$HOME/.config}"
// local version = "$(cat ${PROJECT_ROOT}/VERSION)"
// local next_port = "$((BASE_PORT + ${#services[@]}))"
// echo "Deploying ${APP_NAME} v${version} (PID=$$)"
}
@{expr} interpolation
Embed Rust expressions inside direct ordinary or raw $V and $L string
literals with @{expr}. The macro decodes the Rust literal, parses each embedded
expression at compile time, and emits code that evaluates the expressions at
runtime. The remaining text passes through for the target language:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let registry = "ghcr.io/myorg";
let app = "api";
let block = sigil_quote!(Bash {
docker push $V("@{registry}/@{app}:${TAG}")
}).unwrap();
// Output: docker push ghcr.io/myorg/api:${TAG}
}
Use $V when the output should be wrapped in the target language’s string delimiter; use $L when you need plain unwrapped text (e.g., type expressions, switch headers).
Raw literals are useful when an embedded expression itself contains strings or braces:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let value = 7;
let block = sigil_quote!(TypeScript {
const rendered = $V(r#"@{format!("}} {}", { let x = value; x })}"#);
}).unwrap();
}
Use @@ to emit a literal @. Bare @ not followed by { passes through
unchanged. Empty, malformed, and unclosed interpolation groups are compile
errors; diagnostics identify the marker and decoded-literal byte offset. When
independent errors occur in one invocation, the macro reports all errors it can
reach at reliable statement or interpolation boundaries.
Only a directly authored string literal is scanned. A dynamic expression such
as $V(make_template()), or a parenthesized literal expression, is evaluated
normally and its resulting text is never parsed as Rust source by the macro.
Literals ($L)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let default_val = "0";
let block = sigil_quote!(TypeScript {
const count = $L(default_val);
}).unwrap();
// Output: const count = 0;
}
$L can also splice structured code via CodeBlock or CodeFragment. Use
CodeFragment when the snippet contains format markers such as %> / %< and
must carry indentation state instead of rendering those markers as text:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::python::Python;
use sigil_stitch::spec::file_spec::FileSpec;
fn main() {
let early_return = CodeFragment::of("if enabled:\n%>return value%<", ()).unwrap();
let block = sigil_quote!(Python {
def choose(enabled: bool, value: str) -> str: {
$L(early_return)
return "fallback"
}
}).unwrap();
let output = FileSpec::builder_with("demo.py", Python::new())
.add_code(block)
.build()
.unwrap()
.render(80)
.unwrap();
assert!(output.contains("if enabled:\n return value"));
}
The value produced by $L is not reparsed as a target format string. A literal
containing %> or %< therefore fails with UnresolvedIndentMarker; wrap that
snippet in CodeFragment::of when the markers are intended to control
indentation. Direct Rust string literals are still inspected for the distinct
@{expr} syntax described above.
CodeFragment must have balanced indentation markers. Write %>...%< inside the
fragment, not %>... with the expectation that the caller will dedent later.
Nested Code Blocks ($C)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let inner = CodeBlock::of("doSomething()", ()).unwrap();
let block = sigil_quote!(TypeScript {
$C(inner);
}).unwrap();
// Output: doSomething();
}
Dollar Escape ($$)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let block = sigil_quote!(TypeScript {
const price = $$100;
}).unwrap();
// Output contains: $ 100
// Note: the tokenizer inserts a space between $ and 100
}
Statement Rules
The macro classifies each line based on how it ends:
Semicolons: add_statement()
Lines ending with ; become statement calls (the renderer adds the language’s
statement terminator):
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
const x = 1; // -> add_statement("const x = 1", ())
const y = x + 1; // -> add_statement("const y = x + 1", ())
})?;
Ok(())
}
Brace Groups: Control Flow
Lines ending with { ... } (without a trailing ;) become control flow:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
if (x > 0) { // -> begin_control_flow_with_intent(If, "if(x > 0)", ())
return true; // -> add_statement("return true", ())
} // -> end_control_flow()
})?;
Ok(())
}
Object Literals vs Control Flow
A { ... } followed by ; is treated as part of a statement, not control flow.
This is how the macro distinguishes object literals:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
const config = { timeout: 5000 }; // statement (has trailing ;)
if (ready) { // control flow (no trailing ;)
start();
}
})?;
Ok(())
}
Blank Lines: add_line()
Blank lines in the macro body insert visual separators:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
const a = 1;
const b = 2; // blank line above becomes add_line()
})?;
Ok(())
}
Comments: $comment(expr)
Rust’s proc macro tokenizer strips // comments, so they’re invisible to the macro.
Use $comment() instead. The argument can be any Rust expression that evaluates to
something displayable — a string literal, a variable, format!(...), or any type
implementing ToString.
Statement-level comments appear at the start of a line:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
$comment("Initialize the connection pool");
const pool = createPool();
})?;
// Output:
// // Initialize the connection pool
// const pool = createPool();
Ok(())
}
Dynamic expressions work as the argument:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let msg = "Initialize the connection pool";
sigil_quote!(TypeScript {
$comment(msg);
const pool = createPool();
})?;
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let name = "Foo";
sigil_quote!(TypeScript {
$comment(format!("Class: {name}"));
const x = 0;
})?;
Ok(())
}
Inline comments appear after a statement on the same line:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let msg = "cleanup";
sigil_quote!(TypeScript {
doStuff($S("x")) $comment(msg)
})?;
// Output: doStuff('x') // cleanup
Ok(())
}
@{expr} interpolation
Embed Rust expressions inside $comment string literals with @{expr}. These are
resolved at compile time:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let name = "World";
sigil_quote!(TypeScript {
$comment("Hello @{name}");
const x = 0;
})?;
// Output: // Hello World
Ok(())
}
@{...} interpolation also works in inline comments:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let count = 42;
sigil_quote!(TypeScript {
doStuff() $comment("processed @{count} items")
})?;
// Output: doStuff() // processed 42 items
Ok(())
}
Use @@ to emit a literal @:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
$comment("user@@host");
})?;
// Output: // user@host
Ok(())
}
An optional trailing ; after $comment(...) is consumed silently and does not
affect the output.
Annotations ($attr)
$attr("text") emits a structural annotation/attribute rendered with the
selected target’s delimiters. The annotation name remains structured until
rendering: $attr("override") becomes @override in TypeScript/Java,
#[override] in Rust, or [[override]] in C++.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
$attr("injectable()");
class MyService {}
})?;
// Output:
// @injectable()
// class MyService {}
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::rust::Rust;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(Rust {
$attr("derive(Debug, Clone, Serialize, Deserialize)");
struct Config {}
})?;
// Output:
// #[derive(Debug, Clone, Serialize, Deserialize)]
// struct Config {}
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::cpp::Cpp;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(Cpp {
$attr("nodiscard");
Result compute();
})?;
// Output: [[nodiscard]] Result compute();
Ok(())
}
Each language defines its own prefix/suffix via attribute_syntax(). Stacking
multiple $attr lines is common:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
$attr("injectable()");
$attr("singleton()");
class AppService {}
})?;
// Output:
// @injectable()
// @singleton()
// class AppService {}
Ok(())
}
$attr works inside $if blocks for conditional annotations:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::rust::Rust;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let needs_serde = true;
sigil_quote!(Rust {
$attr("derive(Debug, Clone)");
$if(needs_serde) {
$attr("serde(rename_all = \"camelCase\")");
}
struct Config {}
})?;
Ok(())
}
Control Flow
if / else / else if
The macro detects else and else if chains after closing braces:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
if (x > 0) {
return 1;
} else if (x < 0) {
return -1;
} else {
return 0;
}
})?;
Ok(())
}
This expands to:
__sigil_builder.begin_control_flow_with_intent(
::sigil_stitch::code_node::BlockIntent::If,
"if(x > 0)",
(),
);
__sigil_builder.add_statement("return 1", ());
__sigil_builder.next_control_flow_with_intent(
::sigil_stitch::code_node::BlockIntent::ElseIf,
"else if(x < 0)",
(),
);
__sigil_builder.add_statement("return - 1", ());
__sigil_builder.next_control_flow_with_intent(
::sigil_stitch::code_node::BlockIntent::Else,
"else",
(),
);
__sigil_builder.add_statement("return 0", ());
__sigil_builder.end_control_flow();
for / while / try-catch
Any tokens followed by { ... } are treated as control flow:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
for (const item of items) {
process(item);
}
})?;
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
try {
riskyOperation();
} catch (e) {
handleError(e);
}
})?;
Ok(())
}
Nested Control Flow
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
if (users.length > 0) {
for (const user of users) {
if (user.active) {
process(user);
}
}
}
})?;
Ok(())
}
Interpolation in Conditions
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let error_type = TypeName::importable_type("./errors", "NotFoundError");
sigil_quote!(TypeScript {
if (!user) {
throw new $T(error_type)($S("not found"));
}
})?;
Ok(())
}
Context-Aware Block Delimiters
The parser classifies each { ... } header into a language-neutral
BlockIntent. The selected adapter maps that intent through complete
render_block_open(), render_block_close(), and
render_branch_transition() operations. The frozen block_syntax() and
block-hook bridge remains only behind the provided defaults for unchanged 0.6.8
external adapters. Legacy nodes remain renderable through the selected
adapter’s complete operations; built-ins do not use those defaults. For
example, Bash maps if to then/fi and for to do/done, while Haskell
maps class to where:
extern crate sigil_stitch;
use sigil_stitch::lang::haskell::Haskell;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Haskell type class — Class intent renders " where"
sigil_quote!(Haskell {
class Functor f {
fmap :: (a -> b) -> f a -> f b;
}
})?;
// Output: class Functor f where
// fmap :: (a -> b) -> f a -> f b
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::lang::ocaml::OCaml;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// OCaml module — Module intent renders " = struct"
sigil_quote!(OCaml {
module Foo {
let x = 42;
}
})?;
// Output: module Foo = struct
// let x = 42
Ok(())
}
Bash maps control-flow keywords to their shell delimiters:
| Condition | Open | Close |
|---|---|---|
if ... | ; then | fi |
for ... | ; do | done |
while ... | ; do | done |
else | "" | "" |
elif ... | ; then | "" |
Lua similarly maps if → then/end and for/while → do/end.
Manual Indent / Dedent ($> / $<)
Use $> and $< as standalone directives to control indent level without
control flow blocks:
extern crate sigil_stitch;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
namespace Foo {
$>
const x = 1;
const y = 2;
$<
}
})?;
// Output:
// namespace Foo {
// const x = 1;
// const y = 2;
// }
Ok(())
}
These map to the %> and %< format specifiers in CodeBlockBuilder.
Splicing Code Block Iterables ($C_each)
$C_each(expr) iterates over a collection of CodeBlock values and splices each
one into the builder sequentially. It must appear at the start of a line.
Declaration specs continue to own generic bindings, kinds, and complete
declaration intent. Emit a spec with the same language used for the surrounding
source, propagate its error, then splice its structured output. For example,
a closed sum may lower to several blocks, so use $C_each rather than assuming
one declaration:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::haskell::Haskell;
fn main() -> Result<(), SigilStitchError> {
let outcome = ClosedSumSpec::builder("Outcome")
.add_generic_param(GenericParamSpec::single("a")?)
.add_case(ClosedSumCaseSpec::positional("Value", vec![TypeName::parameter("a")])?)
.build()?;
let blocks = outcome.emit(&Haskell::new())?;
let block = sigil_quote!(Haskell { $C_each(blocks) })?;
let output = FileSpec::builder("outcome.hs").add_code(block).build()?.render(80)?;
assert!(output.contains("data Outcome a ="));
assert!(output.contains("Value a"));
Ok(())
}
For one emitted block, $C or $L also preserves its type references. Do not
render a spec to a string before splicing it: that would lose structured
references needed for later import collection and alias resolution.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn main() {
let fields = vec!["name", "age"];
let blocks: Vec<CodeBlock> = fields
.iter()
.map(|f| CodeBlock::of(&format!("this.{f} = null"), ()).unwrap())
.collect();
let _ = sigil_quote!(TypeScript {
$C_each(blocks);
});
// Output:
// this.name = null;
// this.age = null;
}
Each item in the iterable is converted via Into<CodeBlock>, so you can pass any
type that implements the conversion. An optional trailing ; after $C_each(expr)
is consumed silently.
$C_each is newline-aware: blocks that already end with a newline (e.g., from
add_statement) are spliced as-is, while blocks that don’t (e.g., from
CodeBlock::of) get an automatic line break appended. This prevents double blank
lines when splicing statement-built blocks.
Meta-Conditionals ($if / $else_if / $else)
Meta-conditionals control which builder calls are emitted at Rust runtime, as
opposed to target-language if/else which emits control flow in the generated
code. Use them when the structure of the output depends on a Rust-side condition.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let include_debug = true;
sigil_quote!(TypeScript {
const x = 1;
$if(include_debug) {
console.log($S("debug: x ="), x);
}
})?;
// When include_debug is true, output includes the console.log line.
// When false, it's omitted entirely.
Ok(())
}
$else_if and $else
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mode = "production";
sigil_quote!(TypeScript {
$if(mode == "debug") {
console.log($S("debug mode"));
} $else_if(mode == "test") {
console.log($S("test mode"));
} $else {
console.log($S("production mode"));
}
})?;
Ok(())
}
The conditions are arbitrary Rust expressions evaluated at runtime. The braces
delimit which sigil_quote! statements are conditionally included — they do not
produce target-language block syntax.
Nesting with Target-Language Control Flow
Meta-conditionals can wrap target-language control flow and vice versa:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let use_guard = true;
sigil_quote!(TypeScript {
$if(use_guard) {
if (!user) {
throw new Error($S("unauthorized"));
}
}
})?;
Ok(())
}
Meta-Loops ($for)
$for iterates over a Rust collection at compile time, emitting the body statements
once per iteration. Like $if, it controls which builder calls are made — it
does not produce target-language loop syntax.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let fields = vec!["name", "age", "email"];
sigil_quote!(TypeScript {
$for(f in &fields) {
this.$N(*f) = null;
}
})?;
// Output:
// this.name = null;
// this.age = null;
// this.email = null;
Ok(())
}
Loop Separators
$for can insert a separator between emitted iterations. This is useful when
each iteration emits a complete chunk and the spacing between chunks should be
owned by the loop, not repeated inside each body:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let handlers = vec!["createUser", "updateUser"];
sigil_quote!(TypeScript {
$for(handler in &handlers; separator = "\n") {
export function $N(*handler)() {
return runHandler($S(*handler));
}
}
})?;
// Output:
// export function createUser() {
// return runHandler('createUser');
// }
//
// export function updateUser() {
// return runHandler('updateUser');
// }
Ok(())
}
Inline $for acts like a join expression: the surrounding statement layout is
preserved, and the separator is inserted between inline fragments. This is useful
when later items need a continuation prefix:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let name = "Pet";
let members = vec![
TypeName::primitive("Cat"),
TypeName::primitive("Dog"),
TypeName::primitive("null"),
];
sigil_quote!(TypeScript {
export type $N(name) =
$for(member in &members; separator = "\n| ") { $T((*member).clone()) };
})?;
// Output:
// export type Pet =
// Cat
// | Dog
// | null;
Ok(())
}
The same pattern works for constructor-style continuations in non-brace languages:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let variants = vec!["Cat", "Dog", "Fish"];
sigil_quote!(Haskell {
data Pet =
$for(variant in &variants; separator = "\n | ") { $N(*variant) }
})?;
// Output:
// data Pet =
// Cat
// | Dog
// | Fish
Ok(())
}
Use trailing = true only when you really want the same separator after the
last emitted iteration. Empty loops emit neither separators nor trailing
separators.
Use $join(sep, iter) when each item is just a value that can be converted to
text. Use inline $for(...; separator = ...) when each item is a fragment that
needs interpolation markers like $T / $N / $S. Use statement $for when
each iteration emits structured code: multiple statements, comments, attributes,
or nested $if.
The separator is a Rust expression. It is converted to a string and inserted via
%L, so format!(...) works too. Statement $for bodies already emit their
normal trailing newline, so start a statement-loop separator with text unless you
intentionally want a blank line:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rows = vec![("a", "1"), ("b", "2")];
let sep = "// next row\n";
sigil_quote!(TypeScript {
$for((name, value) in &rows; separator = sep) {
export const $N(*name) = $L(*value);
}
})?;
// Output:
// export const a = 1;
// // next row
// export const b = 2;
Ok(())
}
Destructuring Patterns
Any Rust for pattern works:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let entries = vec![("x", "number"), ("y", "string")];
sigil_quote!(TypeScript {
$for((name, ty) in &entries) {
let $N(*name): $L(*ty);
}
})?;
// Output:
// let x: number;
// let y: string;
Ok(())
}
Nesting
$for can nest inside $if and vice versa, and can contain target-language
control flow:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let variants = vec!["A", "B", "C"];
sigil_quote!(TypeScript {
$for(v in &variants) {
case $S(*v):
return $S(*v);
}
})?;
Ok(())
}
Combining with Interpolation Markers
All interpolation markers ($T, $N, $S, $L, $C, $W, $join) work
inside $for bodies, and the loop variable is in scope:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
fn get_types() -> Vec<TypeName> { vec![TypeName::primitive("User")] }
fn main() -> Result<(), Box<dyn std::error::Error>> {
let types: Vec<TypeName> = get_types();
sigil_quote!(TypeScript {
$for(t in &types) {
import type { $T(t.clone()) };
}
})?;
Ok(())
}
Inline Expressions
$for and $if also work inline — inside parenthesized groups, array
literals, object literals, and function arguments. They no longer need to be
at column 0:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let items = vec!["hostname", "platform", "arch"];
sigil_quote!(TypeScript {
const defaultKeys = [$for(item in &items) { $S(*item), }];
})?;
// Output: const defaultKeys = ['hostname', 'platform', 'arch'];
Ok(())
}
Inline $for supports the same separator options, which is useful when the loop
body is still structured but the result belongs inside a larger expression:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let items = vec!["hostname", "platform", "arch"];
sigil_quote!(TypeScript {
const defaultKeys = [$for(item in &items; separator = ", ") { $S(*item) }];
})?;
// Output: const defaultKeys = ['hostname', 'platform', 'arch'];
Ok(())
}
Separators are often clearer than writing punctuation inside the loop body when the fragments are function arguments:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let handlers = vec!["createUser", "updateUser", "deleteUser"];
sigil_quote!(TypeScript {
registerHandlers($for(handler in &handlers; separator = ", ") { $N(*handler) });
})?;
// Output: registerHandlers(createUser, updateUser, deleteUser);
Ok(())
}
If the iterable is empty, inline $for emits no body and no separators:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let items: Vec<&str> = vec![];
sigil_quote!(TypeScript {
const defaultKeys = [$for(item in &items; separator = ", ") { $S(*item) }];
})?;
// Output: const defaultKeys = [];
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let is_admin = true;
sigil_quote!(TypeScript {
setPermissions($if(is_admin) { "read-write" } $else { "read-only" });
})?;
// Output: setPermissions("read-write");
Ok(())
}
The $if / $else_if / $else chain also works inline:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let level: u32 = 2;
sigil_quote!(TypeScript {
const label = $if(level == 0) { "trace" } $else_if(level == 1) { "debug" } $else { "info" };
})?;
// Output: const label = "info";
Ok(())
}
Inline meta-directives produce ParsedSplice output — the body is spliced
directly into place without synthetic block delimiters. This means no stray
{} in C-like languages and no stray : in Python.
Meta-Bindings ($let)
$let introduces a Rust-level let binding inside the macro body. It emits a
real let statement in the generated Rust code, making it possible to compute
intermediate values — including fallible expressions with ? — inside $for and
$if bodies.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let fields = vec![("name", "String"), ("age", "u32")];
sigil_quote!(TypeScript {
$for((name, ty) in &fields) {
$let(upper = name.to_uppercase());
const $N(upper): $L(*ty);
}
})?;
// Output:
// const NAME: String;
// const AGE: u32;
Ok(())
}
Syntax
The content between the parentheses is emitted verbatim as let <content>;.
All Rust let forms work:
$let(x = expr); // simple binding
$let(x: Type = expr); // with type annotation
$let((a, b) = pair); // destructuring
$let(mut x = 0); // mutable binding
Fallible Expressions (?)
The primary motivation for $let is supporting the ? operator inside $for
bodies. Since sigil_quote! expands to a plain block (not a closure), ?
propagates to the enclosing function:
fn emit_enum(en: &Enum) -> Option<FileSpec> {
let block = sigil_quote!(Rust {
$for(v in &en.values) {
$let(s = v.value.as_str()?);
$let(variant = s.to_pascal_case());
$if(&variant != s) {
#[serde(rename = $S(s))]
}
$L(format!("{variant},"))
}
}).ok()?;
// ...
}
Note that ? also works directly inside interpolation expressions without
$let — use $let only when you need to bind the result for reuse:
// Simple case: ? inside $L() works without $let
$for(v in &values) {
$L(format!("{},", v.as_str()?.to_pascal_case()))
}
Separator-Joined Lists ($join)
$join(sep, iter) joins the string representations of an iterable’s items with
a separator. It expands to a %L specifier internally.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let items = vec!["a", "b", "c"];
sigil_quote!(TypeScript {
const values = [$join(", ", items)];
})?;
// Output: const values = [a, b, c];
Ok(())
}
The separator is any Rust expression that evaluates to something accepted by
Vec<String>::join() (typically a &str). Each item is converted via ToString.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let fields = vec!["name", "age", "email"];
let assignments: Vec<String> = fields.iter().map(|f| format!("this.{f} = {f}")).collect();
sigil_quote!(TypeScript {
$join(";\n", assignments)
})?;
// Output:
// this.name = name;
// this.age = age;
// this.email = email
Ok(())
}
Type Join ($T_join)
$T_join(sep, iter) joins TypeName items with a separator, tracking imports for
each item. Unlike $join (which calls .to_string() on each element), $T_join
uses %T slots so every type in the join contributes its import to the file.
Items may be complete applications or callables, not just simple names. Nested
types retain their imports and participate in the file’s ordinary alias
resolution. The separator is caller-supplied target syntax: $T_join does not
turn its items into generic bindings, callable slots, or expansion segments.
Represent those semantics inside a complete TypeName instead.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let types = vec![
TypeName::importable_type("./models", "User"),
TypeName::importable_type("./models", "Admin"),
TypeName::primitive("null"),
];
sigil_quote!(TypeScript {
export type Actor = $T_join(" | ", &types);
})?;
// Output: export type Actor = User | Admin | null;
// Imports: import type { Admin, User } from './models'
Ok(())
}
The separator can be any string — " | " for TypeScript unions, " & " for
intersections, " + " for Rust trait bounds, "\n" for Go interface embedding:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::rust::Rust;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let traits = vec![
TypeName::importable_type("./traits", "Serializable"),
TypeName::importable_type("./traits", "Cloneable"),
];
sigil_quote!(Rust {
fn process(stream: &mut (dyn $T_join(" + ", &traits))) {}
})?;
// Output: fn process(stream: &mut (dyn Serializable + Cloneable)) {}
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::go::Go;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let ifaces = vec![
TypeName::importable_type("./io", "Reader"),
TypeName::importable_type("./io", "Writer"),
];
sigil_quote!(Go {
type FileOps interface {
$T_join("\n", &ifaces)
}
})?;
// Output:
// type FileOps interface {
// Reader
// Writer
// }
Ok(())
}
Line Continuation ($+)
sigil_quote! splits statements on line breaks — each source line becomes a
separate statement in the generated code. This works well for languages like
Kotlin and Python where each line is typically a statement.
For expressions that span multiple lines (common in Haskell, OCaml, or long
function calls), place $+ at the end of a line to suppress the split and
continue the statement on the next line:
extern crate sigil_stitch;
use sigil_stitch::lang::haskell::Haskell;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(Haskell {
mapM_ $+
putStrLn $+
items
})?;
// Output: mapM_ putStrLn items
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::lang::kotlin::Kotlin;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(Kotlin {
val result = someFunction( $+
arg1, arg2);
})?;
// Output: val result = someFunction(arg1, arg2);
Ok(())
}
Without $+, each source line becomes its own statement. For semicolon-based
languages, ; still takes priority as the statement terminator regardless of
line breaks.
Multi-Language Support
The same syntax works with any language type:
extern crate sigil_stitch;
use sigil_stitch::lang::python::Python;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(Python {
if x > 0:
return True
})?;
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::lang::go::Go;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(Go {
x := 42;
})?;
Ok(())
}
extern crate sigil_stitch;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(Rust {
let x: i32 = 42;
})?;
Ok(())
}
Paren-Delimited Blocks (Go)
Go uses parenthesized blocks for multi-line declarations — const ( ... ),
var ( ... ), import ( ... ), and type ( ... ). sigil_quote! recognizes
these as structural blocks, so $for, $if, $C_each, and other directives
expand inside them:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::go::Go;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let variants = vec!["A", "B", "C"];
sigil_quote!(Go {
const (
$for(v in &variants) {
$L("@{v}Kind @{v} = \"@{v}\"");
}
)
})?;
// Output:
// const (
// AKind A = "A"
// BKind B = "B"
// CKind C = "C"
// )
Ok(())
}
The paren-block body is indented automatically (the codegen emits %> after the
opening header and %< before the closing )). Interpolation markers, meta-loops,
and meta-conditionals all work normally inside the block.
This detection is language-aware — only Go recognizes const, var,
import, and type as paren-block headers. In other languages, const ( ... )
is treated as a plain statement.
Known Limitations and Quirks
Language-Aware Tokenization
sigil_quote! recognizes certain language identifiers and applies language-specific spacing
rules at compile time. For example, shell languages (Bash, Zsh) get correct handling of flags
(-q, --amend), paths (/usr/local/bin), and standalone dots (find .). Go gets tight
<-ch channel receive, and Haskell gets correct $ operator spacing.
Languages without dedicated support use universal heuristics that handle most cases correctly. See Language-Aware Tokenizer (MacroLang) for the full design.
Tokenization
sigil_quote! uses Rust’s proc_macro2 tokenizer, which means the input is tokenized
as Rust tokens, not as the target language’s tokens. This creates some edge cases:
-
Single-quoted strings don’t work.
'hello'is tokenized as a Rust lifetime. Use$S("hello")instead. -
Colon spacing is context-aware. The macro tracks a
ColonContextto decide whether:gets a space before it:Context Example Space before :Type annotation name: stringno Map entry { key: value }no Path separator std::memno Ternary x ? y : zyes Walrus assign x := 42yes The context is set automatically:
?(standalone) enters ternary mode,:and;reset to type-annotation mode,{enters map-entry mode, and:=/::are detected via one-token lookahead. Path separators (std::mem::size_of) render tightly with no extra spaces. -
Other multi-character operators. Operators like
===,!==,->are tokenized as separate punctuation characters. The macro reconstructs them via proc_macro2’sSpacing::Jointflag. A pre-scan annotation pass classifies generic angle brackets (Vec<T>,HashMap<K, V>), path separators (std::mem), macro bangs (println!(...)), and prefix operators (&self,*ptr) — these render tightly without extra spaces. The generic</>heuristic relies on the preceding identifier starting with uppercase, sofn foo<T>may keep a space before<(useFunSpecfor generic function declarations). -
Keyword spacing before
(. Control-flow keywords (if,for,while,else,match,return,try,catch, etc.) automatically get a space before(. Regular identifiers do not, somyFunc(x)stays tight whileif (x)gets the expected space. This covers the common case but isn’t configurable per-language. -
Template literals. Backtick strings (
`${expr}`) aren’t representable. Use$L(expr)for dynamic content. -
Percent signs. Literal
%in your code is auto-escaped to%%in the format string, so it renders correctly.
Comments
// comments are stripped by the Rust tokenizer before the proc macro sees them.
Use $comment("text") for comments in generated code.
Expressions in Interpolation
The expression inside $T(...), $S(...), etc. is passed through as an opaque
token stream. Any valid Rust expression works:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(TypeScript {
const x: $T(TypeName::primitive("string")) = $S("hello".to_uppercase());
})?;
Ok(())
}
Blank Line Detection
Blank line detection uses proc_macro2 span locations. It requires the
span-locations feature (enabled by the macros crate). If spans aren’t available,
blank lines may not be detected.
Code Templates
CodeTemplate provides named parameters on top of CodeBlock’s positional
format strings. The template type has no language parameter, but literal text
in its pattern is target syntax. Reuse a template across targets only when that
syntax is genuinely shared.
Syntax
Templates use #{name:K} for named parameters, where K specifies the kind:
| Kind | Specifier | Argument Type |
|---|---|---|
T | %T | TypeName |
N | %N | NameArg |
S | %S | StringLitArg |
L | %L | &str, String, or CodeBlock |
Use ## to emit a literal # character.
Bare positional specifiers (%T, %N, etc.) are rejected in templates. You must use the named #{...} syntax.
Basic Usage
extern crate sigil_stitch;
use sigil_stitch::code_template::CodeTemplate;
use sigil_stitch::code_block::NameArg;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::type_name::TypeName;
use sigil_stitch::prelude::*;
fn main() {
let tmpl = CodeTemplate::new("const #{var:N}: #{type:T} = #{init:L}").unwrap();
let block = tmpl.apply()
.set("var", NameArg("user".into()))
.set("type", TypeName::primitive("string"))
.set("init", "null")
.build()
.unwrap();
// Output: const user: string = null
}
The template is parsed once by CodeTemplate::new(). Arguments are supplied
via .apply().set(...).build(), producing a structured CodeBlock that retains
typed placeholders alongside the pattern’s target-language literals.
Reuse Across Types
The same template works for different types and values:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let field_tmpl = CodeTemplate::new("#{name:N}: #{type:T}").unwrap();
// Apply for a string field
let string_field = field_tmpl.apply()
.set("name", NameArg("username".into()))
.set("type", TypeName::primitive("string"))
.build()
.unwrap();
// Apply for a number field
let number_field = field_tmpl.apply()
.set("name", NameArg("age".into()))
.set("type", TypeName::primitive("number"))
.build()
.unwrap();
}
Reuse Where Syntax Is Shared
The same template can be reused when multiple targets share that particular
fragment grammar. In this example, both TypeScript and Rust accept the
name: type = value fragment, while their surrounding declaration keywords
would require separate templates:
extern crate sigil_stitch;
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::prelude::*;
fn main() {
let decl = CodeTemplate::new("#{name:N}: #{type:T} = #{value:L}").unwrap();
// TypeScript
let ts_block = decl.apply()
.set("name", NameArg("count".into()))
.set("type", TypeName::primitive("number"))
.set("value", "0")
.build()
.unwrap();
// Rust
let rs_block = decl.apply()
.set("name", NameArg("count".into()))
.set("type", TypeName::primitive("i32"))
.set("value", "0")
.build()
.unwrap();
}
Duplicate Parameters
The same parameter name can appear multiple times. The value you set is used at each occurrence:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let tmpl = CodeTemplate::new("#{type:T} -> #{type:T}").unwrap();
let block = tmpl.apply()
.set("type", TypeName::primitive("string"))
.build()
.unwrap();
// Output: string -> string
}
Import Tracking
Templates using #{name:T} track imports just like %T in CodeBlocks. When the resulting CodeBlock is rendered inside a FileSpec, all type references are collected for the import header:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let tmpl = CodeTemplate::new("const #{var:N}: #{type:T} = new #{type:T}()").unwrap();
let user = TypeName::importable_type("./models", "User");
let block = tmpl.apply()
.set("var", NameArg("user".into()))
.set("type", user)
.build()
.unwrap();
// When rendered: import type { User } from './models'
// Output: const user: User = new User()
}
Validation
.build() validates that:
- All parameters have been set (missing parameters produce an error)
- Argument kinds match the parameter kind (
#{name:T}must receive aTypeName, not a string)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let tmpl = CodeTemplate::new("#{name:N}: #{type:T}").unwrap();
// Missing parameter
let result = tmpl.apply()
.set("name", NameArg("x".into()))
// forgot to set "type"
.build();
assert!(result.is_err());
}
Introspection
Use param_names() to inspect a template’s parameters:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let tmpl = CodeTemplate::new("#{name:N}: #{type:T} = #{init:L}").unwrap();
let params = tmpl.param_names();
// [("name", ParamKind::Name), ("type", ParamKind::Type), ("init", ParamKind::Literal)]
}
When to Use Templates vs CodeBlock
- CodeBlock: When you’re building code imperatively and the structure varies at runtime.
- CodeTemplate: When you have a fixed pattern that gets reused with different values. Templates make the pattern explicit and prevent positional argument errors.
- sigil_quote!: When you can write the target code inline at compile time.
Language Cookbook
This chapter collects practical, copy-paste-ready recipes for each supported language. Each example shows the builder calls and the rendered output. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Languages
- TypeScript – class with imports, interface with generics, type alias, enum, abstract class
- Rust – struct with impl, enum with variants, newtype, trait, type alias
- Go – struct with tags, newtype, interface, generic function
- Python – function with type hints, type alias, class with bases, dataclass, enum
- Java – class with annotations, interface, enum, abstract class
- Kotlin – data class, enum, interface, suspend function
- Swift – struct with protocol conformance, enum, enum with associated values, protocol
- C++ – class with template, using alias, enum class, virtual method, namespace wrapping
- C – typedef, struct with fields, function declaration, enum
- C# – class with XML doc, interface, enum, record with imports
- Lua – function, module with require, control flow, table constructor
- Scala – case class, trait with type parameter, enum, bounded type parameter, newtype
- Haskell – data record with deriving, type class, function with split signature, newtype, type alias
- OCaml – record type, function with curried params, module block, type alias, pattern match
Cross-language comparison
The same declaration intent – a simple data type with two fields – lowered by different language adapters:
| Language | Output |
|---|---|
| TypeScript | export class Point { x: number; y: number; } |
| Rust | pub struct Point { pub x: f64, pub y: f64, } + separate impl block |
| Go | type Point struct { X float64; Y float64 } |
| Python | class Point: x: float; y: float |
| C# | public class Point { public double X; public double Y; } |
| Lua | (no type system – use CodeBlock directly for table constructors) |
| Scala | case class Point(x: Double, y: Double) |
| Haskell | data Point = Point { pointX :: Double, pointY :: Double } |
| OCaml | type point = { x : float; y : float } |
Each CodeLang adapter owns the declaration’s complete target grammar:
keywords, delimiters, field ordering, visibility rendering, and whether methods
live inside the type body or in a separate impl block. You build declaration
intent once; each compatible adapter validates and lowers it independently.
TypeScript Cookbook
Practical, copy-paste-ready recipes for TypeScript code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Class with imports
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let user_type = TypeName::importable_type("./models", "User");
let repo_type = TypeName::importable("./repository", "UserRepository");
let body = CodeBlock::of("return this.repo.findById(id)", ()).unwrap();
let type_spec = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("repo", repo_type.clone())
.visibility(Visibility::Private)
.is_readonly()
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("getUser")
.is_async()
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::application(TypeName::primitive("Promise"), vec![TypeArgument::Single(user_type)]))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
let output = FileSpec::builder("user_service.ts")
.add_type(type_spec)
.build()
.unwrap()
.render(80)
.unwrap();
}
import type { User } from './models'
import { UserRepository } from './repository'
export class UserService {
private readonly repo: UserRepository;
async getUser(id: string): Promise<User> {
return this.repo.findById(id)
}
}
Interface with generics
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.visibility(Visibility::Public)
.add_generic_param(GenericParamSpec::single("T").unwrap())
.add_method(
FunSpec::builder("findById")
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::application(TypeName::primitive("Promise"), vec![TypeArgument::Single(TypeName::primitive("T"))]))
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.returns(TypeName::application(TypeName::primitive("Promise"), vec![TypeArgument::Single(TypeName::primitive("void"))]))
.build()
.unwrap(),
)
.build()
.unwrap();
}
export interface Repository<T> {
findById(id: string): Promise<T>;
save(entity: T): Promise<void>;
}
Type alias
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("UserId", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.extends(TypeName::primitive("string"))
.build()
.unwrap();
}
export type UserId = string;
Enum
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Direction", TypeKind::Enum)
.visibility(Visibility::Public)
.add_variant(
EnumVariantSpec::builder("Up")
.discriminant(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Down")
.discriminant(CodeBlock::of("'DOWN'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Left")
.discriminant(CodeBlock::of("'LEFT'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Right")
.discriminant(CodeBlock::of("'RIGHT'", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
}
export enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
Abstract class
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("console.log('handled')", ()).unwrap();
let type_spec = TypeSpec::builder("BaseController", TypeKind::Class)
.visibility(Visibility::Public)
.is_abstract()
.add_method(
FunSpec::builder("handleRequest")
.is_abstract()
.add_param(ParameterSpec::new("req", TypeName::primitive("Request")).unwrap())
.returns(TypeName::primitive("Response"))
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("log")
.visibility(Visibility::Protected)
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
}
export abstract class BaseController {
abstract handleRequest(req: Request): Response;
protected log() {
console.log('handled')
}
}
Rust Cookbook
Practical, copy-paste-ready recipes for Rust code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Struct with impl
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("Self { name: name.into(), port }", ()).unwrap();
let type_spec = TypeSpec::builder("Config", TypeKind::Struct)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("name", TypeName::primitive("String"))
.visibility(Visibility::Public)
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("port", TypeName::primitive("u16"))
.visibility(Visibility::Public)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("new")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("name", TypeName::primitive("&str")).unwrap())
.add_param(ParameterSpec::new("port", TypeName::primitive("u16")).unwrap())
.returns(TypeName::primitive("Self"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
}
pub struct Config {
pub name: String,
pub port: u16,
}
impl Config {
pub fn new(name: &str, port: u16) -> Self {
Self { name: name.into(), port }
}
}
Enum with variants
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
fn main() {
let type_spec = TypeSpec::builder("Expr", TypeKind::Enum)
.visibility(Visibility::Public)
.add_variant(EnumVariantSpec::new("Nil").unwrap())
.add_variant(
EnumVariantSpec::builder("Literal")
.positional_payload(TypeName::primitive("i64"))
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Binary")
.record_payload_field(FieldSpec::builder("left", TypeName::primitive("Box<Expr>")).build().unwrap())
.record_payload_field(FieldSpec::builder("op", TypeName::primitive("Op")).build().unwrap())
.record_payload_field(FieldSpec::builder("right", TypeName::primitive("Box<Expr>")).build().unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
}
pub enum Expr {
Nil,
Literal(i64),
Binary {
left: Box<Expr>,
op: Op,
right: Box<Expr>,
},
}
Newtype
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.visibility(Visibility::Public)
.extends(TypeName::primitive("f64"))
.build()
.unwrap();
}
pub struct Meters(f64);
Trait
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Summary", TypeKind::Trait)
.visibility(Visibility::Public)
.add_method(
FunSpec::builder("summarize")
.add_param(ParameterSpec::new("&self", TypeName::primitive("")).unwrap())
.returns(TypeName::primitive("String"))
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("preview")
.add_param(ParameterSpec::new("&self", TypeName::primitive("")).unwrap())
.returns(TypeName::primitive("String"))
.body(CodeBlock::of("self.summarize()[..50].to_string()", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
}
pub trait Summary {
fn summarize(&self) -> String;
fn preview(&self) -> String {
self.summarize()[..50].to_string()
}
}
Type alias
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Result", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.add_generic_param(GenericParamSpec::single("T").unwrap())
.extends(TypeName::application(TypeName::primitive("std::result::Result"), vec![TypeArgument::Single(TypeName::primitive("T")), TypeArgument::Single(TypeName::primitive("MyError"))]))
.build()
.unwrap();
}
pub type Result<T> = std::result::Result<T, MyError>;
Qualified paths (no import)
Use TypeName::qualified() to render types with their full module path inline without generating a use statement:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let field = FieldSpec::builder("data", TypeName::qualified("serde_json", "Value"))
.visibility(Visibility::Public)
.build()
.unwrap();
// In a generic:
let map_type = TypeName::application(TypeName::qualified("std::collections", "HashMap"), vec![TypeArgument::Single(TypeName::primitive("String")), TypeArgument::Single(TypeName::qualified("serde_json", "Value"))]);
}
pub data: serde_json::Value
std::collections::HashMap<String, serde_json::Value>
Go Cookbook
Practical, copy-paste-ready recipes for Go code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Struct with tags
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("User", TypeKind::Struct)
.add_field(
FieldSpec::builder("Name", TypeName::primitive("string"))
.tag("json:\"name\" db:\"name\"")
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("Email", TypeName::primitive("string"))
.tag("json:\"email\" db:\"email\"")
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("Age", TypeName::primitive("int"))
.tag("json:\"age,omitempty\"")
.build()
.unwrap(),
)
.build()
.unwrap();
}
type User struct {
Name string `json:"name" db:"name"`
Email string `json:"email" db:"email"`
Age int `json:"age,omitempty"`
}
Newtype (distinct type)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.extends(TypeName::primitive("float64"))
.build()
.unwrap();
}
type Meters float64
Interface
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.doc("Repository defines data access methods.")
.add_method(
FunSpec::builder("FindByID")
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::raw("(Entity, error)"))
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("Save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("Entity")).unwrap())
.returns(TypeName::primitive("error"))
.build()
.unwrap(),
)
.build()
.unwrap();
let file = FileSpec::builder("repo.go")
.header(CodeBlock::of("package repo", ()).unwrap())
.add_type(type_spec)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
package repo
// Repository defines data access methods.
type Repository interface {
FindByID(id string) (Entity, error)
Save(entity Entity) error
}
Generic function
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let tp = GenericParamSpec::single("T").unwrap().with_bound(TypeName::primitive("comparable")).unwrap();
let mut body_b = CodeBlock::builder();
body_b.begin_control_flow("if a > b", ());
body_b.add_statement("return a", ());
body_b.end_control_flow();
body_b.add_statement("return b", ());
let body = body_b.build().unwrap();
let fun = FunSpec::builder("Max")
.add_generic_param(tp)
.add_param(ParameterSpec::new("a", TypeName::primitive("T")).unwrap())
.add_param(ParameterSpec::new("b", TypeName::primitive("T")).unwrap())
.returns(TypeName::primitive("T"))
.body(body)
.build()
.unwrap();
let file = FileSpec::builder("max.go")
.header(CodeBlock::of("package main", ()).unwrap())
.add_function(fun)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
package main
func Max[T comparable](a T, b T) T {
if a > b {
return a
}
return b
}
Const block with enum generation
Use sigil_quote! with a $for inside const ( ... ) to generate enum-like
const blocks:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::go::Go;
fn main() {
let variants = vec!["Alpha", "Beta", "Gamma"];
let const_block = sigil_quote!(Go {
const (
$for(v in &variants) {
$L("@{v}Kind @{v} = \"@{v}\"");
}
)
}).unwrap();
let file = FileSpec::builder("kind.go")
.header(CodeBlock::of("package main", ()).unwrap())
.add_code(const_block)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
package main
const (
AlphaKind Alpha = "Alpha"
BetaKind Beta = "Beta"
GammaKind Gamma = "Gamma"
)
The parser recognizes const (, var (, import (, and type ( as structural
blocks in Go, so $for, $if, $C_each, and interpolation markers all
work inside the parentheses. The body is auto-indented with tabs.
Python Cookbook
Practical, copy-paste-ready recipes for Python code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Function with type hints
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let user_type = TypeName::importable("models", "User");
let body = CodeBlock::of("return await db.query(User).filter(active=True)", ()).unwrap();
let fun = FunSpec::builder("get_active_users")
.is_async()
.add_param(ParameterSpec::new("db", TypeName::primitive("Database")).unwrap())
.returns(TypeName::application(TypeName::primitive("list"), vec![TypeArgument::Single(user_type)]))
.body(body)
.build()
.unwrap();
}
async def get_active_users(db: Database) -> list[User]:
return await db.query(User).filter(active=True)
Type alias
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("UserId", TypeKind::TypeAlias)
.extends(TypeName::primitive("str"))
.build()
.unwrap();
}
type UserId = str
Class with bases
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("AdminService", TypeKind::Class)
.extends(TypeName::primitive("BaseService"))
.implements(TypeName::primitive("Authenticatable"))
.add_method(
FunSpec::builder("is_admin")
.add_param(ParameterSpec::new("self", TypeName::primitive("")).unwrap())
.returns(TypeName::primitive("bool"))
.body(CodeBlock::of("return True", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
}
class AdminService(BaseService, Authenticatable):
def is_admin(self) -> bool:
return True
Dataclass
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Config", TypeKind::Class)
.doc("Application configuration.")
.annotation(CodeBlock::of("@dataclass", ()).unwrap())
.add_field(
FieldSpec::builder("name", TypeName::primitive("str"))
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("port", TypeName::primitive("int"))
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("debug", TypeName::primitive("bool"))
.initializer(CodeBlock::of("False", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
}
@dataclass
class Config:
"""Application configuration."""
name: str
port: int
debug: bool = False
Enum
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let enum_base = TypeName::importable("enum", "Enum");
let type_spec = TypeSpec::builder("Direction", TypeKind::Enum)
.extends(enum_base)
.add_variant(
EnumVariantSpec::builder("UP")
.discriminant(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("DOWN")
.discriminant(CodeBlock::of("'DOWN'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("LEFT")
.discriminant(CodeBlock::of("'LEFT'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("RIGHT")
.discriminant(CodeBlock::of("'RIGHT'", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
let file = FileSpec::builder("direction.py")
.add_type(type_spec)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
from enum import Enum
class Direction(Enum):
UP = 'UP'
DOWN = 'DOWN'
LEFT = 'LEFT'
RIGHT = 'RIGHT'
Java Cookbook
Practical, copy-paste-ready recipes for Java code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Class with annotations
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::spec::annotation_spec::AnnotationSpec;
fn main() {
let inject = AnnotationSpec::new("Inject");
let body = CodeBlock::of("return repository.findById(id)", ()).unwrap();
let type_spec = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("repository", TypeName::primitive("UserRepository"))
.visibility(Visibility::Private)
.annotate(inject)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("getUser")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.returns(TypeName::primitive("User"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
}
public class UserService {
@Inject
private UserRepository repository;
public User getUser(String id) {
return repository.findById(id);
}
}
Interface
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.visibility(Visibility::Public)
.add_generic_param(GenericParamSpec::single("T").unwrap())
.doc("Generic data repository.")
.add_method(
FunSpec::builder("findById")
.returns(TypeName::primitive("T"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.returns(TypeName::primitive("void"))
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("delete")
.returns(TypeName::primitive("void"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
}
/**
* Generic data repository.
*/
public interface Repository<T> {
T findById(String id);
void save(T entity);
void delete(String id);
}
Enum
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.visibility(Visibility::Public)
.doc("Supported colors.")
.add_variant(EnumVariantSpec::new("RED").unwrap())
.add_variant(EnumVariantSpec::new("GREEN").unwrap())
.add_variant(EnumVariantSpec::new("BLUE").unwrap())
.build()
.unwrap();
}
/**
* Supported colors.
*/
public enum Color {
RED,
GREEN,
BLUE
}
Abstract class
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let desc_body = CodeBlock::of("return this.getClass().getSimpleName();", ()).unwrap();
let type_spec = TypeSpec::builder("Shape", TypeKind::Class)
.visibility(Visibility::Public)
.doc("Abstract shape.")
.add_method(
FunSpec::builder("describe")
.visibility(Visibility::Public)
.returns(TypeName::primitive("String"))
.body(desc_body)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("area")
.visibility(Visibility::Public)
.is_abstract()
.returns(TypeName::primitive("double"))
.build()
.unwrap(),
)
.is_abstract()
.build()
.unwrap();
}
/**
* Abstract shape.
*/
public abstract class Shape {
public String describe() {
return this.getClass().getSimpleName();
}
public abstract double area();
}
Kotlin Cookbook
Practical, copy-paste-ready recipes for Kotlin code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Data class
let type_spec = TypeSpec::builder("User", TypeKind::Class)
.visibility(Visibility::Public)
.add_modifier("data")
.add_field(FieldSpec::builder("name", TypeName::primitive("String")).build().unwrap())
.add_field(FieldSpec::builder("email", TypeName::primitive("String")).build().unwrap())
.build()
.unwrap();
data class User(
val name: String,
val email: String,
)
Enum
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.doc("Supported colors.")
.add_variant(EnumVariantSpec::new("RED").unwrap())
.add_variant(EnumVariantSpec::new("GREEN").unwrap())
.add_variant(EnumVariantSpec::new("BLUE").unwrap())
.build()
.unwrap();
}
/**
* Supported colors.
*/
internal enum class Color {
RED,
GREEN,
BLUE
}
Interface
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.add_generic_param(GenericParamSpec::single("T").unwrap())
.doc("Generic data repository.")
.add_method(
FunSpec::builder("findById")
.returns(TypeName::primitive("T?"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("delete")
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
}
/**
* Generic data repository.
*/
internal interface Repository<T> {
internal fun findById(id: String): T?
internal fun save(entity: T)
internal fun delete(id: String)
}
Suspend function
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let user = TypeName::importable("com.example.model", "User");
let body = CodeBlock::of("return api.fetchUser(id)", ()).unwrap();
let fun = FunSpec::builder("fetchUser")
.is_async()
.returns(user)
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.body(body)
.build()
.unwrap();
let file = FileSpec::builder("Api.kt")
.add_function(fun)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
import com.example.model.User
internal suspend fun fetchUser(id: String): User {
return api.fetchUser(id)
}
Swift Cookbook
Practical, copy-paste-ready recipes for Swift code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Struct with protocol conformance
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Point", TypeKind::Struct)
.implements(TypeName::primitive("Codable"))
.add_field(FieldSpec::builder("x", TypeName::primitive("Double")).build().unwrap())
.add_field(FieldSpec::builder("y", TypeName::primitive("Double")).build().unwrap())
.build()
.unwrap();
}
struct Point: Codable {
var x: Double
var y: Double
}
Enum
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.visibility(Visibility::Public)
.doc("Supported colors.")
.add_variant(EnumVariantSpec::new("red").unwrap())
.add_variant(EnumVariantSpec::new("green").unwrap())
.add_variant(EnumVariantSpec::new("blue").unwrap())
.build()
.unwrap();
}
/// Supported colors.
public enum Color {
case red
case green
case blue
}
Enum with associated values
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("NetworkResult", TypeKind::Enum)
.visibility(Visibility::Public)
.doc("Result of a network request.")
.add_variant(
EnumVariantSpec::builder("success")
.positional_payload(TypeName::primitive("Data"))
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("failure")
.positional_payload(TypeName::primitive("Error"))
.positional_payload(TypeName::primitive("Int"))
.build()
.unwrap(),
)
.add_variant(EnumVariantSpec::new("loading").unwrap())
.build()
.unwrap();
}
/// Result of a network request.
public enum NetworkResult {
case success(Data)
case failure(Error, Int)
case loading
}
Protocol
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.add_generic_param(GenericParamSpec::single("T").unwrap())
.doc("Generic data repository.")
.add_method(
FunSpec::builder("findById")
.returns(TypeName::primitive("T?"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("delete")
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
}
/// Generic data repository.
protocol Repository<T> {
func findById(id: String) -> T?
func save(entity: T)
func delete(id: String)
}
C++ Cookbook
Practical, copy-paste-ready recipes for C++ code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Class with template
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("data_.push_back(value)", ()).unwrap();
let type_spec = TypeSpec::builder("Stack", TypeKind::Class)
.add_generic_param(GenericParamSpec::single("T").unwrap())
.add_field(
FieldSpec::builder("data_", TypeName::application(TypeName::primitive("std::vector"), vec![TypeArgument::Single(TypeName::primitive("T"))]))
.visibility(Visibility::Private)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("push")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("value", TypeName::reference(TypeName::primitive("T"))).unwrap())
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
}
template <typename T>
class Stack {
private:
std::vector<T> data_;
public:
void push(const T& value) {
data_.push_back(value);
}
};
Using alias (C++ type alias)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("StringVec", TypeKind::TypeAlias)
.extends(TypeName::application(TypeName::primitive("std::vector"), vec![TypeArgument::Single(TypeName::primitive("std::string"))]))
.build()
.unwrap();
}
using StringVec = std::vector<std::string>;
Enum class
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.doc("Available colors.")
.add_variant(EnumVariantSpec::new("Red").unwrap())
.add_variant(EnumVariantSpec::new("Green").unwrap())
.add_variant(EnumVariantSpec::new("Blue").unwrap())
.build()
.unwrap();
}
/// Available colors.
enum class Color {
Red,
Green,
Blue
};
Virtual method
C++ abstract classes with pure virtual methods require the extra_member escape hatch. Use FunSpec::emit() to render each method signature as a CodeBlock, then attach it to the type via extra_member.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::cpp::Cpp;
fn main() {
fn emit_fun(fun: &FunSpec) -> CodeBlock {
let lang = Cpp::new();
fun.emit(&lang, DeclarationContext::Member).unwrap()
}
let mut pub_section = CodeBlock::builder();
pub_section.add("%<", ());
pub_section.add("public:", ());
pub_section.add_line();
pub_section.add("%>", ());
pub_section.add_code(emit_fun(
&FunSpec::builder("area")
.is_abstract()
.returns(TypeName::primitive("double"))
.suffix("const")
.suffix("= 0")
.build()
.unwrap(),
));
pub_section.add_line();
pub_section.add_code(emit_fun(
&FunSpec::builder("~Shape")
.is_abstract()
.suffix("= default")
.build()
.unwrap(),
));
let type_spec = TypeSpec::builder("Shape", TypeKind::Class)
.doc("Abstract shape base class.")
.extra_member(pub_section.build().unwrap())
.build()
.unwrap();
}
/// Abstract shape base class.
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
Namespace wrapping
Use FileSpec::add_raw to wrap generated code in a namespace block.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let mut b = CodeBlock::builder();
b.add("int square(int x) {", ());
b.add_line();
b.add("%>", ());
b.add("return x * x;", ());
b.add_line();
b.add("%<", ());
b.add("}", ());
b.add_line();
let block = b.build().unwrap();
let file = FileSpec::builder("math.hpp")
.header(CodeBlock::of("#pragma once", ()).unwrap())
.add_raw("namespace math {\n")
.add_code(block)
.add_raw("\n} // namespace math\n")
.build()
.unwrap();
let output = file.render(80).unwrap();
}
#pragma once
namespace math {
int square(int x) {
return x * x;
}
} // namespace math
C Cookbook
Practical, copy-paste-ready recipes for C code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Typedef
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Meters", TypeKind::TypeAlias)
.extends(TypeName::primitive("double"))
.build()
.unwrap();
}
typedef double Meters;
Struct with fields
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Config", TypeKind::Struct)
.doc("Application configuration.")
.add_field(
FieldSpec::builder("timeout", TypeName::primitive("int"))
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("name", TypeName::primitive("char*"))
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("verbose", TypeName::primitive("int"))
.build()
.unwrap(),
)
.build()
.unwrap();
let file = FileSpec::builder("config.h")
.header(CodeBlock::of("#pragma once", ()).unwrap())
.add_type(type_spec)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
#pragma once
/* Application configuration. */
struct Config {
int timeout;
char* name;
int verbose;
};
Function declaration
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let fun = FunSpec::builder("process")
.add_param(ParameterSpec::new("data", TypeName::primitive("const char*")).unwrap())
.add_param(ParameterSpec::new("len", TypeName::primitive("size_t")).unwrap())
.returns(TypeName::primitive("int"))
.build()
.unwrap();
let file = FileSpec::builder("api.h")
.add_function(fun)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
int process(const char* data, size_t len);
Enum
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Direction", TypeKind::Enum)
.doc("Cardinal directions.")
.add_variant(EnumVariantSpec::new("UP").unwrap())
.add_variant(EnumVariantSpec::new("DOWN").unwrap())
.add_variant(EnumVariantSpec::new("LEFT").unwrap())
.add_variant(EnumVariantSpec::new("RIGHT").unwrap())
.build()
.unwrap();
}
/* Cardinal directions. */
enum Direction {
UP,
DOWN,
LEFT,
RIGHT
};
C# Cookbook
Practical, copy-paste-ready recipes for C# code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Class with XML doc
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::csharp::CSharp;
fn main() {
let body = CodeBlock::of("return $\"Hello, {name}!\";", ()).unwrap();
let ts = TypeSpec::builder("Greeter", TypeKind::Class)
.visibility(Visibility::Public)
.add_method(
FunSpec::builder("Greet")
.visibility(Visibility::Public)
.returns(TypeName::primitive("string"))
.add_param(ParameterSpec::new("name", TypeName::primitive("string")).unwrap())
.doc("<summary>\nGreets a user by name.\n</summary>\n<param name=\"name\">The name to greet.</param>\n<returns>A greeting string.</returns>")
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
let file = FileSpec::builder_with("Greeter.cs", CSharp::new())
.add_type(ts)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
public class Greeter {
/// <summary>
/// Greets a user by name.
/// </summary>
/// <param name="name">The name to greet.</param>
/// <returns>A greeting string.</returns>
public string Greet(string name) {
return $"Hello, {name}!";
}
}
Interface
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::csharp::CSharp;
fn main() {
let ts = TypeSpec::builder("IRepository", TypeKind::Interface)
.visibility(Visibility::Public)
.add_generic_param(GenericParamSpec::single("T").unwrap())
.doc("Generic data repository.")
.add_method(
FunSpec::builder("FindById")
.returns(TypeName::primitive("T"))
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("Save")
.returns(TypeName::primitive("void"))
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
let file = FileSpec::builder_with("IRepository.cs", CSharp::new())
.add_type(ts)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
/// Generic data repository.
public interface IRepository<T> {
internal T FindById(string id);
internal void Save(T entity);
}
Enum
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::csharp::CSharp;
fn main() {
let ts = TypeSpec::builder("Direction", TypeKind::Enum)
.visibility(Visibility::Public)
.add_variant(EnumVariantSpec::new("North").unwrap())
.add_variant(
EnumVariantSpec::builder("South")
.discriminant(CodeBlock::of("1", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("East")
.discriminant(CodeBlock::of("2", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("West")
.discriminant(CodeBlock::of("3", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
let file = FileSpec::builder_with("Direction.cs", CSharp::new())
.add_type(ts)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
public enum Direction {
North,
South = 1,
East = 2,
West = 3
}
Async method with imports
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::csharp::CSharp;
fn main() {
let task_user = TypeName::importable("System.Threading.Tasks", "Task<User>");
let user = TypeName::importable("MyApp.Models", "User");
let body = CodeBlock::of("return await repo.GetByIdAsync(id);", ()).unwrap();
let fun = FunSpec::builder("GetUserAsync")
.visibility(Visibility::Public)
.is_async()
.returns(task_user)
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.body(body)
.build()
.unwrap();
let ts = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
.add_method(fun)
.build()
.unwrap();
let file = FileSpec::builder_with("UserService.cs", CSharp::new())
.add_type(ts)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
using System.Threading.Tasks;
using MyApp.Models;
public class UserService {
public async Task<User> GetUserAsync(string id) {
return await repo.GetByIdAsync(id);
}
}
Lua Cookbook
Practical, copy-paste-ready recipes for Lua code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Lua is an end-delimited language (end instead of }). sigil-stitch handles this via close_on_transition: false in its block syntax config – you get correct if/elseif/else ... end without spurious end before else. Since Lua has no type system, you’ll mostly use CodeBlock directly and sigil_quote! rather than TypeSpec.
Function
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::lua::Lua;
fn main() {
let body = sigil_quote!(Lua {
function greet(name) {
return "Hello, "..name
}
}).unwrap();
let file = FileSpec::builder_with("greeter.lua", Lua::new())
.add_code(body)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
function greet(name)
return "Hello, "..name
end
Module with require
Use TypeName::importable to track Lua require() imports. The module path is converted to a slash-separated path in the require() call.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::lua::Lua;
fn main() {
let json = TypeName::importable("dkjson", "json");
let inspect = TypeName::importable("inspect", "inspect");
let mut cb = CodeBlock::builder();
cb.add_statement("-- %T %T", (json, inspect));
let block = cb.build().unwrap();
let file = FileSpec::builder_with("app.lua", Lua::new())
.add_code(block)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
local json = require("dkjson");
local inspect = require("inspect");
-- json inspect
Control flow with sigil_quote!
sigil_quote! supports if/elseif/else, for/do, and while/do blocks. Use { and } in the macro to delimit bodies – they render as indented blocks closed by end.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::lua::Lua;
fn main() {
let block = sigil_quote!(Lua {
if x > 0 then {
return $S("positive")
} elseif x < 0 then {
return $S("negative")
} else {
return $S("zero")
}
}).unwrap();
let file = FileSpec::builder_with("classify.lua", Lua::new())
.add_code(block)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
if x > 0 then
return "positive"
elseif x < 0 then
return "negative"
else
return "zero"
end
Table constructor with sigil_quote!
Braces after = or in assignments are recognized as table constructors (not control flow). No end is emitted – the braces render as literal {...}.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::lua::Lua;
fn main() {
let block = sigil_quote!(Lua {
local user = {
name = $S("Bob"),
age = 42,
}
print(user.name)
}).unwrap();
let file = FileSpec::builder_with("user.lua", Lua::new())
.add_code(block)
.build()
.unwrap();
let output = file.render(80).unwrap();
}
local user = {name = "Bob", age = 42,}
print(user.name)
Ruby Cookbook
Classes and Modules
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(Ruby {
class Greeter {
attr_reader :name
def initialize(name) {
@name = name
}
def greet {
$V("Hello, #{@name}!")
}
}
})?;
Ok(())
}
Key points:
- Ruby uses
{ }blocks insigil_quote!— the Ruby backend translates them todo/endor indent/dedent as appropriate. - Symbol literals like
:nameget correct spacing (space before:, none after). - Inheritance uses
<with space before it:class Dog < Animal. $Vpasses strings through for Ruby interpolation (#{...}).
PHP Cookbook
Classes and Methods
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
sigil_quote!(Php {
class Calculator {
public function add(int $$a, int $$b): int {
return $$a + $$b;
}
}
})?;
Ok(())
}
Key points:
- PHP uses
?Typefor nullable type declarations (?string,?User). - PHP does not use
<>for generics — the tokenizer correctly treats<as comparison. $in PHP variable names must be escaped as$$in templates:$$aproduces$a.
Scala Cookbook
Practical, copy-paste-ready recipes for Scala code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Case class
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("User", TypeKind::Struct)
.doc("A user case class.")
.add_primary_constructor_param(
ParameterSpec::new("name", TypeName::primitive("String")).unwrap(),
)
.add_primary_constructor_param(
ParameterSpec::new("age", TypeName::primitive("Int")).unwrap(),
)
.add_primary_constructor_param(
ParameterSpec::new("email", TypeName::primitive("String")).unwrap(),
)
.build()
.unwrap();
}
/**
* A user case class.
*/
case class User(name: String, age: Int, email: String) {
}
Trait with type parameter
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Repository", TypeKind::Trait)
.add_generic_param(GenericParamSpec::single("T").unwrap())
.doc("Generic data repository.")
.add_method(
FunSpec::builder("findById")
.returns(TypeName::primitive("Option[T]"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
}
/**
* Generic data repository.
*/
trait Repository[T] {
def findById(id: String): Option[T]
def save(entity: T)
}
Enum
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
fn main() {
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.doc("Supported colors.")
.add_variant(EnumVariantSpec::new("Red").unwrap())
.add_variant(EnumVariantSpec::new("Green").unwrap())
.add_variant(EnumVariantSpec::new("Blue").unwrap())
.build()
.unwrap();
}
/**
* Supported colors.
*/
enum Color {
Red,
Green,
Blue
}
Bounded type parameter
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("if (a.compareTo(b) >= 0) a else b", ()).unwrap();
let fun = FunSpec::builder("max")
.add_generic_param(
GenericParamSpec::single("T").unwrap().with_bound(TypeName::primitive("Comparable[T]")).unwrap(),
)
.returns(TypeName::primitive("T"))
.add_param(ParameterSpec::new("a", TypeName::primitive("T")).unwrap())
.add_param(ParameterSpec::new("b", TypeName::primitive("T")).unwrap())
.body(body)
.build()
.unwrap();
}
def max[T <: Comparable[T]](a: T, b: T): T = {
if (a.compareTo(b) >= 0) a else b
}
Newtype
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.extends(TypeName::primitive("Double"))
.build()
.unwrap();
}
class Meters(val value: Double)
Haskell Cookbook
Practical, copy-paste-ready recipes for Haskell code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Data record with deriving
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Person", TypeKind::Struct)
.add_field(
FieldSpec::builder("personName", TypeName::primitive("String")).build().unwrap(),
)
.add_field(
FieldSpec::builder("personAge", TypeName::primitive("Int")).build().unwrap(),
)
.implements(TypeName::primitive("Show"))
.implements(TypeName::primitive("Eq"))
.build()
.unwrap();
}
data Person =
Person {
personName :: String,
personAge :: Int,
}
deriving (Show, Eq)
Type class
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Printable", TypeKind::Trait)
.doc("Things that can be printed.")
.add_method(
FunSpec::builder("prettyPrint")
.add_param(ParameterSpec::new("a", TypeName::primitive("a")).unwrap())
.returns(TypeName::primitive("String"))
.build()
.unwrap(),
)
.build()
.unwrap();
}
-- | Things that can be printed.
class Printable where
prettyPrint :: a -> String
Function with split signature
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("x + y", ()).unwrap();
let fun = FunSpec::builder("add")
.add_param(ParameterSpec::new("x", TypeName::primitive("Int")).unwrap())
.add_param(ParameterSpec::new("y", TypeName::primitive("Int")).unwrap())
.returns(TypeName::primitive("Int"))
.body(body)
.build()
.unwrap();
}
add :: Int -> Int -> Int
add x y =
x + y
Newtype
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.extends(TypeName::primitive("Int"))
.build()
.unwrap();
}
newtype Meters = Meters Int
Type alias
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("Name", TypeKind::TypeAlias)
.extends(TypeName::primitive("String"))
.build()
.unwrap();
}
type Name = String
OCaml Cookbook
Practical, copy-paste-ready recipes for OCaml code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Record type
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("person", TypeKind::Struct)
.doc("A person record.")
.add_field(
FieldSpec::builder("name", TypeName::primitive("string")).build().unwrap(),
)
.add_field(
FieldSpec::builder("age", TypeName::primitive("int")).build().unwrap(),
)
.add_field(
FieldSpec::builder("email", TypeName::primitive("string")).build().unwrap(),
)
.build()
.unwrap();
}
(** A person record. *)
type person =
{
name : string;
age : int;
email : string;
}
Function with curried params
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let body = CodeBlock::of("List.map f xs", ()).unwrap();
let fun = FunSpec::builder("transform")
.add_param(ParameterSpec::new("f", TypeName::primitive("'a -> 'b")).unwrap())
.add_param(ParameterSpec::new("xs", TypeName::primitive("'a list")).unwrap())
.returns(TypeName::primitive("'b list"))
.body(body)
.build()
.unwrap();
}
let transform (f : 'a -> 'b) (xs : 'a list) : 'b list =
List.map f xs
Module block
OCaml modules are structurally different from types – they can contain multiple types and values. Use the OCaml::module_block helper to build a module Name = struct ... end block as a raw CodeBlock.
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::lang::ocaml::OCaml;
use sigil_stitch::prelude::*;
fn main() {
let mut inner = CodeBlock::builder();
inner.add_statement("let greeting = \"hello\"", ());
inner.add_statement("let farewell = \"goodbye\"", ());
let body = inner.build().unwrap();
let module = OCaml::module_block("MyModule", body).unwrap();
}
module MyModule = struct
let greeting = "hello"
let farewell = "goodbye"
end
Type alias
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let type_spec = TypeSpec::builder("string_list", TypeKind::TypeAlias)
.extends(TypeName::primitive("string list"))
.build()
.unwrap();
}
type string_list = string list
Pattern match
Pattern matching is built using CodeBlock control-flow methods. Use begin_control_flow for the outer binding and for the match expression — the OCaml backend’s Match block intent automatically suppresses the block opener for match ... with.
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let mut b = CodeBlock::builder();
b.begin_control_flow("let describe color", ());
b.begin_control_flow("match color with", ());
b.add("| Red -> \"red\"", ());
b.add_line();
b.add("| Green -> \"green\"", ());
b.add_line();
b.add("| Blue -> \"blue\"", ());
b.add_line();
b.end_control_flow();
b.end_control_flow();
let block = b.build().unwrap();
}
let describe color =
match color with
| Red -> "red"
| Green -> "green"
| Blue -> "blue"
Shell (Bash/Zsh) Cookbook
Practical, copy-paste-ready recipes for Bash and Zsh script generation. Covers sigil_quote! with shell-aware control flow, $V verbatim strings for preserving shell interpolation, and the builder API.
Basic function with sigil_quote!
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let body = sigil_quote!(Bash {
local name=$$1
echo $V("\"Hello, ${name}!\"")
}).unwrap();
let fun = FunSpec::builder("greet")
.body(body)
.build()
.unwrap();
let output = FileSpec::builder("greet.bash")
.add_function(fun)
.build()
.unwrap()
.render(80)
.unwrap();
}
function greet() {
local name=$1
echo "Hello, ${name}!"
}
Control flow (if/then/fi, for/do/done)
Use { } blocks in sigil_quote! — the backend maps them to the correct shell delimiters.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let body = sigil_quote!(Bash {
if [ -z $$1 ]; {
echo $S("Error: no argument")
return 1
}
for file in $$@; {
echo $V("\"Processing: ${file}\"")
}
}).unwrap();
let fun = FunSpec::builder("process_files")
.body(body)
.build()
.unwrap();
let output = FileSpec::builder("process.bash")
.add_function(fun)
.build()
.unwrap()
.render(80)
.unwrap();
}
function process_files() {
if [ -z $1 ]; then
echo "Error: no argument"
return 1
fi
for file in $@; do
echo "Processing: ${file}"
done
}
$V vs $S — when to use which
$S escapes everything and wraps in quotes (safe for static strings). $V is pure passthrough — no quoting, no escaping. Use $V when you want shell to expand variables, command substitutions, or arithmetic at runtime. Include your own quotes in the $V content when shell quoting is needed.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let block = sigil_quote!(Bash {
echo $S("$HOME")
echo $V("$HOME")
echo $V("\"$HOME\"")
}).unwrap();
let output = FileSpec::builder("test.bash")
.add_code(block)
.build()
.unwrap()
.render(80)
.unwrap();
// Line 1: echo "\$HOME" ← $S escapes the dollar sign, wraps in quotes
// Line 2: echo $HOME ← $V passthrough, no quotes (word-splitting possible)
// Line 3: echo "$HOME" ← $V passthrough with user-provided quotes (safe)
}
Complex shell interpolation with $V
$V handles all shell expansion patterns — braced defaults, command substitution, arithmetic, arrays, special variables. Since $V is passthrough, include quotes in the content when the generated shell code should have them:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let body = sigil_quote!(Bash {
local config_dir=$V("\"${XDG_CONFIG_HOME:-$HOME/.config}\"")
local version=$V("\"$(git describe --tags 2>/dev/null || echo dev)\"")
local port=$V("\"$((BASE_PORT + WORKER_ID))\"")
echo $V("\"Deploying ${APP_NAME} v${version}\"")
echo $V("\"Config: ${config_dir}/${APP_NAME}.conf\"")
echo $V("\"Status: exit=$? pid=$$\"")
echo $V("\"Args: count=$# all=$@\"")
echo $V("\"Array: ${services[@]}\"")
}).unwrap();
let fun = FunSpec::builder("setup")
.body(body)
.build()
.unwrap();
let output = FileSpec::builder("setup.bash")
.add_function(fun)
.build()
.unwrap()
.render(80)
.unwrap();
}
function setup() {
local config_dir="${XDG_CONFIG_HOME:-$HOME/.config}"
local version="$(git describe --tags 2>/dev/null || echo dev)"
local port="$((BASE_PORT + WORKER_ID))"
echo "Deploying ${APP_NAME} v${version}"
echo "Config: ${config_dir}/${APP_NAME}.conf"
echo "Status: exit=$? pid=$$"
echo "Args: count=$# all=$@"
echo "Array: ${services[@]}"
}
@{expr} interpolation in $V
When you need to mix Rust compile-time values with shell runtime variables, use @{expr} inside $V strings. The @{...} parts are evaluated at compile time; everything else passes through verbatim for shell to interpret at runtime:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let registry = "ghcr.io/myorg";
let app = "api-server";
let services = ["web", "worker", "scheduler"];
let block = sigil_quote!(Bash {
docker push $V("@{registry}/@{app}:${TAG}")
echo $V("Deploying @{services.len()} services to ${ENVIRONMENT}")
echo $V("Contact: admin@@@{app}.internal")
}).unwrap();
}
docker push ghcr.io/myorg/api-server:${TAG}
echo Deploying 3 services to ${ENVIRONMENT}
echo Contact: admin@api-server.internal
Use @@ to emit a literal @ in the output. Bare @ not followed by { passes through unchanged.
Shebang and header
Use FileSpec::header() for the shebang and preamble:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let header = CodeBlock::of("#!/usr/bin/env bash\nset -euo pipefail", ()).unwrap();
let body = sigil_quote!(Bash {
echo $S("Starting...")
}).unwrap();
let main_fn = FunSpec::builder("main")
.body(body)
.build()
.unwrap();
let output = FileSpec::builder_with("script.bash", Bash::new())
.header(header)
.add_function(main_fn)
.build()
.unwrap()
.render(80)
.unwrap();
}
#!/usr/bin/env bash
set -euo pipefail
function main() {
echo "Starting..."
}
Imports (source)
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let body = CodeBlock::of("# uses imported helpers", ()).unwrap();
let output = FileSpec::builder_with("app.bash", Bash::new())
.add_import(ImportSpec::side_effect("./lib/utils.sh"))
.add_import(ImportSpec::side_effect("./lib/config.sh"))
.add_code(body)
.build()
.unwrap()
.render(80)
.unwrap();
// Generates:
// source "./lib/config.sh"
// source "./lib/utils.sh"
}
Zsh-specific features
Zsh works identically to Bash for control flow. Use $V for Zsh-specific parameter expansion:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::zsh::Zsh;
fn main() {
let body = sigil_quote!(Zsh {
local lower=$V("\"${(L)USERNAME}\"")
local joined=$V("\"${(j:,:)array}\"")
local sliced=$V("\"${array[2,-1]}\"")
local replaced=$V("\"${input//old/new}\"")
}).unwrap();
let fun = FunSpec::builder("zsh_features")
.body(body)
.build()
.unwrap();
let output = FileSpec::builder_with("demo.zsh", Zsh::new())
.add_function(fun)
.build()
.unwrap()
.render(80)
.unwrap();
}
function zsh_features() {
local lower="${(L)USERNAME}"
local joined="${(j:,:)array}"
local sliced="${array[2,-1]}"
local replaced="${input//old/new}"
}
Double-bracket tests with [[ ]]
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let body = sigil_quote!(Bash {
if [[ $$1 == $$2 ]]; {
echo $S("match")
}
}).unwrap();
let fun = FunSpec::builder("check_equal")
.body(body)
.build()
.unwrap();
let output = FileSpec::builder("check.bash")
.add_function(fun)
.build()
.unwrap()
.render(80)
.unwrap();
}
function check_equal() {
if [[ $1 == $2 ]]; then
echo "match"
fi
}
Combining $V with runtime Rust values
Mix $V (shell-expanded at runtime) with $L/$S (Rust values baked in at generation time):
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let app_name = "myapp";
let log_dir = "/var/log";
// Build the whole string in Rust and pass via $V (most ergonomic):
let log_pattern = format!("\"${{LOG_DIR:-{log_dir}}}/{app_name}.log\"");
let body = sigil_quote!(Bash {
local log_file=$V(log_pattern)
echo $V("\"Writing to ${log_file}\"")
}).unwrap();
}
File extension
Use .with_extension("sh") for POSIX-compatible scripts:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let bash = Bash::new().with_extension("sh");
let output = FileSpec::builder_with("script.sh", bash)
.build()
.unwrap()
.render(80)
.unwrap();
}
Architecture
This chapter describes how sigil-stitch carries declaration intent to source text. It covers ownership, the materialization and rendering pipeline, and import resolution.
The type-declaration, function, field, property, and enum-variant lowering seams described here are implemented for every built-in language. External adapters that retain permissive capabilities may still use frozen pre-0.6.8 compatibility lowerers. See Declaration Specs and Language Lowering for the ownership decision and 0.6.8 Legacy Compatibility and Migration for the versioned compatibility contract.
The type-name-lowering pass, language-owned declaration-generic grammar, complete-set fallible import resolver, and direct renderer-event methods described here are implemented. The compatibility appendix records the frozen shared renderer grammar used only by 0.6.8 bridges.
Pipeline and Ownership
Declaration specs + TypeName + opaque CodeBlock payloads
|
+-- intrinsic validation
+-- target capability validation
|
v
target-language adapter
owns complete declaration lowering
|
v
CodeBlock tree
|
+-- rewrite each source tree exactly once
+-- validate rewritten structure
+-- fallible language-owned TypeName lowering
+-- validate lowered type blocks
+-- collect and resolve imports
+-- final render with no rewrite or type lowering
|
v
source text
The important seam is between declaration intent and target grammar. Specs own
the former; a language adapter owns the latter. CodeBlock is the shared
structured source container passed through source rewrite, type-name lowering,
import resolution, and final rendering. A block is associated with its selected
target and is not a portable cross-language program.
Language Interfaces
src/lang/mod.rs defines two traits:
RendererLangis the renderer-only interface used bycode_renderer.rs. It covers file extensions, string literals, block rendering, one complete falliblelower_type_name()seam, and other stable final-rendering policy. Implementing it is sufficient for directCodeBlockrendering. Built-in adapters lower completeTypeNamevalues to non-emptyCodeBlocks; the provided default retains only frozen pre-0.6.8 compatibility behavior.CodeLang: RendererLangadds declaration representability, lowering, imports, and spec-level documentation. A complete type crossesvalidate_type()/collect_type_validation_errors()and thenlower_type(ValidatedType) -> Vec<CodeBlock>. The result contains one or more non-empty blocks; an empty vector or block fails closed. The validated view contains crate-validated child wrappers, so the type adapter owns the declaration’s preamble, header, relationships, body order, primary constructor, close, and output cardinality while reusing complete child lowerers for child grammar. Closed sums use a separate complete-declaration seam:validate_closed_sum()andlower_closed_sum(ValidatedClosedSum). The dedicated view tells the adapter that the declaration is a closed sum; the adapter validates its complete case set and chooses one declaration, nested case declarations, or several sibling blocks. There is no shared nesting or sealed-syntax interface. After crate-owned validation against the selected adapter,validate_function()may add target-local checks to a classifiedFunctionIntent. sigil-stitch then constructs aValidatedFunction;lower_function()accepts that validated read-only view and returns a structuredCodeBlock. A strict adapter that advertises a function profile but omits this operation fails withMissingFunctionLowerer; only permissive pre-0.6.8 adapters receive the frozen shared-grammar default. Fields follow the same pattern at sequence granularity:validate_fields()receivesFieldSequenceIntent,collect_field_validation_errors()preserves independent sibling failures, andlower_fields()receivesValidatedFields. Properties usePropertyIntentwith a direct-or-owner-awarePropertyContext;collect_property_validation_errors()preserves independent failures andlower_property()receives a crate-constructedValidatedProperty. The adapter decides whether one property becomes separate accessor declarations, a computed-property body, or target-local methods. After every member family has been checked, one validation-onlyTypeMembersIntentcontaining the owner’s semantic fields, properties, and explicit methods passes throughvalidate_type_members()and its additive collector. This seam handles cross-family relationships and has no lowering counterpart. Enum variants likewise use a complete sequence:validate_variants()sees the owning declaration, complete orderedVariantIntent, and whether non-variant members exist; adapters with independent per-variant checks implement the additivecollect_variant_validation_errors()seam.lower_variants()receivesValidatedVariantsand owns preambles, payload spelling, separators, and section termination. Callers do not assemble target declaration grammar from fragments, and adapters cannot construct or bypass the validated wrappers.
Each supported language implements both traits in its own module
(src/lang/typescript.rs, etc.). Control-flow nodes carry a language-neutral
BlockIntent; each adapter maps that intent locally through
render_block_open(), render_block_close(), and
render_branch_transition(). The renderer calls those complete events
directly. Intent-aware and legacy block hooks remain only behind the provided
0.6.8 compatibility defaults. Languages can
implement rewrite_nodes() for structural or literal fixups such as Go IIFE
}() fusion or C++ lambda }; semicolons. The core invokes this existing
source-tree seam once per source block after declaration lowering and before
type-name lowering. It validates the rewritten structure before continuing.
Type-name-lowering results and raw import metadata are not rewritten.
Deprecated grammar and type-presentation accessors remain only at compatibility boundaries for external adapters and direct compatibility facades. Built-in type and function lowerers spell type parameters, bounds, lifetimes, kinds, context bounds, and explicit constraint clauses locally; shared helpers may merge semantic constraint values but select no tokens or placement. New adapters and new syntax dimensions use language-owned lowering. The complete inventory and migration replacements are in 0.6.8 Legacy Compatibility and Migration.
At the macro level, the MacroLang enum (macros/src/parse/lang.rs) provides compile-time language-aware tokenizer annotations. Languages like Bash, Zsh, Go, and Haskell get specialized spacing rules in sigil_quote! without runtime overhead. See Language-Aware Tokenizer.
Public container types have no language generic parameter. The language enters
as &dyn RendererLang for direct rendering or &dyn CodeLang for declaration
materialization. FileSpec stores a Box<dyn CodeLang> internally. A
CodeBlock can nevertheless contain target-specific literal text; language
independence of its Rust type is not a promise that every block is portable.
Macro Front End
sigil_quote! has a private typed pipeline before the public CodeBlock layer:
macro tokens
-> parse::parse_input
-> private FormattedCode / QuoteArg / Statement parse forms
-> infallible codegen
-> caller-scope CodeBlockBuilder calls
Rust-bearing values cross the parser boundary as syn::Expr, syn::Pat, or
syn::Local; codegen quotes those nodes directly and never reparses token
strings. A FormattedCode privately couples each target format string to its
typed arguments, deriving the format specifier from the argument variant so
the two cannot drift apart.
Parsing returns syn::Error. Independent failures are combined while recovery
can advance to a reliable sibling statement, interpolation group, or loop
option boundary. No partial parse model reaches codegen. Direct ordinary and raw string
literals use syn::LitStr decoding. A single-pass lexical boundary scan skips
Rust strings, characters, nested comments, and nested braces before each
@{...} body is parsed once as a Rust expression; dynamic string expressions
are not scanned.
Generated parsed blocks and splices use nested builders. Their runtime failures
flow into a local first-error slot rather than unwrap. Flat guarded lowering
skips later work after a helper failure, introducing a scoped continuation only
when a subsequent $let must remain visible to later statements. Caller ?,
return, break, and continue targets remain unchanged. A validation
pass limits these guarded $let continuations to 128 levels so pathological
input fails with a macro diagnostic instead of exhausting rustc while parsing
the generated nesting. The public CodeBlock, error, and rendering contracts
are unaffected.
Semantic Type References: TypeName
src/type_name.rs defines type references. Key variants:
| Variant | Example | Import Tracked? |
|---|---|---|
Primitive | string, i32 | No |
Importable | User from ./models | Yes |
Generic | Promise<User> | Recursively |
Parameter | A supplied binder reference such as n | No |
Application | A supplied constructor/operator with scalar or expanded arguments | Base and complete argument patterns tracked |
Callable | Required/optional slots and repeated or expanded segments | Every contained type tracked |
Array | User[], Vec<User> | Inner type tracked |
ReadonlyArray | readonly User[] | Inner type tracked |
Optional | User?, Option<User> | Inner type tracked |
Union | string | number | All members tracked |
Intersection | A & B, A + B | All members tracked |
Tuple | [A, B], (A, B) | All members tracked |
Reference | &T, const T& | Inner type tracked |
Function | (x: string) => void | Params + return tracked |
Map | Map<string, User> | Key + value tracked |
Pointer / Slice | *const T, &[T] | Inner type tracked |
StringLiteral | 'active', Literal["active"] | Target-derived imports tracked after lowering |
Raw | any string | No |
Every variant that contains other types remains structured until the selected
adapter lowers the complete root. The lowering result retains importable leaf
references in its CodeBlock, so ordinary nested imports and target-derived
imports are collected together before alias resolution.
Declarations store generic bindings in one private ordered list and expose
one borrowed GenericParamView sequence. Modern domains and kinds remain
complete through native validation, constraint merging, and lowering. Legacy
kind metadata remains separate from modern KindExpr; frozen compatibility
lowerers reject domains they cannot represent. Application and callable
expressions do not introduce inference, pack evaluation, or a shared target
grammar. Each language preserves the concrete request or returns
SigilStitchError.
Type-Name Lowering
TypeName variants are semantic: Array(T) means an array type and
StringLiteral(value) means one decoded string singleton. They do not select a
shared prefix, delimiter, precedence, or fallback. RendererLang owns one
fallible lower_type_name(&TypeName) -> Result<CodeBlock, _> method.
The crate validates the semantic value before the call and validates the
returned block afterward. Successful blocks are non-empty, contain only type
expression structure, and leave no unresolved compound TypeName. Unsupported
forms fail before import collection instead of inheriting TypeScript-like
defaults or widening to a primitive. See TypeName Validation and
Lowering for the complete contract.
Structured Source Container: CodeBlock
A CodeBlock stores nodes: Vec<CodeNode> — a tree of self-contained nodes (Literal, TypeRef, NameRef, StringLit, Comment, Nested, etc.). Format strings are parsed at build time and immediately converted to CodeNode nodes. Each node is self-contained: TypeRef(TypeName) carries its type reference directly, and control-flow nodes carry a language-neutral BlockIntent (BlockOpenIntent, BlockCloseIntent, BranchCloseIntent) with no per-language rendering policy.
CodeBlocks are immutable after construction. The builder (CodeBlockBuilder) validates argument counts and indent balance before producing a block.
Declaration Specs
src/spec/ contains builders for target-independent declaration intent.
TypeSpec, FunSpec, FieldSpec, and related types record what the caller
wants to declare. They are a semantic superset: target capability validation
may reject intent that one language cannot represent.
Specs enforce intrinsic coherence, select declaration context, and delegate
target representability and lowering. They do not own keyword spelling, token
order, separators, type-parameter placement, or other target grammar. The
language adapter returns CodeBlock, never a type-bearing raw string, so
semantic TypeName references survive import collection and alias resolution.
An enum is lowered as one owner-aware variant sequence. VariantIntent
contains the owner name and kind, all variants in declaration order, whether
non-variant members exist, the accepted arity ranges of structured
constructors, and whether opaque members may provide target-specific
constructor syntax. The type lowerer chooses where the sequence appears. A
language profile distinguishes discriminants, enum-entry constructor
arguments, positional payloads, record payloads, and attributes.
VariantContext is only
the deprecated positional input to the permissive external-adapter
compatibility path; strict built-ins reject ownerless direct emission because
caller-supplied first/last flags cannot prove valid separators or section
termination.
A closed sum is a sibling declaration family to TypeSpec; it reuses the
unit, positional-payload, and record-payload shapes without reusing ordinary
enum storage or lowering. ClosedSumCapabilityProfile opts a target into
validation and lowering. Closed-sum case validation is separate from ordinary
enum-entry profiles: accepting a record case for a Java sealed hierarchy must
not make record payloads valid on an ordinary Java enum.
The case sequence may be empty. That declaration is the empty sum, a named
uninhabited type. It is not unit or void. A Never reference or bottom type
may also have no values, but it belongs to type-expression and subtype
semantics rather than declaring this caller-named case set. The shared model
therefore does not identify the two. A target may reuse a canonical empty type
only when doing so exactly preserves the requested declaration, including its
name and valid use positions; otherwise it rejects the empty shape even when
it supports non-empty closed sums.
For non-empty sums, Case(T) means a generated named case carrying T; it
does not enroll an existing T declaration as a nominal subtype. Java may
therefore lower cases inside one public sealed root, while Kotlin may use a
private-constructor sealed root with nested cases to close the hierarchy within
the generated module. Those choices remain language-local and do not justify
a shared nested-declaration model. Wire discriminators, serialization tags,
and identifier derivation remain caller or annotation concerns.
Fields are lowered as one FieldSequenceIntent. Its FieldContext
distinguishes direct emission, ordinary type members, ordinary variant record
payloads, and closed-sum case record payloads without carrying punctuation or
a new placement policy. Keeping the two payload contexts separate prevents a
sealed-hierarchy representation from widening ordinary enum behavior. The
Direct(DeclarationContext) payload preserves only the pre-0.6.8 direct-field
placement input as a narrow compatibility exception; it is not a reusable
target-grammar abstraction. Field capability profiles declare which semantic
facts each context supports or requires.
Intrinsic checks run even when the owning type or payload form is unsupported,
so malformed serialized fields still participate in aggregate validation.
Adapter-local collection then validates identifiers, emitted-name collisions,
modifier combinations, annotations, tags, and other target rules. Only the
crate can construct ValidatedFields, and only after the complete sequence has
passed every phase.
FieldCapability::OptionalPresence means that the containing value may omit a
field. TypeName::Optional(T) means that a present field can carry an option or
null value. Keeping those semantics separate prevents an adapter from silently
turning absence into nullability. Built-in adapters accept optional presence
only where the target representation preserves it.
A computed property is lowered as one PropertyIntent. Its PropertyContext
distinguishes the pre-0.6.8 direct facade from a member owned by a complete type
declaration. Property profiles declare support and requirements for explicit
types, read access, write access, attributes, and static behavior. Intrinsic
validation requires at least one accessor and rejects empty bodies, empty
setter names, and unrelated deserialized modifiers. Adapter-local validation
owns identifier, visibility, accessor-combination, and other target rules.
Only the crate can construct ValidatedProperty, and only after every phase
succeeds.
Owner-wide validation is a separate concern from property lowering.
TypeMembersIntent exposes one type’s name and kind plus its semantic fields,
properties, and explicit methods after the per-family checks have run. The
crate rejects exact duplicate property names; an adapter uses
collect_type_members_validation_errors() for relationships created by its
own lowering. PHP checks the case-insensitive method namespace that contains
derived property accessors and explicit methods. TypeScript, Kotlin, Swift,
and Scala reject field/property names that their lowering maps into the same
target-local namespace; TypeScript private names and the TypeScript and Swift
static namespaces remain distinct. TypeScript, Swift, and Scala also reject
corresponding explicit-method collisions within the same namespace. These
rules remain language-local because the namespaces and derived names differ.
This intent contains no placement or syntax data, has no validated wrapper,
and never enters the materialization pipeline.
The intended declaration path is:
TypeSpec / FunSpec
|
+-- intrinsic validation
+-- language capability validation
|
v
CodeLang complete declaration lowering
|
v
CodeBlock with TypeRef nodes
|
v
source rewrite -> validate rewritten tree -> lower TypeRefs
|
v
collect imports -> resolve aliases -> final CodeRenderer -> source text
Raw bodies, annotations, suffixes, and file fragments are explicit escape
hatches. They may contain target-specific syntax, but remain opaque to generic
specs and shared lowerers; their existence does not move ownership of the
surrounding declaration grammar into the spec. A private Python validator
recognizes the documented 0.6.8 is_static plus decorator pattern solely as a
frozen adapter-local compatibility exception. New semantics must not extend
that recognizer or add a shared syntax hook.
File Rendering Pipeline
FileSpec::render(width) owns one ordered preparation and rendering pipeline.
It does not emit an import header or body text until declaration lowering,
source rewrite, type-name lowering, lowered-block validation, and import
resolution have all succeeded.
Declaration validation checks every TypeSpec against the type, function,
field, property, and enum-variant profiles returned by
CodeLang::capabilities(). Public FileSpec::validate() exposes the stored
intent checks; render preparation performs the same declaration checks without
calling the public method and retains successful lowered output.
After those per-family checks, one owner-wide type-members pass rejects
intrinsic duplicate property names and lets the adapter report target-derived
cross-member collisions.
Function validation distinguishes free functions, receiver methods, concrete
members, and interface members, then selects an ordinary-function, constructor,
or destructor profile within that context. Profiles declare supported and
required semantic capabilities, body policy, and forbidden capability pairs.
This rejects missing return or parameter types, unsupported annotations,
invalid body placement, malformed rest-parameter lists, and incompatible
modifiers before plausible wrong code can render. Adapters written for
sigil-stitch 0.6.8 inherit the permissive compatibility profile.
When a strict member profile requires a return type but its constructor
profile does not, direct FunSpec emission preserves the legacy ambiguous
constructor-shaped member convention because it has no declaring-type owner.
TypeSpec has the owner context and validates constructor identities exactly:
fixed names such as constructor and init, owner-derived Java/C#/C++ names,
and Dart named constructors are classified before capability validation. New
direct-emission code should use is_constructor() explicitly when the name
does not identify the form on its own.
Constructor classification remains language-specific after modifiers and return types are known. A static owner-named member may be an ordinary method in one language and a static constructor in another; Java also permits a same-named ordinary method when an explicit return type disambiguates it. Modifier-aware hooks refine the selected profile’s body policy, parameter limit, visibility, default-parameter ordering, and type-constraint representability without weakening the declared capability matrix. Constraint validation is syntax-independent by default. Adapters whose local lowering attaches constraint subjects to declared type parameters opt into the shared structural check explicitly; Rust retains its broader where-subject model.
Type kinds select their member validation context through the language. Most interfaces and traits use contract-member profiles, while module- or trait-backed concrete constructs such as Ruby modules and PHP traits retain concrete member rules. The same language policy decides which type kinds may carry an explicit abstract modifier.
For languages where is_abstract represents an abstract method, a concrete
type containing such a method must itself be marked abstract. C++ remains the
exception because a pure virtual member makes the class abstract structurally.
Validate and Lower Declarations
Declaration specs are validated and converted to CodeBlocks:
FileMember::Type(TypeSpec)callstype_spec.emit(&lang)->Vec<CodeBlock>FileMember::Fun(FunSpec)callsfun_spec.emit(&lang, ctx)->CodeBlockFileMember::Code(CodeBlock)is cloned into an owned source blockFileMember::RawContent(String)remains opaqueFileMember::RawContentWithImportsretains opaque text plus separate type metadata
The public type, function, field, property, and owner-aware variant emit
paths apply crate-owned semantic validation, call the corresponding
CodeLang::validate_*() method for additional target-local checks, construct a
ValidatedType, ValidatedFunction, ValidatedFields,
ValidatedProperty, or ValidatedVariants, and then call the matching
CodeLang::lower_*() method. ValidatedType contains the validated child
wrappers produced against that same adapter and deliberately does not
dereference to unvalidated TypeIntent. The defaults delegate to frozen
legacy-syntax compatibility modules so pre-0.6.8 external adapters remain
source compatible. Built-in complete lowerers do not consume deprecated
declaration configuration.
TypeMembersIntent is validation evidence only. Its pass runs after the
per-family checks and creates neither a validated wrapper nor a CodeBlock.
Language lowering composes structured child blocks and preserves every
TypeName as a TypeRef. Construction errors propagate from this pass; they
are never converted to empty output, and complete type lowering rejects empty
vectors or blocks. After materialization, everything is either a CodeBlock
or explicitly raw content.
Rewrite and Lower Source Blocks
The core calls RendererLang::rewrite_nodes() exactly once for every owned
source block: the header, each direct caller block, and each block returned by a
declaration lowerer. The adapter may recurse through Nested and Sequence
with the standard rewrite walker; the core does not call the hook again for
those children. The complete rewritten tree is then checked for structural
errors.
Rewrite sees semantic, unaliased TypeRef nodes. It is a target source
correction seam, not declaration or type grammar. The existing public hook can
change those nodes, so the next step always observes the rewritten result.
The core then walks every rewritten source tree and lowers each
CodeNode::TypeRef through the selected adapter. Intrinsic type-name validation
runs before RendererLang::lower_type_name(); the returned non-empty block is
validated afterward and replaces the original node. The validator recurses
through adapter-produced blocks and permits only terminal, import-aware type
references. Unresolved compound types, empty output, or statement and
control-flow nodes fail the complete file. Blocks returned by
lower_type_name() are not source-rewrite inputs and are not rewritten again.
Opaque raw content is neither rewritten nor type-lowered. The separate types
listed by RawContentWithImports are import metadata rather than source trees:
the core lowers and validates them to discover imports but never passes them
through rewrite_nodes() or substitutes their spelling into the raw text.
FileSpec::validate() checks stored declaration and type intent but does not
invoke source rewrite or emit dynamic blocks. Render preparation remains the
authoritative check for the actual rewritten output.
Collect and Resolve Imports
import_collector then walks the fully lowered tree. Each remaining
CodeNode::TypeRef yields its ImportRef (module, name, and optional alias).
This includes target-derived imports introduced by type-name lowering, such as
Python’s structured typing.Literal reference. Lowered raw-import metadata
contributes imports through the same collector without becoming source text.
Nested CodeBlocks (CodeNode::Nested) and sequences are walked recursively.
Import Resolution
The accepted fallible path merges explicit imports, deduplicates identical semantic imports, reserves names from non-conflicting bindings, and constructs every ambiguous requested-name class before calling a resolver. Imports in one class are peers: the public context has no incoming import, current owner, winner, loser, or mutable claim table.
Each peer request is one of:
- Exact – an explicit local binding that must be preserved because opaque caller source may refer to it;
- Preferred – a soft alias requested through
TypeName::with_alias(); or - Natural – the original simple name, also a soft request.
A resolver receives the complete ambiguous set once per file and returns an atomic assignment for every peer. Core validation requires every peer exactly once, preserves exact bindings, rejects blank or unsafe names, and enforces global uniqueness before the selected language validates identifier grammar, reserved words, alias support, and import form. Any failure aborts the file before an import header or body is returned.
Ordinary FileSpec rendering uses a deterministic default resolver. Encounter
order is only that resolver’s compatibility tie-break: it may give the natural
name to one peer and module-derived aliases to the others, but this does not
make that peer an owner in the model. A borrowed custom resolver can choose a
different complete assignment. It is supplied to the render call and is never
stored or serialized in FileSpec or ProjectSpec.
The fallible ImportGroup::try_resolve*() entry points implement the current
core contract. The pre-0.6.8 infallible resolve() and
resolve_with_explicit() implementations remain frozen deprecated
compatibility APIs rather than wrappers that discard fallible errors.
After assignment, qualify_import_reference() receives the module, original
symbol, and resolved binding. Go uses it to render http.Server with a
package-level import of "net/http". Haskell uses the same hook to turn an
assigned symbol alias into a module-qualified reference and renders the
corresponding import as qualified. The old two-argument
qualify_import_name() remains only as the 0.6.8 compatibility hook.
Final Render
After aliases are resolved, the private final-rendering entry point in
CodeRenderer walks each prepared CodeBlock’s CodeNode sequence. It does
not rewrite the tree or lower another type root:
| Node | Action |
|---|---|
Literal(s) | Emit string directly |
TypeRef(tn) | Resolve and emit one already-lowered terminal type reference |
NameRef(s) | Emit identifier |
StringLit(s) | Call lang.render_string_literal() |
VerbatimStr(s) | Call lang.render_verbatim_string() |
InlineLiteral(s) | Emit raw literal |
Nested(block) | Recursively render the inner CodeBlock |
Comment(s) | Emit with lang.line_comment_prefix() |
SoftBreak | Pretty-print decision point |
Indent / Dedent | Adjust indent level |
StatementBegin / StatementEnd | Statement boundaries; render_statement_end() supplies the complete suffix |
Newline | Emit newline + indent |
BlockOpenIntent / BlockCloseIntent | Map BlockIntent + condition through render_block_open() / render_block_close() |
BranchCloseIntent | Ask render_branch_transition() for the complete outgoing closer and connector whitespace |
BlockOpen / BlockClose / BranchClose | Deprecated legacy string-only nodes retained for source construction, rendering, and unchanged external adapters; their Serde representation is not versioned |
Sequence(children) | Recursively render a sub-sequence of nodes |
Width-aware rendering: One semantic walker interprets every prepared
CodeNode. CodeBlocks without SoftBreak use a direct string adapter. When a
SoftBreak exists anywhere in the tree, the same walker uses a pretty::BoxDoc
adapter for the whole tree so the Wadler-Lindig algorithm can choose between a
space and an indented line break. Nested and Sequence nodes form layout
groups without resetting renderer state. Both adapters preserve the language’s
indent_unit() string exactly.
Import Conflict Resolution
A concrete example of the conflict resolution:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let user_a = TypeName::importable_type("./models", "User");
let user_b = TypeName::importable_type("./legacy", "User");
let mut cb = CodeBlock::builder();
cb.add_statement("const a: %T = getA()", (user_a,));
cb.add_statement("const b: %T = getB()", (user_b,));
let body = cb.build().unwrap();
let output = FileSpec::builder("test.ts")
.add_code(body)
.build()
.unwrap()
.render(80)
.unwrap();
}
The output would contain:
import type { User } from './models'
import type { User as LegacyUser } from './legacy'
const a: User = getA();
const b: LegacyUser = getB();
The two imports are peers in one conflict class. The default resolver uses
encounter order only as a deterministic compatibility tie-break, so this
example assigns User to ./models and the module-derived LegacyUser to
./legacy. A custom complete-set resolver may assign both peers differently
while still satisfying exact bindings, uniqueness, and TypeScript identifier
rules.
Language-Independent Containers and Target-Specific Payloads
Public types such as CodeBlock, TypeName, TypeSpec, and FunSpec have no
target-language generic parameter. The target is supplied through
&dyn RendererLang or &dyn CodeLang when a block or declaration is
materialized and rendered.
The distinction is about the Rust interface, not automatic portability of all values:
TypeName::Array(T)and aFunSpectype-parameter list are semantic and can be lowered for different targets.- A
CodeBlockcontaining the literalconst u = ...is already target-language source, even though theCodeBlocktype itself is shared. TypeRef,StringLit, comments, layout intent, and import references remain structured until the renderer applies target policy.
FileSpec::builder("user.ts") auto-detects the adapter from the file
extension. FileSpec::builder_with(...) selects one explicitly. In both cases,
the adapter must validate declaration intent and own its concrete grammar.
Design
These chapters record the intended seams and ownership rules behind sigil-stitch. They complement the architecture overview, which explains how data flows through the implementation.
- Declaration Specs and Language Lowering defines the distinction between declaration intent, semantic capabilities, target-language grammar, structured source blocks, and final rendering.
- TypeName Validation and Lowering defines the
fallible language-owned seam that materializes semantic
TypeNamevalues before import collection. - Language-Aware Tokenizer describes the private typed pipeline
used by
sigil_quote!.
Design chapters describe the accepted 0.7 built-in architecture. Declaration lowering, type-name lowering, and complete-set import resolution are implemented. Frozen 0.6.8 compatibility lowerers remain for external adapters and legacy direct facades; each chapter points to the compatibility appendix where that boundary matters.
These chapters document the selected design and its invariants, not a catalogue
of every rejected alternative. When the history of a hard-to-reverse trade-off
is important, it belongs in a focused record under docs/adr/.
Declaration Specs and Language Lowering
This chapter defines the implemented ownership model for structured
declarations in spec/* and the built-in lang/* adapters. Frozen pre-0.6.8
compatibility paths for external adapters and legacy direct facades are
described in 0.6.8 Legacy Compatibility and
Migration.
Decision
Declaration specs describe what to declare. A language adapter decides whether that intent is representable and owns how to spell it. Specs do not assemble a target-language grammar by interpreting a shared collection of keywords, separators, placement enums, or ordering flags.
The complete pipeline is:
builder
|
v
declaration spec target-independent intent
|
+-- intrinsic validation invariants of the intent itself
|
+-- capability validation target-specific representability
|
v
language-local lowering exact grammar, spelling, and token order
|
v
CodeBlock / CodeNode::TypeRef target-associated structured source
|
+-- source rewrite, then TypeName lowering
+-- import collection and alias resolution
+-- layout and indentation
|
v
source text
This is a compiler pipeline, not a general declaration-formatting engine.
Ownership
| Concern | Owner | Examples |
|---|---|---|
| Declaration intent | spec/* | Name, parameters, result type, type parameters, bounds, members, visibility intent, modifiers, body |
| Intrinsic coherence | spec/* | Non-empty names, internally consistent parameter lists, valid builder state |
| Target representability | language capabilities and validation | Whether a context supports type parameters, requires typed parameters, permits a body, or accepts a constructor |
| Target grammar | language adapter | Keywords, ordering, placement, punctuation, modifier spelling, constructor syntax |
| Structured output | CodeBlock | Target literals plus semantic TypeRef, nesting, statement, and layout nodes |
| Final text mechanics | renderer | Imports, aliases, indentation, width decisions, and string emission |
The ownership test is deliberately simple:
- A fact about the requested declaration belongs to the spec.
- A statement that the target supports, requires, or forbids a semantic fact belongs to capability validation.
- A decision about which token appears, where it appears, or in what order it appears belongs to language-local lowering.
- A decision about import names, indentation, width, or document layout belongs to the renderer.
Declaration Specs
A declaration spec is a target-independent declaration model, not the syntax tree of a hypothetical universal language. It may be richer than any one target. A target adapter either lowers the requested semantics or returns a validation error; it must not silently discard unsupported intent.
For example, one function declaration may contain:
name: id
type parameters: T
parameters: x of type T
result: T
body: ...
That intent can become:
Kotlin: fun <T> id(x: T): T
Rust: fn id<T>(x: T) -> T
Java: <T> T id(T x)
There is no semantic type parameter placement property in the declaration.
Placement exists only after selecting a target grammar.
Specs can contain target-specific CodeBlock payloads for bodies, raw
annotations, suffixes, or other escape hatches. These payloads are explicitly
opaque to generic specs and shared lowerers: their presence does not make the
declaration shell or its grammar a shared syntax model. Lowering composes them
structurally and preserves their TypeRef nodes, but does not reinterpret their
literal syntax. A private Python validator recognizes the documented 0.6.8
is_static plus decorator pattern as a frozen adapter-local compatibility
exception; new behavior must use semantic intent instead of extending it.
Capabilities Are Semantic
The shared capability vocabulary describes representability. For example,
ParametricPolymorphism says that a declaration context can express type
parameters; TypedParameters says that parameter types are supported or
required; and FunctionBodyPolicy says whether a body is legal. None of these
concepts describes the position or spelling of a token.
Capabilities may be contextual. A target can support a bodyful top-level function while forbidding a body on an interface member, or support ordinary methods while rejecting constructors. Such differences remain semantic validation rules even though the rules are language-specific.
If a proposed capability cannot be defined without mentioning a keyword, delimiter, token order, or formatting example, it is probably target grammar rather than a semantic capability.
Language-Local Lowering
The external declaration seams first validate classified intent and then lower a complete validated declaration into a structured block:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{TypeIntent, ValidatedType};
trait Example {
fn validate_type(
&self,
type_: TypeIntent<'_>,
) -> Result<(), SigilStitchError>;
fn collect_type_validation_errors(
&self,
type_: TypeIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
fn lower_type(
&self,
type_: ValidatedType<'_>,
) -> Result<Vec<CodeBlock>, SigilStitchError>;
}
}
The vector return is target grammar: an adapter may produce one declaration or
several related blocks, such as a Rust definition and impl.
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{FunctionIntent, ValidatedFunction};
trait Example {
fn validate_function(
&self,
function: FunctionIntent<'_>,
) -> Result<(), SigilStitchError>;
fn lower_function(
&self,
function: ValidatedFunction<'_>,
) -> Result<CodeBlock, SigilStitchError>;
}
}
Enum variants use the same shape at sequence granularity:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{ValidatedVariants, VariantIntent};
trait Example {
fn validate_variants(&self, variants: VariantIntent<'_>)
-> Result<(), SigilStitchError>;
fn collect_variant_validation_errors(
&self,
variants: VariantIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
fn lower_variants(&self, variants: ValidatedVariants<'_>)
-> Result<CodeBlock, SigilStitchError>;
}
}
Fields also cross the adapter boundary as one complete sequence:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{FieldSequenceIntent, ValidatedFields};
trait Example {
fn validate_fields(&self, fields: FieldSequenceIntent<'_>)
-> Result<(), SigilStitchError>;
fn collect_field_validation_errors(
&self,
fields: FieldSequenceIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
fn lower_fields(&self, fields: ValidatedFields<'_>)
-> Result<CodeBlock, SigilStitchError>;
}
}
Computed properties cross as one complete semantic declaration:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{PropertyIntent, ValidatedProperty};
trait Example {
fn validate_property(
&self,
property: PropertyIntent<'_>,
) -> Result<(), SigilStitchError>;
fn collect_property_validation_errors(
&self,
property: PropertyIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
fn lower_property(
&self,
property: ValidatedProperty<'_>,
) -> Result<Vec<CodeBlock>, SigilStitchError>;
}
}
Relationships among different member families use a validation-only owner view rather than another lowering abstraction:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::TypeMembersIntent;
trait Example {
fn validate_type_members(
&self,
members: TypeMembersIntent<'_>,
) -> Result<(), SigilStitchError>;
fn collect_type_members_validation_errors(
&self,
members: TypeMembersIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
}
}
FunctionIntent provides read-only access after context and form
classification and crate-owned semantic validation against the selected
adapter. ValidatedFunction can only be constructed by the crate after the
adapter’s additional validation succeeds. FunSpec::emit() remains the
convenience facade: it delegates validation and lowering without interpreting
target grammar switches itself.
TypeIntent provides one complete declaration before target-local validation:
kind, semantic modifiers, preamble data, type parameters and constraints,
nominal relationships, primary-constructor parameters, variants, and member
families. ValidatedType is constructed only after type-level and child
validation succeeds. It exposes fields, properties, methods, and variants
through their validated wrappers so lower_type() can own member order and
declaration shape while reusing each child’s complete lowerer. The type adapter
also owns alias/newtype forms, empty-body behavior, and whether output is inline
or split; it must return one or more non-empty blocks. None of those choices
lives in TypeSpec.
VariantIntent provides the owner name and kind, every variant in declaration
order, a has_non_variant_members() fact covering fields, properties, methods,
embedded types, and opaque members, structured-constructor arity evidence, and
separate evidence that opaque members may provide target-specific constructor
syntax. The variant adapter derives first/last position and owns preambles,
payload grammar, separators, and section termination for the complete
sequence. The type adapter chooses the sequence’s position. Variant
capabilities name semantic forms—discriminant, constructor arguments,
positional payload, record payload, and attributes—not their spelling. The
additive collector reports independent sibling failures; ValidatedVariants
is constructed only after intrinsic, profile, and every adapter-local
validation phase succeeds.
FieldSequenceIntent provides every field in declaration order and a semantic
FieldContext: direct emission, ordinary type members, or a variant record
payload. Owner and variant names are included when they exist. Field profiles
declare supported and required semantic capabilities for each context, while
the adapter-local validator handles identifier rules, escaped-name collisions,
modifier combinations, and target-specific restrictions. The additive
collector retains independent sibling failures during FileSpec validation;
ValidatedFields is created only after intrinsic, profile, and adapter-local
validation all succeed. lower_fields() owns the sequence’s complete grammar,
including documentation, annotations, access sections, tags, separators, and
declarator restrictions.
Optional presence and optional values are separate semantics. A field marked
with FieldSpec::is_optional() may be absent from its containing value and
requests FieldCapability::OptionalPresence. A TypeName::Optional(T) field
is still present but may hold the target language’s option or null
representation. An adapter must not substitute one meaning for the other.
PropertyIntent provides one property and its semantic PropertyContext:
direct emission with the legacy declaration context, or a member of an owning
TypeKind. Property profiles distinguish explicit type information, read and
write behavior, attributes, and static behavior. A getter or setter body is
semantic implementation input; whether the target expresses it as accessor
declarations, a field-style computed property, or ordinary target-local methods
belongs entirely to lower_property(). ValidatedProperty is constructed only
after intrinsic, profile, and adapter-local validation succeeds.
TypeMembersIntent provides the owning type’s name and kind together with its
semantic fields, computed properties, and explicit methods. It is constructed
once after the per-family validation passes. The crate rejects exact duplicate
property names; the adapter owns collisions created by target lowering, such
as PHP’s case-insensitive generated accessor names colliding with another
property accessor or an explicit method. The intent contains no target grammar,
has no validated wrapper, and has no lowering method: every accepted property
still follows ValidatedProperty -> lower_property() independently.
Each adapter owns the complete ordering and spelling of a declaration. Private leaf helpers may render structured fragments such as a parameter list or body, but do not choose their relative order. Related adapters may additionally share a genuinely family-specific lowering helper. An adapter can bypass either without adding a new variant to a shared grammar interface.
Built-in type and function lowerers spell declaration generics locally,
including bounds, lifetimes, kinds, context bounds, and explicit constraint
clauses. Only the frozen permissive compatibility path interprets the
deprecated shared generic configuration. A strict adapter that advertises a
function profile but omits lower_function() fails with
MissingFunctionLowerer instead of silently selecting compatibility grammar.
A useful locality test is to add a language with a previously unseen syntax. The change should be confined to that adapter, its private helpers, and its tests. If the change requires a new shared placement enum and new branches in a generic spec emitter, target grammar has crossed the seam.
Closed Sum Declarations
A closed sum is a type declaration with a complete ordered set of named cases.
The set itself is semantic intent; sealed, enum, data, nesting, and
sibling placement are possible target representations rather than shared
configuration.
The public construction entry point is ClosedSumSpec::builder(...) with
ClosedSumCaseSpec cases. It is a sibling of TypeSpec, not an enum mode and
not a modifier on TypeSpecBuilder. ClosedSumCapabilityProfile is the
opt-in representability profile; ordinary enum profiles and lowering remain
unchanged.
Both declaration families use TypeDeclarationCapability for polymorphism
and root attributes. Ordinary TypeKindCapabilityProfiles keep those features
separate from ordinary TypeCapability values. Closed-sum case forms and
scoped record fields live only in ClosedSumCapabilityProfile. Case metadata
does not request root attributes; each target validates and places it locally.
Closed sums describe these case shapes independently of ordinary enum storage:
| Case shape | Meaning |
|---|---|
| No cases | Empty sum; a named uninhabited type |
| Unit case | One named alternative with no carried data |
| Positional payload | One named alternative carrying types in order |
| Record payload | One named alternative carrying named typed fields |
Discriminants, legacy variant values, and enum-entry constructor arguments are invalid on a closed sum. They describe a value representation or an expression evaluated at an enum declaration, not data carried by a sum case. Structured and opaque case annotations remain metadata, while wire discriminators and serialization tagging stay in the caller or its annotations.
An empty closed sum declares a named uninhabited type. It is not the unit type:
the empty sum has no values, while the empty product or unit type has exactly
one. A Never reference or bottom type can likewise have no values, but it is a
type-expression or subtype concept rather than a declaration of this named
case set. This feature does not add TypeName::Never or treat the two concepts
as shared semantic identity. A target may reuse its canonical empty type only
if the result preserves the requested name and every valid use position; it
otherwise rejects the empty form even if it supports non-empty closed sums.
Case(T) always means a generated case that carries T. It does not claim
that an existing declaration for T is a subtype of the root. Nominal
membership of pre-existing types has different declaration ownership and is
outside this interface.
Complete ClosedSum lowering owns the output topology. Rust, Swift, Haskell, OCaml,
and Scala can use native algebraic declarations. Java uses one public sealed
root with nested case declarations so one generated file does not contain
several public top-level types. Kotlin uses a private-constructor sealed class
with nested data object and data class cases so no additional direct case
can be declared elsewhere in the same module. Dart uses a sealed root and
generated final cases. Every other built-in returns a structured
unsupported-intent error until it has an accepted exact representation; no
adapter widens a closed sum to Object, Any, an open hierarchy, or an
ordinary enum.
Support for the empty shape is validated separately from general closed-sum
support because several targets require additional language features or lack
an exact named uninhabited declaration. Each accepted case shape still follows the existing
intrinsic validation, target capability checks, complete target-local
validation, non-empty CodeBlock output checks, type-name lowering, import
resolution, and rendering pipeline.
Compatibility and Migration
Public declaration grammar that was already part of the 0.6.8 adapter surface may remain behind a deprecated, frozen compatibility lowerer. Compatibility is not permission to extend that design:
- Do not add new shared declaration-placement enums, flags, or keyword fields.
- Do not add new branches in specs to interpret target grammar.
- New built-in behavior should enter through a complete language-owned lowering seam.
- Concepts introduced after 0.6.8 may be changed or removed instead of being preserved as another compatibility layer.
- External adapters can migrate one declaration family at a time, with rendered-output tests at the adapter seam and parity coverage across direct and pretty paths. Built-in adapters already use complete lowerers.
Compatibility is bounded by validity: a built-in adapter may restrict an old entry point rather than emit malformed or unverifiable target code. The full version boundary, deprecated-surface matrix, builder recipes, and external-adapter sequence are centralized in 0.6.8 Legacy Compatibility and Migration.
Scope of This Decision
This decision governs declaration grammar interpreted by spec/*. It does not
prohibit shared semantic data, structured rendering nodes, or private reusable
helpers. It also does not by itself redesign lower-level seams such as
TypeName presentation or block layout; those mechanisms have their own design
documents and must be evaluated against their own callers and invariants.
TypeName Validation and Lowering
Status: implemented for every current TypeName variant, including string
literal types, parameter references, applications, and callable sequences.
TypeName records semantic type-reference structure. It does not describe a
shared target grammar. One selected language adapter must either lower the
complete value into structured output or reject it before any source text is
rendered.
This chapter defines the 0.7 type-name seam. The frozen pre-0.6.8 presentation configuration is documented only in 0.6.8 Legacy Compatibility and Migration.
Ownership
The core owns:
- the language-independent
TypeNamevariants and their intrinsic coherence; - recursive discovery of every
CodeNode::TypeRef; - the order of source-tree rewrite, type-name lowering, import collection, alias resolution, and final rendering;
- validation of every adapter-produced type block; and
- all-or-error behavior when any type name is invalid or unsupported.
The selected language adapter owns:
- whether the complete type name is representable;
- target precedence, punctuation, delimiters, and wrapping;
- the spelling and placement of every accepted type construct;
- decoded string-literal quoting and escaping; and
- target-derived type references such as Python’s
typing.Literal.
No TypeExpressionCapability, presentation matrix, or universal syntax
configuration sits between those responsibilities. Detailed type grammar
varies together and remains local to one adapter.
Legacy Generic / Function and modern Application / Callable inputs
share semantic validation and traversal; they do not create a second
preparation pipeline. Every adapter must preserve the supplied structure or
return SigilStitchError. An unsupported expansion, kind, label, or presence
rule is not silently erased, and the library performs no type-level
evaluation, argument inference, or pack-length solving.
Generic declaration lowerers consume borrowed GenericParamView values from
the owner’s single ordered binding store. Kind expressions remain structural
until that declaration’s language-owned lowering. Haskell callers supply
required extensions themselves through file headers or compiler flags;
sigil-stitch does not infer pragmas from type expressions.
One Fallible Interface
RendererLang exposes one complete type-name lowering method:
fn lower_type_name(
&self,
type_name: &TypeName,
) -> Result<CodeBlock, SigilStitchError>;
There is no public validated wrapper. TypeName is immutable, lowering is a
pure operation, and the successful non-empty CodeBlock is the proof that the
selected adapter accepted the value. Crate-owned callers perform intrinsic
validation before the hook and validate its output afterward.
Validation paths may invoke this pure lowering operation for every type root and discard the successful blocks while retaining all independent failures. Render preparation retains successful blocks for import collection and rendering. This avoids separate validation and lowering implementations that can disagree.
The method belongs to RendererLang, not only CodeLang, because a direct
CodeBlock may contain %T references without using declaration specs.
Preparation Pipeline
Declaration lowering first produces source CodeBlocks. The selected adapter
then rewrites each source block exactly once before type names are lowered and
imports are collected:
declaration lowerers and caller CodeBlocks
-> CodeBlock tree containing TypeRef(TypeName)
-> RendererLang::rewrite_nodes exactly once
-> validate the rewritten source tree
-> recursively find every TypeRef
-> intrinsic TypeName validation
-> RendererLang::lower_type_name
-> validate every lowered type block
-> collect imports from the lowered CodeBlock tree
-> resolve aliases
-> render through one layout adapter with no further rewrite or lowering
No source text is emitted until every type name in every prepared file member has succeeded. A failure in a direct, nested, or sequenced block aborts the complete file just like a declaration-lowering failure.
Declaration lowerers continue to place semantic TypeName values in %T
slots. They do not call lower_type_name, render a type early, or duplicate
type grammar.
rewrite_nodes() is a source-tree correction seam, not a type-lowering hook.
It runs for headers, direct caller blocks, and declaration-lowered blocks. It
does not run for blocks returned by lower_type_name(), opaque raw content, or
the type metadata attached to RawContentWithImports. Listed raw-import types
are lowered and validated only so their derived imports can be collected.
Lowered Block Contract
A successful adapter result must:
- be non-empty;
- contain only structure meaningful inside one type expression;
- balance every
IndentandDedentmarker; - preserve soft layout choices as
SoftBreakand nested groups; - retain import-bearing leaves as terminal
TypeRefnodes; and - contain no unresolved compound
TypeName.
The terminal type-reference leaves are target-aware Primitive and Raw
values plus unqualified Importable references whose aliases are resolved
later. A qualified importable reference and every compound variant must be
fully lowered by the adapter.
The crate rejects adapter output that contains statement or control-flow nodes, is empty, leaves a compound type reference unresolved, or otherwise cannot be interpreted as one type expression. This catches incomplete external adapters instead of allowing recursive or silently empty output.
CodeBlock remains the only shared structured source container. It carries
target-associated type-expression structure rather than defining a portable
cross-language program. There is no separate type document tree and no public
BoxDoc-producing language hook.
Imports Stay Structural
An adapter expresses a target-derived import by retaining an importable
TypeName leaf in its lowered block. It does not return a parallel import
list.
For example, Python lowers a string literal type to the structural equivalent of:
%T[%S]
where %T contains TypeName::importable("typing", "Literal") and %S
contains the decoded string value. Import collection therefore discovers
typing.Literal from the same structure that renders Literal["value"].
Alias resolution cannot drift from the generated type syntax.
Direct CodeRenderer use retains its existing contract: the caller supplies
the resolved ImportGroup. FileSpec owns complete source preparation,
derived import collection, and import-header emission.
String Literal Types
The focused 0.7 extension is:
TypeName::StringLiteral(String)
The string is the decoded semantic value. It contains neither target quotes nor target escape sequences. The adapter uses its language-local string literal rules when that value is valid in a type position.
- TypeScript lowers one value to a string literal type such as
'active'. - Python lowers one value through
typing.Literal["active"]. A non-empty directUnioncontaining onlyStringLiteralmembers becomes onetyping.Literal[...], preserving member order and duplicates. Mixed unions and nested unions lower recursively through ordinary Python union grammar; this direct-union rule never flattens them. - A built-in adapter without an exact string singleton type rejects the
variant instead of widening it to
String,str, or another primitive.
Several values compose through ordinary union structure:
TypeName::Union([
TypeName::string_literal("active"),
TypeName::string_literal("inactive"),
])
The core does not add LiteralValue, StringEnum, LiteralSet, or numeric
literal variants. A future proven type-expression semantic receives its own
explicit variant; it does not reinterpret the string payload as source code.
Compatibility
Adding StringLiteral makes the pre-0.6.8 public TypeName enum source
incompatible for downstream exhaustive matches. The 0.7 change therefore also
marks TypeName as #[non_exhaustive] and documents the required wildcard
match. The compatibility bridge preserves supported 0.6.8 Rust constructors
and captures the specific TypeName JSON values documented before 0.7 as
checked fixtures. This is not a promise that every Serde representation remains
stable: sigil-stitch defines no binary serialization protocol, enum-ordinal
contract, struct-field order, or serializer-specific byte format. No
forward-compatible interpretation of unknown serialized variants is added;
deserialization returns an error instead of changing generated code silently.
RendererLang::lower_type_name has a provided compatibility implementation.
It reproduces pre-0.6.8 behavior for old variants through the frozen
TypePresentationConfig, TypePresentation, GenericSyntaxConfig, and
qualified-name accessors. It rejects StringLiteral and every later semantic
variant that did not exist in 0.6.8.
Every built-in adapter overrides the complete method and does not consult the compatibility configuration. The legacy accessors and data types are deprecated, receive no new fields or variants, and remain referenced only by the compatibility implementation and its tests.
The pre-0.6.8 TypeName::to_doc_with_lang() convenience remains as a
deprecated terminal compatibility facade. The current file and standalone
rendering pipelines do not call it, and no replacement BoxDoc-producing
language hook is introduced.
Verification Contract
The implementation must prove:
- every language in
tests/renderer_parity_tests.rshandles every oldTypeNamevariant through its local lowerer or rejects it explicitly; - direct and pretty paths agree at wide widths and preserve intentional soft breaks at narrow widths;
- unsupported compound forms fail in direct, nested, and
Sequenceblocks; - lowered imports survive nesting and alias collisions;
- TypeScript and Python correctly handle empty strings, both quote characters, backslashes, newlines, NUL, and Unicode;
- Python union lowering retains the canonical
Literalimport; - a compatibility adapter that implements only pre-0.6.8 methods preserves
its old output and rejects
StringLiteral; - empty, statement-bearing, or unresolved adapter output fails closed; and
- old serialized
TypeNamefixtures retain their exact shapes.
Language-Aware Tokenizer (MacroLang)
sigil_quote! uses Rust’s proc-macro tokenizer to parse target-language code. Since the
tokenizer sees Rust tokens, not the target language’s tokens, certain patterns are ambiguous:
shell flags (-q) look like negation, paths (/usr) look like division, and standalone dots
(.) look like member access. The MacroLang system resolves these ambiguities by making the
tokenizer annotation pass language-aware.
How It Works
The sigil_quote! macro pipeline has three stages:
sigil_quote!(Go { val := <-ch; })
│
▼
┌─ parse_input ─────────────────────────────────────┐
│ 1. Extract language: MacroLang::Go │
│ 2. Parse body tokens │
│ 3. annotate_tokens(tokens, lang) │
│ └─ Pre-scan: classify each token │
│ 4. tokens_to_format(tokens, annotations, lang) │
│ └─ Build format string + args │
└───────────────────────────────────────────────────┘
│
▼
CodeBlockBuilder method calls
The MacroLang enum is extracted from the first identifier in the macro invocation
(Bash, Zsh, Go, Haskell, etc.) and threaded through the entire parse pipeline.
Languages not in the enum get MacroLang::Unaware, which applies only universal heuristics.
MacroLang Variants
| Variant | Recognized from | Tokenizer behavior |
|---|---|---|
Unaware | All other languages | Universal heuristics only |
Bash | sigil_quote!(Bash { ... }) | Shell-specific (see below) |
C | sigil_quote!(C { ... }) | No angle generics, postfix * pointer |
Cpp | sigil_quote!(Cpp { ... }) | Postfix * pointer, postfix & reference |
CSharp | sigil_quote!(CSharp { ... }) | Postfix * pointer, postfix ? nullable |
Dart | sigil_quote!(Dart { ... }) | Postfix ? nullable |
Go | sigil_quote!(Go { ... }) | <- prefix receive, paren blocks |
Haskell | sigil_quote!(Haskell { ... }) | $$ dollar operator spacing |
Kotlin | sigil_quote!(Kotlin { ... }) | Postfix ? nullable |
OCaml | sigil_quote!(OCaml { ... }) | Space before :, prefix ? nullable |
Php | sigil_quote!(Php { ... }) | Prefix ? nullable |
Ruby | sigil_quote!(Ruby { ... }) | Symbol colon, inheritance angle |
Swift | sigil_quote!(Swift { ... }) | Postfix ? nullable |
TypeScript | sigil_quote!(TypeScript { ... }) | Postfix ? nullable |
Zsh | sigil_quote!(Zsh { ... }) | Shell-specific (same as Bash) |
Gated annotations (language-aware)
These annotations used to fire for ALL languages but are now restricted to languages where the syntax is valid:
| Annotation | Gates | Languages | Effect |
|---|---|---|---|
PostfixStar | has_postfix_star() | C, Cpp, CSharp | Config* — no space before * |
PostfixAmpersand | has_postfix_ampersand() | Cpp only | auto& — no space before & |
PostfixQuestion | has_postfix_question_type() | CSharp, Dart, Kotlin, Swift, TypeScript | int? — no space before ? |
AssignAdjacent | is_shell() | Bash, Zsh | NAME=val — no space around = |
GenericOpen (ordinary) | has_angle_generics() | Excludes C, Go, Haskell, OCaml, Php, Bash, Zsh, Ruby | < as generic opener |
NullablePrefix | nullable_prefix_is_valid() | Php, OCaml | ?User — no space before ? |
Shell Languages (Bash, Zsh)
These share a common is_shell() check and enable:
- DashFlag:
-q,-avz— standalone-span-adjacent to the next identifier suppresses space after it, sodeclare -arenders correctly. - DashSep downgrade:
-- file.txt— the second-of--is downgraded fromPrefixOptoNormalwhen NOT span-adjacent to the next token, preserving the separator space.--amend(flag, adjacent) stays tight. - SlashSep leading path:
/usr/local/bin— allowsSlashSepannotation with no left neighbor (relaxes thei > 0requirement for shell mode). - DotArg:
find .,cd ..— standalone.or..not span-adjacent to the previous token is marked as a shell argument, not member access. Space is preserved on both sides. Guard: if the dot is adjacent to the next token (.gitignore), it stays asNormal.
Go
<-prefix receive: When-follows a Joint<(not GenericOpen) and is span-adjacent to the next token, it getsPrefixOpannotation — suppressing the space to produce<-ch. When NOT adjacent (ch <- 42), the-staysNormaland the space is preserved.- Paren-delimited blocks:
const (,var (,import (, andtype (are detected as structural blocks. The parser recursively processes the body so$for,$if, and other directives expand inside. The codegen emits%>after the header and%<before the closing)for proper indentation.
Haskell
$$dollar operator: The$$escape normally setsPrevTokenKind::DollarLiteral, which suppresses space after$(designed for shell$VAR). For Haskell, it setsPrevTokenKind::Punct('$', Alone)instead, allowing the normal spacing rule to insert a space — producingputStrLn $ show 42.
Ruby
- Symbol colon (
:name)::span-adjacent to the next ident but NOT span-adjacent to the previous token getsSymbolColonannotation — space before:but none after:attr_reader :name, :age. - Inheritance angle (
<):<following an ident is markedInheritanceAngleinstead ofGenericOpen— space before<is preserved:class Dog < Animal. - No angle generics: Ruby is excluded from
has_angle_generics(), so$T(...)<...>does not triggerGenericOpen.
PHP / OCaml
- Nullable prefix (
?User):?span-adjacent to the following ident getsNullablePrefixannotation — suppressing space on both sides:?string,?User. - No angle generics: Both are excluded from
has_angle_generics().
C / C++ / C#
- Postfix pointer (
Config*):*span-adjacent to the preceding ident getsPostfixStar— no space before:Config* p. - Postfix reference (
auto&): C++ only —&span-adjacent to the preceding ident getsPostfixAmpersand— no space before:auto& x. - Postfix nullable (
int?): C# only —?span-adjacent to the preceding ident getsPostfixQuestion— no space before:int? count. - No angle generics (C only): C is excluded from
has_angle_generics().
Inline $for / $if Meta-Directives
$for and $if (with $else_if/$else chaining) now work inline — inside parenthesized
groups, array/dict literals, function arguments, and indented blocks. They no longer require
column-0 position. The parser produces ParsedSplice (no synthetic block delimiters) so inline
output splices cleanly without stray {} or :.
When a source line ends with continuation punctuation such as = or |, an inline
$for/$if on the next line remains part of the same statement. A plain newline before $for
still starts a statement-level meta-loop.
Universal Heuristics (all languages)
These annotations fire regardless of MacroLang:
| Annotation | Pattern | Effect |
|---|---|---|
PathSepComplete | :: span-adjacent to left | Suppress space after (path: std::fmt) |
DoubleColonOp | :: NOT adjacent to left | Space before (Haskell: fmap :: Type) |
MethodCallColon | : adjacent to both sides | Suppress space (Lua: obj:method()) |
GenericOpen/Close | </> with type context | Suppress space (generics: Vec<T>) |
ArrowOp | -> adjacent to left | Suppress space (member: ptr->field) |
PrefixOp | &, *, - as prefix | Suppress space after (&self, *ptr) |
PostfixStar | */& adjacent to ident | Suppress space before (Config*) |
PostfixIncDec | ++/-- after ident | Suppress space before (i++) |
PostfixQuestion | ? adjacent to ident | Suppress space before (Int?) |
SafeCallQ | ?. | Suppress space before (x?.y) |
MacroBang | ! after ident | Suppress space before (println!()) |
CallOpen | (/[ adjacent to ident | Suppress space (call: f(x)) |
AssignAdjacent | = adjacent to ident | Suppress space (shell: NAME=val) |
DashSep | - adjacent to both sides | Hyphenated word (from-oci-layout) |
SlashSep | / adjacent to both sides | Path separator (linux/amd64) |
Runtime Rewrite Passes
Block semantics are carried as BlockIntent nodes from both builder and macro
paths. Remaining runtime passes are local to each language:
| Language | Pass | Purpose | Applies to |
|---|---|---|---|
| Go | rewrite_iife | Fuse }() for closes with BlockIntent::Function | Builder API |
| Go | rewrite_receive_op | <- ch → <-ch | Literal/InlineLiteral text; builder API only (tokenizer handles sigil_quote!) |
| C++ | rewrite_lambda_semicolon | } → }; for closes with BlockIntent::Lambda | Builder API |
| Lua | rewrite_method_colon | obj: method() → obj:method() | Literal/InlineLiteral text; builder API only (tokenizer handles sigil_quote!) |
| Haskell | rewrite_dollar_spacing | $word → $ word | Literal/InlineLiteral text; builder API only (tokenizer handles sigil_quote!) |
Block delimiter selection no longer re-parses condition keywords at render
time. Language adapters match the carried BlockIntent locally; Bash and Zsh
own independent copies of their shell policy.
For sigil_quote! users, the tokenizer-level fixes mean correct output without runtime
patching. The runtime passes remain as safety nets for the builder API path.
Adding MacroLang Support for a New Language
If your language has tokenizer conflicts that universal heuristics can’t handle:
- Add a variant to
MacroLanginmacros/src/parse/types.rs - Map the language identifier in
parse_macro_lang()inmacros/src/parse/mod.rs - Add language-guarded annotation logic in
annotate_tokens()inmacros/src/parse/format.rs - If the fix is in spacing after a token, you may also need to adjust
state.prevassignment intokens_to_format_inner() - Add tests in
tests/<lang>/quote_edge_cases.rs
Only add a MacroLang variant when the universal heuristics produce wrong output for your
language. Most languages work correctly with Unaware.
Adding a Language
This guide uses the implemented complete type-name-lowering, fallible import resolution, and direct renderer-event interfaces. The current source still exposes compatibility-backed renderer defaults described in the legacy appendix, but new adapters implement the complete events directly.
sigil-stitch supports new languages by implementing two traits: RendererLang (renderer-only methods) and CodeLang (spec-layer methods). CodeLang extends RendererLang, so implementing CodeLang requires both. If you only need CodeBlock-level rendering without specs, RendererLang alone is sufficient.
RendererLang covers rendering essentials. CodeLang adds declaration
validation, materialization, and file-level behavior. The trait retains
deprecated pre-0.6.8 grammar hooks only so existing external adapters can use
the frozen compatibility lowerers.
Do not treat those declaration syntax structs as an extensible universal grammar. New syntax dimensions belong in complete language-local lowering. See Declaration Specs and Language Lowering for the ownership model and 0.6.8 Legacy Compatibility and Migration for the deprecated surface.
This guide walks through the process using a hypothetical language, with references to real implementations you can study.
Overview
Adding a language takes five steps:
- Create
src/lang/your_lang.rswith exact capabilities, validation, and complete lowerers - Add
pub mod your_lang;tosrc/lang/mod.rs - Write integration tests in
tests/ - Run the full validation suite
- Bless and inspect golden files only for intentional output changes
If your language has tokenizer conflicts in sigil_quote! that the universal heuristics
can’t handle (e.g., shell flags, Go channel operators), you may also need to add a
MacroLang variant. See Language-Aware Tokenizer for details.
The RendererLang Trait
These methods are used by the renderer (code_renderer.rs) and type lowering:
Required Methods
Only two methods have no default:
| Method | Example (TypeScript) | Purpose |
|---|---|---|
file_extension() | "ts" | File extension for output files |
line_comment_prefix() | "//" | Single-line comment prefix |
Common Overrides
| Method | Default | Purpose |
|---|---|---|
reserved_words() | Empty | Words that need escaping |
render_string_literal() | C-style double quotes | Language-specific string quoting |
render_verbatim_string() | Delegates to render_string_literal() | Minimal escaping for interpolated strings |
indent_unit() | Delegates to legacy block_syntax() | Exact indentation bytes |
render_statement_end() | Delegates to legacy block_syntax() | Complete statement-end suffix |
render_block_open() | Delegates to legacy block hooks | Complete opener suffix for one BlockIntent |
render_block_close() | Delegates to legacy block hooks | Complete final closer |
render_branch_transition() | Delegates to legacy block hooks | Outgoing closer plus connector whitespace |
lower_type_name() | Frozen pre-0.6.8 compatibility lowering | Validate and lower one complete type expression |
Override render_verbatim_string() if your language has string interpolation (e.g., Bash "$x", TypeScript `${x}`, Python f"{x}").
Implement all four renderer-event methods as local target-language behavior.
For keyword-delimited languages, match on BlockIntent; for brace languages,
several arms may deliberately return the same bytes. The legacy
block_syntax(), block_open_for(), block_close_for(), and intent-aware
bridge hooks remain supported only for old nodes and external adapters. A new
adapter does not assemble current renderer behavior from that shared config.
rewrite_nodes() is the language-local source-tree correction seam for cases
that require a tree-level view after macro expansion or declaration lowering.
The core calls it exactly once for each source CodeBlock, then validates the
rewritten tree, lowers every TypeRef, collects imports, resolves aliases, and
renders without rewriting again. The hook sees semantic, unaliased type
references and must not depend on resolved imports or rendered type text.
Use the existing recursive walker when a rule must visit Nested or Sequence
children; the core does not call the hook separately for them. The hook does
not run for type-name-lowering results, raw content, raw import metadata, or a
public FileSpec::validate() call. Semantic rejection belongs in the
applicable validation or lowering hook; structural errors left by rewrite fail
during the core’s post-rewrite validation.
Prefer intent-keyed structural rewrites for blocks. Declaration grammar belongs
to language-local declaration lowering, and type grammar belongs to
lower_type_name(). Do not add rewrite context, capability, configuration, or
per-syntax hooks. The existing declaration syntax structs are a compatibility
path, not the place to add another ordering or placement concept.
Every new adapter must override lower_type_name(). Its provided default
exists only so a pre-0.6.8 external adapter can continue to compile while it
uses the frozen presentation configuration. A new implementation matches the
complete TypeName, returns a non-empty structured CodeBlock for every
accepted form, and returns SigilStitchError for unsupported forms. See
TypeName Validation and Lowering for the output
contract.
The CodeLang Trait
Extends RendererLang with the additional methods needed by the spec layer.
Implement capabilities() for new adapters. Return a local
LanguageCapabilities::strict() matrix, add TypeKindCapabilityProfiles with
with_types(), add FunctionCapabilityProfiles with with_functions(), and
add FieldCapabilityProfiles with with_fields(),
PropertyCapabilityProfiles with with_properties(), and
VariantCapabilityProfiles with with_variants() for every supported
declaration context or owning type kind.
Ordinary type profiles take separate declaration-wide and ordinary-kind
capability slices. TypeDeclarationCapability owns polymorphism and root
attributes; TypeCapability owns fields, methods, relationships, constructors,
and variants. Missing declaration-wide capabilities are reported before
ordinary-kind capabilities, without suppressing either diagnostic group.
Function profiles are keyed by both context (TopLevel, ReceiverMethod,
Member, or InterfaceMember) and form (Function, Constructor, or
Destructor). Omit a profile when that combination is unsupported. Include
ExplicitReturnType and TypedParameters only where the form can represent
them. Use with_required_capabilities() for semantic facts that every
declaration must provide, with_body_policy() for required or forbidden
implementation bodies, and with_incompatible_capabilities() for supported
features that cannot be combined. Use with_maximum_parameters() for
form-specific arity limits, such as a zero-parameter destructor. Adapters
written for sigil-stitch 0.6.8 inherit
LanguageCapabilities::permissive() so their existing CodeLang
implementations remain source-compatible.
Variant profiles distinguish Discriminant, ConstructorArguments,
PositionalPayload, RecordPayload, and Attributes. They must not encode
keywords, delimiters, placement, or separator policy. Omit the owner profile if
the language cannot represent variants for that TypeKind; use an empty
capability list when simple variants are valid but no richer form is.
Closed sums are a dedicated ClosedSumSpec declaration family. Advertise a
ClosedSumCapabilityProfile with LanguageCapabilities::with_closed_sum(...)
only when the adapter can preserve a complete case set. The profile lists
supported unit, positional, and record case forms, declaration-wide
capabilities, and whether an empty sum is representable. It does not change
ordinary enum profiles, and permissive() does not opt an adapter into this
new family.
Implement CodeLang::validate_closed_sum() and
CodeLang::lower_closed_sum() in the language module. Validation receives the
complete ClosedSumIntent; lowering receives a crate-constructed
ValidatedClosedSum and returns every target block, including nested or
sibling case declarations. Keep all payload TypeName values in %T slots so
the normal materialization, import, and renderer pipeline can process them.
Field profiles are keyed by FieldContext: direct member emission, ordinary
members of one TypeKind, record payloads of one ordinary variant owner kind,
or closed-sum case record payloads. They distinguish explicit type information,
initializers, attributes, static and readonly fields, and OptionalPresence.
Add ExplicitType to the required set only where an untyped field cannot be
valid. Optional presence means the member may be absent; value nullability is
expressed separately with TypeName::Optional.
Named fields on a closed-sum case use
FieldContext::ClosedSumRecordPayload. Give this context its own exact field
profile and lower it through ValidatedFields; do not widen the ordinary enum
record-payload profile to reuse the syntax of a sealed hierarchy.
Property profiles are keyed by PropertyContext: direct member emission or a
member of one owning TypeKind. They distinguish explicit type information,
read access, write access, attributes, and static behavior. Require
ReadAccessor where a write-only computed property is invalid and require
ExplicitType where inference cannot preserve the declaration. Getter/setter
spelling and whether the target uses accessor declarations, a field-style
body, or ordinary methods are lowering decisions, not capabilities.
Declaration Lowering and Compatibility Methods
CodeLang::validate_type() receives one complete, read-only TypeIntent.
Override collect_type_validation_errors() when independent target-local
failures should survive file-level aggregation. CodeLang::lower_type() then
receives a crate-constructed ValidatedType whose fields, properties, methods,
and variants have already passed their own validation against the same
adapter. It returns Vec<CodeBlock> because a target may use one declaration
or several related blocks. The vector and every returned block must be
non-empty; sigil-stitch rejects empty output with EmptyTypeLowering. The type
lowerer owns preamble order, alias and newtype forms, headers, inheritance,
primary constructors, member-family order, empty bodies, closing syntax, and
output cardinality.
CodeLang::validate_function() receives classified, read-only FunctionIntent
after sigil-stitch applies its semantic capability matrix against the actual
adapter. An override returns Result<(), SigilStitchError> and can add
target-local checks, but cannot construct or bypass ValidatedFunction.
CodeLang::lower_function() receives the validated view and returns a
structured CodeBlock. New adapters implement this method as the owner of the
target’s complete function grammar. Both views expose the function form and
context as well as names, types, parameters, modifiers, annotations,
constraints, delegation, suffix escape hatches, and the body.
CodeLang::validate_variants() and CodeLang::lower_variants() are the
corresponding complete-sequence seams for enum variants. Adapters that can find
multiple independent target-local errors override
collect_variant_validation_errors() as the additive validation entry point;
its default appends the single validate_variants() result. VariantIntent
exposes the owner, ordered variants, payloads, annotations, a
has_non_variant_members() fact covering fields, properties, methods,
embedded types, and opaque members, structured-constructor arity evidence, and
separate evidence that opaque members may provide constructor syntax. The
variant lowerer derives positions and owns its sequence grammar; the type
lowerer chooses the sequence’s placement. Use
AnnotationSpec::emit_with_syntax() when a local annotation spelling must keep
an importable annotation name as a structured %T reference.
Closed-sum validation and lowering own the complete declaration topology. Do
not route the new family through validate_type(), validate_variants(), or
the compatibility type/variant lowerers. A lowerer may reuse private field
helpers, but it returns all nested or sibling declarations together. Record
payloads use the dedicated FieldContext::ClosedSumRecordPayload profile,
configured with ClosedSumCapabilityProfile::with_record_fields(...); this
context is never inferred from ordinary field profiles.
CodeLang::validate_fields() and CodeLang::lower_fields() form the
corresponding complete-sequence seam for fields. FieldSequenceIntent exposes
the semantic context, owner names when present, and the ordered read-only field
data. Override collect_field_validation_errors() as well when independent
sibling failures should survive file-level aggregation; its default appends
the single validate_fields() result. ValidatedFields is crate-constructed
after intrinsic, profile, and adapter-local validation. The lowerer owns all
field grammar, including documentation and annotation order, access sections,
tags, delimiters, and terminators. Keep every field type in a %T slot and
compose initializers or raw annotations as nested CodeBlocks.
CodeLang::validate_property() and CodeLang::lower_property() form the
corresponding seam for one computed property. PropertyIntent exposes the
semantic context, owner when present, property type, read and write bodies,
modifiers, documentation, and attributes. Override
collect_property_validation_errors() when multiple independent target-local
failures should survive file-level aggregation. ValidatedProperty is
crate-constructed after intrinsic, profile, and adapter-local validation. The
lowerer returns Vec<CodeBlock> because one property may become separate read
and write accessor declarations. Preserve its type in %T slots and compose
bodies and raw annotations structurally.
CodeLang::validate_type_members() is the validation-only seam for
relationships among one type’s fields, computed properties, and explicit
methods after their per-family validation has run. Override
collect_type_members_validation_errors() when several independent owner-wide
failures should be aggregated. Use it for target-derived relationships such as
case-folded accessor/method collisions. It has no matching lowerer: properties
still lower one at a time through lower_property(), and the intent must not
grow placement, namespace-layout, or other grammar policy.
The remaining interface mixes semantic validation hooks with older grammar fragments used by compatibility lowerers. Grammar-oriented methods must be absorbed by complete language-local lowering rather than multiplied:
| Method | Example | Purpose |
|---|---|---|
capabilities() | Strict type, function, field, property, and variant profiles | Declare semantic representability by context and form |
validate_type() | TypeIntent -> Result<(), _> | Add target-local checks after crate-owned complete-type validation |
collect_type_validation_errors() | TypeIntent + error sink | Add independent target-local type failures during file validation |
lower_type() | ValidatedType -> non-empty Vec<CodeBlock> | Own complete type-declaration grammar; permissive adapters default to frozen compatibility lowering |
render_visibility() | "public ", "pub " | Visibility prefix |
function_keyword() | "function", "fn" | Function declaration keyword |
abstract_modifier_capability() | AbstractMethod, VirtualMethod | Semantic meaning of the legacy abstract modifier |
function_form() | Function, Constructor, Destructor | Classify declaration form for capability validation |
constructor_name_matches() | constructor, init, or declaring type | Recognize implicit constructor spellings with or without an owning type |
static_constructor_name_matches() | true / false for name and owner | Decide whether a constructor-shaped static member is still a constructor |
constructor_name_with_return_type_is_function() | true / false | Let an explicit return type disambiguate an owner-named ordinary method |
constructor_name_is_valid() | true / false for name and owner | Reject explicitly marked constructors whose names violate local syntax |
type_member_declaration_context() | Member, InterfaceMember | Select concrete or contract member rules for each TypeKind |
function_parameters_are_typed() | true / false for the complete list | Refine required typing for receiver spellings or shared annotations |
function_body_policy() | Required, Forbidden, Optional | Refine profile body policy when modifiers change the rule |
maximum_function_parameters() | maximum arity or None | Refine profile arity when modifiers change the limit |
function_visibility_is_valid() | true / false | Reject form- or modifier-specific visibility before emission |
function_parameters_require_trailing_defaults() | true / false | Require every defaulted parameter to follow required parameters |
validate_function_type_constraints() | Result<(), SigilStitchError> | Validate whether the complete type-constraint set is semantically representable |
requires_complete_function_type_information() | true / false | Require partial type metadata to form one complete typed declaration |
constructor_return_type_is_valid() | true / false for one type | Restrict constructor return annotations after capability validation |
validate_function() | FunctionIntent -> Result<(), _> | Add target-local checks after crate-owned semantic validation |
lower_function() | ValidatedFunction -> CodeBlock | Own complete function grammar; defaults to the frozen compatibility lowerer |
validate_fields() | FieldSequenceIntent -> Result<(), _> | Add target-local checks after crate-owned field validation |
collect_field_validation_errors() | FieldSequenceIntent + error sink | Add independent target-local sibling errors during file validation |
lower_fields() | ValidatedFields -> CodeBlock | Own complete field-sequence grammar; defaults to frozen compatibility lowering |
validate_property() | PropertyIntent -> Result<(), _> | Add target-local checks after crate-owned property validation |
collect_property_validation_errors() | PropertyIntent + error sink | Add independent target-local property errors during file validation |
lower_property() | ValidatedProperty -> Vec<CodeBlock> | Own complete property grammar; defaults to frozen compatibility lowering |
validate_type_members() | TypeMembersIntent -> Result<(), _> | Add target-local checks across semantic member families after per-family validation |
collect_type_members_validation_errors() | TypeMembersIntent + error sink | Add independent target-derived cross-member errors during file validation |
validate_variants() | VariantIntent -> Result<(), _> | Add target-local checks after crate-owned sequence validation |
collect_variant_validation_errors() | VariantIntent + error sink | Add independent target-local sibling errors during file validation |
lower_variants() | ValidatedVariants -> CodeBlock | Own complete variant-sequence grammar; defaults to frozen compatibility lowering |
Legacy type hooks such as type_keyword(),
methods_inside_type_body(), emit_newtype_decl(),
abstract_type_modifier_is_valid(), and type_decl_syntax() exist only for
the permissive compatibility lowerer. A new adapter does not implement them.
See the legacy surface matrix.
Renderer Events
The renderer requests five complete language-owned results:
indent_unit() -> borrowed indentation bytes
render_statement_end() -> complete statement suffix or error
render_block_open(intent, condition) -> opener suffix or error
render_block_close(intent, condition) -> final closer or error
render_branch_transition(intent, condition) -> outgoing closer and connector or error
These methods expose operations the renderer actually performs, not a public
grammar matrix. A language may use private local helpers, but punctuation,
keywords, and event ordering stay in its module. The provided defaults read
BlockSyntaxConfig only to preserve 0.6.8 external adapters. The shared config
is deprecated compatibility state and receives no new fields.
Standalone Override Methods
These methods don’t belong to a config struct but have sensible defaults you can override:
escape_reserved()– how reserved words are escaped.qualify_import_reference()– receives the module, original name, and resolved name after complete-set alias assignment. The default returns the resolved name; Go prefixes its package and Haskell uses a module-qualified original name when an alias was assigned, paired with aqualifiedimport for that symbol. The two-argumentqualify_import_name()is the frozen 0.6.8 bridge.line_comment_suffix()– suffix for line comments (default"").
Deprecated standalone declaration hooks such as type fragments, preamble ordering, optional-field style, and property style are listed with their replacements in the legacy surface matrix.
render_imports() receives a deduplicated, alias-resolved ImportGroup and
emits the file’s import header. render_doc_comment() emits spec-level doc
comments. Study src/lang/typescript.rs for ES module imports or
src/lang/rust.rs for use paths.
Use Arg::TypeName or %T for every semantic type and compose child blocks
structurally; do not render a TypeName to a string inside a lowerer. A
complete sequence lowerer such as lower_fields() owns every line boundary
its sequence requires, including the boundary after its final declaration. The
complete type lowerer decides spacing and order among child declaration
families.
Step-by-Step Walkthrough
1. Create the language file
Create src/lang/your_lang.rs. Keep semantic types in %T slots. Fragment
hooks omit surrounding whitespace, while complete lowerers own the internal
and terminating line boundaries required by their grammar. Hook errors should
be returned unchanged.
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::import::ImportGroup;
use sigil_stitch::lang::capability::{
FieldCapability, FieldCapabilityProfile, FieldContext, LanguageCapabilities,
TypeCapability, TypeKindCapabilityProfile,
};
use sigil_stitch::lang::{
BlockIntent, CodeLang, RendererLang, TypeIntent, ValidatedFields,
ValidatedType,
};
use sigil_stitch::spec::modifiers::{DeclarationContext, TypeKind, Visibility};
use sigil_stitch::type_name::TypeName;
#[derive(Debug, Clone, Default)]
pub struct YourLang;
impl YourLang {
pub fn new() -> Self {
Self
}
}
const RESERVED: &[&str] = &["if", "else", "for", "while", /* ... */];
const FIELD_CAPABILITIES: &[FieldCapability] = &[
FieldCapability::ExplicitType,
FieldCapability::Initializer,
];
const REQUIRED_FIELD_CAPABILITIES: &[FieldCapability] =
&[FieldCapability::ExplicitType];
const TYPE_PROFILES: &[TypeKindCapabilityProfile<'_>] = &[
TypeKindCapabilityProfile::new(
TypeKind::Class,
&[],
&[TypeCapability::RecordFields],
),
];
const FIELD_PROFILES: &[FieldCapabilityProfile<'_>] = &[
FieldCapabilityProfile::new(
FieldContext::Direct(DeclarationContext::Member),
FIELD_CAPABILITIES,
)
.with_required_capabilities(REQUIRED_FIELD_CAPABILITIES),
FieldCapabilityProfile::new(
FieldContext::TypeMember(TypeKind::Class),
FIELD_CAPABILITIES,
)
.with_required_capabilities(REQUIRED_FIELD_CAPABILITIES),
];
impl RendererLang for YourLang {
fn file_extension(&self) -> &str { "yl" }
fn reserved_words(&self) -> &[&str] { RESERVED }
fn line_comment_prefix(&self) -> &str { "//" }
fn render_string_literal(&self, s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
fn indent_unit(&self) -> &str { " " }
fn render_statement_end(&self) -> Result<&str, SigilStitchError> {
Ok(";")
}
fn render_block_open(
&self,
_intent: BlockIntent,
_condition: &str,
) -> Result<&str, SigilStitchError> {
Ok(" {")
}
fn render_block_close(
&self,
_intent: BlockIntent,
_condition: &str,
) -> Result<&str, SigilStitchError> {
Ok("}")
}
fn render_branch_transition(
&self,
_intent: BlockIntent,
_condition: &str,
) -> Result<String, SigilStitchError> {
Ok("} ".to_owned())
}
fn lower_type_name(
&self,
type_name: &TypeName,
) -> Result<CodeBlock, SigilStitchError> {
lower_your_lang_type_name(type_name)
}
}
impl CodeLang for YourLang {
fn capabilities(&self) -> LanguageCapabilities<'_> {
// Add the language's exact type, function, and variant profiles too.
LanguageCapabilities::strict()
.with_types(TYPE_PROFILES)
.with_fields(FIELD_PROFILES)
}
fn render_doc_comment(&self, lines: &[&str]) -> String {
let mut out = String::from("/**\n");
for line in lines {
out.push_str(&format!(" * {line}\n"));
}
out.push_str(" */\n");
out
}
fn render_imports(&self, imports: &ImportGroup) -> String {
let mut out = String::new();
for entry in imports.entries() {
out.push_str(&format!(
"import {{ {} }} from \"{}\";\n",
entry.resolved_name(),
entry.module,
));
}
out
}
fn validate_type(&self, type_: TypeIntent<'_>) -> Result<(), SigilStitchError> {
let mut chars = type_.name().chars();
let valid_identifier = chars
.next()
.is_some_and(|first| first == '_' || first.is_ascii_alphabetic())
&& chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric());
if !valid_identifier || self.reserved_words().contains(&type_.name()) {
return Err(SigilStitchError::InvalidTypeDeclaration {
type_name: type_.name().to_string(),
reason: "YourLang requires a non-keyword identifier".to_string(),
});
}
if !matches!(
type_.modifiers().visibility,
Visibility::Inherited | Visibility::Public
) {
return Err(SigilStitchError::InvalidTypeDeclaration {
type_name: type_.name().to_string(),
reason: "YourLang types support only inherited or public visibility".to_string(),
});
}
if type_.modifiers().is_abstract || !type_.extra_members().is_empty() {
return Err(SigilStitchError::InvalidTypeDeclaration {
type_name: type_.name().to_string(),
reason: "YourLang classes do not support abstract or opaque members".to_string(),
});
}
Ok(())
}
fn lower_type(
&self,
type_: ValidatedType<'_>,
) -> Result<Vec<CodeBlock>, SigilStitchError> {
let mut block = CodeBlock::builder();
if !type_.doc().is_empty() {
let lines: Vec<&str> = type_.doc().iter().map(String::as_str).collect();
block.add("%L", self.render_doc_comment(&lines));
block.add_line();
}
block.add(
"%Lclass %L {",
(
self.render_visibility(
type_.modifiers().visibility,
DeclarationContext::TopLevel,
),
type_.name(),
),
);
block.add_line();
block.add("%>", ());
if let Some(fields) = type_.fields() {
block.add_code(self.lower_fields(fields.clone())?);
}
block.add("%<}", ());
block.add_line();
Ok(vec![block.build()?])
}
fn lower_fields(
&self,
fields: ValidatedFields<'_>,
) -> Result<CodeBlock, SigilStitchError> {
let mut block = CodeBlock::builder();
for field in fields.fields() {
if !field.doc().is_empty() {
let lines: Vec<&str> = field.doc().iter().map(String::as_str).collect();
block.add("%L", self.render_doc_comment(&lines));
block.add_line();
}
block.add(
"%L%L: %T",
(
self.render_visibility(
field.modifiers().visibility,
DeclarationContext::Member,
),
self.escape_field_name(field.name()),
field.field_type().clone(),
),
);
if let Some(initializer) = field.initializer() {
block.add(" = %L", initializer.clone());
}
block.add(";", ());
block.add_line();
}
block.build()
}
// Remaining spec support methods...
fn render_visibility(&self, vis: Visibility, _ctx: DeclarationContext) -> &str {
match vis {
Visibility::Public => "public ",
Visibility::Private => "private ",
Visibility::Protected => "protected ",
_ => "",
}
}
}
This abbreviated walkthrough is intentionally ignored by rustdoc because the
hypothetical adapter omits complete helper implementations. The runnable
CodeLang rustdoc example in the crate compiles as part of cargo test --doc;
use that example as the executable contract while implementing an adapter.
2. Register the module
Add to src/lang/mod.rs:
/// YourLang language support.
pub mod your_lang;
3. Write tests
Create a test directory tests/your_lang/ with a main.rs entry point and submodules:
tests/your_lang/main.rs:
mod golden;
mod quote_basic;
mod builder_basic;
tests/your_lang/quote_basic.rs – sigil_quote! macro tests:
use sigil_stitch::prelude::*;
fn render(block: &CodeBlock) -> String {
FileSpec::builder("test.yl")
.add_code(block.clone())
.build()
.unwrap()
.render(80)
.unwrap()
}
#[test]
fn test_basic_statement() {
let block = sigil_quote!(YourLang {
const x = 1;
});
golden::assert_golden("your_lang/basic_statement.yl", &render(&block));
}
tests/your_lang/builder_basic.rs – builder API tests (CodeBlock, TypeSpec, FunSpec, FileSpec).
4. Run the full validation suite
Run the repository checks after the adapter’s advertised declaration families have complete validation and lowering. A strict profile without its matching complete lowerer is an implementation error, not a reason to bless output.
just check
5. Review intentional golden changes
just bless
This runs tests with BLESS=1 and writes test-goldens/your_lang/*.yl from the
actual output. Use it only when the output change is intentional, then inspect
every changed fixture. A strict adapter that advertises a type profile but
omits lower_type() fails closed with MissingTypeLowerer; blessing cannot
turn an incomplete adapter into a valid one. Returning an empty vector or
empty block likewise fails with EmptyTypeLowering. Follow the external-adapter
migration sequence
when migrating an existing adapter family by family.
lower_type_name() owns generic application and every other type-expression
form. Declaration placement—where declared type parameters, bounds, bases,
constructors, and members appear—belongs in the relevant complete declaration
lowerer. Spell the complete generic declaration grammar locally; do not call
generic_syntax() or render_type_params() from a new lowerer. A strict
function profile without lower_function() fails closed with
MissingFunctionLowerer, just as an incomplete strict type family fails with
MissingTypeLowerer.
Use the owner’s generic_params() iterator and borrowed GenericParamView
accessors for binding intent. Ordinary, named, and constructor kinds are not
interchangeable: preserve the supplied domain and kind or reject them with
SigilStitchError. Do not copy modern bindings into deprecated
TypeParamSpec values to invoke a native lowerer. That conversion is reserved
for the frozen compatibility boundary, which rejects unrepresentable modern
domains instead of dropping metadata.
Type applications and callable sequences likewise retain complete expansion
patterns, labels, and presence intent until local type-name lowering. Targets
own ordering, precedence, and representability; the library does not infer
bindings or evaluate packs. For generated-source compiler examples, see the
local fixtures in tests/generated-source/README.md.
Reference Implementations
Study these existing implementations for patterns similar to your target:
| Language | File | Notable Patterns |
|---|---|---|
| TypeScript | src/lang/typescript.rs | ES module imports, type-only imports, single-quoted strings |
| Rust | src/lang/rust.rs | use paths, struct+impl split, pub(crate) visibility |
| Python | src/lang/python.rs | Indent-only blocks (no braces), docstrings inside body, from x import y |
| Go | src/lang/go.rs | Package-qualified names (http.Server), bracket generics, func keyword |
| C | src/lang/c.rs | Type-before-name, #include, __attribute__, struct close semicolon |
| C++ | src/lang/cpp.rs | virtual instead of abstract, #include + using, [[attributes]] |
| Bash | src/lang/bash.rs | Keyword-based block closers (fi/done/esac), source imports, shell escaping |
| Scala | src/lang/scala.rs | case class, trait, [T] generics, <: bounds, = {/} blocks |
| Haskell | src/lang/haskell.rs | Split signature style, where/indentation blocks, postfix generics, deriving |
| OCaml | src/lang/ocaml.rs | Postfix generics, let keyword, = /indentation blocks, open Module imports, module_block helper |
Type-Name Lowering
Implement one pure, fallible lower_type_name() match for the complete
TypeName. The adapter owns representability, precedence, punctuation,
wrapping, string escaping, qualified-name spelling, and target-derived imports.
It may use private local helpers, but it must not expose a public matrix of
syntax fragments.
The returned CodeBlock is validation evidence. It must be non-empty and may
contain only type-expression structure. Nested semantic types must be lowered
recursively. Leave only terminal import-aware TypeRef values for the core to
resolve later; never leave an unresolved array, optional, union, function, or
other compound TypeName in the result. Statement boundaries, block-control
nodes, and declaration fragments are invalid in this block.
Use %T for terminal imported symbols introduced by lowering. For example,
Python’s StringLiteral branch composes an importable typing.Literal leaf
with a structured string-literal node. Import collection then sees the same
symbol that final rendering uses. Do not return a parallel import list.
Return an error when the target cannot preserve a variant exactly. Identity
lowering and “closest equivalent” substitutions are valid only when they are
semantically exact for that target. In particular, a language without string
singleton types rejects TypeName::StringLiteral instead of widening it to a
string primitive.
After source rewrite, the core recursively invokes the hook for TypeRef nodes
in direct, nested, and sequenced blocks before import collection, validates each
returned block, and aborts the complete file on any failure. Direct and pretty
rendering then consume the same fully lowered tree. The complete contract and
compatibility rules are in TypeName Validation and
Lowering.
Code Generation Vocabulary
This appendix defines the vocabulary used throughout sigil-stitch. The architecture overview describes how these concepts flow through the implementation, while Declaration Specs and Language Lowering records their ownership boundaries. Versioned exceptions are catalogued in 0.6.8 Legacy Compatibility and Migration.
Declaration Intent
Declaration spec
A structured, language-independent request for a declaration such as a type, function, field, property, or variant. It records semantic intent, not target-language token placement or spelling.
Capability
A semantic feature that a target language may support, require, or reject for a particular declaration kind and context. A capability describes what can be represented; it is not a switch for target grammar.
Language adapter
The target-specific boundary that decides whether declaration intent is representable and converts accepted intent into target-language structure. It owns detailed declaration grammar such as keyword order, punctuation, and metadata placement.
Target grammar
The language-specific keyword order, punctuation, precedence, escaping, and metadata placement used to express accepted declaration intent. Target grammar belongs to the language adapter; it is not a general format abstraction or a capability.
Function intent
A complete function declaration classified by its role and context before target-language validation. It remains semantic and does not contain a partially rendered signature.
Type intent
One complete type declaration before target-language validation. TypeIntent
exposes its kind, semantic modifiers, documentation, annotations, type
parameters and constraints, type relationships, primary-constructor
parameters, variants, and member families without choosing their source order
or spelling.
Closed sum
A type declaration whose complete ordered set of named cases is part of its semantic intent. Each case is unit-shaped or carries positional or named record data. A closed sum may contain no cases: the empty sum is uninhabited. The declaration does not prescribe whether a target uses an enum, algebraic data type, sealed root with generated case declarations, nested declarations, or sibling declarations.
The zero-case form declares a named uninhabited type. A particular target may have a Never or bottom type with the same absence of values, but that is a type-expression or subtype concept rather than this caller-named declaration. The shared model does not equate them; a target may reuse such a type only if it preserves the declaration’s name and every valid use position.
Closed-sum intent is not a sealed modifier or an enum-formatting option. A
case carrying a TypeName owns that payload relationship; it does not assert
that an already-declared type is a nominal subtype of the sum root.
Validated type
A crate-constructed view whose type-level intent and every child declaration
have passed intrinsic, capability, and target-local validation against the
same adapter. ValidatedType exposes child declarations only through their
validated wrappers. The adapter lowers the complete declaration and chooses
whether it produces one block or several; every returned block must be
non-empty.
Type name
A semantic type reference. It remains structured until one selected language
adapter lowers the complete value before import collection. A TypeName may
contain other type names, but it never contains a language-neutral choice of
punctuation, precedence, or layout. Primitive and Raw are explicit
target-aware leaves rather than a general type grammar.
Structured source block
A CodeBlock is the shared Rust container for source nodes associated with one
selected target. Its Rust type is language-agnostic so declaration lowerers,
rewrite, import collection, and rendering can compose it, but its literal
content and structure are not a portable cross-language program.
Generic binding and kind expression
A declaration-owned name with a single, pack, or lifetime domain, optional
kind intent, and supplied bounds. GenericParamSpec records the binding;
GenericParamView borrows the owner’s complete ordered sequence. Uses refer
to names without a shared scope resolver. The target compiler owns inference
and instantiation.
KindExpr records Type, a named kind, or a constructor’s parameter and result
kinds. Declaration lowering owns representation or rejection. Named kinds are
not inferred; legacy raw binder suffixes are not modern kind expressions.
Type application and expansion pattern
Application of a supplied type-level base to ordered arguments. An expansion retains its complete pattern, including every referenced pack. The library does not evaluate the pattern, solve its arity, or reinterpret a tuple as an argument list.
Callable parameter sequence
Ordered scalar slots, repeated-element segments, and complete expansion patterns. Optional presence belongs to a scalar slot and differs from an optional value. Labels and ordering restrictions belong to the selected language, not to a shared rest-parameter grammar.
String literal type
A type inhabited by exactly one decoded string value.
TypeName::StringLiteral stores the value without target quotes or escapes.
The language adapter either lowers it exactly or rejects it; several string
literal types compose through TypeName::Union, not a string-enum or
literal-set abstraction.
Field sequence
The ordered fields owned by one type declaration or one record payload, considered together in their semantic context. A language adapter handles the complete sequence so it can validate collisions and own sequence-level grammar.
Field context
The semantic role in which a field sequence appears: direct emission, ordinary
type members, an ordinary variant record payload, or a closed-sum case record
payload. Keeping the payload contexts distinct prevents support for generated
closed-sum cases from widening ordinary enum behavior. A field context
identifies representability; it does not prescribe placement, punctuation, or
separators. The
Direct(DeclarationContext) payload retains only the pre-0.6.8 direct-emission
placement input. It is a narrow compatibility exception, not a reusable
placement or target-grammar model.
Property intent
One computed property with a value type, read and/or write behavior, semantic modifiers, documentation, and attributes before target-language validation. It does not choose accessor syntax or a field-style representation.
Property context
The semantic role in which one computed property appears: direct emission or a
member of an owning TypeKind. The Direct(DeclarationContext) payload exists
only to retain the pre-0.6.8 public emission facade; it is not a general
accessor-placement model.
Type members intent
A validation-only view of one owning type’s semantic fields, computed
properties, and explicit methods. TypeMembersIntent exists for relationships
that cannot be checked within one member family, such as target-derived name
collisions. It contains no target grammar, has no validated wrapper, and does
not participate in lowering. It is not a sequence-level replacement for
PropertyIntent. Each adapter defines its own emitted namespaces: a
field/property pair collides in TypeScript, Kotlin, Swift, and Scala only when
both declarations occupy the same target-local namespace, while PHP properties
instead derive case-insensitive accessor-method names.
Read accessor and write accessor
Semantic read and write behavior supplied by a property’s getter and setter bodies. A language adapter may express that behavior as accessor declarations, a computed-property body, or target-local methods. The capability names do not prescribe getter keywords, setter placement, or surrounding grammar.
Optional presence
A field semantic in which the containing value may omit the field entirely.
FieldSpec::is_optional() requests this meaning. It is distinct from an
optional value.
Optional value
A value semantic in which a present field can carry the target language’s
absence or null representation. TypeName::Optional expresses this meaning;
it does not make the field itself omissible.
Variant sequence
The ordered variants owned by one type declaration, together with the semantic presence of non-variant members. A variant lowerer handles the sequence as a whole; the owning type lowerer chooses where that sequence appears relative to other member families. Non-variant members include fields, properties, methods, embedded types, and opaque members.
For a closed sum, an empty sequence is meaningful rather than missing input: it declares an empty sum. Ordinary value-enum validation remains independent and may continue to require at least one member for a particular target.
Variant Data
Discriminant
An explicit value that identifies an enum member in a representation where members map to values. It is distinct from an expression passed to an enum constructor.
Constructor arguments
Expressions passed when an enum entry constructs an instance of its declaring enum type. They are values evaluated at the declaration site, not types carried by a sum-type case.
Positional payload
Types carried in order by a sum-type constructor or enum case. The payload has positions but no field names.
Record payload
Named, typed fields carried by a sum-type constructor or enum case. These are case-local payload fields, not ordinary members of the enclosing type.
Transformation Boundaries
Source-tree rewrite
One language-local structural correction is applied exactly once to each
source CodeBlock after declaration lowering and before type-name lowering. It
is for target source fixups that require a tree-level view, such as joining a
Go IIFE close to its invocation. It does not own declaration grammar, type
grammar, validation, or final layout. Raw content, raw import metadata, and
blocks returned by type-name lowering are not source-rewrite inputs.
Type-name lowering
The fallible conversion of one complete TypeName into a non-empty
CodeBlock before import collection. The selected language adapter owns
representability, precedence, punctuation, string escaping, and any
target-derived type imports. A successful lowering block may retain only
terminal import-aware type references; unresolved compound type names fail
closed.
Import conflict set
The complete peer set of semantic imports that request the same local binding within one file. Exact explicit bindings are hard constraints; preferred aliases and natural names are soft requests. The resolver assigns every peer atomically and does not receive an incoming import, current owner, winner/loser pair, or mutable claim table.
Lowering
The conversion of validated declaration intent into structured output that follows one target language’s grammar. Lowering decides source structure but does not perform final layout.
Rendering
The final interpretation of structured output into source text, including layout, indentation, import aliases, and width-aware line breaking. Rendering does not decide whether a declaration is representable.
Escape hatch
An explicitly target-specific payload embedded in otherwise structured intent when the shared declaration vocabulary cannot express a source fragment. An escape hatch deliberately gives up portability for that fragment.
0.6.8 Legacy Compatibility and Migration
This appendix is the reference for public behavior inherited from sigil-stitch 0.6.8. It explains what remains available, where compatibility is intentionally restricted, and how callers and external language adapters move to the current declaration model.
The declaration-lowering design defines the current ownership model. This appendix documents the compatibility bridge; it does not extend that bridge or define a second architecture.
Compatibility Boundary
In this guide, legacy means a public declaration API, serialized contract, or adapter hook that was available in 0.6.8. It does not include capability, intent, or validated-view concepts introduced after 0.6.8.
The compatibility contract is:
- Public 0.6.8 declaration surfaces remain available during 0.7 unless an explicit compatibility decision says otherwise.
- Legacy grammar-oriented APIs are deprecated so new use is visible at compile time. They may still be read by a frozen compatibility lowerer.
- Existing external
CodeLangimplementations inherit permissive capability profiles and provided compatibility lowerers. - Compatibility preserves valid old behavior when the semantic input can prove it. It does not require a built-in adapter to keep generating malformed or unverifiable target code.
- Concepts introduced after 0.6.8 may change without another compatibility layer.
- Requiring strict profiles or removing provided compatibility lowerers is a separate 0.8 decision, not an automatic consequence of deprecation.
Deprecated does not mean that the shared grammar model is still extensible. Do not add a field, flag, or enum variant to a legacy configuration type for new syntax.
Which Path Applies?
| Reader | Current path | Compatibility responsibility |
|---|---|---|
| Ordinary builder user | Use semantic builders and owner-aware TypeSpec composition | Replace deprecated aliases and direct facades when the owner affects validity |
| Existing 0.6.8 external adapter | Provided permissive profiles and frozen lowerers keep the adapter source-compatible | Migrate one declaration family at a time and retain output-parity tests |
| New external adapter | Declare strict capabilities and implement complete validate_* / lower_* seams | Do not model new grammar through deprecated configuration |
| Built-in adapter | Exact strict profiles and language-local lowering | Never consult migrated-family legacy grammar outside compatibility code |
Current Migration State
Types, functions, field sequences, computed properties, and enum-variant
sequences use complete language-owned lowering for every built-in adapter.
TypeSpec validates one complete declaration, constructs ValidatedType with
validated children, and delegates once to CodeLang::lower_type().
The compatibility bridge restores the exact 0.6.8 source signatures touched by
this migration and marks the shared grammar surface deprecated. A checked
external-adapter fixture overrides the complete old trait surface, the finite
documented TypeName JSON set is checked as serde_json::Value, and
cargo-semver-checks 0.50.0 currently reports no unapproved break from tag
0.6.8. The compatibility manifest and fixtures live under
tests/compatibility/.
Type expressions now use complete fallible
RendererLang::lower_type_name() implementations for every built-in adapter
before import collection. The provided default is only the frozen
type-presentation bridge for external adapters written against 0.6.8.
Complete-set fallible import resolution and language-local quote handling are
also implemented. Built-in declaration-generic grammar has moved out of
GenericSyntaxConfig. Final rendering now calls indent_unit(),
render_statement_end(), render_block_open(), render_block_close(), and
render_branch_transition() directly. Every built-in adapter owns all five
operations. Their provided defaults are the only renderer path that interprets
legacy block configuration and hooks for an unchanged 0.6.8 external adapter.
The source-read inventory below names each retained compatibility reader.
The provided external-adapter lowerers remain private implementation details. They freeze 0.6.8 behavior; they are not examples for new adapters.
Current Configuration-Read Inventory
This inventory distinguishes current source reads from the accepted target state. A built-in method that returns a legacy config is a provider, not by itself evidence that the current built-in path consumes that config. Update the inventory whenever a reader moves behind a frozen compatibility boundary.
| Shared surface | Current production readers | Classification | Retirement owner and retained boundary |
|---|---|---|---|
TypePresentationConfig, TypePresentation, FunctionPresentation, AssociatedTypeStyle, BoundsPresentation, WildcardPresentation; RendererLang::type_presentation() | src/type_name_lowering/compatibility.rs and the deprecated direct document facade in src/type_name_render.rs | Compatibility-only type grammar | Built-ins implement complete local lower_type_name() operations; only the frozen 0.6.8 default and direct compatibility facade retain the matrix |
RendererLang::module_separator() | src/type_name_lowering/compatibility.rs and the deprecated direct document facade in src/type_name_render.rs | Compatibility-only qualified-name grammar | Built-ins own qualified-name spelling; the old accessor remains only in the frozen type bridge and direct facade |
GenericSyntaxConfig; RendererLang::generic_syntax() in type rendering | src/type_name_lowering/compatibility.rs and the deprecated direct document facade in src/type_name_render.rs | Compatibility-only type grammar | Built-ins own generic type application locally; the frozen bridge and direct facade retain the old delimiters and placement |
GenericSyntaxConfig; RendererLang::generic_syntax() in declarations | src/spec/where_spec.rs, src/lang/function_lowering/compatibility.rs, src/lang/type_lowering/compatibility.rs, src/lang/compatibility_markers.rs, and the deprecated direct newtype facades in src/lang/{go,kotlin,scala}.rs | Compatibility-only declaration grammar | Built-in complete lowerers own type-parameter, bound, lifetime, kind, context-bound, and constraint-clause grammar; the named compatibility modules and direct facades retain the frozen 0.6.8 read |
BlockSyntaxConfig::indent_unit | src/spec/where_spec.rs reads it only in deprecated direct where-clause helpers; compatibility lowerers and the provided renderer-event defaults retain their bridge reads | Compatibility-only declaration and renderer behavior | indent_unit() owns final-renderer indentation and built-in declaration lowerers use target-local indentation; compatibility paths retain the frozen field |
BlockSyntaxConfig::{uses_semicolons, block_open, block_close, close_on_transition} | Only compatibility lowerers and the provided renderer-event defaults consume these fields in production | Compatibility-only renderer and declaration grammar | Complete renderer events and declaration lowerers own built-in grammar; frozen compatibility paths continue to interpret old adapters |
BlockSyntaxConfig::{field_terminator, type_close_terminator, bases_close} | Only src/lang/field_lowering/compatibility.rs and src/lang/type_lowering/compatibility.rs consume these fields in production | Compatibility-only declaration grammar | No current replacement config; complete declaration lowerers own these bytes locally and the old fields remain frozen |
FunctionSyntaxConfig, OptionalFieldStyle, PropertyStyle, and property_getter_keyword() | The function, field, property, and type compatibility modules consume the applicable surfaces | Compatibility-only declaration grammar | Already outside built-in complete lowerers; retain only for the deprecated 0.6.8 bridge |
TypeDeclSyntaxConfig | The function, field, property, and type compatibility modules read it; deprecated ParameterSpec::emit_into() also reads it for the direct 0.6.8 parameter facade | Compatibility-only declaration grammar | Complete built-in lowerers already own these bytes; retain the reads only in frozen compatibility modules and the deprecated direct facade |
EnumAndAnnotationConfig and VariantValueFormat | The function, field, property, type, and variant compatibility modules read them; AnnotationSpec::emit_with() and deprecated ParameterSpec::emit_into() retain direct 0.6.8 facade behavior; permissive variant dispatch reads variants_before_fields through the variant compatibility module | Compatibility-only annotation, parameter, and variant grammar | Complete built-in lowerers use emit_with_syntax() and target-local variant grammar; retain shared reads only at the named compatibility boundaries |
Shared QuoteStyle, the three public quote_style fields, and with_quote_style() | One narrow helper in each of TypeScript, JavaScript, and Python normalizes the preserved field to a target-local quote character; downstream string and import rendering no longer read the shared enum | Compatibility-held user preference whose concrete grammar belongs to each language | Language-local quote handling owns escaping and conveniences; the old enum, field, and setter remain deprecated shims |
Built-in unit tests that directly inspect config-return values are temporary
migration expectations, not additional production readers.
tests/renderer_parity_tests.rs protects the exact built-in renderer-event
matrix, direct/pretty parity, and legacy indentation compatibility;
the field/property custom-adapter tests exercise compatibility defaults; and
tests/assert_quote_tests.rs plus the three language unit suites protect the
quote shim. Definitions and overrides under src/lang/*.rs remain until the
corresponding compatibility surface can be removed in a future major version.
Legacy Surface Matrix
| Family | Legacy surface | Compatibility behavior | Current replacement |
|---|---|---|---|
| Capabilities | No capabilities() override | External adapters receive LanguageCapabilities::permissive() | Return a strict matrix with exact family profiles |
| Type expressions | type_presentation(), TypePresentationConfig, TypePresentation, FunctionPresentation, generic_syntax(), GenericSyntaxConfig, qualified-name presentation accessors, and TypeName::to_doc_with_lang() | The provided lower_type_name() reproduces 0.6.8 output for old TypeName variants and rejects StringLiteral or any later variant; the direct document method remains only as a deprecated terminal facade | Implement complete fallible RendererLang::lower_type_name() and keep imports in the returned CodeBlock |
TypeName matching and documented JSON values | Exhaustive matches over the pre-0.6.8 variants; concrete TypeName JSON values documented before 0.7 | Supported Rust constructors remain; checked fixtures preserve the documented JSON values. Generic Serde support does not promise compatibility for other representations, binary encodings, enum ordinals, field order, or serializer bytes | Add a wildcard arm to downstream matches; do not reinterpret unknown data or rely on an undocumented wire format |
| Functions | function_keyword(), fun_block_open(), function_syntax(), FunctionSyntaxConfig, ParamListStyle, FunctionSignatureStyle, ConstructorDelegationStyle, and WhereClauseStyle | The provided lower_function() interprets them for external adapters | validate_function() and complete lower_function() |
| Types | type_keyword(), methods_inside_type_body(), type_kind_suffix(), emit_newtype_decl(), type_header_block_open(), type_body_prefix() / type_body_suffix(), emit_type_close_suffix(), abstract_type_modifier_is_valid(), type_decl_syntax(), and type-emitter reads of function_syntax() / enum_and_annotation() | The provided lower_type() interprets them only for permissive external adapters and does not infer later closed-sum intent | validate_type(), complete lower_type(), and the dedicated closed-sum builder |
| Type parameters | generic_syntax(), render_type_params(), render_type_param_kind(), and ParameterSpec::emit_into() | The provided permissive declaration lowerers and direct facades preserve frozen 0.6.8 grammar | Complete language-owned type and function lowering; strict adapters without a complete function lowerer fail with MissingFunctionLowerer |
| Type application inputs | TypeName::Generic, TypeName::generic() | Explicitly deprecated; old storage and checked JSON fixtures remain supported | TypeName::Application / application() with ordered TypeArgument values |
| Callable type inputs | TypeName::Function, TypeName::function() | Explicitly deprecated; existing scalar-slot meaning remains supported | TypeName::Callable / callable() with CallableParam values |
| Declaration binding inputs | TypeParamSpec, TypeParamKind, and FunSpecBuilder::add_type_param() / TypeSpecBuilder::add_type_param() | Explicitly deprecated; released bounds, context bounds, lifetime intent, and raw Scala suffix metadata remain compatibility inputs | Fallible GenericParamSpec, GenericParamDomain, KindExpr, and add_generic_param() |
| Variable spelling | variable_prefix() | Frozen function, field, property, and type compatibility lowerers interpret the adapter’s prefix | Complete language-owned declaration lowering |
| Preambles | doc_before_annotations(), doc_comment_inside_body() | Frozen compatibility lowerers may read them | Emit documentation and attributes in each complete lowerer |
| Fields | optional_field_style(), OptionalFieldStyle | The provided lower_fields() freezes the old field emitter | FieldCapability, FieldContext, TypeName::Optional, and complete lower_fields() |
| Properties | property_style(), property_getter_keyword(), PropertyStyle | The provided lower_property() freezes the old property emitter | PropertyContext, property capabilities, and complete lower_property() |
| Variants | VariantContext, .value(), VariantValueFormat, variants_before_fields | Only permissive external adapters retain ownerless positional lowering; strict built-ins require an owner and complete sequence | Add variants to TypeSpec; use .discriminant() or .constructor_argument() |
| Variant payload builders | .associated_type(), .add_field() | Deprecated aliases remain available | .positional_payload(), .record_payload_field() |
| Renderer events and block nodes | block_syntax(), BlockSyntaxConfig, block_open_for(), block_close_for(), intent-aware bridge hooks, and legacy string-only block nodes | Provided event defaults interpret old config and hooks; old nodes remain source-constructible and renderable, unchanged external adapters remain compatible, and no versioned Serde representation is promised | BlockIntent, indent_unit(), render_statement_end(), render_block_open(), render_block_close(), and render_branch_transition() |
Direct FieldSpec::emit() and PropertySpec::emit() remain public facades. Their
DeclarationContext input is retained only as a compatibility payload. Prefer
adding members to TypeSpec whenever the owning TypeKind or other members can
affect validity.
Structured Parametric Inputs
New bindings are constructed fallibly; existing released constructors keep
their signatures. Owners retain one ordered binding sequence, including mixed
old and new inputs, and expose it through borrowed generic_params() views.
Modern kind annotations are never reconstructed from legacy raw suffixes.
GenericParamView::legacy_kind() is explicitly deprecated and exists only
for that retained compatibility metadata.
Ordinary legacy application arguments migrate to TypeArgument::Single.
Ordinary legacy callable slots migrate to unnamed required
CallableParam::Single values. Expansion patterns, optional presence, and
repeated-element segments are new explicit intent; no old vector is
reinterpreted as a pack. A frozen compatibility adapter rejects modern
application/callable values or binding domains it cannot preserve.
The new expression enums and binding domains are non-exhaustive for downstream matching. This adds no unknown-node or cross-version serialization contract. Unreleased owner-view APIs and the unreleased closed-sum builder are not classified as released legacy surfaces.
Frozen Grammar Configuration
The legacy structs mix renderer mechanics with type-expression and declaration
grammar. Built-in type-name and declaration lowering no longer read
type_presentation() or generic_syntax(); only the frozen external-adapter
bridges and direct compatibility facades do. Final renderer paths no longer
read block_syntax(). Complete language-local lowerers own type and declaration
grammar, while direct renderer-event methods plus indent_unit() own final
rendering. Frozen compatibility defaults and lowerers may continue interpreting
the old values; none of these structs receives new fields or variants.
TypePresentationConfig and GenericSyntaxConfig
These values describe the pre-0.6.8 shared type-expression and declaration
grammar: generic delimiters, bounds, prefix and postfix wrappers, infix
separators, qualified-name separators, and function-type placement. The
provided RendererLang::lower_type_name(), permissive declaration lowerers,
and deprecated direct facades continue to interpret the applicable fields so
an existing external adapter remains source compatible.
These bridges are intentionally closed. Type-name compatibility rejects
TypeName::StringLiteral and every later semantic variant, even if one of the
old presentation patterns could produce plausible text. New and built-in
adapters implement complete fallible type-name and declaration lowering instead
of extending the configuration.
FunctionSyntaxConfig
| Field | 0.6.8 meaning |
|---|---|
return_type_separator | Text between a parameter list and suffix return type |
async_keyword, async_suffix, async_suffix_before_return | Async spelling and placement |
abstract_keyword | Abstract/virtual spelling |
param_list_style | Tupled or curried parameter layout |
function_signature_style | Merged or split declaration layout |
constructor_keyword, constructor_delegation_style | Constructor spelling and delegation placement |
where_clause_style | Inline, block, or repeated where-clause placement |
empty_body | Legacy body placeholder |
type_params_before_return_type | Legacy type-parameter placement switch |
Complete function lowerers own all of these choices locally. An adapter may share private policy-free helpers, but new syntax must not add another field to this table.
TypeDeclSyntaxConfig
| Field | Frozen compatibility meaning |
|---|---|
type_before_name, return_type_is_prefix, type_annotation_separator | Type/name ordering used by compatibility lowerers |
super_type_keyword, super_type_separator, super_type_subsequent_separator | Base-type grammar |
implements_keyword | Implemented-interface grammar |
type_alias_target_first | Alias target/name ordering |
supports_primary_constructor | Legacy primary-constructor switch |
These fields may be read only by frozen compatibility lowerers. New adapters implement complete declaration lowering instead.
EnumAndAnnotationConfig
| Field | Transitional or compatibility meaning |
|---|---|
variant_prefix, variant_prefix_first, variant_separator, variant_trailing_separator, variants_before_fields, variant_value_format | Frozen external-adapter variant grammar |
annotation_prefix, annotation_suffix | Legacy annotation spelling; complete lowerers use local structured emission |
readonly_keyword, mutable_field_keyword | Frozen parameter/property-promotion fragments |
Quote-style compatibility
QuoteStyle, the public quote_style fields, and
with_quote_style(QuoteStyle) predate 0.6.8 and remain source-compatible. They
are deprecated shims rather than a general quote configuration shared by new
languages. TypeScript, JavaScript, and Python each own quote normalization,
escaping, and output locally. Their with_single_quotes() and
with_double_quotes() conveniences update the preserved field so there is one
stored choice and no precedence rule.
Import resolver compatibility
ImportGroup::resolve() and resolve_with_explicit() remain the exact
deprecated, infallible 0.6.8 algorithms. They preserve first-encountered and
explicit-entry precedence, including cases that can produce duplicate local
bindings. ImportGroup::try_resolve() and try_resolve_with() are the current
fallible complete-set entry points; the old methods are not implemented by
unwrapping the new resolver.
Builder Migration Recipes
Type names and exhaustive matches
TypeName gains StringLiteral(String) in 0.7 and is marked
#[non_exhaustive]. Downstream code that previously matched every variant must
add a wildcard arm and decide whether an unknown type should be rejected or
passed back to sigil-stitch for language-owned lowering. Do not widen an
unknown variant to Primitive or Raw.
The string-literal payload is the decoded string value. Several values compose
as TypeName::Union; do not preserve hand-written quotes in Raw when the
semantic singleton form is available. Exact fixtures cover the TypeName JSON
values documented before 0.7. No other Serde representation or binary format
receives a cross-version guarantee. Deserializing an unknown variant remains
an error; it is never reinterpreted as another type.
Closed sums
Use the dedicated closed-sum builder for a complete set of unit, positional,
or record cases. Do not encode this intent as TypeKind::Enum plus a sealed
flag, discriminants, or constructor arguments. The 0.6.8 TypeKind enum stays
unchanged; ordinary enum construction and matching remain source-compatible.
A zero-case closed sum is a named uninhabited declaration. A target may use a canonical empty type only when that representation preserves the declaration’s name and valid use positions exactly. The shared model does not add a Never type reference or equate declaration intent with bottom-subtype semantics in this feature. Targets without an exact named empty-sum declaration reject that shape.
Closed-sum intent is new in 0.7 and has no mixed-version interpretation. Producers, consumers of newly serialized specs, and external adapters using the new semantic views must upgrade together. This requirement does not create a general cross-version Serde or binary-format contract.
Enum variants
- Replace direct
EnumVariantSpec::emit(..., VariantContext)withTypeSpec::add_variant()so the adapter receives the owner and full sequence. - Replace
.value(x)with.discriminant(x)when the value identifies the member, or.constructor_argument(x)when the enum entry invokes a constructor. - Replace
.associated_type(t)with.positional_payload(t)and.add_field(f)with.record_payload_field(f).
Strict built-ins reject an ownerless variant when first/last flags cannot prove valid separators, payload grammar, or section termination.
Optional fields
FieldSpec::is_optional() means that the containing value may omit the field.
TypeName::Optional(T) means that a present field can carry an absent or null
value. Replace OptionalFieldStyle with the semantic form actually intended;
do not infer one meaning from the other.
Computed properties
Add a PropertySpec to TypeSpec instead of relying on direct placement when
the target’s owning type or member namespaces affect validity. New adapters
lower read and write behavior from PropertyIntent; they do not select an
accessor model through PropertyStyle.
Primary constructor parameters
Pass only an identifier to ParameterSpec. For Kotlin and Scala, use
.is_property() for an immutable promoted property and
.is_mutable_property() for a mutable one. Do not encode val or var in the
parameter name. Complete language lowerers own that spelling; unsupported
languages reject primary-constructor intent instead of ignoring it.
Haskell and OCaml constructor data is not a primary constructor. Model it with enum-variant positional or record payloads so the algebraic-data adapter sees the payload semantics directly.
C++ static member initializers
For class and struct members, the C++ adapter preserves the pre-C++17
static const spelling only when FieldSpec proves an integral primitive
type. It rejects:
- initialized mutable static members, which require either a C++17
inlinedeclaration or a separate out-of-class definition; and - initialized read-only static members whose type is not provably integral.
TypeName does not currently distinguish an enum type from another named
type, so the adapter does not guess from capitalization or a raw type name. Use
TypeSpecBuilder::extra_member(CodeBlock) for an enum-typed class constant, or
materialize the declaration and out-of-class definition as target-specific
blocks. This restriction prevents a compatibility path from silently emitting
invalid C++.
External Adapter Migration
Migrate one declaration family at a time:
- Implement complete
lower_type_name()handling for every accepted old variant and explicit errors for unsupported forms. - Add a strict profile for every supported semantic context or owner kind.
- Add adapter-local validation for identifier rules, modifier combinations, and relationships the profile cannot express.
- Implement the complete
lower_*seam and preserve every acceptedTypeNameas a%Treference and every nested block as structured%L. - Cover direct and owner-aware success and failure paths, import aliases, and both direct and pretty renderer paths where soft breaks are reachable.
- Remove migrated-family reads of deprecated grammar from the adapter.
Keep rendered-output fixtures while migrating. A provided default is a compatibility bridge, not evidence that an adapter has completed the new seam.
Python Static-Decorator Compatibility
Python retains one adapter-local compatibility recognizer for the 0.6.8 pattern
that combines FunSpec::is_static() with a staticmethod or classmethod
decorator. It applies only to non-constructor member and interface-member
functions.
The recognizer accepts:
AnnotationSpec::new("staticmethod")orAnnotationSpec::new("classmethod"), including an importable annotation with that simple name; or- an opaque annotation block made only of literal/nested-literal nodes whose
trimmed text is exactly
@staticmethodor@classmethod(and the equivalent attribute node).
Other spellings do not acquire static-method semantics. New code should prefer
the structured AnnotationSpec form. This exception is Python-local and must
not become a shared decorator parser or syntax hook.
Compatibility Testing
Run the focused compatibility gates with:
cargo test --test compatibility_0_6_8
just semver-check
The first command compiles the old adapter as an external crate, checks the
restored signatures and structural marker bridges, and compares the bounded
JSON fixtures. The second command tests the report parser and then compares the
complete cargo-semver-checks 0.50.0 record set with the checked allowlist.
Missing, duplicate, malformed, and unexpected approved records fail closed.
For a migrated family, keep tests for:
- an adapter implementing only the 0.6.8 trait surface;
- valid legacy output preserved by the provided lowerer;
StringLiteralrejected by an adapter that implements only the 0.6.8 type presentation surface;- invalid or ownerless built-in input rejected before materialization;
- direct and
FileSpecpaths selecting the actual adapter; - semantic replacements for every deprecated builder alias; and
- serialized legacy nodes or fields that remain part of the public contract.
What Does Not Belong Here
This appendix is not a release history, exhaustive API reference, or rejected-
design catalogue. Release-by-release changes belong in CHANGELOG.md, exact
signatures and deprecation attributes belong in rustdoc, and durable design
rationale belongs in focused records under docs/adr/.