Adding a Language
sigil-stitch supports new languages by implementing the CodeLang trait. The trait has 33 methods: 10 required (no default) plus 6 config struct accessors and 17 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:
- Create
src/lang/your_lang.rsimplementingCodeLang - Add
pub mod your_lang;tosrc/lang/mod.rs - Write integration tests in
tests/ - Run
just blessto generate golden files
The CodeLang Trait
The trait methods fall into natural groups.
Core Methods (6 required)
These are enough for CodeBlock-level code generation:
| Method | Example (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 |
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_lang.rs for use paths.
Spec Support Methods (4 required)
These enable TypeSpec, FunSpec, and FieldSpec rendering:
| Method | Example | Purpose |
|---|---|---|
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 / false | Key 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 separateimplblock. TypeSpec emits two blocks:struct Foo { fields }andimpl 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:
| Field | Default | Purpose |
|---|---|---|
block_open | " {" | Opening delimiter. Python overrides to ":". |
block_close | "}" | Closing delimiter. Python overrides to "" (indent-only). |
indent_unit | " " (2 spaces) | Indentation per level. |
uses_semicolons | true | Statement 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:
| Field | Default | Purpose |
|---|---|---|
return_type_separator | ": " | Between params and return type. Rust overrides to " -> ". |
async_keyword | "async " | Async function prefix. |
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_style | (default) | How where clauses are rendered. |
empty_body | "" | Empty method body. Python overrides to "...". |
type_decl_syntax()
Returns TypeDeclSyntaxConfig controlling type declarations:
| Field | Default | Purpose |
|---|---|---|
type_before_name | false | C/C++/Java override to true for int count. |
return_type_is_prefix | false | C/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_first | false | C overrides to true for typedef target name;. |
supports_primary_constructor | false | Kotlin overrides to true. |
generic_syntax()
Returns GenericSyntaxConfig controlling generic/type-parameter syntax:
| Field | Default | Purpose |
|---|---|---|
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:
| Field | Default | Purpose |
|---|---|---|
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_separator | false | Rust/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()– returnsOption<&str>. DefaultNone. Override toSome("::")(Rust/C++) orSome(".")(Go/Python/Java/etc.) to enableTypeName::qualified()inline rendering.type_kind_suffix()– suffix after type close for specific type kinds.render_newtype_line()– default emits Rust tuple structstruct 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()– defaultAccessor(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 two test files following the existing pattern:
tests/your_lang_tests.rs – basic CodeBlock rendering:
mod golden;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::lang::your_lang::YourLang;
use sigil_stitch::type_name::TypeName;
#[test]
fn test_basic_statement() {
let mut cb = CodeBlock::builder();
cb.add_statement("const x = 1", ());
let block = cb.build().unwrap();
let lang = YourLang::new();
let imports = sigil_stitch::import::ImportGroup::new();
let mut renderer = sigil_stitch::code_renderer::CodeRenderer::new(&lang, &imports, 80);
let output = renderer.render(&block).unwrap();
golden::assert_golden("your_lang", "basic_statement", "yl", &output);
}
tests/phase2_your_lang_tests.rs – spec-layer rendering (TypeSpec, FunSpec, FileSpec).
4. Generate golden files
just bless
This runs all tests with BLESS=1, which creates tests/golden/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 setblock_open,block_close; overridefunction_syntax()to setempty_body - If types come before names (
int xinstead ofx: int): overridetype_decl_syntax()to settype_before_name,return_type_is_prefix - If generics use brackets instead of angle brackets: override
generic_syntax()to setopen,close
Reference Implementations
Study these existing implementations for patterns similar to your target:
| Language | File | Notable Patterns |
|---|---|---|
| TypeScript | src/lang/typescript.rs | ES module imports, type-only imports, single-quoted strings |
| Rust | src/lang/rust_lang.rs | use paths, struct+impl split, pub(crate) visibility |
| Python | src/lang/python.rs | Indent-only blocks (no braces), docstrings inside body, from x import y |
| Go | src/lang/go_lang.rs | Package-qualified names (http.Server), bracket generics, func keyword |
| C | src/lang/c_lang.rs | Type-before-name, #include, __attribute__, struct close semicolon |
| C++ | src/lang/cpp_lang.rs | virtual instead of abstract, #include + using, [[attributes]] |
| Bash | src/lang/bash.rs | Keyword-based block closers (fi/done/esac), source imports, shell escaping |
| Scala | src/lang/scala.rs | case class, trait, [T] generics, <: bounds, = {/} blocks |
| Haskell | src/lang/haskell.rs | Split signature style, where/indentation blocks, postfix generics, deriving |
| OCaml | src/lang/ocaml.rs | Postfix generics, let keyword, = /indentation blocks, open Module imports, module_block helper |
Type 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 yourgeneric_syntax().open/generic_syntax().closePrefix { prefix }—prefix inner(e.g., Go[]T, Rust*const T)Postfix { suffix }—inner suffix(e.g., TypeScriptT[], KotlinT?)Surround { prefix, suffix }—prefix inner suffix(e.g., C++const T&, Cconst T*)Delimited { open, sep, close }—open P1 sep P2 close(e.g., Swift[K: V], Gomap[K]V)Infix { sep }—P1 sep P2(e.g., TypeScriptA | B, RustA + 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.