Frontend Pipeline
The tswift frontend is a pure-Rust Swift compiler front-end that turns Swift
source into a stable, runtime-facing typed AST. There is no C code, no LLVM,
no unsafe, and no Swift toolchain involved.
Stage 1 — Lexer (tswift-lexer)
The lexer converts raw Swift source into a stream of tokens. It handles:
- All Swift literal forms: integers (dec/hex/oct/bin with
_separators), floats (including hex exponents), booleans,nil, strings (including multiline""", raw#"…"#, extended delimiters, and\(expr)interpolation). - Unicode identifiers and operator characters.
- Line comments (
//), block comments (/* … */), and nested block comments. - Regex literals
/…/and#/…/#.
String interpolation is particularly tricky: the lexer emits a token stream that the parser then re-enters for the interpolated expression, allowing arbitrary nesting.
Stage 2 — AST (tswift-ast)
The AST crate defines the node types shared by all pipeline stages. Every node carries:
kind: NodeKind— the syntactic kind (e.g.FuncDecl,BinaryExpr,CallExpr)text: &str— the source text spanline: u32— source line number
After semantic analysis, nodes also carry:
resolved_type: Option<SwiftType>— the inferred Swift typemodifiers: Modifiers— decoded declaration modifiers (mutating,static, …)
Stage 3 — Parser (tswift-parser)
A recursive-descent parser that consumes the token stream and produces an untyped AST. Highlights:
- Full expression hierarchy (primary → postfix → unary → binary with precedence climbing)
- Pattern matching (
switch,if case,for case, destructuring) - All declaration forms:
func,var/let,class,struct,enum,protocol,extension,actor,typealias,operator,precedencegroup - Generics:
<T: P>,whereclauses, parameter packs - Attributes:
@escaping,@autoclosure,@discardableResult,@main, … - Concurrency:
async,await,actor,@MainActor,async let
flowchart TD
tokens["Token stream"]
decls["Declarations\n(func, var, class, …)"]
exprs["Expressions\n(binary, call, closure, …)"]
stmts["Statements\n(if, for, switch, guard, …)"]
pats["Patterns\n(case, tuple, optional, …)"]
raw["Untyped AST"]
tokens --> decls & exprs & stmts & pats --> raw
style raw fill:#1e1e27,stroke:#a855f7,color:#eeeef5
Stage 4 — Semantic Analysis (tswift-sema)
The semantic analysis pass resolves types and names:
- Type inference — bidirectional propagation; literal types from context
- Name resolution — lexical scoping, module-level declarations, cross-file references
- Overload resolution — function overloads, operator overloads, method dispatch
- Protocol conformance — builds a witness table for each
struct/class/enumconformance declaration - Associated type binding — resolves
associatedtypeplaceholders to concrete types
The output is a typed AST where each expression node carries its resolved
SwiftType.
Stage 5 — Runtime-facing AST (tswift-frontend)
tswift-frontend is the only seam between the frontend and the runtime. It:
- Drives the full pipeline (lexer → parser → sema) behind a single
Analysis::analyze(source)entry point. - Exposes the result as
Analysis/Node/NodeKind— whereNodeKindistswift_ast::NodeKind. The frontend and the runtime share one node vocabulary, not two.
A Node is a thin cursor straight over the parse AST — there is no separate
lowered tree or arena copy. The payloads the runtime reads (modifier bitmasks,
numeric literal values, the #-directive name) are decoded on the fly. Because
the seam is just a cursor, the runtime never depends on frontend internals, and
the parse AST stays the single source of truth.
Historically the runtime was written against the decommissioned msf C frontend’s tree shape, so this crate carried a structural compat lowerer that reproduced it. That layer has been removed: the runtime now consumes the clean parse AST directly.
Why no unsafe?
The entire frontend stack is #![forbid(unsafe_code)]. Rust’s ownership
model gives us memory safety during parsing for free — no garbage collector,
no arena lifetime tricks, just owned Vec and Box trees.