Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 are language-agnostic — no generic parameter. The language enters at render time when you call FileSpec::render() or pass &dyn CodeLang to a renderer. You can build code blocks once and render them for different languages.

Import-aware. When you use %T with a TypeName::Importable, the library records that import. At render time, FileSpec collects all imports from every code block, deduplicates them, and resolves naming conflicts automatically. If two modules export a type named User, the first one encountered keeps the simple name User and the second gets an aliased name (e.g., OtherUser). You never write 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. The RendererLang and CodeLang traits abstract everything that varies between languages: string delimiters, statement terminators, import syntax, visibility keywords, type formatting, annotation style, and more. sigil-stitch ships with implementations for TypeScript, JavaScript, Rust, Go, Python, Java, Kotlin, Swift, Dart, Scala, Haskell, OCaml, C, C++, C#, Lua, Bash, and Zsh. The same CodeBlock, TypeName, and Spec types work across all of them – only the language passed to render() changes.

Design philosophy

Specs emit CodeBlocks, never raw strings. A FunSpec produces a CodeBlock via its .emit() method. A TypeSpec produces one or two CodeBlocks (depending on whether the language places methods inside or outside the type body). The renderer and import system only ever see CodeBlock trees. This means you can add new spec types – or build your own – without touching the renderer or import collector. The format-specifier system and the spec system are fully decoupled.

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 structured declarations. They produce CodeBlocks internally but carry metadata like visibility, annotations, type parameters, and modifiers that the language trait uses to emit correct syntax.
  • FileSpec to render a complete file. It orchestrates the three-pass pipeline: materialize specs into code blocks, collect and resolve imports, then render everything with proper formatting. Pass a target width to file.render(80) and get a String back.

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:

  • %T with user_type rendered as User in the code and added import type { User } from './models' at the top of the file.
  • %S with StringLitArg rendered 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 structural metadata (name, return type, visibility, modifiers) and emit CodeBlocks internally.

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 Emit CodeBlocks

Every spec type follows the same pattern: you configure it with a builder, call .build(), and eventually FileSpec calls .emit() on it to get a CodeBlock. This means:

  • You never write raw import statements. %T handles it.
  • You never manually format function signatures. FunSpec handles it.
  • 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::lang::config::QuoteStyle;
use sigil_stitch::prelude::*;
fn main() {
// Prettier-style: double quotes, no semicolons, .tsx extension.
let ts = TypeScript::new()
    .with_quote_style(QuoteStyle::Double)
    .with_semicolons(false)
    .with_extension("tsx")
    .with_indent("    ");
}
Languagewith_quote_stylewith_indentwith_semicolonswith_extension
TypeScriptyesyesyesyes
JavaScriptyesyesyesyes
Pythonyesyesn/ayes (e.g. pyi)
Javan/ayesn/ayes
Rustn/ayesn/ayes
Gon/ayesn/ayes
Kotlinn/ayesn/ayes (e.g. kts)
Swiftn/ayesn/ayes
Dartn/ayesn/ayes
CSharpn/ayesn/ayes
Luan/ayesn/ayes
Cn/ayesn/ayes (e.g. h)
Cppn/ayesn/ayes (e.g. hpp, cxx)
Bashn/ayesn/ayes (e.g. sh)
Zshn/ayesn/ayes

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

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

SpecifierNameArgumentPurpose
%TTypeTypeNameEmit type reference, track import
%NNameNameArgEmit identifier name
%SStringStringLitArgEmit escaped string literal
%VVerbatimVerbatimStrArgEmit string with interpolation preserved
%RRemarkCommentArgEmit inline comment
%LLiteral&str, String, CodeBlock, CodeFragmentEmit raw value or nested block/fragment
%WWrap(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::generic(
    TypeName::primitive("Promise"),
    vec![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 CodeLang::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"DelimiterEscapes only
Bash/Zsh$x(passthrough)(none)
JavaScript/TS`$x``...`\ `
Pythonf"$x"f"..."\ "
Kotlin/Swift"$x""..."\ "
Dart'$x''...'\ '
C#$"$x"$"..."\ "
Scalas"$x"s"..."\ "
OthersSame 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. 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, the renderer does direct string concatenation with indent tracking. When %W is present, it builds a pretty::BoxDoc tree for width-aware layout. BoxDoc (not RcDoc) is used so rendered documents are Send + Sync.

%> 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 TypeMaps ToConsumed By
()empty vec(no specifiers)
TypeNameArg::TypeName%T
&strArg::Literal%L
StringArg::Literal%L
CodeBlockArg::Code%L
CodeFragmentArg::Code%L
NameArg(String)Arg::Name%N
StringLitArg(String)Arg::StringLit%S
VerbatimStrArg(String)Arg::VerbatimStr%V
CommentArg(String)Arg::Comment%R
Vec<Arg>passthroughany

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();
}

Argument Count Validation

The builder checks that the number of argument-consuming specifiers (%T, %N, %S, %V, %L) 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"],
// })
}

An unrecognised specifier character (anything after % that isn’t T, N, S, V, L, W, >, <, [, ], or %) produces Err(SigilStitchError::InvalidFormatSpecifier { format, specifier }) instead.

TypeName

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 is language-agnostic — it carries no generic parameter. The same TypeName value can be rendered for any target language at FileSpec::render() time.

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. If two modules export the same name, the first keeps the simple name and the second gets an auto-generated alias.

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 separator between module and name comes from CodeLang::module_separator()"::" for Rust/C++, "." for Go/Python/Java/Kotlin/Scala/Swift/Dart/Haskell/OCaml. Languages without module-qualified paths (TypeScript, JavaScript, C, Bash, Zsh) silently fall back to rendering just the name.

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::generic(
    TypeName::qualified("std::collections", "HashMap"),
    vec![
        TypeName::primitive("String"),
        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>  (via type_presentation().array)
// 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>  (via type_presentation().map)
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

Wrap a base type with type parameters:

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// TypeScript: Promise<User>
let promise = TypeName::generic(
    TypeName::primitive("Promise"),
    vec![TypeName::importable("./models", "User")],
);

// Rust: HashMap<String, Vec<User>>
let map = TypeName::generic(
    TypeName::primitive("HashMap"),
    vec![
        TypeName::primitive("String"),
        TypeName::generic(
            TypeName::primitive("Vec"),
            vec![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 TypeScript. Other languages render them using their closest equivalent (e.g., Python uses X | Y for unions).

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 rendering adapts per language through the optional field in lang.type_presentation().

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

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::function(
    vec![TypeName::primitive("string"), TypeName::primitive("number")],
    TypeName::primitive("boolean"),
);
}

Function type rendering varies significantly across languages. The function field in lang.type_presentation() returns a FunctionPresentation struct that controls keyword, delimiters, arrow syntax, parameter order, and optional outer wrappers.

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.

Cross-language rendering

The same TypeName variant renders differently per language. This is powered by the TypePresentation system – each language returns a rendering pattern (prefix, postfix, surround, delimited, generic-wrap, or infix) for each type construct, and the rendering engine in type_name.rs interprets the pattern into formatted output. Language implementations never build BoxDoc directly.

TypeNameTypeScriptRustGoC++
array(T)T[]Vec<T>[]Tstd::vector<T>
optional(T)T | nullOption<T>*Tstd::optional<T>
tuple(A, B)[A, B](A, B)n/astd::tuple<A, B>
reference(T)T&TTconst T&
reference_mut(T)T&mut T*TT&
map(K, V)Record<K, V>HashMap<K, V>map[K]Vstd::map<K, V>
function(A) -> R(A) => Rfn(A) -> Rfunc(A) Rstd::function<R(A)>

See Type Presentation for the full technical details of how this rendering system works.

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 structural builders that produce Vec<CodeBlock>. They encapsulate common declaration patterns – classes, functions, fields, enums – so you work with named concepts instead of raw format strings. Every spec takes a &dyn CodeLang language reference at emit time, which means the same builder definition renders correctly for any target language.

All spec types live in src/spec/. They follow a consistent builder pattern:

  • mut self for setters – owning chainable configuration methods that return Self
  • self for .build() – consumes the builder and returns Result<Spec, SigilStitchError>
  • Chain calls fluentlyBuilder::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 adapts to the target language. TypeScript emits name: type, C emits type name, and Python omits the type annotation when the type is empty. The is_property() and is_mutable_property() methods prepend the language’s readonly/mutable keyword — val/var in Kotlin, readonly in C# — so you don’t need to embed language-specific keywords in the parameter name.

FieldSpec

A struct field or class property: name, type, visibility, static/readonly flags, initializer, annotations, and doc comments.

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 (distinct from a value that can be null). Rendering is language-specific, delegated to CodeLang::optional_field_style():

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;
// JavaScript:  email;                (marker stripped — no optionality in JS)
// Rust:        email: Option<String>,
// Go:          Email *string
// Python:      email: str | None
// Java:        Optional<String> email;   (caller must import java.util.Optional)
// Kotlin:      name: String?
// Swift:       name: String?
// Dart:        String? name;
// C:           string *email;
// C++:         std::optional<string> email;   (caller must #include <optional>)
}

Use is_optional() for “the key might not be there” (e.g., an OpenAPI property not listed in required). Use TypeName::optional(...) for “the value might be null” at the type level.

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::generic(
        TypeName::primitive("Promise"),
        vec![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 = TypeParamSpec::new("T")
    .with_bound(TypeName::primitive("Serializable"));

let body = CodeBlock::of("return JSON.stringify(value)", ()).unwrap();
let fun = FunSpec::builder("serialize")
    .add_type_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 emit super(...) or this(...) calls. The placement is language-dependent: body-style (TS, Java, Dart, Swift) emits it as the first statement; signature-style (Kotlin) emits 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 declaration form.

.build() returns Err(SigilStitchError::DuplicateFieldName { type_name, field_name }) when two fields in the same type share a name.

Single-block output (TypeScript class)

When lang.methods_inside_type_body(kind) returns true, TypeSpec emits a single CodeBlock with fields and methods inside the body:

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)

When methods_inside_type_body(kind) returns false (Rust structs and enums), TypeSpec emits two separate CodeBlocks: one for the data definition, 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() }
//     }
// }
}

This split is the key structural decision. It is fully automatic – you build one TypeSpec, and the language’s methods_inside_type_body() determines whether the output is one block or two.

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 {
// }
}

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
// }
}

Embedded types render before regular fields. If the embedded type is TypeName::importable(...), its import is tracked automatically via %T. This works across languages — for Go interfaces, embedded types produce 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
// }
}

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();
}

Per-language rendering is controlled by type_keyword(TypeKind::TypeAlias):

  • TypeScript/Rust: type Foo = Bar;
  • C++: using Foo = Bar;
  • C: typedef Bar Foo; (target-first, via type_decl_syntax().type_alias_target_first)
  • 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_type_param(TypeParamSpec::new("T"))
    .extends(TypeName::generic(
        TypeName::primitive("std::result::Result"),
        vec![TypeName::primitive("T"), 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 controlled by render_newtype_line():

  • 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)
  • C: typedef float Meters; (typedef)

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")
            .value(CodeBlock::of("'UP'", ()).unwrap())
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("Down")
            .value(CodeBlock::of("'DOWN'", ()).unwrap())
            .build()
            .unwrap(),
    )
    .build()
    .unwrap();
// enum Direction {
//     Up = 'UP',
//     Down = 'DOWN',
// }
}

PropertySpec

Computed properties with getter and/or setter. Rendering depends on lang.property_style():

  • Accessor (TypeScript, JavaScript): emits separate get name(): T { ... } and set name(v: T) { ... } methods
  • Field (Swift, Kotlin): emits a field with inline get/set blocks
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 (Accessor style):
// get name(): string {
//     return this._name
// }
// set name(value: string) {
//     this._name = value
// }
}

For Swift and Kotlin, the same PropertySpec renders as a field with inline body blocks instead.

AnnotationSpec

Structured annotations that render with language-appropriate syntax. The prefix and suffix adapt automatically:

LanguageSyntax
Java, Kotlin, TS@Name(args)
Rust#[name(args)]
C++[[name(args)]]
C__attribute__((name(args)))
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

Individual enum variants. Four 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,
}

Valued 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")
    .value(CodeBlock::of("'UP'", ()).unwrap())
    .build()
    .unwrap();
// TypeScript: Up = 'UP',
}

Tuple variant (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")
    .associated_type(TypeName::primitive("i64"))
    .build()
    .unwrap();
// Rust: Literal(i64),

// Multi-element tuple
let variant = EnumVariantSpec::builder("Pair")
    .associated_type(TypeName::primitive("String"))
    .associated_type(TypeName::primitive("i32"))
    .build()
    .unwrap();
// Rust: Pair(String, i32),
}

Struct variant (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")
    .add_field(
        FieldSpec::builder("x", TypeName::primitive("i32")).build().unwrap(),
    )
    .add_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 controls separators (enum_and_annotation().variant_separator), trailing separators (enum_and_annotation().variant_trailing_separator), and prefixes (Swift’s case).

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, type declarations, and functions, then drives the three-pass render pipeline:

  1. Materialize – Specs (TypeSpec, FunSpec) emit CodeBlocks
  2. Collect imports – Walk all blocks, extract import references from %T types
  3. Render – Emit the import header, then the body with resolved names and pretty printing
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();
}

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();
}

Each file resolves imports independently. render() returns Vec<RenderedFile> with path and content fields. write_to() creates parent directories as needed.

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::generic(
                TypeName::primitive("Promise"),
                vec![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;
use sigil_stitch::lang::config::QuoteStyle;
fn main() {
let block = sigil_quote!(Python {
    print($S("hi"))
}).unwrap();

assert_rendered!(
    Python::new().with_quote_style(QuoteStyle::Double),
    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

SyntaxSpecifierArgument TypePurpose
$T(expr)%TTypeNameType reference, tracks imports
$N(expr)%Nimpl ToStringName identifier
$S(expr)%Simpl ToStringString literal (quoted in output)
$V(expr)%Vimpl ToStringVerbatim string (interpolation preserved)
$L(expr)%Limpl Into<Arg>Literal value, nested code, or parsed fragment
$C(expr)%LCodeBlockNested 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 ToStringStructural annotation (language-specific prefix/suffix)
$T_join(sep, iter)%Tseparator + impl IntoIterator<Item: TypeName>Type name join with per-item import tracking
$if(cond) { ... }Rust expressionMeta-conditional (runtime codegen control)
$for(pat in expr) { ... }Rust pattern + iterableMeta-loop (emit body per iteration)
$for(pat in expr; separator = expr, trailing = bool) { ... }Rust pattern + iterable + optionsMeta-loop with separator control
$let(binding);Rust let bindingRust-level variable binding inside macro body
$join(sep, iter)%Lseparator + 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'
}

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 $V or $L string literals with @{expr}. These are resolved at compile time while the rest passes through for the target language’s runtime:

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).

Use @@ to emit a literal @. Bare @ not followed by { passes through unchanged. Works with all languages.

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"));
}

Raw strings passed through $L are not reparsed. A raw string containing %> or %< fails with UnresolvedIndentMarker; wrap that snippet in CodeFragment::of when the markers are intended to control indentation.

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("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 target language’s syntax. This keeps the macro body language-agnostic — write $attr("override") and it renders as @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("if(x > 0)", ());
__sigil_builder.add_statement("return 1", ());
__sigil_builder.next_control_flow("else if(x < 0)", ());
__sigil_builder.add_statement("return - 1", ());
__sigil_builder.next_control_flow("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

By default, { ... } in sigil_quote! uses the language’s block_syntax().block_open. Language backends can override the opener and closer per condition via block_open_for and block_close_for. For example, Bash maps ifthen/fi and fordo/done, while Haskell maps classwhere:

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 — block_open_for returns " where" for "class ..."
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 — block_open_for returns " = struct" for "module ..."
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:

ConditionOpenClose
if ...; thenfi
for ...; dodone
while ...; dodone
else""""
elif ...; then""

Lua similarly maps ifthen/end and for/whiledo/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.

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.

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:

  1. Single-quoted strings don’t work. 'hello' is tokenized as a Rust lifetime. Use $S("hello") instead.

  2. Colon spacing is context-aware. The macro tracks a ColonContext to decide whether : gets a space before it:

    ContextExampleSpace before :
    Type annotationname: stringno
    Map entry{ key: value }no
    Path separatorstd::memno
    Ternaryx ? y : zyes
    Walrus assignx := 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.

  3. Other multi-character operators. Operators like ===, !==, -> are tokenized as separate punctuation characters. The macro reconstructs them via proc_macro2’s Spacing::Joint flag. 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, so fn foo<T> may keep a space before < (use FunSpec for generic function declarations).

  4. Keyword spacing before (. Control-flow keywords (if, for, while, else, match, return, try, catch, etc.) automatically get a space before (. Regular identifiers do not, so myFunc(x) stays tight while if (x) gets the expected space. This covers the common case but isn’t configurable per-language.

  5. Template literals. Backtick strings (`${expr}`) aren’t representable. Use $L(expr) for dynamic content.

  6. 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. Templates are language-agnostic: you define the pattern once, then apply it with concrete arguments for any target language.

Syntax

Templates use #{name:K} for named parameters, where K specifies the kind:

KindSpecifierArgument Type
T%TTypeName
N%NNameArg
S%SStringLitArg
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 language-agnostic CodeBlock.

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 Across Languages

Since templates are language-agnostic, the same template can target different languages:

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 a TypeName, 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 logical concept – a simple data type with two fields – rendered across four languages from the same builder structure:

LanguageOutput
TypeScriptexport class Point { x: number; y: number; }
Rustpub struct Point { pub x: f64, pub y: f64, } + separate impl block
Gotype Point struct { X float64; Y float64 }
Pythonclass 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)
Scalacase class Point(x: Double, y: Double)
Haskelldata Point = Point { pointX :: Double, pointY :: Double }
OCamltype point = { x : float; y : float }

The language’s CodeLang trait controls every syntax detail: keywords, delimiters, field ordering, visibility rendering, and whether methods live inside the type body or in a separate impl block. You build the spec once and the language passed to render() does the rest.

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::generic(TypeName::primitive("Promise"), vec![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_type_param(TypeParamSpec::new("T"))
    .add_method(
        FunSpec::builder("findById")
            .add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
            .returns(TypeName::generic(TypeName::primitive("Promise"), vec![TypeName::primitive("T")]))
            .build()
            .unwrap(),
    )
    .add_method(
        FunSpec::builder("save")
            .add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
            .returns(TypeName::generic(TypeName::primitive("Promise"), vec![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")
            .value(CodeBlock::of("'UP'", ()).unwrap())
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("Down")
            .value(CodeBlock::of("'DOWN'", ()).unwrap())
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("Left")
            .value(CodeBlock::of("'LEFT'", ()).unwrap())
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("Right")
            .value(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")
            .associated_type(TypeName::primitive("i64"))
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("Binary")
            .add_field(FieldSpec::builder("left", TypeName::primitive("Box<Expr>")).build().unwrap())
            .add_field(FieldSpec::builder("op", TypeName::primitive("Op")).build().unwrap())
            .add_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_type_param(TypeParamSpec::new("T"))
    .extends(TypeName::generic(
        TypeName::primitive("std::result::Result"),
        vec![TypeName::primitive("T"), 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::generic(
    TypeName::qualified("std::collections", "HashMap"),
    vec![
        TypeName::primitive("String"),
        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 = TypeParamSpec::new("T").with_bound(TypeName::primitive("comparable"));

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_type_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::generic(
        TypeName::primitive("list"),
        vec![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")
            .value(CodeBlock::of("'UP'", ()).unwrap())
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("DOWN")
            .value(CodeBlock::of("'DOWN'", ()).unwrap())
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("LEFT")
            .value(CodeBlock::of("'LEFT'", ()).unwrap())
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("RIGHT")
            .value(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_type_param(TypeParamSpec::new("T"))
    .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_type_param(TypeParamSpec::new("T"))
    .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")
            .associated_type(TypeName::primitive("Data"))
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("failure")
            .associated_type(TypeName::primitive("Error"))
            .associated_type(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_type_param(TypeParamSpec::new("T"))
    .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_type_param(TypeParamSpec::new("T"))
    .add_field(
        FieldSpec::builder("data_", TypeName::generic(
            TypeName::primitive("std::vector"),
            vec![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::generic(
        TypeName::primitive("std::vector"),
        vec![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_type_param(TypeParamSpec::new("T"))
    .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")
            .value(CodeBlock::of("1", ()).unwrap())
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("East")
            .value(CodeBlock::of("2", ()).unwrap())
            .build()
            .unwrap(),
    )
    .add_variant(
        EnumVariantSpec::builder("West")
            .value(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 in sigil_quote! — the Ruby backend translates them to do/end or indent/dedent as appropriate.
  • Symbol literals like :name get correct spacing (space before :, none after).
  • Inheritance uses < with space before it: class Dog < Animal.
  • $V passes 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 ?Type for 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: $$a produces $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_type_param(TypeParamSpec::new("T"))
    .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_type_param(
        TypeParamSpec::new("T").with_bound(TypeName::primitive("Comparable[T]")),
    )
    .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 block_open_for 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 utils = TypeName::importable("./lib/utils.sh", "");
let config = TypeName::importable("./lib/config.sh", "");

let body = CodeBlock::of("# uses %T and %T", (utils, config)).unwrap();

let output = FileSpec::builder_with("app.bash", Bash::new())
    .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 works internally. It covers the four-layer design, the three-pass rendering pipeline, and the import resolution system.

Four Layers

The library is organized in four layers, each building on the one below:

┌─────────────────────────────────────┐
│  Spec Layer (TypeSpec, FunSpec, ...) │  Structural builders
├─────────────────────────────────────┤
│  CodeBlock + Format Specifiers       │  Composable code fragments
├─────────────────────────────────────┤
│  TypeName                            │  Type references with import tracking
├─────────────────────────────────────┤
│  CodeLang Trait                      │  Language abstraction
└─────────────────────────────────────┘

Layer 1: RendererLang + CodeLang

src/lang/mod.rs defines two traits:

  • RendererLang (14 methods) — the renderer-only surface used by code_renderer.rs and TypeName::to_doc_with_lang. Covers file extension, string literals, verbatim strings, block syntax, type presentation, generic syntax, imports, and a few rendering helpers. If you only need CodeBlock-level rendering (no specs), implementing RendererLang is sufficient.
  • CodeLang: RendererLang — extends RendererLang with the additional methods needed by the spec layer (TypeSpec, FunSpec, FieldSpec). Covers visibility, keywords, function syntax, type-decl syntax, enum/annotation config, and structural decisions like methods_inside_type_body.

Each supported language implements both traits in its own module (src/lang/typescript.rs, etc.). The 6 config struct accessors (type_presentation(), generic_syntax(), block_syntax(), function_syntax(), type_decl_syntax(), enum_and_annotation()) return data structs with sensible defaults. Languages can also implement rewrite_nodes() to transform the CodeNode tree after macro expansion for language-specific fixups (e.g., Go IIFE }() fusion, C++ lambda }; semicolons).

At the macro level, the MacroLang enum (macros/src/parse/types.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 types are language-agnostic — no generic parameter. The language enters as &dyn RendererLang (for rendering) or &dyn CodeLang (for spec emission) at render time. FileSpec stores a Box<dyn CodeLang> internally; all other types (CodeBlock, TypeName, specs) are language-independent.

Layer 2: TypeName

src/type_name.rs defines type references. Key variants:

VariantExampleImport Tracked?
Primitivestring, i32No
ImportableUser from ./modelsYes
GenericPromise<User>Recursively
ArrayUser[], Vec<User>Inner type tracked
ReadonlyArrayreadonly User[]Inner type tracked
OptionalUser?, Option<User>Inner type tracked
Unionstring | numberAll members tracked
IntersectionA & B, A + BAll members tracked
Tuple[A, B], (A, B)All members tracked
Reference&T, const T&Inner type tracked
Function(x: string) => voidParams + return tracked
MapMap<string, User>Key + value tracked
Pointer / Slice*const T, &[T]Inner type tracked
Rawany stringNo

Every variant that contains other types recursively collects imports via collect_imports(). This means Generic(Promise, [Importable(User)]) tracks the User import even though Promise is a primitive.

TypeName also renders to pretty::BoxDoc for width-aware output of complex type signatures. BoxDoc is used (rather than RcDoc) so rendered documents are Send + Sync and can cross thread boundaries.

Type Presentation Layer

TypeName variants are semanticArray(T) means “array of T” regardless of language. Cross-language rendering is handled by a data-driven presentation layer:

  1. Each TypeName variant asks the language for a TypePresentation — a data enum describing the syntactic pattern (e.g., GenericWrap, Prefix, Postfix, Surround, Delimited, Infix).
  2. A single rendering engine in type_name.rs interprets the pattern into BoxDoc output.

BoxDoc never appears in the CodeLang trait. Languages return pure data; the engine does all rendering. See Type Presentation for the full design.

Layer 3: 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, with no separate arg-index lookup.

CodeBlocks are immutable after construction. The builder (CodeBlockBuilder) validates argument counts and indent balance before producing a block.

Layer 4: Spec Layer

src/spec/ contains structural builders that emit Vec<CodeBlock>. TypeSpec emits one or two blocks depending on methods_inside_type_body(). FunSpec emits one block. FileSpec orchestrates the full rendering pipeline.

The key design decision: specs emit CodeBlocks, never raw strings. This means the renderer and import system never need to change when new spec types are added. A new WidgetSpec would just emit CodeBlocks with %T references, and imports would work automatically.

Three-Pass Rendering Pipeline

FileSpec::render(width) drives everything. It runs three passes over the file’s members.

Pass 0: Materialize

Specs are converted to CodeBlocks:

  • FileMember::Type(TypeSpec) calls type_spec.emit(&lang) -> Vec<CodeBlock>
  • FileMember::Fun(FunSpec) calls fun_spec.emit(&lang, ctx) -> CodeBlock
  • FileMember::Code(CodeBlock) passes through unchanged
  • FileMember::RawContent(String) passes through as-is

After this phase, everything is either a CodeBlock or raw content.

Pass 1: Collect Imports

import_collector walks every CodeBlock tree. For each CodeNode::TypeRef in any block, it calls type_name.collect_imports() to extract ImportRef structs (module + name + optional alias).

Nested CodeBlocks (CodeNode::Nested) are walked recursively. RawContentWithImports members have their type list walked for imports even though the content itself is opaque.

Import Resolution

ImportGroup::resolve() takes the collected ImportRef list and:

  1. Deduplicates: Same module + same name = one import
  2. Detects conflicts: Two different modules exporting the same name (e.g., User from ./models and User from ./legacy)
  3. Assigns aliases: First-encountered User wins the simple name. The second gets aliased using a module-derived prefix (e.g., LegacyUser)
  4. Merges explicit imports: ImportSpec entries (aliased, side-effect, wildcard) are merged into the resolved set

The result is an ImportGroup that maps each module to its resolved names with aliases.

Go’s qualify_import_name() adds another layer: instead of importing Server directly, it renders as http.Server in code, with a package-level import of "net/http".

Pass 2: Render

CodeRenderer walks each CodeBlock’s CodeNode sequence:

NodeAction
Literal(s)Emit string directly
TypeRef(tn)Resolve import name via ImportGroup, emit
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()
SoftBreakPretty-print decision point
Indent / DedentAdjust indent level
StatementBegin / StatementEndStatement boundaries (; if applicable)
NewlineEmit newline + indent
BlockOpen / BlockCloseBlock delimiters from lang.block_syntax()
BlockOpenOverride(s)Emit custom block opener (e.g. " where")
BlockCloseTransitionClose delimiter + space (for } else { chains)
Sequence(children)Recursively render a sub-sequence of nodes

Width-aware rendering: When a CodeBlock contains SoftBreak nodes, the renderer builds a pretty::BoxDoc tree (Send + Sync) via nodes_to_doc instead of doing direct string concatenation. The Wadler-Lindig algorithm then decides at each SoftBreak point whether to insert a line break or a space, based on the target width. CodeBlocks without SoftBreak use the simpler direct-concat path for efficiency.

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 first User (from ./models) wins the simple name. The second (from ./legacy) gets the alias LegacyUser, derived from the module path.

Language-Agnostic Types

All public types (CodeBlock, TypeName, TypeSpec, FunSpec, etc.) are language-agnostic. The language is supplied at render time via &dyn CodeLang:

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let user = TypeName::importable_type("./models", "User");
let mut cb = CodeBlock::builder();
cb.add("const u: %T = getUser()", (user,));
let block = cb.build().unwrap();
// Render for any language:
let output_ts = FileSpec::builder("user.ts")
    .add_code(block.clone())
    .build()
    .unwrap()
    .render(80)
    .unwrap();

let output_rs = FileSpec::builder("user.rs")
    .add_code(block)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
}

FileSpec::builder("user.ts") auto-detects the language from the file extension. Use FileSpec::builder_with("user.ts", TypeScript::new()) for explicit control.

Type Presentation

This chapter describes how sigil-stitch renders TypeName variants across different languages using a data-driven presentation layer.

The Problem

TypeName is a semantic type algebra — Array(T) means “array of T” regardless of target language. But the surface syntax varies widely:

TypeNameTypeScriptRustGoPythonC++
Array(T)T[]Vec<T>[]Tlist[T]std::vector<T>
Optional(T)T | nullOption<T>*TT | Nonestd::optional<T>
Map(K, V)Record<K, V>HashMap<K, V>map[K]Vdict[K, V]std::map<K, V>
Pointer(T)n/a*const T*Tn/aT*
Tuple(A, B)[A, B](A, B)n/atuple[A, B]std::tuple<A, B>
Reference(T)(identity)&T(identity)(identity)const T&
Reference(T, mut)(identity)&mut T*T(identity)T&

Each variant needs language-specific rendering, but the rendering follows a small set of structural patterns. Rather than writing per-language rendering code for every variant, we identify these patterns and let languages declare which pattern to use.

Architecture

              ┌──────────────┐
              │   TypeName   │  Semantic type algebra
              │  (unchanged) │  Array, Optional, Map, ...
              └──────┬───────┘
                     │ to_doc_with_lang(resolve, lang)
                     ▼
          ┌──────────────────────────────┐
          │  lang.type_presentation()    │  CodeLang returns TypePresentationConfig (DATA)
          └──────────────┬───────────────┘
                         ▼
          ┌─────────────────────┐
          │  Rendering engine   │  Single function: (TypePresentation, inner docs) → BoxDoc
          │  (one place)        │  Lives in type_name.rs, NEVER in CodeLang impls
          └──────────┬──────────┘
                     ▼
                BoxDoc output

The key invariant: BoxDoc never appears in the CodeLang trait. Languages declare data (which syntactic pattern to use). The rendering engine — a single function in type_name.rs — interprets that data into BoxDoc output.

This separates three concerns that were previously tangled:

  1. What a type meansTypeName variants (semantic, language-independent)
  2. How a language spells itTypePresentation data (per-language, no rendering logic)
  3. How to assemble output — rendering engine (one place, all patterns)

TypePresentation

TypePresentation is an enum of syntactic patterns. Each variant describes a structural template for assembling already-rendered inner type docs:

#![allow(unused)]
fn main() {
pub enum TypePresentation<'a> {
    /// `name<P1, P2>` — delimiters from generic_syntax().open/.close.
    /// Vec<T>, Option<T>, HashMap<K,V>, List<T>.
    GenericWrap { name: &'a str },

    /// `prefix inner` — *T, &T, []T, &mut T.
    Prefix { prefix: &'a str },

    /// `inner suffix` — T[], T?, T*.
    Postfix { suffix: &'a str },

    /// `prefix inner suffix` — const T&, const T*.
    Surround { prefix: &'a str, suffix: &'a str },

    /// `open P1 sep P2 sep ... close` — (A, B), [T], [K: V], dict[K, V].
    Delimited {
        open: &'a str,
        sep: &'a str,
        close: &'a str,
    },

    /// `P1 sep P2 sep ... Pn` — A | B, A & B, A + B.
    Infix { sep: &'a str },
}
}

Six patterns cover every type rendering need across all supported languages. A language implementation never builds BoxDoc — it returns one of these variants with the appropriate strings filled in.

FunctionPresentation

Function types are too complex for a single TypePresentation variant — they have parameter lists, return types, arrows, optional keywords, and wrappers that combine in language-specific ways. They get their own struct:

#![allow(unused)]
fn main() {
pub struct FunctionPresentation<'a> {
    pub keyword: &'a str,        // "fn", "func", ""
    pub params_open: &'a str,    // "(", "Callable[["
    pub params_sep: &'a str,     // ", "
    pub params_close: &'a str,   // ")", "]]"
    pub arrow: &'a str,          // " -> ", " => ", ", "
    pub return_first: bool,      // Dart: R Function(A, B)
    pub curried: bool,           // Haskell: A -> B -> R
    pub wrapper_open: &'a str,   // C++: "std::function<"
    pub wrapper_close: &'a str,  // C++: ">"
}
}

This declaratively covers TypeScript (A, B) => R, Rust fn(A, B) -> R, Python Callable[[A, B], R], C++ std::function<R(A, B)>, Dart R Function(A, B), and Haskell A -> B -> R — all from a single rendering engine interpreting the data.

CodeLang Trait Method

Languages declare their type syntax by returning a TypePresentationConfig from a single method on the CodeLang trait:

trait CodeLang {
    fn type_presentation(&self) -> TypePresentationConfig<'_>;
}

TypePresentationConfig bundles every type-rendering decision into one struct — never BoxDoc:

pub struct TypePresentationConfig<'a> {
    pub array: TypePresentation<'a>,
    pub readonly_array: Option<TypePresentation<'a>>,
    pub optional: TypePresentation<'a>,
    pub optional_absent_literal: &'a str,
    pub map: TypePresentation<'a>,
    pub union: TypePresentation<'a>,
    pub intersection: TypePresentation<'a>,
    pub pointer: TypePresentation<'a>,
    pub slice: TypePresentation<'a>,
    pub tuple: TypePresentation<'a>,
    pub reference: TypePresentation<'a>,
    pub reference_mut: TypePresentation<'a>,
    pub function: FunctionPresentation<'a>,
    pub associated_type: AssociatedTypeStyle<'a>,
    pub impl_trait: BoundsPresentation<'a>,
    pub dyn_trait: BoundsPresentation<'a>,
    pub wildcard: WildcardPresentation<'a>,
}

Every field has a sensible default via Default::default(). TypeScript needs almost no overrides. Most languages override 3–5 fields with struct-update syntax (..Default::default()).

Rendering Engine

A single private function in type_name.rs interprets presentations:

fn render_presentation(
    pres: &TypePresentation<'_>,
    inner_docs: Vec<BoxDoc<'static, ()>>,
    gs: &GenericSyntaxConfig<'_>,
) -> BoxDoc<'static, ()> {
    match pres {
        TypePresentation::GenericWrap { name } => {
            // name<P1, P2> using lang.generic_syntax().open / .close
        }
        TypePresentation::Prefix { prefix } => {
            // prefix inner
        }
        TypePresentation::Postfix { suffix } => {
            // inner suffix
        }
        TypePresentation::Surround { prefix, suffix } => {
            // prefix inner suffix
        }
        TypePresentation::Delimited { open, sep, close } => {
            // open P1 sep P2 close
        }
        TypePresentation::Infix { sep } => {
            // P1 sep P2 sep P3
        }
    }
}

Each TypeName variant in to_doc_with_lang becomes a three-step process:

  1. Recursively render inner types to BoxDoc
  2. Ask the language for a TypePresentation
  3. Pass both to render_presentation
TypeName::Array(inner) => {
    let inner_doc = inner.to_doc_with_lang(resolve, lang);
    let tp = lang.type_presentation();
    let gs = lang.generic_syntax();
    render_presentation(&tp.array, vec![inner_doc], &gs)
}

Per-Language Examples

TypeScript

TypeScript overrides five fields from the defaults:

fn type_presentation(&self) -> TypePresentationConfig<'_> {
    TypePresentationConfig {
        map: TypePresentation::GenericWrap { name: "Record" },
        tuple: TypePresentation::Delimited { open: "[", sep: ", ", close: "]" },
        associated_type: AssociatedTypeStyle::IndexAccess { open: "[\"", close: "\"]" },
        impl_trait: BoundsPresentation { keyword: "", separator: " & " },
        wildcard: WildcardPresentation { unbounded: "unknown", .. },
        ..Default::default()
    }
}

The remaining fields use defaults: ArrayPostfix { suffix: "[]" }, OptionalInfix { sep: " | " } with optional_absent_literal set to "null".

Rust

fn type_presentation(&self) -> TypePresentationConfig<'_> {
    TypePresentationConfig {
        array: TypePresentation::GenericWrap { name: "Vec" },
        optional: TypePresentation::GenericWrap { name: "Option" },
        map: TypePresentation::GenericWrap { name: "HashMap" },
        intersection: TypePresentation::Infix { sep: " + " },
        pointer: TypePresentation::Prefix { prefix: "*const " },
        slice: TypePresentation::Delimited { open: "&[", sep: "", close: "]" },
        reference: TypePresentation::Prefix { prefix: "&" },
        reference_mut: TypePresentation::Prefix { prefix: "&mut " },
        ..Default::default()
    }
}

C++

fn type_presentation(&self) -> TypePresentationConfig<'_> {
    TypePresentationConfig {
        array: TypePresentation::GenericWrap { name: "std::vector" },
        optional: TypePresentation::GenericWrap { name: "std::optional" },
        pointer: TypePresentation::Postfix { suffix: "*" },
        reference: TypePresentation::Surround { prefix: "const ", suffix: "&" },
        reference_mut: TypePresentation::Postfix { suffix: "&" },
        tuple: TypePresentation::GenericWrap { name: "std::tuple" },
        ..Default::default()
    }
}

The Surround variant was introduced specifically for C++’s const T& pattern, where a type needs both a prefix and a suffix. C uses it similarly for const T*.

Go

fn type_presentation(&self) -> TypePresentationConfig<'_> {
    TypePresentationConfig {
        array: TypePresentation::Prefix { prefix: "[]" },
        map: TypePresentation::Delimited { open: "map[", sep: "]", close: "" },
        ..Default::default()
    }
}

Note that GenericWrap reuses generic_syntax().open/.close, so Go’s List[T] works automatically because Go already sets generic_syntax().open to "[".

Swift

fn type_presentation(&self) -> TypePresentationConfig<'_> {
    TypePresentationConfig {
        array: TypePresentation::Delimited { open: "[", sep: "", close: "]" },
        optional: TypePresentation::Postfix { suffix: "?" },
        map: TypePresentation::Delimited { open: "[", sep: ": ", close: "]" },
        ..Default::default()
    }
}

Design Properties

  1. BoxDoc never appears in CodeLang — languages declare data, the engine renders.
  2. Adding a TypeName variant requires one new field on TypePresentationConfig. No per-language render code needed.
  3. 17 fields on TypePresentationConfig replace what would otherwise be ~20+ render methods. Each override is a single struct field.
  4. One rendering engine in type_name.rs handles all patterns uniformly.
  5. Semantic types are preservedArray(T) stays Array(T). The language says “render Array as GenericWrap(Vec)” not “rewrite Array to Generic(‘Vec’, [T])”.
  6. GenericWrap reuses generic_syntax().open/.close — languages that already configure these delimiters get correct rendering automatically.

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

VariantRecognized fromTokenizer behavior
UnawareAll other languagesUniversal heuristics only
Bashsigil_quote!(Bash { ... })Shell-specific (see below)
Csigil_quote!(C { ... })No angle generics, postfix * pointer
Cppsigil_quote!(Cpp { ... })Postfix * pointer, postfix & reference
CSharpsigil_quote!(CSharp { ... })Postfix * pointer, postfix ? nullable
Dartsigil_quote!(Dart { ... })Postfix ? nullable
Gosigil_quote!(Go { ... })<- prefix receive, paren blocks
Haskellsigil_quote!(Haskell { ... })$$ dollar operator spacing
Kotlinsigil_quote!(Kotlin { ... })Postfix ? nullable
OCamlsigil_quote!(OCaml { ... })Space before :, prefix ? nullable
Phpsigil_quote!(Php { ... })Prefix ? nullable
Rubysigil_quote!(Ruby { ... })Symbol colon, inheritance angle
Swiftsigil_quote!(Swift { ... })Postfix ? nullable
TypeScriptsigil_quote!(TypeScript { ... })Postfix ? nullable
Zshsigil_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:

AnnotationGatesLanguagesEffect
PostfixStarhas_postfix_star()C, Cpp, CSharpConfig* — no space before *
PostfixAmpersandhas_postfix_ampersand()Cpp onlyauto& — no space before &
PostfixQuestionhas_postfix_question_type()CSharp, Dart, Kotlin, Swift, TypeScriptint? — no space before ?
AssignAdjacentis_shell()Bash, ZshNAME=val — no space around =
GenericOpen (ordinary)has_angle_generics()Excludes C, Go, Haskell, OCaml, Php, Bash, Zsh, Ruby< as generic opener
NullablePrefixnullable_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, so declare -a renders correctly.
  • DashSep downgrade: -- file.txt — the second - of -- is downgraded from PrefixOp to Normal when NOT span-adjacent to the next token, preserving the separator space. --amend (flag, adjacent) stays tight.
  • SlashSep leading path: /usr/local/bin — allows SlashSep annotation with no left neighbor (relaxes the i > 0 requirement 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 as Normal.

Go

  • <- prefix receive: When - follows a Joint < (not GenericOpen) and is span-adjacent to the next token, it gets PrefixOp annotation — suppressing the space to produce <-ch. When NOT adjacent (ch <- 42), the - stays Normal and the space is preserved.
  • Paren-delimited blocks: const (, var (, import (, and type ( 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 sets PrevTokenKind::DollarLiteral, which suppresses space after $ (designed for shell $VAR). For Haskell, it sets PrevTokenKind::Punct('$', Alone) instead, allowing the normal spacing rule to insert a space — producing putStrLn $ show 42.

Ruby

  • Symbol colon (:name): : span-adjacent to the next ident but NOT span-adjacent to the previous token gets SymbolColon annotation — space before : but none after: attr_reader :name, :age.
  • Inheritance angle (<): < following an ident is marked InheritanceAngle instead of GenericOpen — space before < is preserved: class Dog < Animal.
  • No angle generics: Ruby is excluded from has_angle_generics(), so $T(...)<...> does not trigger GenericOpen.

PHP / OCaml

  • Nullable prefix (?User): ? span-adjacent to the following ident gets NullablePrefix annotation — 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 gets PostfixStar — no space before: Config* p.
  • Postfix reference (auto&): C++ only — & span-adjacent to the preceding ident gets PostfixAmpersand — no space before: auto& x.
  • Postfix nullable (int?): C# only — ? span-adjacent to the preceding ident gets PostfixQuestion — 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:

AnnotationPatternEffect
PathSepComplete:: span-adjacent to leftSuppress space after (path: std::fmt)
DoubleColonOp:: NOT adjacent to leftSpace before (Haskell: fmap :: Type)
MethodCallColon: adjacent to both sidesSuppress space (Lua: obj:method())
GenericOpen/Close</> with type contextSuppress space (generics: Vec<T>)
ArrowOp-> adjacent to leftSuppress space (member: ptr->field)
PrefixOp&, *, - as prefixSuppress space after (&self, *ptr)
PostfixStar*/& adjacent to identSuppress space before (Config*)
PostfixIncDec++/-- after identSuppress space before (i++)
PostfixQuestion? adjacent to identSuppress space before (Int?)
SafeCallQ?.Suppress space before (x?.y)
MacroBang! after identSuppress space before (println!())
CallOpen(/[ adjacent to identSuppress space (call: f(x))
AssignAdjacent= adjacent to identSuppress space (shell: NAME=val)
DashSep- adjacent to both sidesHyphenated word (from-oci-layout)
SlashSep/ adjacent to both sidesPath separator (linux/amd64)

Runtime Rewrite Passes

Some language-specific fixups operate on the rendered CodeNode tree rather than the source token stream. These handle cases that the tokenizer can’t reach — either because the pattern is structural (node-level, not token-level) or because it applies to the builder API (manually-constructed format strings, not sigil_quote!).

LanguagePassPurposeApplies to
Gorewrite_iifeFuse }() for immediately-invoked functionsBuilder API
Gorewrite_receive_op<- ch<-chBuilder API only (tokenizer handles sigil_quote!)
C++rewrite_lambda_semicolon}}; for lambda block closeBuilder API
Luarewrite_method_colonobj: m()obj:m()Builder API only (tokenizer handles sigil_quote!)
Haskellrewrite_dollar_spacing$word$ wordBuilder API only (tokenizer handles sigil_quote!)

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:

  1. Add a variant to MacroLang in macros/src/parse/types.rs
  2. Map the language identifier in parse_macro_lang() in macros/src/parse/mod.rs
  3. Add language-guarded annotation logic in annotate_tokens() in macros/src/parse/format.rs
  4. If the fix is in spacing after a token, you may also need to adjust state.prev assignment in tokens_to_format_inner()
  5. 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

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.

The RendererLang trait has 14 methods covering rendering essentials. CodeLang adds the spec-layer methods: 4 required plus 6 config struct accessors and override methods — all with sensible defaults. You only need to override the defaults when your language diverges from the common patterns.

This guide walks through the process using a hypothetical language, with references to real implementations you can study.

Overview

Adding a language takes four steps:

  1. Create src/lang/your_lang.rs implementing CodeLang
  2. Add pub mod your_lang; to src/lang/mod.rs
  3. Write integration tests in tests/
  4. Run just bless to generate golden files

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 rendering:

Core Methods (6 required)

These are enough for CodeBlock-level code generation:

MethodExample (TypeScript)Purpose
file_extension()"ts"File extension for output files
reserved_words()&["async", "await", ...]Words that need escaping
render_imports()import { Foo } from '...'Emit the import header
render_string_literal()'hello'Language-specific string quoting
render_doc_comment()/** ... */Doc comment block
line_comment_prefix()"//"Single-line comment prefix

Override Methods (with defaults)

MethodDefaultPurpose
render_verbatim_string()Delegates to render_string_literal()Minimal escaping for interpolated strings

Override render_verbatim_string() if your language has string interpolation (e.g., Bash "$x", TypeScript `${x}`, Python f"{x}").

render_imports() is the most complex. It receives an ImportGroup (deduplicated, with aliases resolved) and must emit the full import header string. Study src/lang/typescript.rs for ES module imports or src/lang/rust.rs for use paths.

The CodeLang Trait

Extends RendererLang with the additional methods needed by the spec layer.

Spec Support Methods (4 required)

These enable TypeSpec, FunSpec, and FieldSpec rendering:

MethodExamplePurpose
render_visibility()"public ", "pub "Visibility prefix
function_keyword()"function", "fn"Function declaration keyword
type_keyword()"class", "struct"Type declaration keyword
methods_inside_type_body()true / falseKey structural decision (see below)

The methods_inside_type_body Decision

This is the most important method for structural correctness. It determines whether TypeSpec emits one CodeBlock or two:

  • Returns true (TypeScript, Java, Python, Swift, Dart, Kotlin, C++): Methods go inside the type body. TypeSpec emits a single block: class Foo { fields; methods; }.
  • Returns false (Rust struct/enum): Methods go in a separate impl block. TypeSpec emits two blocks: struct Foo { fields } and impl Foo { methods }.

The method takes a TypeKind parameter, so you can vary by type. Rust returns true for TypeKind::Trait (trait methods go inside) but false for TypeKind::Struct and TypeKind::Enum.

Config Struct Accessors and Default Methods

Instead of dozens of individual trait methods, the v2.0 API groups related configuration into 6 config structs returned by accessor methods. Each struct uses ..Default::default() so you only specify fields where your language differs. The remaining standalone override methods cover cases that don’t fit neatly into a struct.

block_syntax()

Returns BlockSyntaxConfig controlling block delimiters and formatting:

FieldDefaultPurpose
block_open" {"Opening delimiter. Python overrides to ":".
block_close"}"Closing delimiter. Python overrides to "" (indent-only).
indent_unit" " (2 spaces)Indentation per level.
uses_semicolonstrueStatement terminator behavior.
field_terminator","After each field. Java/C++ override to ";".
type_close_terminator(default)Terminator after closing brace for types.
bases_close(default)Closing syntax for base-class lists.

function_syntax()

Returns FunctionSyntaxConfig controlling function declarations:

FieldDefaultPurpose
return_type_separator": "Between params and return type. Rust overrides to " -> ".
async_keyword"async "Async function prefix.
async_suffix""Async suffix after params. Dart: " async".
async_suffix_before_returnfalseWhen true, suffix goes before return type. Swift: func f() async -> T.
abstract_keyword"abstract "Abstract method prefix. C++ overrides to "virtual ".
param_list_style(default)How parameter lists are formatted.
function_signature_style(default)Controls overall signature layout.
constructor_keyword""Constructor keyword. Python: "def". Rust: "fn".
constructor_delegation_style(default Body)Super/this call placement. Kotlin: Signature.
where_clause_styleInlineInline: bounds in <T: Bound>. WhereBlock: Rust where\n T: Bound,. SeparateWhere: C# where T : Bound per constraint.
empty_body""Empty method body. Python overrides to "...".

type_decl_syntax()

Returns TypeDeclSyntaxConfig controlling type declarations:

FieldDefaultPurpose
type_before_namefalseC/C++/Java override to true for int count.
return_type_is_prefixfalseC/C++/Java override to true for int add(...).
type_annotation_separator": "Between name and type annotation.
super_type_keyword(default)Inheritance keyword, e.g. " extends ".
super_type_separator(default)Separator between multiple super types.
super_type_subsequent_separator(default)Separator for subsequent super types.
implements_keyword(default)Interface keyword, e.g. " implements ".
type_alias_target_firstfalseC overrides to true for typedef target name;.
supports_primary_constructorfalseKotlin overrides to true.

generic_syntax()

Returns GenericSyntaxConfig controlling generic/type-parameter syntax:

FieldDefaultPurpose
open"<"Generic opening bracket. Go overrides to "[".
close">"Generic closing bracket. Go overrides to "]".
application_style(default)How generics are applied to types.
constraint_keyword": "Generic bounds keyword. Java/TS override to " extends ".
constraint_separator" + "Between multiple bounds. Java/TS override to " & ".
context_bound_keyword(default)Context bound syntax (e.g. Scala’s :).

enum_and_annotation()

Returns EnumAndAnnotationConfig controlling enums, annotations, and field modifiers:

FieldDefaultPurpose
variant_prefix""Enum variant prefix. Swift overrides to "case ".
variant_prefix_first(default)Prefix for the first variant specifically.
variant_separator","Between enum variants. Python/Swift override to "".
variant_trailing_separatorfalseRust/TypeScript override to true.
annotation_prefix"@"Annotation opening. Rust: "#[". C++: "[[".
annotation_suffix""Annotation closing. Rust: "]". C++: "]]".
readonly_keyword"const "TS: "readonly ". Kotlin: "val ". Java: "final ".
mutable_field_keyword""Kotlin overrides to "var ".

type_presentation()

Returns TypePresentationConfig controlling how semantic types (arrays, optionals, maps, tuples, references, function types, etc.) are rendered. See the Type Presentation section below for details.

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_name() – default passthrough. Go overrides to return "http.Server" (package-qualified names).
  • module_separator() – returns Option<&str>. Default None. Override to Some("::") (Rust/C++) or Some(".") (Go/Python/Java/etc.) to enable TypeName::qualified() inline rendering.
  • type_kind_suffix() – suffix after type close for specific type kinds.
  • render_newtype_line() – default emits Rust tuple struct struct Name(Inner);. Go: type Name Inner, Kotlin: value class Name(val value: Inner), Python: Name = NewType("Name", Inner), C: typedef Inner Name;.
  • fun_block_open() – custom block opener for functions.
  • type_header_block_open() – custom block opener for type headers.
  • doc_comment_inside_body() – whether doc comments go inside the body (Python docstrings).
  • doc_before_annotations() – whether doc comments appear before annotations.
  • optional_field_style() – how optional fields are represented.
  • property_style() – default Accessor (TS/JS: get name()). Swift/Kotlin: Field (inline get/set).
  • property_getter_keyword() – default "get". Kotlin: "get()".
  • render_type_context() – additional context for type rendering.
  • type_body_prefix() – content emitted before the type body.
  • type_body_suffix() – content emitted after the type body.
  • render_type_close_suffix() – suffix after type close brace.
  • render_type_param_kind() – how type parameters are annotated with variance.
  • line_comment_suffix() – suffix for line comments (default "").

Step-by-Step Walkthrough

1. Create the language file

Create src/lang/your_lang.rs:

use crate::import::ImportGroup;
use crate::lang::CodeLang;
use crate::spec::modifiers::{DeclarationContext, TypeKind, Visibility};

#[derive(Debug, Clone, Default)]
pub struct YourLang;

impl YourLang {
    pub fn new() -> Self {
        Self
    }
}

const RESERVED: &[&str] = &["if", "else", "for", "while", /* ... */];

impl CodeLang 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 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 {
        // Build your import statements from imports.by_module()
        let mut out = String::new();
        for (module, entries) in imports.by_module() {
            let names: Vec<&str> = entries.iter().map(|e| e.resolved_name.as_str()).collect();
            out.push_str(&format!("import {{ {} }} from \"{}\";\n", names.join(", "), module));
        }
        out
    }

    // Spec support methods...
    fn render_visibility(&self, vis: Visibility, _ctx: DeclarationContext) -> &str {
        match vis {
            Visibility::Public => "public ",
            Visibility::Private => "private ",
            Visibility::Protected => "protected ",
            _ => "",
        }
    }

    fn function_keyword(&self, _ctx: DeclarationContext) -> &str { "function" }
    fn type_keyword(&self, kind: TypeKind) -> &str {
        match kind {
            TypeKind::Class => "class",
            TypeKind::Interface | TypeKind::Trait => "interface",
            TypeKind::Enum => "enum",
            TypeKind::Struct => "class",
            TypeKind::TypeAlias => "type",
            TypeKind::Newtype => "class",
        }
    }
    fn methods_inside_type_body(&self, _kind: TypeKind) -> bool { true }

    // Config struct overrides...
    fn block_syntax(&self) -> BlockSyntaxConfig<'_> {
        BlockSyntaxConfig {
            uses_semicolons: true,
            indent_unit: "    ",
            field_terminator: ";",
            ..Default::default()
        }
    }
    fn type_decl_syntax(&self) -> TypeDeclSyntaxConfig<'_> {
        TypeDeclSyntaxConfig {
            super_type_keyword: " extends ",
            implements_keyword: " implements ",
            ..Default::default()
        }
    }
    fn generic_syntax(&self) -> GenericSyntaxConfig<'_> {
        GenericSyntaxConfig {
            constraint_keyword: " extends ",
            constraint_separator: " & ",
            ..Default::default()
        }
    }
    fn function_syntax(&self) -> FunctionSyntaxConfig<'_> {
        FunctionSyntaxConfig {
            return_type_separator: ": ",
            ..Default::default()
        }
    }
}

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.rssigil_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. Generate golden files

just bless

This runs all tests with BLESS=1, which creates test-goldens/your_lang/*.yl files from the actual output. Review them manually, then commit.

5. Override defaults

Run the full test suite and review golden file output. Override config struct accessors and default methods where your language’s syntax differs. Common overrides:

  • If your language uses indentation instead of braces: override block_syntax() to set block_open, block_close; override function_syntax() to set empty_body
  • If types come before names (int x instead of x: int): override type_decl_syntax() to set type_before_name, return_type_is_prefix
  • If generics use brackets instead of angle brackets: override generic_syntax() to set open, close

Reference Implementations

Study these existing implementations for patterns similar to your target:

LanguageFileNotable Patterns
TypeScriptsrc/lang/typescript.rsES module imports, type-only imports, single-quoted strings
Rustsrc/lang/rust.rsuse paths, struct+impl split, pub(crate) visibility
Pythonsrc/lang/python.rsIndent-only blocks (no braces), docstrings inside body, from x import y
Gosrc/lang/go.rsPackage-qualified names (http.Server), bracket generics, func keyword
Csrc/lang/c.rsType-before-name, #include, __attribute__, struct close semicolon
C++src/lang/cpp.rsvirtual instead of abstract, #include + using, [[attributes]]
Bashsrc/lang/bash.rsKeyword-based block closers (fi/done/esac), source imports, shell escaping
Scalasrc/lang/scala.rscase class, trait, [T] generics, <: bounds, = {/} blocks
Haskellsrc/lang/haskell.rsSplit signature style, where/indentation blocks, postfix generics, deriving
OCamlsrc/lang/ocaml.rsPostfix generics, let keyword, = /indentation blocks, open Module imports, module_block helper

Type Presentation

When your language uses type expressions (generics, arrays, optionals, maps, etc.), you configure how each semantic type concept renders by returning a TypePresentationConfig from the type_presentation() accessor. You never build BoxDoc directly.

How it works

Each TypeName variant (Array, Optional, Map, etc.) uses your language’s TypePresentationConfig to determine the syntactic pattern via TypePresentation — a small enum:

  • GenericWrap { name }name<P1, P2> using your generic_syntax().open/generic_syntax().close
  • Prefix { prefix }prefix inner (e.g., Go []T, Rust *const T)
  • Postfix { suffix }inner suffix (e.g., TypeScript T[], Kotlin T?)
  • Surround { prefix, suffix }prefix inner suffix (e.g., C++ const T&, C const T*)
  • Delimited { open, sep, close }open P1 sep P2 close (e.g., Swift [K: V], Go map[K]V)
  • Infix { sep }P1 sep P2 (e.g., TypeScript A | B, Rust A + B)

Configuring type presentation

All fields in TypePresentationConfig have defaults matching TypeScript conventions. Override only when your language differs:

impl CodeLang for YourLang {
    fn type_presentation(&self) -> TypePresentationConfig<'_> {
        TypePresentationConfig {
            // Array: default is Postfix { suffix: "[]" } (TS: T[])
            // Override for Rust-style Vec<T>:
            array: TypePresentation::GenericWrap { name: "Vec" },

            // Optional: default is Infix { sep: " | " } with "null" literal
            // Override for Kotlin-style T?:
            optional: TypePresentation::Postfix { suffix: "?" },

            // Map: default is GenericWrap { name: "Map" }
            // Override for Go-style map[K]V:
            map: TypePresentation::Delimited { open: "map[", sep: "]", close: "" },

            // Tuple: default is Delimited { open: "(", sep: ", ", close: ")" }
            // TS overrides to "[", "]" for [A, B] syntax. This shows Go-style (A, B):
            tuple: TypePresentation::Delimited { open: "(", sep: ", ", close: ")" },

            // Reference: default is Prefix { prefix: "" } (identity — for GC languages)
            // Override for Rust-style &T:
            reference: TypePresentation::Prefix { prefix: "&" },

            // Function types: default is TypeScript (A, B) => R
            function: FunctionPresentation {
                keyword: "fn",
                params_open: "(",
                params_sep: ", ",
                params_close: ")",
                arrow: " -> ",
                return_first: false,
                curried: false,
                wrapper_open: "",
                wrapper_close: "",
            },

            ..Default::default()
        }
    }
}

See Type Presentation for the full enum definition, all available fields, and examples for every supported language.