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

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:

  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

The CodeLang Trait

The trait methods fall into natural groups.

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

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:

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

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 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 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_lang.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_lang.rsPackage-qualified names (http.Server), bracket generics, func keyword
Csrc/lang/c_lang.rsType-before-name, #include, __attribute__, struct close semicolon
C++src/lang/cpp_lang.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.