1. Syntax: from text to trees#

temper-syntax turns source text into the tree the rest of the compiler works on. It has no idea what let or if mean. That comes later, from macros. Here, almost everything is an operator, and a keyword is just a name.

Source textLiterate split:.temper.md onlyLexer: tokensToken adapter:semicolons, bracketsParser: operator treeGrammar step: calls,blocks, namesFinish the module: calljoins rewritten
Source textLiterate split:.temper.md onlyLexer: tokensToken adapter:semicolons, bracketsParser: operator treeGrammar step: calls,blocks, namesFinish the module: calljoins rewritten
Diagram source
flowchart LR
  text["Source text"] --> lit["Literate split: .temper.md only"]
  lit --> lex["Lexer: tokens"]
  lex --> adapt["Token adapter: semicolons, brackets"]
  adapt --> parse["Parser: operator tree"]
  parse --> ast["Grammar step: calls, blocks, names"]
  ast --> module["Finish the module: call joins rewritten"]

The steps#

Errors are values all the way through. An operator with a missing operand stays in the tree with an error attached, so the rest of the file still parses and still gets checked.

Why each step is a port of Kotlin Temper's, and what that costs, is in the detailed page, along with bracket recovery and errors and depth limits.

Worked example#

The lexer sees triple.temper as tokens. The first few:

Word         0   3  let
Space        3   4
Word         4  10  triple
Punctuation 10  11  (
Word        11  12  n
...

The parser groups them by precedence. Note Curly: a block is an operator applied to what comes before it.

(Semi
  (Curly (HighColon (Paren [let triple] "(" (HighColon n : Int) ")") : Int) { (Star n * 3) })
  ;
  (Paren
    (Dot console . log)
    "("
    (ParenGroup
      "("
      (QuotedGroup "\"" "triple(14) = " (DollarCurly ${ (Paren triple "(" 14 ")") }) "\"")
      ")")
    ")")
  ;)

The grammar step makes calls of everything. let is just the callee of a call, * is desugarOperation, and the string is stringExpr:

(Block
  (Call
    let
    \word
    ^triple
    (Block \_complexArg_ n \type Int)
    \outType
    Int
    (Fun (Block (Call #desugarOperation * n 3))))
  (Call (Call %. console \log) (Call #stringExpr #null #false "triple(14) = " (Call triple 14)))
  #void)

^triple is a name being declared, \word a symbol naming the next argument, and #desugarOperation a builtin. Nothing here knows yet that let declares something. The settle loop finds that out.

You can print these yourself with the crate's small tools: lex-tokens, parse-tree --pretty, and ast-tree --pretty.

One tree type#

Both trees above use one type, Tree: flat arrays of node kinds, spans, data, and children, addressed by NodeId. The parse tree is kept unchanged, for tools that care about layout and comments. The settle loop works on a copy built by the grammar step, whose nodes remember where they came from. Its NodeKind already lists the core constructs that macros will reduce everything to. The trade-offs are in one tree type and two copies.

Where in the code#

More: notes/design.md, sections 3 and 4.

In detail: Syntax in detail: each decision with its alternatives and Kotlin Temper's way, the main types, the tests, and known weaknesses.