Skip to content

Prototype: Builder pattern for code emission#152

Closed
clank-hayen[bot] wants to merge 1 commit into
masterfrom
feature/codegen-builders
Closed

Prototype: Builder pattern for code emission#152
clank-hayen[bot] wants to merge 1 commit into
masterfrom
feature/codegen-builders

Conversation

@clank-hayen

@clank-hayen clank-hayen Bot commented Jan 2, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR introduces a fluent builder pattern for C++ code emission in the Bishop compiler codegen phase.

What's Added

  • CppBuilder class (`cpp_builder.hpp/cpp`): A fluent API for generating C++ code with:
    • Automatic indentation management
    • Block nesting depth tracking
    • Runtime validation for mismatched braces

Key Methods

Category Methods
Loops `for_range()`, `for_each()`, `while_loop()`
Conditionals `if_stmt()`, `else_if()`, `else_block()`
Functions `function()`, `static_function()`
Structs `struct_def()`, `field()`, `end_struct()`
Statements `statement()`, `return_stmt()`, `break_stmt()`, `continue_stmt()`
Blocks `block()`, `end()`
Raw `raw()`, `line()`, `blank()`, `comment()`

Example Usage

```cpp
CppBuilder b;
b.function("int", "sum_range", "int n")
.statement("int sum = 0")
.for_range("i", "0", "n")
.statement("sum += i")
.end()
.return_stmt("sum")
.end();

std::string code = b.str();
// Output:
// int sum_range(int n) {
// int sum = 0;
// for (int i = 0; i < n; i++) {
// sum += i;
// }
// return sum;
// }
```

Convenience Functions

For one-shot generation without the builder:

```cpp
builder::for_range_stmt(var, start, end, body);
builder::for_each_stmt(var, collection, body);
builder::function_def(return_type, name, params, body);
builder::struct_def(name, fields);
builder::if_stmt(condition, then_body, else_body);
```

Test plan

  • Review fluent API design
  • Verify indentation handling
  • Test block nesting validation

Related


🤖 Generated with Claude Code

Introduces CppBuilder class with fluent API for generating C++ code:
- Automatic indentation management
- Methods for common patterns (for_range, for_each, if_stmt, etc.)
- Block open/close with nesting depth tracking
- Catches mismatched braces at runtime with exceptions
- Convenience functions for one-shot generation

Key methods:
- for_range(var, start, end) / for_each(var, collection)
- if_stmt(condition) / else_if(condition) / else_block()
- function(return_type, name, params)
- struct_def(name) / field(type, name) / end_struct()
- statement(stmt) / return_stmt(expr)
- block() / end()

This is a prototype for exploring builder-based code generation as
an alternative to inline string concatenation.

Refs: #150

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@clank-hayen clank-hayen Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW: Builder Pattern Prototype

Summary: Well-structured fluent builder with good fundamentals, but the convenience functions have issues and the prototype isn't integrated.

Critical Issues

1. Convenience functions don't use the builder properly

Location: codegen/cpp_builder.cpp:333-356 (and other convenience functions)

The convenience functions (for_range_stmt, for_each_stmt, etc.) bypass the builder's indentation system entirely and hardcode "\t" for indentation:

std::string for_range_stmt(...) {
    CppBuilder b;
    b.for_range(var, start, end);
    for (const auto& stmt : body) {
        b.raw(b.str().empty() ? "" : "");  // no-op, just need to add statements
        b.line(stmt);
    }
    // Fix: we need to manually add statements
    CppBuilder builder;
    builder.raw("for (int " + var + " = " + start + ...);  // bypasses builder
    for (const auto& stmt : body) {
        builder.raw("\t" + stmt + "\n");  // hardcoded indent
    }
    builder.raw("}");
    return builder.str();
}

This is confusing code - it creates a builder, abandons it, creates another, and then uses raw() to bypass all the fluent API advantages. The convenience functions should use the builder properly:

std::string for_range_stmt(...) {
    CppBuilder b;
    b.for_range(var, start, end);
    for (const auto& stmt : body) {
        b.statement(stmt);
    }
    b.end();
    return b.str();
}

2. Nesting depth can go negative silently

Location: codegen/cpp_builder.cpp:318-323

CppBuilder& CppBuilder::dedent() {
    if (indent_level_ > 0) {
        --indent_level_;
    }
    return *this;
}

This silently clamps to 0 rather than throwing an error like end() does. Inconsistent error handling - if end() throws on mismatched blocks, dedent() should throw on underflow or at least match the behavior.


Suggestions (Non-blocking)

1. No integration with existing codegen

The builder isn't used anywhere in the codebase. The current emit functions in emit_for.cpp, emit_if.cpp, etc. use fmt::format with manual string concatenation. For this prototype to be properly evaluated, it would help to see at least one emit function converted to use the builder.

2. Missing const auto& option for for_each

for_each() hardcodes auto&, but read-only iteration should use const auto&. Consider:

CppBuilder& for_each(const std::string& var, const std::string& collection, bool is_const = false);

3. depth() exposed but never used

The depth() method is public for "debugging" but there's no assertion or validation that depth returns to 0 after generating complete code. Consider adding a validate() or is_complete() method.

4. No tests included

A prototype should include at least basic unit tests demonstrating the API works correctly. This would also serve as documentation of expected usage.


Comparison with Current Approach

Aspect Current (fmt::format) Builder Pattern
Readability Explicit string building Fluent method chains
Indentation Manual ("\t") Automatic tracking
Brace matching Manual (error-prone) Runtime validation
Nested structures Harder to visualize Natural nesting
Performance Direct fmt::format ostringstream buffering
Integration Already in use Would require migration

Pros of Builder:

  • Automatic indentation is a real improvement - no more counting tabs
  • Runtime validation catches mismatched braces (current approach doesn't)
  • Fluent API reads more like the output structure

Cons of Builder:

  • Requires learning new API
  • end() calls can be easy to forget (though the runtime check catches this)
  • The convenience functions demonstrate the API friction - they bypass the builder

Decision: COMMENT

This is a reasonable prototype that demonstrates the builder pattern approach. The core CppBuilder class is solid, but the convenience functions have bugs that need fixing before the pattern can be fully evaluated.

For comparing with PR #151 (templates), I'd recommend:

  1. Fix the convenience functions to actually use the builder
  2. Convert one real emit function (e.g., emit_for.cpp) to use the builder
  3. Add basic tests

This will give a fair comparison of how each pattern handles real codegen scenarios.

@clank-hayen clank-hayen Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW: Builder Pattern for Code Emission

Summary: Well-implemented fluent builder API with solid error handling, but the convenience functions reveal some design limitations.


✅ Strengths

1. Fluent API Design
The method chaining is clean and intuitive:

b.function("int", "sum", "int n")
    .for_range("i", "0", "n")
        .statement("sum += i")
    .end()
.end();

2. Automatic Indentation Management
Eliminates manual \t concatenation seen in current codegen. Indentation is correctly tracked via indent_level_.

3. Runtime Safety for Block Nesting
Good use of nesting_depth_ to throw on mismatched blocks:

if (nesting_depth_ <= 0) {
    throw std::runtime_error("CppBuilder::end() called with no open block");
}

4. Good Documentation
All methods have docstrings as required by project guidelines. Header has clear usage example.


⚠️ Issues to Address

1. Convenience Functions Don't Use the Builder
The convenience functions (for_range_stmt, for_each_stmt, etc.) bypass the fluent API and use raw string concatenation:

// Lines 347-354 in cpp_builder.cpp
CppBuilder builder;
builder.raw("for (int " + var + " = " + start + ...);
for (const auto& stmt : body) {
    builder.raw("\t" + stmt + "\n");  // Hardcoded \t
}

This defeats the purpose of the builder pattern. If we're using raw concatenation anyway, why have the builder?

Fix: Either:

  • Use the fluent API in convenience functions: builder.for_range(...).statement(...).end()
  • Remove convenience functions (prefer fluent API only)
  • Accept that convenience functions are thin wrappers for simple cases

2. Dead Code in for_range_stmt
Lines 337-345 create a builder b, do nothing useful, then create a new builder:

CppBuilder b;
b.for_range(var, start, end);
for (const auto& stmt : body) {
    b.raw(b.str().empty() ? "" : "");  // no-op
    b.line(stmt);
}
// Fix: we need to manually add statements
CppBuilder builder;  // starts over!

This looks like unfinished refactoring. Should be cleaned up.

3. Nested Indentation Not Handled in Convenience Functions
The convenience functions hardcode single-level indentation (\t). For nested structures, this won't work:

// If you nest a for_range inside a function_def, body gets \t not \t\t

💭 Comparison with Template Pattern (PR #151)

Aspect Builder (this PR) Template (#151)
Code Shape Visibility Methods obscure final shape Templates show exact output
Extensibility Add new methods Add new template files
Nested Structures Natural via chaining Requires careful indentation
Error Detection Runtime (block mismatch) Runtime (missing placeholder)
File I/O None Template loading
IDE Support Autocomplete on methods Syntax highlighting in templates

Builder Pros:

  • Single class, no external files
  • Method chaining for complex nesting
  • Type-safe API (method names vs string placeholders)

Builder Cons:

  • Harder to visualize final C++ output
  • More verbose for simple cases
  • The convenience functions show the pattern gets awkward when you want one-shot generation

📋 Suggestions (Non-blocking)

  1. Consider removing convenience functions or making them properly use the fluent API
  2. Add a validate() method to check nesting_depth_ == 0 before calling str()
  3. The for_range method hardcodes int for loop variable - consider parameterizing this

Decision: COMMENT

No critical bugs or security issues. This is a valid prototype for evaluation against the template approach. The convenience function issues should be addressed if this pattern is chosen for adoption, but they're fine for a prototype.

The choice between this and templates should depend on which pattern the team finds easier to read and extend. I'd lean slightly toward templates for visibility of final output, but the builder pattern's nested block handling is genuinely useful for complex structures.


🤖 Generated with Claude Code

@clank-hayen

clank-hayen Bot commented Jan 2, 2026

Copy link
Copy Markdown
Contributor Author

REVIEW: Builder Pattern Prototype

Summary: Solid fluent builder API design with good core implementation, but convenience functions defeat the pattern's purpose.


Critical Issues

1. Convenience functions bypass the builder pattern entirely

Location: codegen/cpp_builder.cpp:333-436

All convenience functions (for_range_stmt, for_each_stmt, function_def, struct_def, if_stmt) use raw() with hardcoded "\t" indentation instead of the fluent API. This means they:

  • Don't benefit from automatic indentation tracking
  • Can't handle nested structures (always single-level \t)
  • Defeat the purpose of having a builder pattern

Example from for_range_stmt (lines 347-355):

CppBuilder builder;
builder.raw("for (int " + var + " = " + start + "; " + var + " < " + end + "; " + var + "++) {\n");
for (const auto& stmt : body) {
    builder.raw("\t" + stmt + "\n");  // hardcoded indent, ignores builder's indentation
}
builder.raw("}");

The correct implementation should use the fluent API:

std::string for_range_stmt(const std::string& var, const std::string& start, 
                           const std::string& end, const std::vector<std::string>& body) {
    CppBuilder b;
    b.for_range(var, start, end);
    for (const auto& stmt : body) {
        b.statement(stmt);
    }
    b.end();
    return b.str();
}

2. Dead code in for_range_stmt

Location: codegen/cpp_builder.cpp:337-344

CppBuilder b;
b.for_range(var, start, end);
for (const auto& stmt : body) {
    b.raw(b.str().empty() ? "" : "");  // no-op
    b.line(stmt);
}
// Fix: we need to manually add statements
CppBuilder builder;  // Discards b and starts fresh

This creates a builder b, uses it partially, then creates a new builder and discards b. Looks like an incomplete refactor with a comment explaining the problem but not fixing it.


Suggestions (Non-blocking)

1. dedent() silently clamps vs end() throws

end() throws when nesting_depth_ <= 0, but dedent() silently clamps to 0. For consistency:

CppBuilder& CppBuilder::dedent() {
    if (indent_level_ <= 0) {
        throw std::runtime_error("CppBuilder::dedent() called at indent level 0");
    }
    --indent_level_;
    return *this;
}

2. No validation that code is complete

There's no way to verify nesting_depth_ == 0 before extracting code. Consider:

std::string str_validated() const {
    if (nesting_depth_ != 0) {
        throw std::runtime_error("CppBuilder has " + std::to_string(nesting_depth_) + " unclosed blocks");
    }
    return str();
}

3. for_each() always uses auto&

Read-only iteration should use const auto&. Consider adding a bool is_const parameter or separate for_each_const() method.

4. for_range() hardcodes int loop variable

The loop type is fixed to int. Consider parameterizing for size_t and other types:

CppBuilder& for_range(const std::string& var, const std::string& start,
                      const std::string& end, const std::string& type = "int");

5. Not integrated with existing codegen

The builder isn't used anywhere in the codebase. For fair comparison with PR #151, it would help to convert at least one emit function (e.g., emit_for.cpp) to use the builder.


Comparison Assessment

Aspect Builder (this PR) Current (fmt::format)
Indentation Automatic tracking Manual "\t" strings
Block safety Runtime validation None (error-prone)
Readability Method chains show nesting Explicit string building
Nested structures Natural via chaining Harder to visualize
Migration effort New API to learn Already in use

The builder pattern's core value proposition (automatic indentation + block validation) is sound. The fluent API is well-designed and the CppBuilder class implementation is solid. However, the convenience functions completely undermine the pattern by falling back to raw string concatenation.


Decision: COMMENT

This is a valid prototype for comparison with the template approach in PR #151. The core CppBuilder class is well-implemented. However:

  1. The convenience functions need to be fixed to actually use the fluent API, or removed
  2. An integration example would help evaluate real-world ergonomics

For comparing prototypes, I'd recommend fixing the convenience functions first so both PRs demonstrate their patterns cleanly.


🤖 Generated with Claude Code

@clank-hayen

clank-hayen Bot commented Jan 2, 2026

Copy link
Copy Markdown
Contributor Author

REVIEW: Builder Pattern for Code Emission

Summary: Well-designed fluent builder API with automatic indentation management, but convenience functions have implementation issues and the pattern may add complexity without solving the core problem.


Strengths

1. Clean Fluent API
The builder API is intuitive and readable:

b.function("int", "sum_range", "int n")
    .statement("int sum = 0")
    .for_range("i", "0", "n")
        .statement("sum += i")
    .end()
    .return_stmt("sum")
.end();

2. Runtime Block Validation
Good error handling for mismatched braces - throws runtime_error on end() without matching open block. This catches structural errors early.

3. Automatic Indentation
Indentation is managed automatically via indent_level_ and nesting_depth_ tracking, reducing manual \t management.

4. Good Documentation
All methods have proper docstrings per project standards.


Issues

Critical: Convenience functions bypass the builder

The convenience functions (lines 333-436) have problems:

  1. for_range_stmt() has dead code (lines 337-345):

    b.for_range(var, start, end);
    for (const auto& stmt : body) {
        b.raw(b.str().empty() ? "" : "");  // no-op, just need to add statements
        b.line(stmt);
    }
    // Fix: we need to manually add statements  <-- comment acknowledges the problem
    CppBuilder builder;  // Creates a NEW builder, discarding 'b'

    The first builder b is created, used, then completely ignored. A new builder is created that uses raw string concatenation.

  2. Convenience functions don't use the builder pattern:
    All convenience functions fall back to builder.raw() with manual string concatenation - exactly what we already do in emit_for.cpp, emit_if.cpp, etc. This defeats the purpose of the builder.

  3. Hardcoded single-level indentation:

    builder.raw("\t" + stmt + "\n");  // Always uses single tab

    This doesn't support nested code generation - if you call for_range_stmt() from within a function body, the inner statements will have incorrect indentation.


🔶 Architectural Concerns

1. Doesn't address nested AST emission

The current codegen works by recursively calling generate_statement() which returns strings. The builder pattern works well for linear code but doesn't naturally compose with this recursive approach.

For example, generate_function() in emit_function.cpp does:

for (const auto& stmt : fn.body) {
    body.push_back(generate_statement(state, *stmt));  // Returns string
}

Using CppBuilder would require either:

  • Threading the builder through all emit functions (large refactor)
  • Or using convenience functions that lose the indentation benefits

2. Comparison with current approach

Current (emit_for.cpp):

string out = fmt::format("for (int {} = {}; {} < {}; {}++) {{\n", ...);
for (const auto& stmt : body) { out += "\t" + stmt + "\n"; }
out += "}";

Builder convenience function (cpp_builder.cpp):

builder.raw("for (int " + var + " = " + start + ...);
for (const auto& stmt : body) { builder.raw("\t" + stmt + "\n"); }
builder.raw("}");

These are functionally identical. The fluent API is nicer but the convenience functions that would be used in practice don't leverage it.


📋 Suggestions

  1. Remove or fix the convenience functions - They're either dead code or don't use the builder pattern properly.

  2. Consider a context-based approach - Where the builder is passed through emission functions rather than each function returning a string. This would require a larger refactor but would actually leverage the indentation tracking.

  3. Add unit tests - No tests exist for cpp_builder. Given CI is passing, this code isn't exercised yet.


Decision: COMMENT

This is a reasonable prototype for exploration. The fluent API design is good, but the implementation has issues and it's unclear how the builder would integrate with the existing recursive codegen architecture without significant refactoring.

Recommend comparing this against the template pattern (PR #151) before deciding which approach to pursue. The key question is: how does each pattern handle nested AST emission?

@chrishayen chrishayen closed this Jan 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants