Brinch Hansen's Pipeline-of-Processes Compilers
Per Brinch Hansen took a different route to the stream-processor compilation discipline than Wirth or Hejlsberg. Where the Wirth line and Turbo Pascal built the compiler as an integrated single-pass parser that emits target code inside its recursive-descent procedures, Brinch Hansen decomposed the compiler into a pipeline of independent processes communicating through bounded queues. The lexer, the parser, the semantic analyser, and the code generator each ran as a separate process. Each process consumed an input stream from its upstream neighbour and produced an output stream for its downstream neighbour. No process held the entire program. No process communicated with any process other than its immediate neighbours.
The pipeline architecture mattered for two reasons. The first reason was decomposition-for-testability. Each stage could be tested in isolation by driving it with a synthesised input stream and inspecting the output stream. The second reason was that the pipeline shape mapped directly onto the concurrent-programming discipline that Brinch Hansen had been developing since Concurrent Pascal in nineteen seventy-five. The compiler was itself a demonstration of the concurrent-programming techniques that Brinch Hansen was advocating for other domains.
A Brief Biographical Arc
Per Brinch Hansen was born in Denmark in nineteen thirty-eight and worked on operating systems and concurrent programming across a career spanning four decades. His work falls into three overlapping periods.
The first period, from the late nineteen sixties into the early nineteen seventies, covered the RC four thousand multiprogramming system at the Danish company Regnecentralen and the Operating System Principles textbook of nineteen seventy-three. This period established Brinch Hansen as one of the founders of the discipline of concurrent programming.
The second period, from the mid nineteen seventies through the mid nineteen eighties, covered the design and implementation of Concurrent Pascal in nineteen seventy-five, the Solo operating system written in Concurrent Pascal in nineteen seventy-six, Distributed Processes as a language design in nineteen seventy-eight, Edison as a teaching language in nineteen eighty-one, and the Brinch Hansen on Pascal Compilers book in nineteen eighty-five. The Pascal Compilers book is the primary source for the pipeline-of-processes compiler architecture.
The third period, from the late nineteen eighties into the two thousands, covered SuperPascal as a language for parallel scientific computing and its associated compiler tooling. Brinch Hansen retired from academic teaching in two thousand four and died in two thousand seven.
The pipeline-of-processes compiler architecture is best documented in the nineteen eighty-five book and best demonstrated in the SuperPascal compiler of the mid nineteen nineties. The rest of this article concentrates on those two artefacts.
The Concurrent Pascal Foundation
Understanding Brinch Hansen’s compiler architecture requires some context on Concurrent Pascal. Concurrent Pascal extended Wirth’s Pascal with two concurrent-programming primitives. The first primitive was the process, a program unit with private state and its own thread of control. The second primitive was the monitor, a shared abstraction that mediated access to shared state through mutual-exclusion methods. The two primitives together allowed a Concurrent Pascal program to be structured as a collection of processes communicating through monitors.
The Solo operating system, written in Concurrent Pascal in nineteen seventy-six, consisted of multiple processes communicating through a set of monitors. The kernel of the operating system was reportedly small, on the order of a few thousand lines of Concurrent Pascal source in the shipped implementation, though the exact size varies across the reported figures. The system was tractable to reason about because each process held only its private state and communicated only through explicit monitor calls.
Brinch Hansen carried this decomposition discipline into his compiler design. In the nineteen eighty-five book the compiler is presented as a series of passes, each of which reads its input as a stream and writes its output as a stream, with the passes executed sequentially in the shipped implementation but decomposed conceptually as if they were independent processes. The passes communicate through buffered streams, not through whole-program intermediate files, which is what places the design inside the stream-processor discipline even absent concurrent execution.
The fully concurrent form, in which the passes run as actual concurrent processes communicating through bounded first-in-first-out channels, matured in SuperPascal in the mid nineteen nineties. The SuperPascal design made explicit the bounded-buffer inter-process communication model that the earlier work had presented in latent form. Each channel had a fixed capacity. A process that produced faster than its consumer could accept would block until buffer space was available. A process that consumed faster than its producer could supply would block until an item was available. The pipeline was back-pressured throughout. No process could run away from its neighbours. The two forms, the sequential pass-based form of the earlier work and the concurrent process-based form of SuperPascal, share the same architectural properties because the pass-based form is a scheduling of the process-based form onto a single processor.
The Pipeline-of-Processes Architecture
A Brinch Hansen compiler consists of four or five stages arranged as a linear pipeline. The stages and their inter-stage streams follow this shape, with the dependency running left to right.
source bytes
↓
[ lexer ] ── tokens ──> [ parser ] ── declarations ──> [ semantic ] ── typed IR ──> [ codegen ] ── bytecode
The lexer consumes source bytes from a file input and yields tokens. The parser consumes tokens and yields top-level declarations one at a time. The semantic analyser consumes declarations and yields type-checked declarations with resolved identifier references and computed types. The code generator consumes type-checked declarations and yields target bytecode one chunk at a time.
Each stage holds only its local working state plus the inter-stage buffer that connects it to its downstream neighbour. The lexer’s working state is a small character buffer sufficient to recognise a single token. The parser’s working state is a bounded parse stack whose depth is the current syntactic nesting depth plus the fixup buffer for the currently parsed declaration. The semantic analyser’s working state decomposes into a nested-scope portion holding the type-environment for the current lexical scope, which is bounded by scope-nesting depth, plus a small buffer for the currently checked declaration, plus the accumulated top-level environment that resolves cross-declaration references. The top-level portion grows with the top-level declaration count and is bounded per module under separate compilation. The code generator’s working state is a small emission buffer.
The unit of work that transits each buffer depends on the buffer’s position in the pipeline. The lexer-parser buffer carries tokens, each a fixed-size record. The parser-semantic buffer carries a single top-level declaration per item. The semantic-codegen buffer carries a type-checked declaration per item. The codegen output carries bytecode chunks. Because the buffer capacity is fixed at compile time, the total memory footprint of all inter-stage buffers is a compile-time constant.
The number of pipeline stages $k$ is also a design-time constant. For a Brinch Hansen Pascal-family compiler, $k$ is typically four, lexer, parser, semantic analyser, and code generator, or five when a separate optimisation stage is introduced. The stage count does not depend on the program being compiled. Formally,
\[k = O(1) \quad \text{in } \lvert P \rvert.\]This property is what allows the memory bound to be decomposed into a program-independent per-declaration portion plus a per-stage top-level environment portion whose growth scales with the top-level declaration count rather than with the total program size.
Bounded Working Memory
The pipeline architecture gives a compile-time bound on the compiler’s total working memory that is tighter and simpler than the integrated-single-pass bound. Denote by $k$ the number of pipeline stages, by $m_i$ the maximum working state of stage $i$ during execution, and by $b_i$ the capacity of the inter-stage buffer between stage $i$ and stage $i+1$. The total compiler memory footprint is bounded by
\[M_{\text{compiler}} \le \sum_{i=1}^{k} m_i + \sum_{i=1}^{k-1} b_i.\]The inter-stage buffer sum is bounded at compile time by the pipeline design choice of buffer capacities. Each per-stage state $m_i$ decomposes into a per-declaration working set bounded by scope-nesting depth and buffer capacities, plus a top-level environment portion where the stage must resolve cross-declaration references, which grows with the top-level declaration count. For the semantic analyser stage in particular, the top-level environment portion is not program-independent. The compiler memory footprint therefore satisfies
\[M_{\text{compiler}} = M_{\text{per-declaration}} + M_{\text{top-level}},\]where $M_{\text{per-declaration}} = O(1)$ in the program size by construction and $M_{\text{top-level}}$ grows with the top-level declaration count, bounded per module under separate compilation.
This is the same order-of-growth pattern as the integrated-single-pass compiler of the Wirth line, where the same decomposition holds for the same reasons, but achieved through a different mechanism. The integrated single-pass compiler achieves the pattern by holding the parse stack, symbol table, and fixup buffer in a single memory region. The pipeline compiler achieves the pattern by giving each stage its own local region and connecting them through fixed-capacity buffers.
Pipeline Throughput and Latency
Two additional properties of the pipeline follow from the fixed-capacity buffers.
The throughput of the pipeline is limited by the throughput of the slowest stage. Denote by $T_i$ the maximum throughput of stage $i$, measured in units of work per second. The overall pipeline throughput is
\[T_{\text{pipeline}} = \min_{i \in \{1, \ldots, k\}} T_i.\]The upstream stages that can produce faster will block on their output buffers whenever the slowest stage falls behind. The downstream stages that can consume faster will block on their input buffers for the same reason. Balancing the throughput of the stages is a first-order design concern for a Brinch Hansen compiler.
The latency from the start of source input to the end of bytecode output is bounded by the sum of the maximum per-stage buffering delays. For a pipeline where each buffer holds at most $b$ units and the slowest stage processes one unit in time $\tau$, the maximum end-to-end latency is on the order of
\[L_{\text{pipeline}} \le (k - 1) \cdot b \cdot \tau.\]For small $k$ and small $b$, which is the design regime Brinch Hansen advocated, the latency is small enough that an interactive user does not perceive a delay between typing a compile command and seeing the first output.
The pipeline shape also admits concurrent execution of the stages on multiprocessor hardware. Under sequential execution, compiling a unit of work requires visiting every stage in turn, so the per-unit cost is the sum of the per-stage inverse throughputs. Under concurrent execution, the stages overlap in time, so the per-unit steady-state cost is the inverse throughput of the slowest stage. The speedup ratio is
\[\text{speedup} = \frac{T_{\text{concurrent}}}{T_{\text{sequential}}} = \frac{\sum_{i=1}^{k} T_i^{-1}}{\max_{i} T_i^{-1}}.\]For a well-balanced pipeline of $k$ stages with equal per-stage throughput, the speedup approaches $k$, which is the theoretical maximum for a linear pipeline. The speedup degrades in proportion to the imbalance between stages, reaching one when a single slow stage dominates the whole pipeline. Balancing the pipeline is therefore a first-order concern for pipeline compilers that intend to exploit concurrent execution.
The SuperPascal Pipeline Realisation
SuperPascal
appeared in
nineteen ninety-three
as an extension of Pascal
for parallel scientific computing.
It added
three parallel-programming primitives
to the Wirth Pascal base language,
namely
parallel blocks
that execute their statements concurrently,
forall loops
that execute their iterations concurrently,
and channels
that transmit typed values
between concurrent components.
The channel primitive
was the direct descendant
of the bounded queues
that Brinch Hansen
had been using
in his compiler designs
for a decade.
SuperPascal provided a language runtime in which the pipeline-of-processes shape that Brinch Hansen had been advocating could be expressed directly. The SuperPascal compiler itself was implemented in standard sequential Pascal, specifically the ISO Level 1 dialect, rather than in SuperPascal. The Brinch Hansen tradition therefore did not achieve a self-hosted stream-processor compiler in the strict sense. It did establish the pipeline architecture and the language design in which such a compiler could later be written.
The pipeline architecture is formally the same shape as the Wirth Oberon result but achieved through a different discipline. Wirth’s Oberon uses an integrated single-pass style, with the compiler written as a large recursive-descent parser that emits target code during parsing. Brinch Hansen’s tradition uses a pipeline of concurrent processes, with each process written as an independent module whose input and output are typed channels. The two approaches occupy opposite ends of the two-axis design space introduced in article A188.
The SuperPascal paper, published in Concurrency Practice and Experience in nineteen ninety-four, documents the language design and reports per-stage throughput measurements. The paper is short by present-day standards, about twenty printed pages, and remains the most accessible primary source for the pipeline-of-processes compiler architecture in the Brinch Hansen tradition.
What This Demonstrates
The pipeline-of-processes compiler architecture occupies the pipeline-and-abstract-syntax-tree-free quadrant of the two-axis design space introduced in article A188. The Wirth line and Turbo Pascal occupy the integrated-and-abstract-syntax-tree-free quadrant. The multi-pass abstract-syntax-tree-materialising tradition represented by GNU Compiler Collection and Clang occupies the multi-pass-and-abstract-syntax-tree quadrant. The pipeline-and-abstract-syntax-tree quadrant is populated by compilers that materialise an AST between the parser stage and later stages but still stream the AST through without ever holding the whole-program tree.
Brinch Hansen’s compilers demonstrate three architectural properties that the Wirth and Hejlsberg integrated compilers do not demonstrate as cleanly.
Independent testability of stages. A pipeline compiler can be tested one stage at a time by driving each stage with a synthesised input stream and inspecting its output stream. The test harness does not need to run the full pipeline to exercise any individual stage. An integrated compiler does not admit this factoring because the stages are inlined into the same procedure call chain.
Compositional reasoning about resources. The pipeline compiler gives a per-stage resource bound that composes to a total resource bound by summation. Reasoning about the pipeline’s total memory footprint reduces to reasoning about each stage’s local footprint plus the buffer capacities. The integrated compiler gives a single global resource bound that combines all working state in one region, which is harder to decompose for verification purposes.
Pipeline parallelism as a design option. The pipeline compiler admits concurrent execution of the stages on multiprocessor hardware without any change to the compiler’s source code. The concurrent execution does not improve throughput past the slowest-stage limit, but it does overlap the pipeline stages in time, which reduces end-to-end latency for interactive compilation. The integrated compiler cannot exploit multiprocessor hardware in this way because its stages are causally serial within the recursive-descent parse.
Legacy
Brinch Hansen’s compiler architecture influenced several subsequent traditions.
The concurrent-language tradition that Brinch Hansen founded carried the pipeline-of-processes discipline forward into occam in the mid nineteen eighties, into Erlang in the late nineteen eighties, and into Go’s goroutines and channels, which were announced by Google in late two thousand nine and reached the one-point-zero release in two thousand twelve. Each of these languages adopts the bounded-buffer inter-process communication model that Concurrent Pascal and later SuperPascal codified. A compiler written in any of these languages in the pipeline-of-processes shape inherits the Brinch Hansen architectural pattern whether or not the compiler author consciously references the tradition.
The compiler-construction pedagogy tradition was more resistant to Brinch Hansen’s approach. Standard compiler textbooks in the nineteen nineties and two thousands, including Aho, Sethi, and Ullman’s Compilers, Principles, Techniques, and Tools and the successive editions that added Lam as an author, adopted the multi-pass abstract-syntax-tree tradition that Brinch Hansen was arguing against. The pipeline architecture persisted in a small academic subculture around concurrent-programming pedagogy but did not displace the AST-based standard treatment.
The coroutine-based
embedded-scripting tradition
of the twenty tens and twenty twenties
returned
to the pipeline model
under different terminology.
A compiler
written as
a series of loop functions
or coroutines
that yield to one another
through typed values
is
formally the same shape
as the Brinch Hansen pipeline,
with
yield playing the role
of a bounded-buffer send
and resume playing the role
of a bounded-buffer receive.
The coroutine implementation
does not require
concurrent execution,
but it preserves
the pipeline’s compositional properties
under sequential execution.
This variant
appears in
several present-day
resource-bounded scripting languages
and is treated
in more detail
in article A197.
Conclusion
Per Brinch Hansen demonstrated that a stream-processor compiler can be built as a pipeline of concurrent processes rather than as an integrated single-pass parser. The pipeline shape gives compositional resource bounds, independent per-stage testability, and optional pipeline parallelism that the integrated shape does not admit. The SuperPascal self-hosting result demonstrates the pipeline architecture at its greatest ambition, namely a stream-processor compiler implemented in a stream-processor language. The pattern has modern realisations in the concurrent-language tradition of Erlang and Go and in the coroutine-based embedded-scripting tradition that subsequent articles in this series will treat. Article A192 opens the theory pair of the series with block-structured control flow and its role in single-pass validation.
References
Book
- Brinch Hansen on Pascal Compilers, Per Brinch Hansen, Prentice-Hall, 1985, ISBN 0-13-083098-4
- Operating System Principles, Per Brinch Hansen, Prentice-Hall, 1973, ISBN 0-13-637843-9
Reference
- Per Brinch Hansen biographical entry
- Concurrent Pascal language entry
- Occam language entry
- Erlang language entry
- Go programming language
Related Post
- Compilation as a Streaming Discipline, article A188 in this series
- Wirth’s Single-Pass Line, PL/0 through Oberon, article A189 in this series
- Turbo Pascal, the Closed-Source Demonstration, article A190 in this series
Research
- Brinch Hansen, SuperPascal, a Publication Language for Parallel Scientific Computing, Concurrency Practice and Experience 6, 1994
- Brinch Hansen, The Programming Language Concurrent Pascal, IEEE Transactions on Software Engineering SE-1, 1975
Erratum
Two corrections apply to earlier revisions of this article.
The SuperPascal self-hosting claim. An earlier revision stated that the SuperPascal compiler was itself implemented in SuperPascal source, which would have placed the Brinch Hansen tradition at a self-hosting endpoint analogous to Wirth’s Oberon result. This claim was incorrect. The SuperPascal compiler and its interpreter were implemented in standard sequential Pascal, specifically the ISO Level 1 dialect, per the published record and the archived source maintained by the classic-tools SuperPascal repository. Brinch Hansen’s tradition established the pipeline architecture and the language design that made a self-hosted stream-processor compiler architecturally natural, but did not close the self-hosting endpoint. Article A199 carries the corresponding correction to the series’s self-hosting summary.
The pipeline total working memory bound. An earlier revision stated that the compiler memory footprint $M_{\text{compiler}}$ was $O(1)$ in the program size, and understated the semantic analyser’s working state as the type-environment for the current lexical scope plus a small per-declaration buffer. This was overstated. The semantic analyser stage must hold the accumulated top-level environment to resolve cross-declaration references, so that stage’s state grows with the top-level declaration count. The corrected framing decomposes $M_{\text{compiler}}$ into a per-declaration working set that is $O(1)$ in the program size plus a top-level portion that grows with the top-level declaration count and is bounded per module under separate compilation. Article A196 carries the analogous correction in the symbol table treatment.