Code Generation

Code generation is the final step in the CDTk pipeline. Override Grammar.Render(SemanticTable) in your output grammar to emit target-language source text from the translated SemanticTable.

Overriding Render()

Grammar.Render(SemanticTable) receives the fully translated SemanticTable and must return a string of target-language source code. The default implementation uses PrettyPrinter.format; override it for custom output:

public class MyOutputGrammar : Grammar {
    public override string Render(SemanticTable table) {
        var sb = new StringBuilder();

        foreach (var fn in table.Morphisms) {
            sb.AppendLine($"func {fn.Name}({fn.Domain}) -> {fn.Codomain} {{");
            sb.AppendLine($"  {fn.Body}");
            sb.AppendLine("}");
        }

        return sb.ToString();
    }
}

Rendering Contract

The most important contract: terminal ObjectRow.Name contains the scanned token text, not the token name.

  • "if" — the actual keyword text in the source file
  • "42" — the literal number
  • "myVar" — the identifier

CDTk's Pipeline.runWithText substitutes scanned text into terminal nodes after parsing. Your Render() implementation can therefore use ObjectRow.Name directly as output text without any lookup.

💡
Do NOT use token field names
If you see "KW_IF" or "INTEGER" in your output, runWithText has not run yet, or you're looking at the wrong field. Always use ObjectRow.Name (which is text), not ObjectRow.Type (which may be the token kind).

CollectTerminals()

Grammar.CollectTerminals(parseTree) flattens a parse tree into a linear list of terminal leaf nodes. This is the standard utility for grammars that need a flat token stream for emission:

public override string Render(SemanticTable table) {
    var sb = new StringBuilder();
    foreach (var fn in table.Morphisms) {
        // CollectTerminals gives a flat list of scanned-text tokens
        var tokens = CollectTerminals(fn.ParseTree);
        foreach (var t in tokens)
            sb.Append(t.Text).Append(' ');
        sb.AppendLine();
    }
    return sb.ToString();
}

Example: Python → WebAssembly

WasmGrammar.Render() compiles Python infix expressions into WAT (WebAssembly Text Format) stack operations. Each infix operator node is transformed by emitting its operands first, then the corresponding WAT instruction:

public override string Render(SemanticTable table) {
    var sb = new StringBuilder();
    sb.AppendLine("(module");
    foreach (var fn in table.Morphisms) {
        // Emit WAT function declaration
        sb.AppendLine($"  (func ${fn.Name} (param $n i32) (result i32)");
        // Convert infix body to stack ops via recursive descent
        EmitWatBody(sb, fn.Body);
        sb.AppendLine("  )");
        sb.AppendLine($"  (export \"{fn.Name}\" (func ${fn.Name}))");
    }
    sb.AppendLine(")");
    return sb.ToString();
}

Example: WAT → C# Decompilation

CSharpGrammar.Render() reverses the process — it reads WAT stack operations and reconstructs C# infix expressions using a stack machine:

public override string Render(SemanticTable table) {
    var sb = new StringBuilder();
    foreach (var fn in table.Morphisms) {
        sb.AppendLine($"public static {fn.Codomain} {fn.Name}({fn.Domain})");
        sb.AppendLine("{");
        // Reconstruct infix expression from WAT stack via a stack machine
        string infix = WatToInfix(fn.Body);
        sb.AppendLine($"    return {infix};");
        sb.AppendLine("}");
    }
    return sb.ToString();
}

Binary Output (GenerateBinary)

For targets that produce binary output (native executables, object files, WASM bytecode), override Grammar.GenerateBinary(SemanticTable) instead of Render(). The Compiler.CompileToBinary() method calls this override if it exists:

public override byte[] GenerateBinary(SemanticTable table) {
    // Custom binary generation — e.g., WASM bytecode
    var writer = new BinaryWriter(new MemoryStream());
    writer.Write(new byte[] { 0x00, 0x61, 0x73, 0x6D });  // WASM magic
    writer.Write(new byte[] { 0x01, 0x00, 0x00, 0x00 });  // version
    // ... emit sections ...
    return ((MemoryStream)writer.BaseStream).ToArray();
}
💡
GenerateBinary vs Render
If your grammar overrides GenerateBinary(), it should still provide a stub Render() for text output (e.g., WAT). Compiler.CompileToBinary() calls GenerateBinary(). Compiler.CompileText() always calls Render().

PE EXE via CRAB

The CRAB sub-project provides a complete pipeline from C# source to native x86-64 Windows PE executables. X86AsmGrammar.GenerateBinary() is called after CDTk translates C# to x86 assembly text:

// CRAB pipeline (CRAB/Grammars/X86AsmGrammar.cs)
public override byte[] GenerateBinary(SemanticTable table) {
    var ast  = X86AsmParser.Parse(table);   // parse x86 asm text
    var code = X86CodeGen.Generate(ast);    // encode to x86 machine code
    return PeWriter.Write(code);            // wrap in PE32+ executable
}

// Usage (CRAB/CrabCompiler.cs)
byte[] exe = CrabCompiler.Compile(csSource);
File.WriteAllBytes("hello.exe", exe);

PrettyPrinter

The default Render() implementation delegates to PrettyPrinter.format(table, grammar). The pretty-printer applies indentation, inserts correct whitespace, and handles TypeKeyword tokens specially — they are not stripped in return-type position:

// PrettyPrinter respects structural roles
// "TypeKeyword" tokens are preserved in "function void Main()" output
// isTypeKw in PrettyPrinter.format checks the token's structural role

// To use the default pretty-printer:
public override string Render(SemanticTable table) =>
    PrettyPrinter.format(table, this);  // uses base implementation

// To bypass it entirely — implement your own formatter:
public override string Render(SemanticTable table) {
    // full custom output
    return MyCustomFormatter.Emit(table);
}