Skip to content

Instantly share code, notes, and snippets.

@yetanotherchris
Last active June 15, 2025 18:48
Show Gist options
  • Save yetanotherchris/c80d0fadb5a2ee5b4beb0a4384020dbf to your computer and use it in GitHub Desktop.
Save yetanotherchris/c80d0fadb5a2ee5b4beb0a4384020dbf to your computer and use it in GitHub Desktop.
Claude's response for improvements to a C# implementation of Go's text templates - https://github.com/yetanotherchris/text-template

Go Text/Template Parser Implementation with ANTLR

Implement a basic Go text/template parser using ANTLR that supports core template functionality. The parser should handle variable interpolation, conditional statements, and loops while maintaining compatibility with Go's template syntax.

Core Requirements

1. Variable Interpolation

  • Support {{ .Field }} syntax for accessing struct fields
  • Support nested field access: {{ .User.Name }}
  • Support array/slice indexing: {{ .Items[0] }}
  • Support map access: {{ .Data.key }}
  • Handle whitespace control with {{- }} and {{ -}}

2. Conditional Statements

  • Support {{ if condition }}...{{ end }} blocks
  • Support {{ if condition }}...{{ else }}...{{ end }}
  • Support {{ if condition }}...{{ else if condition2 }}...{{ end }}
  • Conditions should support:
    • Boolean fields: {{ if .IsActive }}
    • Comparisons: {{ if eq .Status "active" }}
    • Existence checks: {{ if .User }}

3. Loop Statements

  • Support {{ range .Items }}...{{ end }} for iterating over slices/arrays
  • Support {{ range $index, $value := .Items }}...{{ end }} with variables
  • Support {{ range .Map }}...{{ end }} for iterating over maps
  • Support {{ range $key, $value := .Map }}...{{ end }} with key-value variables
  • Handle empty collections with {{ range .Items }}...{{ else }}...{{ end }}

4. Built-in Functions (Basic Set)

  • eq: equality comparison {{ if eq .Status "active" }}
  • ne: not equal {{ if ne .Count 0 }}
  • lt, le, gt, ge: numeric comparisons
  • and, or: logical operations {{ if and .IsActive .IsValid }}
  • not: logical negation {{ if not .IsHidden }}

5. Comments

  • Support {{/* comment */}} syntax
  • Comments should be completely ignored in output

Grammar Structure (ANTLR4)

Your ANTLR grammar should define:

  • Template root with mixed text and actions
  • Action blocks ({{ ... }})
  • Field access with dot notation
  • Conditional blocks with proper nesting
  • Range blocks with optional variable assignments
  • Function calls with arguments
  • String and numeric literals
  • Whitespace control tokens

Edge Cases to Handle

Variable Access Edge Cases

// Nested field access
{{ .User.Profile.Avatar.URL }}

// Array indexing with variables
{{ .Items[.CurrentIndex] }}

// Map access with string keys containing special characters
{{ .Data["key-with-dashes"] }}

// Accessing fields that might not exist (should not panic)
{{ .MaybeNil.Field }}

Conditional Edge Cases

// Nested conditions
{{ if .User }}
  {{ if .User.IsActive }}
    Active user: {{ .User.Name }}
  {{ else }}
    Inactive user
  {{ end }}
{{ end }}

// Complex boolean expressions
{{ if and (eq .Status "active") (gt .Count 0) }}
  Status is active and count is positive
{{ end }}

// Zero value checks
{{ if .Count }}Count: {{ .Count }}{{ else }}No items{{ end }}

Loop Edge Cases

// Empty slice handling
{{ range .Items }}
  Item: {{ . }}
{{ else }}
  No items found
{{ end }}

// Nested loops
{{ range .Categories }}
  Category: {{ .Name }}
  {{ range .Items }}
    - {{ .Title }}
  {{ end }}
{{ end }}

// Range with index over empty slice
{{ range $i, $item := .EmptySlice }}
  Never executed
{{ else }}
  Empty slice message
{{ end }}

Whitespace Control Edge Cases

// Trimming leading whitespace
  {{- .Field }}

// Trimming trailing whitespace
{{ .Field -}}  

// Trimming both
  {{- .Field -}}  

// In loops and conditions
{{ range .Items -}}
{{- .Name }}
{{- end }}

Test Cases for Implementation

Basic Variable Tests

// Test data
data := struct {
    Name   string
    Count  int
    Active bool
    Items  []string
    User   *User
}{
    Name:   "Test",
    Count:  42,
    Active: true,
    Items:  []string{"a", "b", "c"},
    User:   &User{Name: "John", Age: 30},
}

// Test cases
"Hello {{ .Name }}""Hello Test"
"Count: {{ .Count }}""Count: 42"
"User: {{ .User.Name }}""User: John"
"First: {{ .Items[0] }}""First: a"

Conditional Tests

"{{ if .Active }}Active{{ else }}Inactive{{ end }}""Active"
"{{ if .Count }}Has items{{ end }}""Has items"
"{{ if eq .Name \"Test\" }}Match{{ end }}""Match"
"{{ if and .Active (gt .Count 10) }}Both true{{ end }}""Both true"

Loop Tests

"{{ range .Items }}{{ . }} {{ end }}""a b c "
"{{ range $i, $v := .Items }}{{ $i }}:{{ $v }} {{ end }}""0:a 1:b 2:c "
"{{ range .EmptySlice }}Never{{ else }}Empty{{ end }}""Empty"

Error Handling Tests

// Should not panic, should render empty or default
"{{ .NonExistent }}"""
"{{ .User.NonExistent }}"""
"{{ .NilPointer.Field }}"""

Implementation Guidelines

  1. Error Handling: The parser should gracefully handle missing fields and nil pointers without panicking
  2. Type Safety: Implement proper type checking for comparisons and operations
  3. Performance: Cache field lookups and compiled templates where possible
  4. Memory Safety: Avoid infinite recursion in nested templates
  5. Whitespace: Preserve original whitespace unless explicitly trimmed with {{- -}}

Output Format

Provide:

  1. ANTLR4 grammar file (.g4)
  2. Go code for the template engine
  3. Unit tests covering all edge cases mentioned above
  4. Example usage showing how to parse and execute templates
  5. Error handling examples

The implementation should be production-ready for basic use cases while remaining simple enough to understand and extend.

What is a comprehensive list of built in functions

Go text templates support pipelines and chaining of actions, which are powerful features that allow you to process data through a series of transformations in a readable, left-to-right manner.

Pipelines

A pipeline in Go templates uses the pipe operator | to pass the output of one action as input to the next. The basic syntax is:

{{ value | function1 | function2 | function3 }}

The value flows from left to right through each function. Here's a simple example:

{{ .Name | printf "Hello, %s!" | print }}

This takes the .Name field, passes it to printf to format it, then passes the result to print.

Chaining Actions

You can chain multiple template actions together using pipelines. Common patterns include:

String manipulation:

{{ .Title | lower | title }}  // Convert to lowercase, then title case
{{ .Content | html | printf "<p>%s</p>" }}  // HTML escape, then wrap in tags

Conditional processing:

{{ .Value | default "N/A" | printf "Result: %s" }}

Multiple transformations:

{{ .Price | printf "%.2f" | printf "$%s" }}  // Format as decimal, then add dollar sign

Built-in Functions in Pipelines

Go templates provide several built-in functions that work well in pipelines:

  • printf - Format strings (like fmt.Sprintf)
  • print, println - Output functions
  • html - HTML escape
  • js - JavaScript escape
  • urlquery - URL query escape
  • len - Length of arrays, slices, maps, strings
  • index - Get element at index
  • slice - Create slice from array/slice

Custom Functions

You can register custom functions for use in pipelines:

funcMap := template.FuncMap{
    "upper": strings.ToUpper,
    "reverse": func(s string) string {
        runes := []rune(s)
        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
            runes[i], runes[j] = runes[j], runes[i]
        }
        return string(runes)
    },
}

tmpl := template.New("example").Funcs(funcMap)

Then use them in pipelines:

{{ .Name | upper | reverse | printf "Transformed: %s" }}

Complex Pipeline Examples

Processing lists:

{{ range .Items }}
  {{ . | printf "Item: %s" | println }}
{{ end }}

Conditional formatting:

{{ if .IsActive }}
  {{ .Status | upper | printf "STATUS: %s" }}
{{ else }}
  {{ "inactive" | printf "STATUS: %s" }}
{{ end }}

Nested data access:

{{ .User.Email | printf "mailto:%s" | printf "<a href='%s'>Contact</a>" | html }}

Pipelines make Go templates more readable and allow you to build complex data transformations by composing simple functions. The key is that data flows left-to-right through the pipeline, with each function receiving the output of the previous one as its final argument.

Could you provide this in the form of a prompt (with all the same information you just produced) that OpenAI Codex could implement as a class, and with test cases it can use for unit tests

C# sprintf Implementation Prompt for OpenAI Codex

Create a C# class called SprintfFormatter that implements C-style sprintf formatting functionality. The class should provide a static method Format(string format, params object[] args) that mimics the behavior of C's sprintf function.

Required Format Specifiers to Implement

Basic Integer Formats

  • %d, %i - signed decimal integer
    • C# equivalent: {0} or {0:D} or .ToString() or .ToString("D")
  • %u - unsigned decimal integer
    • C# equivalent: {0} or {0:D} or .ToString() or .ToString("D")
  • %o - unsigned octal integer
    • C# equivalent: No direct equivalent, use Convert.ToString(value, 8)
  • %x - unsigned hexadecimal (lowercase a-f)
    • C# equivalent: {0:x} or .ToString("x")
  • %X - unsigned hexadecimal (uppercase A-F)
    • C# equivalent: {0:X} or .ToString("X")

Floating-Point Formats

  • %f, %F - decimal floating point
    • C# equivalent: {0:F} or .ToString("F")
  • %e - scientific notation (lowercase e)
    • C# equivalent: {0:e} or .ToString("e")
  • %E - scientific notation (uppercase E)
    • C# equivalent: {0:E} or .ToString("E")
  • %g - shortest of %f or %e
    • C# equivalent: {0:G} or .ToString("G")
  • %G - shortest of %F or %E
    • C# equivalent: {0:G} or .ToString("G")

Character and String Formats

  • %c - single character
    • C# equivalent: {0} or .ToString()
  • %s - string of characters
    • C# equivalent: {0} or .ToString()
  • %% - literal percent sign
    • C# equivalent: {{ or }} in string.Format, or literal % in interpolation

Pointer Format

  • %p - pointer address (format as hexadecimal with 0x prefix)
    • C# equivalent: No direct equivalent, use custom formatting with IntPtr

Width and Precision Modifiers

  • %10d - minimum width of 10 characters (right-aligned)
    • C# equivalent: {0,10} or .ToString().PadLeft(10)
  • %-10d - left-aligned in 10-character field
    • C# equivalent: {0,-10} or .ToString().PadRight(10)
  • %010d - zero-padded to 10 characters
    • C# equivalent: {0:D10} or .ToString("D10")
  • %.2f - 2 decimal places for floating point
    • C# equivalent: {0:F2} or .ToString("F2")
  • %10.2f - 10 characters wide, 2 decimal places
    • C# equivalent: {0,10:F2} or .ToString("F2").PadLeft(10)
  • %-10.2f - left-aligned, 10 characters wide, 2 decimal places
    • C# equivalent: {0,-10:F2} or .ToString("F2").PadRight(10)

Flags to Support

  • - - left-justify
    • C# equivalent: Use negative width in string.Format {0,-10} or .PadRight()
  • + - always show sign for numbers
    • C# equivalent: Custom formatting with conditional logic, or use ToString("+#;-#;0")
  • (space) - space for positive numbers
    • C# equivalent: Custom formatting with conditional logic
  • # - alternate form (0x for hex, 0 for octal)
    • C# equivalent: Custom formatting, no direct equivalent
  • 0 - zero-pad numbers
    • C# equivalent: Use format specifiers like D10 for integers or width padding

Length Modifiers (Optional but Recommended)

  • %ld - long integer
    • C# equivalent: {0} or .ToString() (long is handled automatically in C#)
  • %hd - short integer
    • C# equivalent: {0} or .ToString() (short is handled automatically in C#)

Implementation Requirements

  1. Create a SprintfFormatter class with a static Format method
  2. Parse the format string to identify format specifiers
  3. Handle all the format types listed above
  4. Support width, precision, and flag modifiers
  5. Throw appropriate exceptions for invalid format strings or argument mismatches
  6. Handle edge cases like null arguments, overflow conditions, etc.

Example Usage

string result1 = SprintfFormatter.Format("%d", 123);              // "123"
string result2 = SprintfFormatter.Format("%.2f", 3.14159);        // "3.14"
string result3 = SprintfFormatter.Format("%10s", "Hello");        // "     Hello"
string result4 = SprintfFormatter.Format("%-10d", 42);            // "42        "
string result5 = SprintfFormatter.Format("%#x", 255);             // "0xff"
string result6 = SprintfFormatter.Format("%+d", 42);              // "+42"

Unit Test Cases to Implement

Create comprehensive unit tests covering all format specifiers and edge cases:

Basic Integer Tests

[Test] public void TestBasicInteger() {
    Assert.AreEqual("123", SprintfFormatter.Format("%d", 123));
    Assert.AreEqual("123", SprintfFormatter.Format("%i", 123));
    Assert.AreEqual("123", SprintfFormatter.Format("%u", 123u));
    Assert.AreEqual("-123", SprintfFormatter.Format("%d", -123));
}

Hexadecimal Tests

[Test] public void TestHexadecimal() {
    Assert.AreEqual("ff", SprintfFormatter.Format("%x", 255));
    Assert.AreEqual("FF", SprintfFormatter.Format("%X", 255));
    Assert.AreEqual("0xff", SprintfFormatter.Format("%#x", 255));
    Assert.AreEqual("0XFF", SprintfFormatter.Format("%#X", 255));
}

Octal Tests

[Test] public void TestOctal() {
    Assert.AreEqual("377", SprintfFormatter.Format("%o", 255));
    Assert.AreEqual("0377", SprintfFormatter.Format("%#o", 255));
}

Floating Point Tests

[Test] public void TestFloatingPoint() {
    Assert.AreEqual("3.140000", SprintfFormatter.Format("%f", 3.14));
    Assert.AreEqual("3.14", SprintfFormatter.Format("%.2f", 3.14));
    Assert.AreEqual("3.1e+00", SprintfFormatter.Format("%.1e", 3.1));
    Assert.AreEqual("3.1E+00", SprintfFormatter.Format("%.1E", 3.1));
    Assert.AreEqual("3.14", SprintfFormatter.Format("%g", 3.14));
    Assert.AreEqual("3.14", SprintfFormatter.Format("%G", 3.14));
}

Width and Alignment Tests

[Test] public void TestWidthAndAlignment() {
    Assert.AreEqual("   123", SprintfFormatter.Format("%6d", 123));
    Assert.AreEqual("123   ", SprintfFormatter.Format("%-6d", 123));
    Assert.AreEqual("000123", SprintfFormatter.Format("%06d", 123));
    Assert.AreEqual("  3.14", SprintfFormatter.Format("%6.2f", 3.14));
    Assert.AreEqual("3.14  ", SprintfFormatter.Format("%-6.2f", 3.14));
}

Character and String Tests

[Test] public void TestCharacterAndString() {
    Assert.AreEqual("A", SprintfFormatter.Format("%c", 'A'));
    Assert.AreEqual("A", SprintfFormatter.Format("%c", 65));
    Assert.AreEqual("Hello", SprintfFormatter.Format("%s", "Hello"));
    Assert.AreEqual("     Hello", SprintfFormatter.Format("%10s", "Hello"));
    Assert.AreEqual("Hello     ", SprintfFormatter.Format("%-10s", "Hello"));
}

Sign and Space Tests

[Test] public void TestSignsAndSpaces() {
    Assert.AreEqual("+123", SprintfFormatter.Format("%+d", 123));
    Assert.AreEqual("-123", SprintfFormatter.Format("%+d", -123));
    Assert.AreEqual(" 123", SprintfFormatter.Format("% d", 123));
    Assert.AreEqual("-123", SprintfFormatter.Format("% d", -123));
}

Special Cases Tests

[Test] public void TestSpecialCases() {
    Assert.AreEqual("%", SprintfFormatter.Format("%%"));
    Assert.AreEqual("0", SprintfFormatter.Format("%d", 0));
    Assert.AreEqual("0.000000", SprintfFormatter.Format("%f", 0.0));
}

Multiple Arguments Tests

[Test] public void TestMultipleArguments() {
    Assert.AreEqual("Hello 123 3.14", SprintfFormatter.Format("%s %d %.2f", "Hello", 123, 3.14159));
    Assert.AreEqual("Value: 42, Hex: 2a", SprintfFormatter.Format("Value: %d, Hex: %x", 42, 42));
}

Error Cases Tests

[Test] public void TestErrorCases() {
    Assert.Throws<ArgumentException>(() => SprintfFormatter.Format("%d"));  // Missing argument
    Assert.Throws<ArgumentException>(() => SprintfFormatter.Format("%d", "not a number"));  // Wrong type
    Assert.Throws<FormatException>(() => SprintfFormatter.Format("%q", 123));  // Invalid format
}

Pointer Tests (if implementing %p)

[Test] public void TestPointer() {
    IntPtr ptr = new IntPtr(0x12345678);
    string result = SprintfFormatter.Format("%p", ptr);
    Assert.IsTrue(result.StartsWith("0x"));
    Assert.IsTrue(result.Contains("12345678"));
}

Implementation Notes

  1. Use regular expressions or state machine parsing to identify format specifiers
  2. Handle type conversion carefully - integers should work with various numeric types
  3. For floating point formats, pay attention to precision and scientific notation thresholds
  4. Consider thread safety if needed
  5. Provide clear error messages for invalid formats or argument mismatches
  6. Handle null arguments gracefully
  7. Consider performance optimization for frequently used format patterns

C# Format Reference for Implementation

When implementing each sprintf format, use these C# equivalents as the underlying mechanism:

sprintf Format C# string.Format C# ToString() C# Interpolation
%d, %i {0} or {0:D} .ToString() or .ToString("D") $"{value}"
%u {0} or {0:D} .ToString() or .ToString("D") $"{value}"
%o Custom logic Convert.ToString(value, 8) $"{Convert.ToString(value, 8)}"
%x {0:x} .ToString("x") $"{value:x}"
%X {0:X} .ToString("X") $"{value:X}"
%f, %F {0:F} .ToString("F") $"{value:F}"
%e {0:e} .ToString("e") $"{value:e}"
%E {0:E} .ToString("E") $"{value:E}"
%g {0:G} .ToString("G") $"{value:G}"
%G {0:G} .ToString("G") $"{value:G}"
%c {0} .ToString() $"{value}"
%s {0} .ToString() $"{value}"
%10d {0,10} .ToString().PadLeft(10) $"{value,10}"
%-10d {0,-10} .ToString().PadRight(10) $"{value,-10}"
%010d {0:D10} .ToString("D10") $"{value:D10}"
%.2f {0:F2} .ToString("F2") $"{value:F2}"

Bonus Features (Optional)

  • Support for positional arguments like %2$d (argument 2)
  • Support for * width and precision specifiers from arguments
  • Additional length modifiers (ll, h, hh)
  • Better error reporting with position information

The implementation should be robust, well-tested, and handle edge cases appropriately while maintaining compatibility with standard C sprintf behavior.

@yetanotherchris
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment