Open-Source Wikis

/

Swift

/

Background

/

Design decisions

apple/swift

Design decisions

A grab-bag of "why does it work this way?" notes. Each item points at the canonical document where the decision was made.

SIL exists

Most modern compilers go AST → LLVM IR. Swift inserts SIL between them. The reasoning, taken from docs/SIL/SIL.rst:

  • LLVM IR cannot represent ownership, generics, or dynamic dispatch faithfully. Lowering directly would require encoding all of those in a way LLVM tools couldn't reason about.
  • SIL preserves type information, ownership, and Swift's calling-conventions. The optimizer can therefore do Swift-specific work (specialization, devirtualization, ARC reduction) before LLVM ever sees the program.
  • SIL is round-trip-able as text -- which makes it scriptable and testable. test/SILOptimizer/ has thousands of .sil test fixtures that exercise individual passes.

The trade-off: SIL is another IR to maintain. The optimizer is large, the verifier is large, and the format has its own tooling. For Swift the trade-off has consistently paid off.

Two demanglers

The demangler exists in two compiled instances: lib/Demangling/ (linked into the compiler and tools) and stdlib/public/Demangling/ (compiled into the runtime so that swift_demangle can run inside any Swift program).

The reasoning: the runtime cannot link against the LLVM toolchain libraries (those are far too large), and tools cannot use the runtime's hand-rolled implementation. The compromise is a single set of shared C++ source files included from both build targets.

Resilience and .swiftinterface

ABI-stable libraries (Apple SDK frameworks, the standard library on Apple platforms) have to evolve without recompiling clients. Two choices made this work:

  • Resilience (-enable-library-evolution) makes most types accessed through value-witness tables -- the layout is private and can change. The fields-in-registers fast path is reserved for @frozen types.
  • .swiftinterface is a textual public-API surface. A client can re-typecheck it with a newer compiler without re-shipping the binary. lib/Frontend/ModuleInterfaceLoader.cpp handles the on-the-fly typecheck.

docs/LibraryEvolution.rst is the design doc. docs/StableBitcode.md and docs/Serialization.md cover the file formats.

Generic specialization is opt-in by @inlinable

Generic functions are by default not specialized for clients of an ABI-stable library. To get specialization, the function must be @inlinable (its SIL body is shipped in the .swiftmodule). For the standard library this means most generics are inlinable; for non-stdlib libraries you opt in case-by-case.

The request evaluator instead of a multi-pass type checker

Older C++ compilers run several passes over the AST in fixed order. Swift's type checker is request-driven: each query (interface type, default arg, generic signature, conformance witness, ...) is a memoized request. The evaluator handles dependency tracking for incremental builds. See docs/RequestEvaluator.md.

OSSA SIL

The shift from "SIL with implicit ARC" to "SIL with explicit ownership annotations" (OSSA) was a multi-year transition. The motivation: many SIL passes correctness-critically depend on knowing whether a value is owned or borrowed. Encoding that in the IR makes the optimizer's job tractable. docs/OwnershipManifesto.md is the design doc.

Distributed actors compose with regular actors

Distributed actors are a layering on top of regular actors rather than a separate concept: a distributed actor is also an actor and benefits from the same isolation rules. The runtime difference is only at the call site -- a remote call goes through a thunk and the user's DistributedActorSystem rather than executing locally. See Distributed actors.

C++ interop bridges in both directions

Swift sees C++ types as if they were Swift types (after import). C++ can see Swift types via PrintAsClang. The choice to support both directions (rather than only Swift-imports-C++) means a Swift library can ship a C++ client API; the alternative (C++ wrapping Swift via Objective-C) was rejected as too restrictive. See docs/CppInteroperability/.

Macros run in a separate process

A Swift macro is user code that runs at compile time. Running it inside the compiler would create trust and crash-isolation issues. The chosen design hosts macros in swift-plugin-server, a separate process. The compiler talks to the plugin via a stable protocol; a faulty macro can crash the plugin without crashing the compile. The price is some IPC latency.

Embedded Swift is a subset, not a separate language

Embedded mode disables features rather than introducing new ones. This keeps the language unitary -- existing code can compile in embedded mode if it avoids the disabled features. The chosen alternative (a separate "Swift Lite" language) was rejected for fragmenting the ecosystem. See docs/EmbeddedSwift/.

A new build (Runtimes/) instead of refactoring stdlib/

stdlib/CMakeLists.txt accumulated 15 years of platform-specific assumptions. Refactoring it in place was deemed too risky. Instead, Runtimes/ is a parallel CMake build that pulls in the same sources but with explicit, opt-in configuration. As Runtimes/ reaches parity, it will replace stdlib/'s build. See Runtimes.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Design decisions – Swift wiki | Factory