Skip to content

LLVM PowerPC backend contains no Swift-calling-convention-specific handling #14090

Description

@lenoil98

Status: Root cause identified via direct live backtrace evidence. Not fixable
from a swift-corelibs-foundation, swift-corelibs-libdispatch, or ports-level
patch — the actual gap is in Swift LLVM's PowerPC target backend (llvm/lib/Target/PowerPC/).

Target: swift-6.3.2, powerpc64-unknown-freebsd14.4. Discovered while
building sourcekit-lsp (Stage 5), specifically its DocumentationLanguageService
module. Confirmed to reproduce with a 20-line, fully isolated, dependency-free
minimal repro (included below) — not specific to sourcekit-lsp's actual code in any way.


1. Symptom

Two distinct symptoms of the same underlying cause, depending on
optimization level and exact code shape:

  • In sourcekit-lsp's real build: an LLVM IR verifier failure —
    error: fatal error encountered during compilation; please submit a bug report / note: Broken module found, compilation aborted! — with
    visibly inconsistent operand types in the dumped IR (a value of the
    Swift enum's opaque type fed directly into icmp/ptrtoint, which
    require raw pointer/integer operands, alongside a phi node elsewhere
    expecting a plain ptr).
  • In the isolated minimal repro (see §3): rather than failing fast,
    swift-frontend instead hangs indefinitely, consuming 95-99% CPU
    continuously for 30+ minutes on a 20-line source file with no sign of
    terminating. (Both are almost certainly the same underlying LLVM
    backend defect; which one manifests likely depends on which specific
    code path gets hit for a given IR shape — see §6.)

2. Minimal reproduction

enum MyError: Error {
    case wrapped(any Error)
    case simple
    case cancelled
}

func doThing(_ fail: Bool) async throws(MyError) -> Int {
    if fail {
        throw MyError.simple
    }
    return 42
}

func caller() async {
    do {
        let result = try await doThing(true)
        print(result)
    } catch {
        print("caught: \(error)")
    }
}

Compiled with: swiftc -c minimal_repro.swift -o minimal_repro.o (any
optimization level; reproduces at both default and -Onone).

Precisely isolated via controlled variation — each of the following
compiles instantly and correctly on their own:

  • The enum alone, with a plain synchronous, non-throwing function using it.
  • throws(MyError) on a synchronous function (no async).
  • async throws (untyped) using the same enum as a thrown value.

Only the combination of async + typed throws
(throws(ConcreteErrorType)) together triggers the bug.
Neither feature
alone is sufficient.

3. Investigation summary — layers ruled out, with direct evidence

This was traced through several layers, each with a specific,
falsifiable hypothesis tested and ruled out with hard evidence rather than
assumption, before reaching the actual root cause:

  1. swift-corelibs-foundation/general enum extra-inhabitant
    representation
    — considered given this session's history of
    similar big-endian struct-layout bugs (_CFInfo, see the companion
    report). Ruled out: ErrorExistentialTypeInfo (the type info for
    any Error, the enum's payload) correctly derives its spare-bits from
    the same generic HeapObjectSpareBits mechanism already fixed and
    confirmed correct elsewhere this session.
  2. SIL generation — ruled out directly: swiftc -emit-sil on the
    minimal repro completes instantly, producing entirely ordinary,
    platform-generic SIL (a standard alloc_stack/store/apply
    sequence for the generic swift_willThrowTyped runtime hook). No
    anomaly visible at this level at all.
  3. NativeConventionSchema::shouldReturnTypedErrorIndirectly() (the
    function deciding whether the typed error is returned directly in
    registers or indirectly via memory) — the leading hypothesis for some
    time, given 17 separate call sites across four IRGen files. Ruled
    out via direct instrumentation and a full targeted compiler rebuild
    :
    all 14 invocations during compilation of the minimal repro return the
    identical, consistent answer (false — "direct"). No disagreement
    between call sites.
  4. combineResultAndTypedErrorType and buildDirectError (the
    actual "direct" codegen path, in swift/lib/IRGen/GenCall.cpp) — read
    in full and confirmed to be provably bounded for this input (every
    loop advances a fixed-size iterator with no possibility of
    non-termination for a two-element aggregate). Ruled out by direct
    source inspection.
  5. The actual location — confirmed via live gdb backtrace while the
    hung process was still running:
    #0 llvm::DAGTypeLegalizer::getTableId(llvm::SDValue)
    #1 llvm::DAGTypeLegalizer::AnalyzeNewNode(llvm::SDNode*)
    #2 llvm::DAGTypeLegalizer::run()
    #3 llvm::SelectionDAG::LegalizeTypes()
    #4 llvm::SelectionDAGISel::CodeGenAndEmitDAG()
    ...
    #8 (anonymous namespace)::PPCDAGToDAGISel::runOnMachineFunction(llvm::MachineFunction&)
    
    This is entirely inside LLVM's own generic backend — specifically
    its target-independent SelectionDAG type-legalization framework,
    running within PowerPC's own instruction selector. Not Swift's IRGen,
    not Clang's Swift-ABI machinery — LLVM's generic code generation
    proper.

4. Root cause

grep -c "CallingConv::Swift\|swiftcc\|SwiftError" \
  llvm/lib/Target/PowerPC/PPCISelLowering.cpp   # → 0

grep -c "CallingConv::Swift\|swiftcc\|SwiftError" \
  llvm/lib/Target/AArch64/AArch64ISelLowering.cpp   # → 11

PowerPC's LLVM backend contains zero Swift-calling-convention-specific
handling anywhere in its main instruction-selection lowering file.

AArch64 — a genuinely well-supported Swift target — has eleven. This is
not a subtle bug in an otherwise-complete implementation; it is the
complete absence of a subsystem other supported targets have.

When Swift's compiler generates a function using the Swift calling
convention (every Swift function does, always) with the swiftasync
parameter attribute and a combined result+error aggregate type (produced
specifically by the async + typed-throws codegen path, see the companion
GenCall.cpp trace in §3.3-3.4), LLVM's generic, target-independent type
legalization framework has no PowerPC-specific guidance for how to
correctly legalize this. It falls through to generic fallback logic that
was evidently never validated against this exact combination on this
target, and enters what appears to be a non-terminating (or
extremely-long-running) type-promotion cycle.

Activity

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