Skip to content

if/else inside Group { … } builder triggers Swift type-checker crash #158

Description

@leogdion

Repro

import SyntaxKit

let _ = Group {
    if true {
        Struct("A") { Variable(.let, name: "x", type: "Int") }
    }
}

Fails to compile with:

error: failed to produce diagnostic for expression; please submit a bug report
let _ = Group {
        ^

Confirmed against Apple Swift version 6.3.2 (swiftlang-6.3.2.1.108) on arm64-apple-macosx26.0 against SyntaxKit at e5b1b73 (research/swift-manifest-codegen).

What should happen

CodeBlockBuilderResult declares buildEither(first:), buildEither(second:), buildOptional, and buildArray (Sources/SyntaxKit/CodeBlocks/CodeBlockBuilderResult.swift:46-71), so the API surface says if/else inside the builder is supported. The Swift type-checker should either accept the expression or emit a real diagnostic.

Probable cause

The type-checker is bailing out on overload resolution. CodeBlockBuilderResult has two buildBlock variants — (any CodeBlock...) -> [any CodeBlock] and ([any CodeBlock]...) -> [any CodeBlock] — combined with buildEither / buildOptional / buildArray over any CodeBlock existentials. The combination is likely exhausting the type-checker's effort budget and triggering the "failed to produce diagnostic" fallback rather than a real error.

Impact

Users can't write conditional codegen at the top level of a builder closure, even though the API documents it as supported. Affects anyone trying to emit conditional structures (platform gates, debug-only fields, optional members from runtime config, etc.). No unit tests under Tests/SyntaxKitTests/Unit/ exercise an if inside Group { … }, which is why this hasn't been caught.

Workaround

Hoist the conditional into a normal function that returns any CodeBlock, then call the function from inside the builder — the type-checker never sees the if at the builder level.

func extraField(_ include: Bool) -> any CodeBlock {
    if include { Variable(.let, name: "debug", type: "Bool") }
    else { EmptyCodeBlock() }
}

Group {
    Variable(.let, name: "name", type: "String")
    extraField(true)
}

References

  • `Docs/research/poc-step1-results.md` §4 — original discovery during the skitrun POC.
  • `Docs/research/codegen-cli-design.md` §7 — tracked as an open SyntaxKit bug, independent of the skitrun CLI design.
  • PR skit: SyntaxKit codegen CLI for v0.0.5 #156 (skitrun POC) — surfaces this for users writing input files to the codegen CLI.

Activity

  1. leogdion commented on May 12, 2026

    @leogdion
    MemberAuthor

    Workaround caveat (from v0.0.5 probes)

    The workaround documented in the issue body ("hoist the conditional into a helper function") only works if the helper uses plain Swift if/else to return one of two CodeBlocks. Wrapping the helper's body in Group { if … } still trips the type-checker crash inside the helper:

    // ❌ Still crashes — the inner Group hits the same overload-resolution issue.
    public func optionalDebugField(_ include: Bool) -> any CodeBlock {
        Group {
            if include {
                Variable(.let, name: "debug", type: "Bool")
            }
        }
    }
    
    // ✅ Works — plain Swift control flow, no result builder.
    public func optionalDebugField(_ include: Bool) -> any CodeBlock {
        if include {
            return Variable(.let, name: "debug", type: "Bool")
        } else {
            return Group {}  // empty Group as the "no-op" CodeBlock; there's no
                              // public EmptyCodeBlock type to return otherwise.
        }
    }

    The second form renders cleanly through skit run. Also worth noting: there's no public empty-CodeBlock type, so the false branch has to return an empty Group { }. A public EmptyCodeBlock (or making CodeBlock? valid in builders) would be a small ergonomic win.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions