Heroes / Examples / tree

examples/tree/

Build an expression tree, then evaluate it, measure its depth and print it.

The tree stores the parts of an expression in an arena, a shared container whose entries are linked by index. It declares more kinds of node than one expression needs. A previous compiler defect appeared when a declared kind was never constructed, so this example keeps that case covered.

Run it

$ heroes run examples/tree/main.hero
(-(2 * (3 + 4)) + 5)
-9
5
8
true
true
false

exit 0 · 7 lines

The program

# Builds one expression as a tree, then reads it three ways.
#
# The expression is `-(2 * (3 + 4)) + 5`, built node by node into the arena the
# way a parser builds one: children first, parents after, every link an index.
#
# What this program is FOR is in `node.hero`'s own doc: the corpus had no
# tree-with-a-vocabulary, and that absence hid a compiler defect until the
# compiler's own syntax tree was ported to Heroes to find it. A program that
# declares more node shapes than it builds is the ordinary case, not a contrived
# one, and it now runs on every commit in three configurations.

use node

function main()
    t: node.Tree @= node.new_tree()

    # The leaves, first — a parser has nothing else to attach to.
    two = node.add_node(@t, .number(value: 2))
    three = node.add_node(@t, .number(value: 3))
    four = node.add_node(@t, .number(value: 4))
    five = node.add_node(@t, .number(value: 5))

    # Then the shapes that hold them.
    sum = node.add_node(@t, .add(left: three, right: four))
    product = node.add_node(@t, .multiply(left: two, right: sum))
    negated = node.add_node(@t, .negate(operand: product))
    whole = node.add_node(@t, .add(left: negated, right: five))

    print(node.render(t, whole))
    print(node.evaluate(t, whole))
    print(node.depth(t, whole))
    print(len(t.nodes))

    # Two trees built the same way are the same value. This is what makes the
    # compiler emit `==` over every case of `Node`, including the ones this
    # program never builds — the assertion that traces the defect.
    u: node.Tree @= node.new_tree()
    _ = node.add_node(@u, .number(value: 2))
    v: node.Tree @= node.new_tree()
    _ = node.add_node(@v, .number(value: 2))
    print(u.nodes == v.nodes)

    # And a node the program never constructs still compares.
    absent: node.Node = .unreadable
    print(absent == .unreadable)
    print(absent == .number(value: 0))
examples/tree/main.hero, 46 lines
node.hero · 146 lines
# A small expression tree, in the shape a compiler's own syntax tree has.
#
# **Why this program exists**: the corpus had nine programs and not
# one of them was shaped like a *tree with a vocabulary* — nodes in an arena,
# linked by index, over a variant that declares every shape the little language
# has while any one program builds only some of them. That shape hid a compiler
# defect for as long as it went unwritten: a case carrying an optional record
# that a program never CONSTRUCTS still gets its `==` generated, and the emitted
# C named a descriptor nothing defined (exit 2, the compiler blaming itself for
# a correct program — `tests/golden/run/fixedbugs-option-payload-descriptor`).
# It was found by porting the compiler's own AST to Heroes, which is a very
# expensive way to find it. This program is the cheap way: the corpus runs it in
# three configurations on every commit.
#
# The vocabulary is deliberately wider than `main.hero` uses. That is not
# padding — it is the point, and it is what every real syntax tree looks like.

## The nodes

# A note an author may attach to a node. Only `.annotated` carries one, and
# nothing in this program builds an `.annotated` — exactly as a real syntax
# tree has cases a given input never produces.
record Note
    text: str
    line: i64

variant Node
    # A number, its value in the node.
    number
        value: i64
    # Two children, by index into the arena.
    add
        left: i64
        right: i64
    multiply
        left: i64
        right: i64
    # One child.
    negate
        operand: i64
    # A node with a note attached — **declared, never constructed here**.
    annotated
        operand: i64
        note: Note?
    # A named hole, for an input the parser could not read.
    unreadable

## The arena

# Every node the program built, in the order it built them. Children are
# indices, never nested values — which is what lets a tree exist at all in a
# language whose values are copied (§4.10).
record Tree
    nodes: [Node]

function new_tree() -> Tree
    nodes: [Node] = []
    return Tree(nodes: nodes)

function add_node(@t: Tree, node: Node) -> i64
    t.nodes @ t.nodes.push(node)
    return t.nodes.len() - 1

## Reading the tree back

# What the expression is worth. The walk names every case, because `_` is
# forbidden on a variant — so the day a seventh node arrives, this function
# stops compiling instead of quietly answering 0.
function evaluate(t: Tree, at: i64) -> i64
    return match t.nodes[at]
        .number n    => n.value
        .add a       => evaluate(t, a.left) + evaluate(t, a.right)
        .multiply m  => evaluate(t, m.left) * evaluate(t, m.right)
        .negate n    => -evaluate(t, n.operand)
        .annotated a => evaluate(t, a.operand)
        .unreadable  => 0

# How deep the tree goes under this node.
function depth(t: Tree, at: i64) -> i64
    return match t.nodes[at]
        .number _    => 1
        .add a       => 1 + deeper(left: depth(t, a.left), right: depth(t, a.right))
        .multiply m  => 1 + deeper(left: depth(t, m.left), right: depth(t, m.right))
        .negate n    => 1 + depth(t, n.operand)
        .annotated a => 1 + depth(t, a.operand)
        .unreadable  => 1

function deeper(left: i64, right: i64) -> i64
    if left > right
        return left
    return right

# The expression, written out. Parentheses everywhere: this is a check that the
# tree is what was built, not a pretty-printer with taste.
function render(t: Tree, at: i64) -> str
    return match t.nodes[at]
        .number n    => n.value.to_str()
        .add a       => "(" + render(t, a.left) + " + " + render(t, a.right) + ")"
        .multiply m  => "(" + render(t, m.left) + " * " + render(t, m.right) + ")"
        .negate n    => "-" + render(t, n.operand)
        .annotated a => render(t, a.operand)
        .unreadable  => "???"

test "a number is its own value and one level deep"
    t: Tree @= new_tree()
    n = add_node(@t, .number(value: 7))
    assert evaluate(t, n) == 7
    assert depth(t, n) == 1
    assert render(t, n) == "7"

test "the arena links children by index"
    t: Tree @= new_tree()
    two = add_node(@t, .number(value: 2))
    three = add_node(@t, .number(value: 3))
    sum = add_node(@t, .add(left: two, right: three))
    assert evaluate(t, sum) == 5
    assert depth(t, sum) == 2
    assert render(t, sum) == "(2 + 3)"

test "nodes compare structurally, cases the program never builds included"
    # **This is the assertion that would have caught the defect.** Comparing
    # two `Node` values makes the compiler emit `==` for EVERY case of the
    # variant, including `.annotated`, whose payload is a `Note?` that nothing
    # here constructs. That used to emit C naming a descriptor
    # nobody defined, and the build died at exit 2.
    a: Node = .number(value: 1)
    b: Node = .number(value: 1)
    c: Node = .unreadable
    assert a == b
    assert a != c

test "a note is a value like any other, when one is built"
    n: Note = Note(text: "check this", line: 12)
    m: Note = Note(text: "check this", line: 12)
    assert n == m
    held: Node = .annotated(operand: 0, note: ok(n))
    absent: Node = .annotated(operand: 0, note: fail(code: "none", msg: "no note"))
    assert held != absent

test "a whole tree is one value, copied and not shared"
    t: Tree @= new_tree()
    _ = add_node(@t, .number(value: 1))
    u: Tree @= t
    _ = add_node(@u, .number(value: 2))
    assert t.nodes.len() == 1
    assert u.nodes.len() == 2
examples/tree/node.hero, 146 lines

fingerprint sha256 361103d8801c · 3 files · index.json

Next: routes. All the examples. ← Previous: spreadsheet

Run one yourself: the four commands. Learn the language, or read the full specification in one file.