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