Modernizing Compiler Design for Carbon's Toolchain (original) (raw)

Chandler Carruth

@chandlerc1024
chandlerc@{google,gmail}.com

CppNow 2023

Traditional compiler design

The 'Dragon Book' cover

Lexer: text → tokens

Parser: tokens → AST (Abstract Syntax Tree)

Semantic analysis: AST → correct AST

Lowering: AST → IR → … → machine code

Historical influences on architecture

Imagined direct model

---------
| Lexer |
---------

Imagined direct model

---------     ----------
| Lexer | --> | Parser |
---------     ----------
        ---^--
        Tokens

Imagined direct model

---------     ----------     -------------
| Lexer | --> | Parser | --> | Semantics |
---------     ----------     -------------
        ---^--        ----^-----
        Tokens        Parse Tree

Imagined direct model

---------     ----------     -------------     ------------
| Lexer | --> | Parser | --> | Semantics | --> | Lowering |
---------     ----------     -------------     ------------
        ---^--        ----^-----           -^-
        Tokens        Parse Tree           AST

Incremental & lazy parsing design

                      ----------                     -------------     ------------
                      |        | -- One function  -> |           |     |          |
                      | Parser |                     | Semantics |     | Lowering |
                      |        |                     |           |     |          |
                      ----------                     -------------     ------------

Incremental & lazy parsing design

                      ----------                     -------------     ------------
                      |        | -- One function  -> |           | --> |          |
                      | Parser |                     | Semantics |     | Lowering |
                      |        |                     |           |     |          |
                      ----------                     -------------     ------------

Incremental & lazy parsing design

                      ----------                     -------------     ------------
                      |        | -- One function  -> |           | --> |          |
                      | Parser | -- Next function -> | Semantics | --> | Lowering |
                      |        |                     |           |     |          |
                      ----------                     -------------     ------------

Incremental & lazy parsing design

                      ----------                     -------------     ------------
                      |        | -- One function  -> |           | --> |          |
                      | Parser | -- Next function -> | Semantics | --> | Lowering |
                      |        | -- Next function -> |           | --> |          |
                      ----------                     -------------     ------------

Incremental & lazy parsing design

---------             ----------                     -------------     ------------
|       | <- Next --- |        | -- One function  -> |           | --> |          |
| Lexer |             | Parser | -- Next function -> | Semantics | --> | Lowering |
|       |             |        | -- Next function -> |           | --> |          |
---------             ----------                     -------------     ------------

Incremental & lazy parsing design

---------             ----------                     -------------     ------------
|       | <- Next --- |        | -- One function  -> |           | --> |          |
| Lexer |             | Parser | -- Next function -> | Semantics | --> | Lowering |
|       | -- Token -> |        | -- Next function -> |           | --> |          |
---------             ----------                     -------------     ------------

Designed around “locality” and streaming compilation

ASTs further exacerbate the cache impact due to pervasive pointers for edges

And real world ASTs are … massive

#include <vector>

int test_sum(std::vector<int> data) { 
  int result = 0;
  for (const auto& element : data) {
    result += element;
  }
  return result;
}

FunctionDecl 0x120d95460 <:4:1, line:10:1> line:4:5 test_sum 'int (std::vector)' |-ParmVarDecl 0x120d95368 <col:14, col:31=""> col:31 used data 'std::vector':'std::vector' destroyed -CompoundStmt 0x120dd5d50 <col:37, line:10:1=""> |-DeclStmt 0x120dcde28 <line:5:3, col:17=""> | -VarDecl 0x120dcdda0 <col:3, col:16=""> col:7 used result 'int' cinit | -IntegerLiteral 0x120dcde08 'int' 0 |-CXXForRangeStmt 0x120dd5c08 <line:6:3, line:8:3=""> | |-<<>> | |-DeclStmt 0x120dce1d0 | | -VarDecl 0x120dcdf98 col:30 implicit used __range1 'std::vector &' cinit | | -DeclRefExpr 0x120dcde40 'std::vector':'std::vector' lvalue ParmVar 0x120d95368 'data' 'std::vector':'std::vector' | |-DeclStmt 0x120dd2f90 | | -VarDecl 0x120dce268 col:28 implicit used __begin1 'iterator':'std::__wrap_iter' cinit | | -CXXMemberCallExpr 0x120dce408 'iterator':'std::__wrap_iter' | | -MemberExpr 0x120dce3d8 '' .begin 0x120db6c60 | | -DeclRefExpr 0x120dce1e8 'std::vector':'std::vector' lvalue Var 0x120dcdf98 '__range1' 'std::vector &' | |-DeclStmt 0x120dd2fa8 | | -VarDecl 0x120dce310 col:28 implicit used __end1 'iterator':'std::__wrap_iter' cinit | | -CXXMemberCallExpr 0x120dd2eb0 'iterator':'std::__wrap_iter' | | -MemberExpr 0x120dd2e80 '' .end 0x120db6fd0 | | -DeclRefExpr 0x120dce208 'std::vector':'std::vector' lvalue Var 0x120dcdf98 '__range1' 'std::vector &' | |-CXXOperatorCallExpr 0x120dd56c0 'bool' '!=' adl | | |-ImplicitCastExpr 0x120dd56a8 'bool (*)(const __wrap_iter &, const __wrap_iter &) noexcept' | | | -DeclRefExpr 0x120dd40f8 'bool (const __wrap_iter &, const __wrap_iter &) noexcept' lvalue Function 0x120dd3460 'operator!=' 'bool (const __wrap_iter &, const __wrap_iter &) noexcept' | | |-ImplicitCastExpr 0x120dd40c8 'const __wrap_iter':'const std::__wrap_iter' lvalue | | | -DeclRefExpr 0x120dd2fc0 'iterator':'std::__wrap_iter' lvalue Var 0x120dce268 '__begin1' 'iterator':'std::__wrap_iter' | | -ImplicitCastExpr 0x120dd40e0 'const __wrap_iter':'const std::__wrap_iter' lvalue | | -DeclRefExpr 0x120dd2fe0 'iterator':'std::__wrap_iter' lvalue Var 0x120dce310 '__end1' 'iterator':'std::__wrap_iter' | |-CXXOperatorCallExpr 0x120dd5860 '__wrap_iter':'std::__wrap_iter' lvalue '++' | | |-ImplicitCastExpr 0x120dd5848 '__wrap_iter &(*)() noexcept' | | | -DeclRefExpr 0x120dd5718 '__wrap_iter &() noexcept' lvalue CXXMethod 0x120dd0528 'operator++' '__wrap_iter &() noexcept' | | -DeclRefExpr 0x120dd56f8 'iterator':'std::__wrap_iter' lvalue Var 0x120dce268 '__begin1' 'iterator':'std::__wrap_iter' | |-DeclStmt 0x120dcdf38 <col:8, col:34=""> | | -VarDecl 0x120dcded0 <col:8, col:28=""> col:20 used element 'int const &' cinit | | -ImplicitCastExpr 0x120dd5b98 'int const':'const int' lvalue | | -CXXOperatorCallExpr 0x120dd5a20 'int':'int' lvalue '' | | |-ImplicitCastExpr 0x120dd5a08 'reference ()() const noexcept' | | | -DeclRefExpr 0x120dd58f0 'reference () const noexcept' lvalue CXXMethod 0x120dd0070 'operator*' 'reference () const noexcept' | | -ImplicitCastExpr 0x120dd58d8 'const std::__wrap_iter' lvalue | | -DeclRefExpr 0x120dd58b8 'iterator':'std::__wrap_iter' lvalue Var 0x120dce268 '__begin1' 'iterator':'std::__wrap_iter' | -CompoundStmt 0x120dd5cf0 <col:36, line:8:3=""> | -CompoundAssignOperator 0x120dd5cc0 <line:7:5, col:15=""> 'int' lvalue '+=' ComputeLHSTy='int' ComputeResultTy='int' | |-DeclRefExpr 0x120dd5c68 'int' lvalue Var 0x120dcdda0 'result' 'int' | -ImplicitCastExpr 0x120dd5ca8 'int':'int' | -DeclRefExpr 0x120dd5c88 'int const':'const int' lvalue Var 0x120dcded0 'element' 'int const &' -ReturnStmt 0x120dd5d40 <line:9:3, col:10=""> -ImplicitCastExpr 0x120dd5d28 'int' -DeclRefExpr 0x120dd5d08 'int' lvalue Var 0x120dcdda0 'result' 'int' </line:9:3,></line:7:5,></col:36,></col:8,></col:8,></line:6:3,></col:3,></line:5:3,></col:37,></col:14,>

Do we need a better approach?

Carbon’s compile-time goals

Carbon has a goal of fast compile times:

Software development iteration has a critical “edit, test, debug” cycle. Developers will use IDEs, editors, compilers, and other tools that need different levels of parsing. For small projects, raw parsing speed is essential; for large software systems, scalability of parsing is also necessary.

Carbon’s goal for fast and scalable development

Not just about compiling to a binary…

We set ourselves a challenge:

Thinking about it the other way is eye opening:

Latency numbers table:

Operation Time in ns Time in ms
CPU cycle 0.3 - 0.5
L1 cache reference 1
Branch misprediction 3
L2 cache reference 4
Mutex lock/unlock 17
Main memory reference 100
SSD Random Read 17,000 0.017
Read 1 MB sequentially from memory 38,000 0.038
Read 1 MB sequentially from SSD 622,000 0.622

Mapping those onto our budgets

😱😱😱

Ok, so how are we going to do this?

Data-oriented compiler design

Data-oriented compiler design

Simple and memory-dense rather than lazy

Core data structure pattern:

Advantages of this pattern:

Let’s look at how this manifests at every layer

Data-oriented lexing!

Lexing directly fits the desired pattern

Lexing details: source locations

Lexing details: balanced delimiters

Lexing implementation: a guided tour, live!

Data-oriented parsing!

Challenge: how to represent a tree

var x: i32 = y + 1;
// TokenizedBuffer:
//     --------
// 1)  | var  |
//     --------
// 2)  | x    |
//     --------
// 3)  | :    |
//     --------
// 4)  | i32  |
//     --------
// 5)  | =    |
//     --------
// 6)  | y    |
//     --------
// 7)  | +    |
//     --------
// 8)  | 1    |
//     --------
// 9)  | ;    |
//     --------
                     var    x    :    i32    =    y    +    1    ;
// TokenizedBuffer:
//     --------
// 1)  | var  |
//     --------
// 2)  | x    |
//     --------
// 3)  | :    |
//     --------
// 4)  | i32  |
//     --------
// 5)  | =    |
//     --------
// 6)  | y    |
//     --------
// 7)  | +    |                                 ( y )     ( 1 )
//     --------                                    \__   __/
// 8)  | 1    |                                      ( + )
//     --------
// 9)  | ;    |
//     --------
                     var    x    :    i32    =    y    +    1    ;
                     var    x    :    i32    =    y    +    1    ;
// TokenizedBuffer:
//     --------
// 1)  | var  |
//     --------
// 2)  | x    |
//     --------
// 3)  | :    |
//     --------
// 4)  | i32  |
//     --------
// 5)  | =    |
//     --------
// 6)  | y    |
//     --------
// 7)  | +    |           ( x )     ( i32 )     ( y )     ( 1 )
//     --------              \__   __/             \__   __/
// 8)  | 1    |                ( : )                 ( + )
//     --------
// 9)  | ;    |
//     --------
                     var    x    :    i32    =    y    +    1    ;
                     var    x    :    i32    =    y    +    1    ;
// TokenizedBuffer:
//     --------
// 1)  | var  |
//     --------
// 2)  | x    |
//     --------
// 3)  | :    |
//     --------
// 4)  | i32  |
//     --------
// 5)  | =    |
//     --------
// 6)  | y    |
//     --------
// 7)  | +    |           ( x )     ( i32 )     ( y )     ( 1 )
//     --------              \__   __/             \__   __/
// 8)  | 1    |    ( var )     ( : )       ( = )     ( + )
//     --------        \__________\___________\_________\______
// 9)  | ;    |                                                ( ; )
//     --------
                     var    x    :    i32    =    y    +    1    ;
                     var    x    :    i32    =    y    +    1    ;
// TokenizedBuffer:
//     --------
// 1)  | var  |
//     --------
// 2)  | x    |
//     --------
// 3)  | :    |
//     --------
// 4)  | i32  |
//     --------
// 5)  | =    |
//     --------
// 6)  | y    |
//     --------
// 7)  | +    |           ( `<2>x` )     ( `<3>i32` )     ( `<6>y` )     ( `<7>1` )
//     --------              \__   __/             \__   __/
// 8)  | 1    |    ( `<1>var` )     ( `<4>:` )       ( `<5>=` )     ( `<8>+` )
//     --------        \__________\___________\_________\______
// 9)  | ;    |                                                ( `<9>;` )
//     --------
                     var    x    :    i32    =    y    +    1    ;
                     var    x    :    i32    =    y    +    1    ;
// TokenizedBuffer:
//     --------
// 1)  | var  |    ( var )
//     --------       |
// 2)  | x    |       |   ( x )
//     --------       |     |
// 3)  | :    |       |     |       ( i32 )
//     --------       |      \__   __/
// 4)  | i32  |       |        ( : )
//     --------       |          |
// 5)  | =    |       |          |         ( = )
//     --------       |          |           |
// 6)  | y    |       |          |           |  ( y )
//     --------       |          |           |    |
// 7)  | +    |       |          |           |    |       ( 1 )
//     --------       |          |           |     \__   __/
// 8)  | 1    |       |          |           |       ( + )
//     --------        \__________\___________\_________\______
// 9)  | ;    |                                                ( ; )
//     --------
                     var    x    :    i32    =    y    +    1    ;
                     var    x    :    i32    =    y    +    1    ;
// TokenizedBuffer:                                                   ParseTree:
//     --------                                                        --------
// 1)  | var  |    ( `<1>var` )                                             | `<1>var`  |
//     --------       |                                                --------
// 2)  | x    |       |   ( `<2>x` )                                        | `<2>x`    |
//     --------       |     |                                          --------
// 3)  | :    |       |     |       ( `<3>i32` )                            | `<3>i32`  |
//     --------       |      \__   __/                                 --------
// 4)  | i32  |       |        ( `<4>:` )                                   | `<4>:`    |
//     --------       |          |                                     --------
// 5)  | =    |       |          |         ( `<5>=` )                       | `<5>=`    |
//     --------       |          |           |                         --------
// 6)  | y    |       |          |           |  ( `<6>y` )                  | `<6>y`    |
//     --------       |          |           |    |                    --------
// 7)  | +    |       |          |           |    |       ( `<7>1` )        | `<7>1`    |
//     --------       |          |           |     \__   __/           --------
// 8)  | 1    |       |          |           |       ( `<8>+` )             | `<8>+`    |
//     --------        \__________\___________\_________\______        --------
// 9)  | ;    |                                                ( `<9>;` )   | `<9>;`    |
//     --------                                                        --------
                     var    x    :    i32    =    y    +    1    ;

Parse tree implementation: a guided tour, live!

How does the parser build the tree?

Parser implementation: more live tour!

Data-oriented semantics!

Core idea: model semantics as an IR

Live tiniest demo of Semantics IR

Built by walking the postorder parse tree

Gets more help from the language

Again, really early days on semantics.

More to come!

Data-oriented lowering!

Eh… not really…

Somewhat data-oriented lowering?

Ultimately, limited by LLVM today

Also, we have a long way to go before this can be our focus!

Aside: testing the compiler

Basic testing follows usual patterns

Fuzz testing is a more interesting challenge

Fuzz testing with more complex inputs

Our goal: a compiler that does not crash

Some key takeaways…

Re-think traditional compiler design

Challenge assumptions with aggressive goals

Compiler & implementation design should be a major input to language design

All part of Carbon’s ongoing efforts to

Thank you!

Resources and more information: