examples/interpreter/
Un interprete che legge, analizza ed esegue un piccolo linguaggio.
- 2.642 righe
- 11 moduli
- riga di comando
- legge un file
Il programma ha una struttura simile a quella del compilatore Heroes. Controlla gli errori che può incontrare negli script, come divisioni per zero, numeri fuori intervallo e ricorsioni troppo profonde, e li restituisce con un codice. Durante la scrittura di questo esempio sono emersi due difetti del compilatore.
Eseguilo
hello, world
14
20
3
2
-3
-1
true
false
false
true
true
15
big
the else arm
42
12
3628800
listen!
again!
nothing
exit 0 · 21 righe
Il programma
# A whole little language, scanned, parsed and run — the corpus's first
# program between a program and a compiler.
#
# Corpus material (CLAUDE.md §9), M-corpus-depth's rung. What this program is
# for was measured over the 78 files then in `examples/`: the
# largest program here was `json/` at 671 lines and the compiler was 50,452,
# and between them there was nothing. So the corpus had never run the compiler
# over the shape the compiler's own source has — many modules, several
# directories, generics crossing a `use`, recursion that has to leave a module
# and come back, and a tree deep enough to matter. Nim keeps a `manyloc` corpus
# and Zig `test/standalone/` for exactly this rung.
#
# Every other program here is checked against its own functions or against a
# published answer. This one is checked against **itself**, twice over: the
# scripts under `scripts/` are run and their output compared, and the tests in
# each module assert the piece they own.
use lex/scan
use lex/token
use syn/cursor
use syn/expr as parse_expr
use run/eval
use run/expr
use run/value
use syn/parse
use syn/tree
use util/each
function main()
given = args()
if given.len() != 1
print("usage: interpreter <script>")
exit(2)
source = read_file(given[0])
if source.is_err()
print("cannot read ", given[0])
exit(2)
nodes = parse.program(source.must())
if nodes.is_err()
print(complaint(nodes))
exit(1)
said = eval.run(nodes.must())
if said.is_err()
print(complaint_of_run(said))
exit(1)
f = said.must()
for line in f.lines
print(line)
# THE SCRIPT'S OWN `exit` BECOMES THIS PROGRAM'S. A script that says
# `exit 0` has asked to end, and the honest way to obey it is the
# language's own `exit` — which is also why this program's
# `main.expected` ends in `!exit: 0` rather than falling off the end of
# `main`. Note what that costs: `exit` bypasses
# `hero_runtime_check_leaks()` by design (`examples/README.md`), so this
# program is not one of the corpus's leak witnesses, and `adventure/` is.
if f.stopping
# A status is a byte (`exit(code: u8)`, defect 2224): a script's code past
# 255 cannot be obeyed, so it is said and the program fails.
match to_u8(f.code)
.ok code => exit(code)
.err _ =>
print("exit ", f.code, " is outside 0 to 255, the statuses a shell reads")
exit(1)
# A parse refusal, as the one line a script author reads.
function complaint(said: tree.Tree?) -> str
return match said
.ok _ => "nothing was refused"
.err e => "error[" + e.code + "]: " + e.msg
function complaint_of_run(said: value.Frame?) -> str
return match said
.ok _ => "nothing was refused"
.err e => "error[" + e.code + "]: " + e.msg
test "the pieces are wired together"
got = scan.scanned("print 1 + 2").must()
assert got.len() == 6
assert got[0] == .kw_print
assert got[1] == .number(value: 1)
assert token.shown(got[2]) == "+"
assert each.counted(got, token.opens_a_value) == 2
built: tree.Tree @= tree.empty()
one = tree.added_expr(@built, .number(value: 1))
two = tree.added_expr(@built, .number(value: 2))
sum = tree.added_expr(@built, .binary(op: .add, left: one, right: two))
assert tree.shown_expr(built, sum) == "(1 + 2)"
walking: cursor.Cursor @= cursor.over(got)
assert cursor.bump(@walking) == .kw_print
assert cursor.here(walking) == .number(value: 1)
test "an expression is read into the tree the renderer describes"
assert rendering("1 + 2 * 3") == "(1 + (2 * 3))"
assert rendering("(1 + 2) * 3") == "((1 + 2) * 3)"
assert rendering("-x + !ok") == "((-x) + (!ok))"
assert rendering("f(1, g(2))") == "f(1, g(2))"
test "a program is a list of statements, in the order they were written"
t = parse.program("let x = 1\nprint x\n").must()
assert t.top.len() == 2
assert t.stmts.len() == 2
assert value.shown(.number(v: 3)) == "3"
test "a script runs and its output is what it printed"
assert ran("print 1 + 2") == "3"
assert ran("let x = 4\nprint x * x") == "16"
test "both modules named expr are in this file, and one had to be renamed"
# `use syn/expr as parse_expr` above is the whole demonstration: a file may
# not bind one name twice (spec § Files and layout), and this program has
# two modules whose last path component is `expr` — the one that READS an
# expression and the one that gives it a MEANING. `examples/pipeline/` is
# the corpus's other program that cannot be written without `as`.
#
# The two are used side by side here: the parser builds `2 * 3`, the
# evaluator's own arithmetic is asked for the same product, and they agree.
assert rendering("2 * 3") == "(2 * 3)"
assert value.shown(expr.combined(op: .multiply, a: .number(v: 2), b: .number(v: 3)).must()) == "6"
assert ran("print 2 * 3") == "6"
test "deep nesting works, and the depth it works to is measured rather than hoped"
# THE MILESTONE ASKED FOR 500. IT IS 83, AND THE REASON IS WRITTEN DOWN
# INSTEAD OF THE WISH — twice now, because the number moved once.
#
# It was 60 when this program landed. Panel 106 then measured why the
# ceiling was where it was: the exit sweep minted a load temporary per
# swept slot per returning block, every one address-taken so clang could
# never promote it, and together they were **58.5% of all declared frame
# bytes**. The repair deletes them, and the ceilings roughly doubled.
#
# Measured on this Mac, one nesting depth per configuration,
# this program, binary-searched:
#
# before after
# --sanitize 104 166
# -O0 191 314
# -O2 349 585
#
# All three stop with `panic: stack exhausted` and the guard naming the
# function (panel 104), which is what makes a ceiling measurable at all.
#
# WHY IT IS STILL NOT 500, and this is the honest half: `--sanitize` tops
# out at 166, the corpus runs every program in all three configurations,
# and half of the tightest measured ceiling is the rule this file has
# followed since it was written — so the case sits at **83**. 500 is not
# reachable by any configuration under the sanitiser today, and saying so
# with the number beats a case that passes on two platforms and is flaky
# on the third.
#
# What still costs the bytes, measured by the same sitting and filed
# rather than done: the store's own `old` temporary, 9.1% more of
# declared bytes, which genuinely needs a temporary because the store
# sits between the load and the decref.
deep = "print " + repeat("(", 83) + "1" + repeat(")", 83)
assert ran(deep) == "1"
# And the tree really is that deep, so a parser that quietly flattened the
# parentheses could not pass this.
nodes = parse.program(deep).must()
assert nodes.exprs.len() == 1
# A hundred and fifty still works in every configuration, which says 83 is
# not sitting on a cliff — it was a hundred against a ceiling of 104
# before the repair, and the margin is wider now.
hundred = "print " + repeat("(", 150) + "2" + repeat(")", 150)
assert ran(hundred) == "2"
test "a script that recurses without an end is refused, and does not crash"
# The other side of the same coin. `run/eval.hero`'s DEPTH stops this at
# 200 nested calls with a message naming the function, rather than letting
# it run the interpreter's own stack out.
forever = "fn down(n)\n return down(n + 1)\nend\nprint down(0)"
assert refusal_code(forever) == "too_deep"
# And a script that recurses deeply but finitely still works, up to the
# guard: 30 nested calls, which is under `run/eval.hero`'s measured 38.
counted = "fn down(n)\n if n <= 0\n return 0\n end\n return 1 + down(n - 1)\nend\nprint down(30)"
assert ran(counted) == "30"
# And 50, which is over it, is the guard's refusal rather than a crash.
# The pair moved together when the guard did (24 -> 38 at panel 106's
# repair): a "works" case and a "refused" case that met in the middle
# would assert nothing, and for one edit they both read 30.
assert refusal_code("fn down(n)\n if n <= 0\n return 0\n end\n return 1 + down(n - 1)\nend\nprint down(50)") == "too_deep"
test "every way arithmetic can stop this language is a refusal, not an abort"
# Each of these is Heroes itself aborting if `run/expr.hero` did not guard
# it first (design.md §1.12). A script is input; input must not be able to
# kill the program reading it.
assert refusal_code("print 1 / 0") == "arithmetic"
assert refusal_code("print 1 % 0") == "arithmetic"
assert refusal_code("print 9223372036854775807 + 1") == "arithmetic"
assert refusal_code("print -9223372036854775807 - 2") == "arithmetic"
assert refusal_code("print 4611686018427387904 * 2") == "arithmetic"
assert refusal_code("print 9223372036854775808") == "number_too_large"
# The extremes that DO fit still work, which is what says the guards are
# not simply refusing everything large.
assert ran("print 9223372036854775807") == "9223372036854775807"
assert ran("print 9223372036854775806 + 1") == "9223372036854775807"
assert ran("print 4611686018427387903 * 2") == "9223372036854775806"
assert ran("print -9223372036854775807 - 1") == "-9223372036854775808"
test "a script that says exit stops the run and carries its code"
nodes = parse.program("print 1\nexit 3\nprint 2").must()
f = eval.run(nodes).must()
assert f.stopping
assert f.code == 3
# The statement after the `exit` did not run.
assert f.lines.len() == 1
assert f.lines[0] == "1"
# `main` above turns this into the language's own `exit(3)`.
test "the runtime refuses what it cannot mean, each with its own code"
assert refusal_code("print missing") == "unknown_name"
assert refusal_code("print nope(1)") == "unknown_name"
assert refusal_code("print 1 + true") == "type"
assert refusal_code("if 1\n print 2\nend") == "type"
assert refusal_code("fn f(a)\n return a\nend\nprint f(1, 2)") == "arity"
assert refusal_code("let x = 1\nprint x(2)") == "unknown_name"
test "the parser refuses what it cannot read, each with a message"
assert refusal_code("let = 1") == "parse"
assert refusal_code("print 1 +") == "parse"
assert refusal_code("if true\n print 1") == "parse"
assert refusal_code("end") == "parse"
assert refusal_code("print 1 2") == "parse"
assert refusal_code("print 1 < 2 < 3") == "parse"
assert refusal_code("fn f(a, a)\n return a\nend") == "parse"
test "the strings a script can hold, including the ones with escapes"
assert ran("print \"a\\tb\"") == "a\tb"
assert ran("print \"one\" + \"two\"") == "onetwo"
assert ran("print \"\"") == ""
assert ran("print \"quote: \\\"\"") == "quote: \""
test "a comment may hold any text at all, which is where the first script died"
# A non-ASCII byte in a comment aborted the scanner until it stopped
# slicing one byte at a time (`lex/scan.hero`'s header). The em dash below
# is the exact byte that found it.
assert ran("print 1 # a note — with an em dash") == "1"
assert ran("# accented: cafe\nprint 2") == "2"
test "short-circuit means the right side is never evaluated"
# `1 / 0` on the right of a decided `&&` or `||` must not be reached — if
# it were, these two would be refusals rather than answers.
assert ran("print false && 1 / 0 == 0") == "false"
assert ran("print true || 1 / 0 == 0") == "true"
# And when the right side IS reached, it decides.
assert ran("print true && false") == "false"
assert refusal_code("print true && 1 / 0 == 0") == "arithmetic"
test "a function sees its parameters and nothing else"
# The scope rule `run/eval.hero` states in one line, asserted: a script
# function cannot see the caller's variables.
assert refusal_code("let outer = 1\nfn peek()\n return outer\nend\nprint peek()") == "unknown_name"
# And the caller's variables survive the call unchanged.
assert ran("let x = 1\nfn f(x)\n return x + 1\nend\nprint f(10)\nprint x") == "11\n1"
# The code a run or a parse refused with — the shape every refusal test above
# uses.
function refusal_code(source: str) -> str
nodes = parse.program(source)
if nodes.is_err()
return code_of_tree(nodes)
return code_of_frame(eval.run(nodes.must()))
function code_of_tree(said: tree.Tree?) -> str
return match said
.ok _ => "nothing was refused"
.err e => e.code
function code_of_frame(said: value.Frame?) -> str
return match said
.ok _ => "nothing was refused"
.err e => e.code
# One script, run, with its output lines joined — the shape every runtime test
# in this file uses.
function ran(source: str) -> str
nodes = parse.program(source).must()
f = eval.run(nodes).must()
return join(f.lines, "\n")
# One expression, parsed and written back out — the shape every parser test in
# this file uses, because it checks the TREE without naming an index.
function rendering(source: str) -> str
tokens = scan.scanned(source).must()
c: cursor.Cursor @= cursor.over(tokens)
t: tree.Tree @= tree.empty()
id = parse_expr.parsed(@c, @t).must()
return tree.shown_expr(t, id)
lex/scan.hero · 426 righe
# Source text into tokens, one pass, no lookahead beyond the next byte.
#
# Part of `examples/interpreter/` — the root `main.hero` carries the program's
# argument. This module knows the shape of the little language's surface and
# nothing about its grammar: it says `.plus` where it sees `+` and never asks
# whether a `+` may stand there.
#
# WHAT IT REFUSES, and every refusal is named. A scanner that returns *no
# tokens* on bad input tells the caller nothing, so the four things that can go
# wrong here each carry a code and a message quoting the offending text:
# an unterminated string, an escape that is not one of the four, a byte that
# begins no token, and a number too large for an `i64`.
#
# THE LAST ONE IS A ROBUSTNESS RULE AND NOT A NICETY (design.md §1.12,
# CLAUDE.md §12). Heroes aborts on integer overflow at every width, so
# accumulating `value * 10 + digit` over a long run of digits does not wrap —
# it stops the whole interpreter with a panic. A script is INPUT, and input must
# not be able to kill the program reading it, so the multiplication is guarded
# before it happens rather than after. The test at the bottom feeds it
# nineteen and twenty digits and asserts a refusal, not a crash.
#
# **AND IT WORKS ON BYTES, WHICH IT DID NOT AT FIRST.** Every single-character
# decision below compares an `i64` taken from `source[at]`, never a
# `slice(from: at, to: at + 1)`. The first draft sliced, and the first script
# it was given killed it: `panic: string slice splits a character`, exit 134,
# because a comment in that script contained one em dash. A `str` is measured
# in bytes and a slice that splits a character ABORTS (spec § Failure), so
# slicing one byte at a time is safe only for ASCII — and a comment with an
# accented letter in it is not a strange input, it is the normal one. The
# slices that remain are all taken between ASCII boundaries (a whole word, a
# run of digits, a run inside a string literal), where they cannot split
# anything. This is the same rule as the number guard above, found the same
# way: by running the program instead of reasoning about it.
#
# THE LANGUAGE IS LINE-ORIENTED, so a newline is a token. Blank lines and
# comment-only lines collapse into the one `.newline` that follows them, which
# is what lets the parser say *a statement ends at the end of its line* without
# ever seeing an empty statement.
use lex/token
constant DIGITS: str
"0123456789"
constant MAX: i64
9223372036854775807
constant ERR_TEXT: str
"unterminated_text"
constant ERR_ESCAPE: str
"bad_escape"
constant ERR_BYTE: str
"unknown_byte"
constant ERR_NUMBER: str
"number_too_large"
# Every token in `source`, ending with exactly one `.finish`.
function scanned(source: str) -> [token.Token]?
out: [token.Token] @= []
at: i64 @= 0
n = source.len()
while at < n
here = byte_at(source: source, at: at)
if here == ' ' || here == '\t'
at @ at + 1
else if here == '#'
at @ line_end(source: source, from: at)
else if here == '\n'
at @ at + 1
# One `.newline` however many blank lines stood there, so the
# parser never meets a statement with nothing in it.
if !ends_with_newline(out)
out @ out.push(.newline)
else if is_digit(here)
read = number_at(source: source, from: at)?
out @ out.push(read.what)
at @ read.next
else if here == '"'
read = text_at(source: source, from: at)?
out @ out.push(read.what)
at @ read.next
else if is_word_start(here)
read = word_at(source: source, from: at)
out @ out.push(read.what)
at @ read.next
else
read = operator_at(source: source, from: at)?
out @ out.push(read.what)
at @ read.next
if !ends_with_newline(out) && out.len() > 0
out @ out.push(.newline)
return ok(out.push(.finish))
# One token and where the scanner continues. A record rather than two returns,
# because the two answers are one answer and splitting them is how a caller
# forgets to advance.
record Read
what: token.Token
next: i64
# Where the line holding `from` ends: the index of its `\n`, or the end of the
# source. Used to skip a comment.
function line_end(source: str, from: i64) -> i64
at: i64 @= from
while at < source.len() && byte_at(source: source, at: at) != '\n'
at @ at + 1
return at
# The byte at `at`, or -1 past the end — so every test below is one comparison
# and no caller has to check the length first. -1 is safe as *no byte* because
# a byte is 0..255 and this returns an `i64`.
function byte_at(source: str, at: i64) -> i64
if at >= source.len() || at < 0
return 0 - 1
return source[at].to_i64().must()
function ends_with_newline(out: [token.Token]) -> bool
if out.len() == 0
return false
return out[out.len() - 1] == .newline
function is_digit(b: i64) -> bool
return b >= '0' && b <= '9'
# A letter or an underscore — what a name may begin with. Written as a range
# test on the byte rather than as a table, because the bytes of `a`..`z` are
# contiguous and a character literal is an integer (spec § Types).
function is_word_start(b: i64) -> bool
return b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b == '_'
function is_word_rest(b: i64) -> bool
return is_word_start(b) || is_digit(b)
# The digits at `from`, accumulated — refusing rather than aborting when they
# do not fit. See the module header: this is the guard that keeps a script from
# panicking the interpreter.
function number_at(source: str, from: i64) -> Read?
value: i64 @= 0
at: i64 @= from
while is_digit(byte_at(source: source, at: at))
digit = byte_at(source: source, at: at) - '0'
# `value * 10 + digit > MAX` is the test somebody writes first, and it
# is the wrong one: computing it is the overflow. The division is what
# makes the question answerable without asking it.
if value > (MAX - digit) / 10
return fail(
code: ERR_NUMBER,
msg: "`" + source.slice(from: from, to: at + 1) + "` does not fit in a 64-bit integer"
)
value @ value * 10 + digit
at @ at + 1
return ok(Read(what: .number(value: value), next: at))
# The string literal at `from`, with its four escapes decoded.
#
# Four and not six: `\n`, `\t`, `\"` and `\\`. The little language has no
# character literals, so `\'` would have nothing to escape, and `\r` is left
# out deliberately — a scanner that accepts an escape it cannot explain is a
# scanner whose author was copying.
function text_at(source: str, from: i64) -> Read?
pieces: [str] @= []
at: i64 @= from + 1
# Where the current unescaped run began. The literal is copied a RUN at a
# time rather than a byte at a time, and that is what keeps this safe on
# text: every slice is taken between two ASCII bytes (the opening quote, an
# escape's backslash, the closing quote), so none of them can split a
# character. A byte-at-a-time loop aborts on the first accented letter.
run: i64 @= at
while at < source.len()
one = byte_at(source: source, at: at)
if one == '"'
pieces @ pieces.push(source.slice(from: run, to: at))
return ok(Read(what: .text(value: join(pieces, "")), next: at + 1))
if one == '\n'
return fail(code: ERR_TEXT, msg: "a string may not run past the end of its line")
if one == '\\'
pieces @ pieces.push(source.slice(from: run, to: at))
next_one = byte_at(source: source, at: at + 1)
if next_one == 0 - 1
return fail(code: ERR_TEXT, msg: "the source ends inside a string")
if next_one == 'n'
pieces @ pieces.push("\n")
else if next_one == 't'
pieces @ pieces.push("\t")
else if next_one == '"'
pieces @ pieces.push("\"")
else if next_one == '\\'
pieces @ pieces.push("\\")
else
return fail(
code: ERR_ESCAPE,
msg: "`\\" + source.slice(from: at + 1, to: at + 2) + "` is not one of the four escapes `\\n` `\\t` `\\\"` `\\\\`"
)
at @ at + 2
run @ at
else
at @ at + 1
return fail(code: ERR_TEXT, msg: "the source ends inside a string")
# The word at `from`, resolved to a keyword or a name by `token.worded` — this
# module does not hold a list of keywords, and that is deliberate: one list, in
# the module that owns the vocabulary.
function word_at(source: str, from: i64) -> Read
at: i64 @= from
while is_word_rest(byte_at(source: source, at: at))
at @ at + 1
return Read(what: token.worded(source.slice(from: from, to: at)), next: at)
# The operator at `from`. The two-byte ones are tried before the one-byte ones
# they begin with, which is the whole subtlety here: `<=` read as `<` then `=`
# is a program that parses and means something else.
function operator_at(source: str, from: i64) -> Read?
one = byte_at(source: source, at: from)
two = byte_at(source: source, at: from + 1)
if one == '<' && two == '='
return ok(Read(what: .less_equal, next: from + 2))
if one == '>' && two == '='
return ok(Read(what: .greater_equal, next: from + 2))
if one == '=' && two == '='
return ok(Read(what: .equal_equal, next: from + 2))
if one == '!' && two == '='
return ok(Read(what: .bang_equal, next: from + 2))
if one == '&' && two == '&'
return ok(Read(what: .ampersands, next: from + 2))
if one == '|' && two == '|'
return ok(Read(what: .bars, next: from + 2))
single = one_byte(one)
if single.is_err()
return unknown_byte(source: source, at: from)
return ok(Read(what: single.must(), next: from + 1))
# A byte that begins no token, quoted in the refusal. The quoting is where the
# non-ASCII case has to be careful one last time: the offending byte may be the
# FIRST of a multi-byte character, so the message slices to the end of the
# line rather than to the next byte — a slice to `at + 1` would abort while
# building the diagnostic, which is the worst place to abort.
function unknown_byte(source: str, at: i64) -> Read?
stop = line_end(source: source, from: at)
return fail(
code: ERR_BYTE,
msg: "`" + source.slice(from: at, to: stop) + "` begins no token this language knows"
)
function one_byte(b: i64) -> token.Token?
if b == '+'
return ok(.plus)
if b == '-'
return ok(.minus)
if b == '*'
return ok(.star)
if b == '/'
return ok(.slash)
if b == '%'
return ok(.percent)
if b == '='
return ok(.equals)
if b == '<'
return ok(.less)
if b == '>'
return ok(.greater)
if b == '!'
return ok(.bang)
if b == '('
return ok(.lparen)
if b == ')'
return ok(.rparen)
if b == ','
return ok(.comma)
return fail(code: ERR_BYTE, msg: "not an operator")
# The code a scan refused with, or a sentence saying it did not — so a test
# asserts WHICH refusal in one line.
function refusal_of(said: [token.Token]?) -> str
return match said
.ok _ => "nothing was refused"
.err e => e.code
test "a whole statement, token by token"
got = scanned("let total = 3 + 4").must()
assert got.len() == 8
assert got[0] == .kw_let
assert got[1] == .name(value: "total")
assert got[2] == .equals
assert got[3] == .number(value: 3)
assert got[4] == .plus
assert got[5] == .number(value: 4)
assert got[6] == .newline
assert got[7] == .finish
test "the empty source is one token, and it is the end"
got = scanned("").must()
assert got.len() == 1
assert got[0] == .finish
test "blank lines and comments collapse into the newline that follows them"
# Three lines of nothing between two statements must not become three
# statements — the parser would then have to know what an empty statement
# is, and it does not.
got = scanned("print 1\n\n\n# a comment\n\nprint 2").must()
newlines: i64 @= 0
for one in got
if one == .newline
newlines @ newlines + 1
assert newlines == 2
assert got[0] == .kw_print
assert got[3] == .kw_print
test "a comment runs to the end of its line and no further"
got = scanned("print 1 # let x = 9\nprint 2").must()
assert got.len() == 7
assert got[0] == .kw_print
assert got[1] == .number(value: 1)
assert got[2] == .newline
assert got[3] == .kw_print
assert got[4] == .number(value: 2)
test "a two-byte operator is never read as the one it begins with"
# `<=` read as `<` then `=` parses and means something else, which is the
# one mistake in this module that a passing program would hide.
got = scanned("a <= b >= c == d != e && f || g").must()
assert got[1] == .less_equal
assert got[3] == .greater_equal
assert got[5] == .equal_equal
assert got[7] == .bang_equal
assert got[9] == .ampersands
assert got[11] == .bars
single = scanned("a < b > c = d ! e").must()
assert single[1] == .less
assert single[3] == .greater
assert single[5] == .equals
assert single[7] == .bang
test "a string keeps its spaces and decodes its four escapes"
got = scanned("print \"a b\\tc\\nd\\\"e\\\\f\"").must()
assert got[1] == .text(value: "a b\tc\nd\"e\\f")
test "the empty string is a token with nothing in it"
got = scanned("print \"\"").must()
assert got[1] == .text(value: "")
test "a name may hold digits and underscores but may not begin with a digit"
got = scanned("total_2 _x").must()
assert got[0] == .name(value: "total_2")
assert got[1] == .name(value: "_x")
mixed = scanned("2x").must()
assert mixed[0] == .number(value: 2)
assert mixed[1] == .name(value: "x")
test "the four refusals, each asserted by its code"
assert refusal_of(scanned("print \"open")) == ERR_TEXT
assert refusal_of(scanned("print \"line\nbreak\"")) == ERR_TEXT
assert refusal_of(scanned("print \"\\q\"")) == ERR_ESCAPE
assert refusal_of(scanned("print @")) == ERR_BYTE
assert refusal_of(scanned("print 1")) == "nothing was refused"
test "a number too large is refused and does not abort the interpreter"
# THE ROBUSTNESS CASE (design.md §1.12). Heroes aborts on overflow, so
# without the guard in `number_at` these three lines would be a panic
# rather than a diagnostic — and the input is a SCRIPT, which is the one
# thing this program does not control.
assert refusal_of(scanned("print 9223372036854775807")) == "nothing was refused"
assert scanned("print 9223372036854775807").must()[1] == .number(value: MAX)
assert refusal_of(scanned("print 9223372036854775808")) == ERR_NUMBER
assert refusal_of(scanned("print 99999999999999999999")) == ERR_NUMBER
assert refusal_of(scanned("print 123456789012345678901234567890")) == ERR_NUMBER
test "the source always ends in exactly one finish, whatever it held"
sources = ["", " ", "\n", "# nothing\n", "print 1", "print 1\n", "print 1\n\n"]
for source in sources
got = scanned(source).must()
finishes: i64 @= 0
for one in got
if one == .finish
finishes @ finishes + 1
assert finishes == 1
assert got[got.len() - 1] == .finish
lex/token.hero · 224 righe
# What a token is, and nothing about how one is read.
#
# Part of `examples/interpreter/`, whose root `main.hero` carries the whole
# program's argument. This module is the vocabulary: `scan.hero` produces these
# and `syn/parse.hero` consumes them, and neither knows anything else about the
# other.
#
# WHY A VARIANT WITH PAYLOADS RATHER THAN A KIND AND A TEXT. A token that is a
# number carries an `i64`; one that is a name carries a `str`; one that is `+`
# carries nothing. Spelling that as `record Token { kind: Kind, text: str }`
# would give every token a `text` field that is empty for most of them and
# would move the question *is this number well formed* out of the scanner and
# into whoever reads it. The payload is where the answer belongs, and the
# `match` in `syn/parse.hero` is exhaustive or a compile error.
# One token of the little language this interpreter runs.
variant Token
# A run of digits, already turned into a number.
number
value: i64
# Text between double quotes, with its escapes already decoded.
text
value: str
# A bare word that is not a keyword.
name
value: str
# The keywords, one case each, so a `match` over them is exhaustive and a
# new keyword cannot be forgotten by a reader.
kw_let
kw_print
kw_if
kw_else
kw_while
kw_fn
kw_return
kw_exit
kw_end
kw_true
kw_false
# The operators and the punctuation.
plus
minus
star
slash
percent
equals
less
less_equal
greater
greater_equal
equal_equal
bang_equal
bang
ampersands
bars
lparen
rparen
comma
# One line's worth of newline, however many blank lines it stood for. The
# language is line-oriented, so this is a token and not whitespace.
newline
# The end of the source, which the parser reads rather than testing an
# index against a length.
finish
# The keyword a word spells, or `.name` if it spells none. The scanner calls
# this once per word, so this list is the only place a keyword is written down.
function worded(word: str) -> Token
if word == "let"
return .kw_let
if word == "print"
return .kw_print
if word == "if"
return .kw_if
if word == "else"
return .kw_else
if word == "while"
return .kw_while
if word == "fn"
return .kw_fn
if word == "return"
return .kw_return
if word == "exit"
return .kw_exit
if word == "end"
return .kw_end
if word == "true"
return .kw_true
if word == "false"
return .kw_false
return .name(value: word)
# How a token reads in a diagnostic. Every case has a spelling, including the
# two that have no text of their own, because a parse error that says
# `expected a name, found ` is a diagnostic with a hole in it.
function shown(t: Token) -> str
return match t
.number n => n.value.to_str()
.text x => "\"" + x.value + "\""
.name n => n.value
.kw_let => "let"
.kw_print => "print"
.kw_if => "if"
.kw_else => "else"
.kw_while => "while"
.kw_fn => "fn"
.kw_return => "return"
.kw_exit => "exit"
.kw_end => "end"
.kw_true => "true"
.kw_false => "false"
.plus => "+"
.minus => "-"
.star => "*"
.slash => "/"
.percent => "%"
.equals => "="
.less => "<"
.less_equal => "<="
.greater => ">"
.greater_equal => ">="
.equal_equal => "=="
.bang_equal => "!="
.bang => "!"
.ampersands => "&&"
.bars => "||"
.lparen => "("
.rparen => ")"
.comma => ","
.newline => "end of line"
.finish => "end of file"
# Whether this token can begin an expression — the one question the parser asks
# that is about the token alone, so it lives here beside the vocabulary.
# Every case is named, and the reason is the language: `_` as an arm over a
# `variant` is `error[wildcard_on_variant]` — *name every case, so that adding
# one breaks this `match` instead of silently falling through*. The first draft
# of this function wrote `_ => false` and the compiler refused it, which is the
# refusal doing its job: a thirty-fifth token added next month has to be
# classified here rather than defaulting to *cannot open a value*.
function opens_a_value(t: Token) -> bool
return match t
.number _ | .text _ | .name _ | .kw_true | .kw_false => true
.minus | .bang | .lparen => true
.kw_let | .kw_print | .kw_if | .kw_else | .kw_while => false
.kw_fn | .kw_return | .kw_exit | .kw_end => false
.plus | .star | .slash | .percent | .equals => false
.less | .less_equal | .greater | .greater_equal => false
.equal_equal | .bang_equal | .ampersands | .bars => false
.rparen | .comma | .newline | .finish => false
test "a keyword is a keyword and a word that only looks like one is not"
assert worded("let") == .kw_let
assert worded("end") == .kw_end
assert worded("letter") == .name(value: "letter")
assert worded("ending") == .name(value: "ending")
assert worded("iff") == .name(value: "iff")
assert worded("") == .name(value: "")
test "every keyword this module knows round-trips through its own spelling"
# The list is written twice on purpose — once in `worded` and once here —
# because the second copy is what notices a keyword whose spelling and
# whose case have drifted apart.
words = ["let", "print", "if", "else", "while", "fn", "return", "exit", "end", "true", "false"]
for word in words
assert shown(worded(word)) == word
assert worded(word) != .name(value: word)
test "a token that carries a payload shows the payload"
assert shown(.number(value: 42)) == "42"
assert shown(.number(value: 0 - 7)) == "-7"
assert shown(.text(value: "hi")) == "\"hi\""
assert shown(.name(value: "total")) == "total"
test "the two tokens with no text of their own still have a spelling"
# A parse error naming `.newline` or `.finish` has to say something, and
# what it says is checked here rather than discovered in a message.
assert shown(.newline) == "end of line"
assert shown(.finish) == "end of file"
test "what can open a value, and what cannot"
assert opens_a_value(.number(value: 1))
assert opens_a_value(.name(value: "x"))
assert opens_a_value(.text(value: "s"))
assert opens_a_value(.kw_true)
assert opens_a_value(.minus)
assert opens_a_value(.bang)
assert opens_a_value(.lparen)
assert !opens_a_value(.plus)
assert !opens_a_value(.rparen)
assert !opens_a_value(.kw_end)
assert !opens_a_value(.newline)
assert !opens_a_value(.finish)
test "two tokens are equal when their payloads are"
# Structural equality over a variant, which the compiler generates by
# walking fields (CLAUDE.md §7) — the parser leans on it for every
# `expect`, so it is asserted rather than assumed.
#
# The annotations are not decoration: `.number(value: 3) == .number(value:
# 3)` is `error[cannot_infer]`, because neither side of the `==` says which
# variant `.number` belongs to. A case literal takes its type from the
# context, and two case literals give each other none.
three: Token = .number(value: 3)
four: Token = .number(value: 4)
letter_a: Token = .name(value: "a")
text_a: Token = .text(value: "a")
assert three == .number(value: 3)
assert three != four
assert letter_a != text_a
assert worded("if") != worded("else")
assert shown(three) == "3"
run/eval.hero · 209 righe
# What a statement does, and the door an expression uses to call a function.
#
# Part of `examples/interpreter/` — the root `main.hero` carries the program's
# argument. This module is the top of the runtime: it walks statements, and
# hands `run/expr.hero` its own `called` so that an expression holding a call
# can run a body without this module and that one importing each other (see
# that file's header for why the language forbids the cycle).
#
# WHERE THE GENERICS CROSS A `use` FOR THE SECOND TIME. `util/each.hero`'s
# templates are instantiated at `str` in `syn/parse.hero` and at
# `run/value.hero`'s `Value` here — `each.joined` renders the arguments a
# mismatched call was given. Two callers in two directories, two layouts, one
# template.
#
# WHAT A SCRIPT FUNCTION CAN SEE, decided rather than inherited: its
# parameters, and nothing else. No closures, no globals, no enclosing scope —
# so `called` builds a fresh variable map holding only the arguments, and puts
# the caller's map back afterwards. The alternative (a chain of scopes) is a
# bigger program and it would teach the corpus nothing it does not already
# carry; this one keeps the rule a reader can state in one line.
#
# THE RECURSION GUARD IS ROBUSTNESS AND NOT A LIMIT (design.md §1.12). A script
# that calls itself for ever would run this interpreter's own `body` for ever,
# and the answer to that in a compiled language is a stack overflow — which
# the runtime now catches and names (panel 104), but which is still a crash
# from the script's point of view. `DEPTH` refuses first, with the name of the
# function and the depth, which is a message a script author can act on.
use run/expr
use run/value
use syn/tree
use util/each
constant ERR_NAME: str
"unknown_name"
constant ERR_ARITY: str
"arity"
constant ERR_DEPTH: str
"too_deep"
# How many nested script calls are allowed — and this number is MEASURED,
# not chosen. The first draft said 200 on the reasoning that a runaway
# recursion should stop "long before the machine's stack does", and 200 is
# past the machine's stack: a script recursing 150 deep exited **139 in
# silence** before the guard was ever consulted.
#
# **Re-measured, after panel 106's repair, and it moved.** The
# exit sweep stopped minting a load temporary per swept slot per returning
# block — 58.5% of every declared frame — so one script call costs a good
# deal less than it did. Nested script calls, one ceiling per configuration,
# binary-searched on this Mac with the guard lifted out of the way:
#
# before after
# --sanitize 40 76 (exit 134, ASan names it)
# -O0 50 107
# -O2 80 239
#
# One script call still costs about nine Heroes frames (`body` → `one` →
# `given_back` → `expr.evaluated` → `applied` → the function value →
# `called` → `entered` → `body`); each of those frames is now roughly half
# the size. So the guard moves 24 → **38**: under half of the tightest
# measured ceiling, which is margin enough for a machine with a smaller
# stack, and far above anything a script here needs
# (`scripts/tour.script`'s deepest is `factorial(10)`, ten frames).
#
# A GUARD ABOVE THE REAL CEILING IS NOT A GUARD. That is the whole lesson of
# this constant, and it was learned by running rather than by reasoning —
# twice now, because the ceiling moved and the guard had to be re-measured
# rather than left where a comment said it belonged.
constant DEPTH: i64
38
# A whole program: every top-level statement in order, on a fresh frame.
function run(nodes: tree.Tree) -> value.Frame?
return body(nodes: nodes, ids: nodes.top, f: value.fresh())
# A list of statements, stopping early the moment a `return` or an `exit` is in
# flight — which is the whole of this language's non-local control flow.
function body(nodes: tree.Tree, ids: [i64], f: value.Frame) -> value.Frame?
walking: value.Frame @= f
for id in ids
if walking.returned || walking.stopping
return ok(walking)
walking @ one(nodes: nodes, id: id, f: walking)?
return ok(walking)
function one(nodes: tree.Tree, id: i64, f: value.Frame) -> value.Frame?
return match nodes.stmts[id]
.bind b => bound(nodes: nodes, name: b.name, at: b.value, f: f)
.say s => said(nodes: nodes, at: s.value, f: f)
.when w => chosen(nodes: nodes, at: w.condition, then: w.then, otherwise: w.otherwise, f: f)
.while_loop w => looped(nodes: nodes, at: w.condition, inside: w.body, f: f)
.give g => given_back(nodes: nodes, at: g.value, f: f)
.stop s => stopped(nodes: nodes, at: s.code, f: f)
.define d => ok(with_function(f: f, name: d.name, at: id))
.evaluate e => spent(nodes: nodes, at: e.value, f: f)
function bound(nodes: tree.Tree, name: str, at: i64, f: value.Frame) -> value.Frame?
step = expr.evaluated(nodes, at, f, called)?
home: value.Frame @= step.frame
home.vars[name] @ step.got
return ok(home)
function said(nodes: tree.Tree, at: i64, f: value.Frame) -> value.Frame?
step = expr.evaluated(nodes, at, f, called)?
home: value.Frame @= step.frame
home.lines @ home.lines.push(value.shown(step.got))
return ok(home)
function chosen(nodes: tree.Tree, at: i64, then: [i64], otherwise: [i64], f: value.Frame) -> value.Frame?
step = expr.evaluated(nodes, at, f, called)?
if value.condition_of(step.got)?
return body(nodes: nodes, ids: then, f: step.frame)
return body(nodes: nodes, ids: otherwise, f: step.frame)
function looped(nodes: tree.Tree, at: i64, inside: [i64], f: value.Frame) -> value.Frame?
walking: value.Frame @= f
while true
step = expr.evaluated(nodes, at, walking, called)?
walking @ step.frame
if !value.condition_of(step.got)?
return ok(walking)
walking @ body(nodes: nodes, ids: inside, f: walking)?
if walking.returned || walking.stopping
return ok(walking)
return ok(walking)
function given_back(nodes: tree.Tree, at: i64, f: value.Frame) -> value.Frame?
step = expr.evaluated(nodes, at, f, called)?
home: value.Frame @= step.frame
home.returned @ true
home.result @ step.got
return ok(home)
function stopped(nodes: tree.Tree, at: i64, f: value.Frame) -> value.Frame?
step = expr.evaluated(nodes, at, f, called)?
home: value.Frame @= step.frame
home.stopping @ true
home.code @ value.number_of(step.got)?
return ok(home)
# A bare expression as a statement — a call written for what it prints. The
# value is thrown away on purpose, which is the one place this interpreter does
# what `_ =` does in Heroes.
function spent(nodes: tree.Tree, at: i64, f: value.Frame) -> value.Frame?
step = expr.evaluated(nodes, at, f, called)?
return ok(step.frame)
function with_function(f: value.Frame, name: str, at: i64) -> value.Frame
home: value.Frame @= f
home.fns[name] @ at
return home
# THE DOOR. This is the function value `run/expr.hero` is handed, and its type
# is `(function(value.Request) -> value.Frame?)` — one parameter wide because
# `Request` packs the four things a call needs.
function called(r: value.Request) -> value.Frame?
where = r.frame.fns[r.name]
if where.is_err()
return fail(code: ERR_NAME, msg: "no function is named `" + r.name + "`")
if r.frame.depth >= DEPTH
return fail(
code: ERR_DEPTH,
msg: "`" + r.name + "` is " + DEPTH.to_str() + " calls deep — a script that recurses without an end stops here rather than in the machine's stack"
)
return match r.nodes.stmts[where.must()]
.define d => entered(r: r, params: d.params, inside: d.body)
.bind _ | .say _ | .when _ | .while_loop _ => not_a_function(r: r)
.give _ | .stop _ | .evaluate _ => not_a_function(r: r)
function not_a_function(r: value.Request) -> value.Frame?
return fail(code: ERR_NAME, msg: "`" + r.name + "` names something that is not a function")
# One call: bind the arguments, run the body on a frame that can see nothing
# else, then put the caller's variables back and carry the result home.
function entered(r: value.Request, params: [str], inside: [i64]) -> value.Frame?
if params.len() != r.given.len()
return fail(
code: ERR_ARITY,
msg: "`" + r.name + "` takes " + params.len().to_str() + " and was given " + r.given.len().to_str() + ": (" + each.joined(r.given, value.shown, ", ") + ")"
)
inner: value.Frame @= r.frame
fresh_vars: {str: value.Value} @= {}
i: i64 @= 0
while i < params.len()
fresh_vars[params[i]] @ r.given[i]
i @ i + 1
inner.vars @ fresh_vars
inner.depth @ r.frame.depth + 1
after = body(nodes: r.nodes, ids: inside, f: inner)?
home: value.Frame @= after
home.vars @ r.frame.vars
home.depth @ r.frame.depth
return ok(home)
run/expr.hero · 266 righe
# What an expression means — and the four ways arithmetic can stop a program
# that this module refuses instead of suffering.
#
# Part of `examples/interpreter/` — the root `main.hero` carries the program's
# argument. **This module's name collides with `syn/expr.hero`** and the header
# there explains why the collision is the honest one: that file reads an
# expression, this one gives it a meaning.
#
# HOW THIS MODULE CALLS A FUNCTION WITHOUT KNOWING HOW TO RUN A BODY. A script
# function's body is statements, and statements are `run/eval.hero`'s job — but
# an expression can hold a call, so the two need each other. Heroes refuses a
# module cycle (`error[module_cycle]`, on the `use` edge whatever it carries),
# so the loop is broken with a **function value**: `evaluated` takes
# `(function(value.Request) -> value.Frame?)` and `run/eval.hero` hands it its
# own `called`. The recursion is real and it crosses a `use` in one direction
# only, which is the shape a language without module cycles forces and the
# reason the corpus wanted a program this size.
#
# THE FOUR REFUSALS, and every one of them is Heroes aborting if it is not
# caught here (design.md §1.12, CLAUDE.md §12 — robustness beats every other
# criterion):
#
# - division by zero, which the spec says aborts;
# - `%` by zero, guarded like `/` for the same reason;
# - `+`, `-` and `*` overflowing, which aborts at every width;
# - `INT64_MIN / -1`, which is the one division that overflows.
#
# A script is INPUT. A program that a line of its input can kill is a program
# with a hole in it, so each of these is a named refusal with a message, and
# the tests at the bottom feed the extremes to prove it.
use run/value
use syn/tree
constant ERR_NAME: str
"unknown_name"
constant ERR_MATH: str
"arithmetic"
constant ERR_TYPE: str
"type"
constant MAX: i64
9223372036854775807
constant MIN: i64
0 - 9223372036854775807 - 1
# A value, and the frame the evaluation left behind — a call can define nothing
# but it can print, so the frame comes back even from an expression.
record Answer
frame: value.Frame
got: value.Value
# One expression. `call` is `run/eval.hero`'s door: see the module header.
function evaluated(nodes: tree.Tree, id: i64, f: value.Frame, call: (function(value.Request) -> value.Frame?)) -> Answer?
return match nodes.exprs[id]
.number n => ok(Answer(frame: f, got: .number(v: n.value)))
.text x => ok(Answer(frame: f, got: .text(s: x.value)))
.truth b => ok(Answer(frame: f, got: .truth(b: b.value)))
.name n => looked_up(f: f, word: n.value)
.negate u => negated(nodes: nodes, id: u.on, f: f, call: call)
.invert u => inverted(nodes: nodes, id: u.on, f: f, call: call)
.binary b => joined(nodes: nodes, op: b.op, left: b.left, right: b.right, f: f, call: call)
.call c => applied(nodes: nodes, name: c.name, ids: c.args, f: f, call: call)
function looked_up(f: value.Frame, word: str) -> Answer?
held = f.vars[word]
if held.is_err()
return fail(code: ERR_NAME, msg: "`" + word + "` has no value — nothing has been let to that name")
return ok(Answer(frame: f, got: held.must()))
function negated(nodes: tree.Tree, id: i64, f: value.Frame, call: (function(value.Request) -> value.Frame?)) -> Answer?
inner = evaluated(nodes, id, f, call)?
n = value.number_of(inner.got)?
if n == MIN
return fail(code: ERR_MATH, msg: "negating the smallest number has no answer that fits")
return ok(Answer(frame: inner.frame, got: .number(v: 0 - n)))
function inverted(nodes: tree.Tree, id: i64, f: value.Frame, call: (function(value.Request) -> value.Frame?)) -> Answer?
inner = evaluated(nodes, id, f, call)?
return ok(Answer(frame: inner.frame, got: .truth(b: !value.condition_of(inner.got)?)))
# Two sides, or one where the operator short-circuits. `&&` and `||` do not
# evaluate their right side when the left decides the answer, which is the
# rule Heroes keeps for its own (spec § Operators) and the reason they cannot
# go through the same path as `+`.
function joined(nodes: tree.Tree, op: tree.Op, left: i64, right: i64, f: value.Frame, call: (function(value.Request) -> value.Frame?)) -> Answer?
first = evaluated(nodes, left, f, call)?
if op == .both
if !value.condition_of(first.got)?
return ok(Answer(frame: first.frame, got: .truth(b: false)))
second = evaluated(nodes, right, first.frame, call)?
return ok(Answer(frame: second.frame, got: .truth(b: value.condition_of(second.got)?)))
if op == .either
if value.condition_of(first.got)?
return ok(Answer(frame: first.frame, got: .truth(b: true)))
second = evaluated(nodes, right, first.frame, call)?
return ok(Answer(frame: second.frame, got: .truth(b: value.condition_of(second.got)?)))
second = evaluated(nodes, right, first.frame, call)?
got = combined(op: op, a: first.got, b: second.got)?
return ok(Answer(frame: second.frame, got: got))
# The operators that want both sides. Kept apart from `joined` so that the
# arithmetic can be tested without a frame, which is what the extremes at the
# bottom of this file do.
function combined(op: tree.Op, a: value.Value, b: value.Value) -> value.Value?
if op == .add
if is_text(a) || is_text(b)
return ok(.text(s: value.text_of(a)? + value.text_of(b)?))
return ok(.number(v: plus(a: value.number_of(a)?, b: value.number_of(b)?)?))
if op == .subtract
return ok(.number(v: minus(a: value.number_of(a)?, b: value.number_of(b)?)?))
if op == .multiply
return ok(.number(v: times(a: value.number_of(a)?, b: value.number_of(b)?)?))
if op == .divide
return ok(.number(v: over(a: value.number_of(a)?, b: value.number_of(b)?)?))
if op == .remainder
return ok(.number(v: rest(a: value.number_of(a)?, b: value.number_of(b)?)?))
if op == .equal
return ok(.truth(b: a == b))
if op == .unequal
return ok(.truth(b: a != b))
return ordered(op: op, a: value.number_of(a)?, b: value.number_of(b)?)
function ordered(op: tree.Op, a: i64, b: i64) -> value.Value?
if op == .less
return ok(.truth(b: a < b))
if op == .less_equal
return ok(.truth(b: a <= b))
if op == .greater
return ok(.truth(b: a > b))
if op == .greater_equal
return ok(.truth(b: a >= b))
return fail(code: ERR_TYPE, msg: "`" + tree.shown_op(op) + "` does not take two values here")
function is_text(v: value.Value) -> bool
return match v
.text _ => true
.number _ => false
.truth _ => false
.nothing => false
# `a + b`, refusing rather than aborting. The test is written as a subtraction
# against the limit because computing the sum is the overflow.
function plus(a: i64, b: i64) -> i64?
if b > 0 && a > MAX - b
return too_big(a: a, b: b, op: "+")
if b < 0 && a < MIN - b
return too_big(a: a, b: b, op: "+")
return ok(a + b)
function minus(a: i64, b: i64) -> i64?
if b < 0 && a > MAX + b
return too_big(a: a, b: b, op: "-")
if b > 0 && a < MIN + b
return too_big(a: a, b: b, op: "-")
return ok(a - b)
# `a * b`, refusing rather than aborting.
#
# The smallest number is handled first and on its own, because the usual trick
# — compare the magnitudes against `MAX / |b|` — needs to negate, and negating
# the smallest number is itself the overflow. What is left after that gate has
# a safe magnitude, so one division answers the question.
#
# ONE DELIBERATE CONSERVATISM, stated rather than hidden: the bound is `MAX`,
# and the true range of a product reaches `MIN`, whose magnitude is one larger.
# So a script that multiplies its way to exactly the smallest number gets a
# refusal instead of an answer. Refusing a value that fits is a smaller wrong
# than aborting, and the alternative is a second bound for the mixed-sign case
# that nothing in this program needs.
function times(a: i64, b: i64) -> i64?
if a == 0 || b == 0
return ok(0)
if a == 1
return ok(b)
if b == 1
return ok(a)
if a == MIN || b == MIN
return too_big(a: a, b: b, op: "*")
am = size_of(a)
bm = size_of(b)
if am > MAX / bm
return too_big(a: a, b: b, op: "*")
return ok(a * b)
# `|n|`, for an `n` that is not the smallest number — the caller has already
# refused that one, and this function says so rather than guessing.
function size_of(n: i64) -> i64
if n < 0
return 0 - n
return n
function over(a: i64, b: i64) -> i64?
if b == 0
return fail(code: ERR_MATH, msg: "dividing " + a.to_str() + " by zero has no answer")
# The one division that overflows: the smallest number by -1. Heroes aborts
# on it and the spec says so, so it is a refusal here.
if a == MIN && b == 0 - 1
return too_big(a: a, b: b, op: "/")
return ok(a / b)
function rest(a: i64, b: i64) -> i64?
if b == 0
return fail(code: ERR_MATH, msg: "the remainder of " + a.to_str() + " by zero has no answer")
if a == MIN && b == 0 - 1
return too_big(a: a, b: b, op: "%")
return ok(a % b)
function too_big(a: i64, b: i64, op: str) -> i64?
return fail(
code: ERR_MATH,
msg: a.to_str() + " " + op + " " + b.to_str() + " does not fit in a 64-bit number"
)
# A call: the arguments first, left to right, then the door into
# `run/eval.hero`.
function applied(nodes: tree.Tree, name: str, ids: [i64], f: value.Frame, call: (function(value.Request) -> value.Frame?)) -> Answer?
walking: value.Frame @= f
given: [value.Value] @= []
for id in ids
step = evaluated(nodes, id, walking, call)?
walking @ step.frame
given @ given.push(step.got)
after = call(value.Request(nodes: nodes, name: name, given: given, frame: walking))?
result = after.result
home: value.Frame @= after
home.returned @ false
home.result @ .nothing
return ok(Answer(frame: home, got: result))
run/value.hero · 203 righe
# What a value is while the little language runs, and the state one run
# carries.
#
# Part of `examples/interpreter/` — the root `main.hero` carries the program's
# argument. `run/expr.hero` and `run/eval.hero` both read this and neither
# declares a value of its own.
#
# WHY THE STATE IS A VALUE AND NOT A THING BEING MUTATED. `Frame` is passed in
# and handed back rather than taken as an `@` parameter, and that is not a
# style choice: it is what makes the recursion between `run/eval.hero` and
# `run/expr.hero` expressible at all. Those two need each other — a statement
# holds expressions, and an expression can call a function whose body is
# statements — and Heroes refuses a module cycle, so one of them has to take
# the other as a **function value**. A function type in this language cannot
# carry an `@` parameter (`error[expected_type]: expected a type, found `@``,
# measured), so the state crosses that boundary by value or not at
# all. Copy-on-write is what makes it cheap: handing a `Frame` down costs a
# refcount until something writes to it.
#
# WHAT A FRAME HOLDS, and why each field is here rather than in a global:
# there are no mutable globals in Heroes (spec § Top-level declarations), so
# *everything* a running program accumulates is in this record — the variables,
# the output written so far, the functions the script has defined, whether a
# `return` or an `exit` is in flight, and how deep the calls are nested.
use syn/tree
constant ERR_TYPE: str
"type"
# One value the script can hold. Four cases and no more: the little language
# has no arrays and no records, which keeps this program about the shape of an
# interpreter rather than about the size of a standard library.
variant Value
number
v: i64
text
s: str
truth
b: bool
# What a call with no `return` gives back, and what `print` of nothing
# would be. It is a value rather than an absence so that every path in the
# evaluator produces one.
nothing
# Everything one run accumulates.
record Frame
vars: {str: Value}
lines: [str]
# A script function's name to the index of its `define` statement. The
# body is in the tree; this is only the door.
fns: {str: i64}
returned: bool
result: Value
stopping: bool
code: i64
depth: i64
function fresh() -> Frame
vars: {str: Value} = {}
fns: {str: i64} = {}
no_lines: [str] = []
return Frame(
vars: vars,
lines: no_lines,
fns: fns,
returned: false,
result: .nothing,
stopping: false,
code: 0,
depth: 0
)
# What `run/expr.hero` hands to the function value that runs a call — the four
# things a call needs, packed so that the function TYPE stays one parameter
# wide and a reader can hold it in their head:
#
# (function(Request) -> Frame?)
#
# The alternative is a four-parameter function type written out at every
# signature that passes it on, which is the same information spelled three
# times.
record Request
nodes: tree.Tree
name: str
given: [Value]
frame: Frame
# How a value prints. This is what `print` writes and what an error message
# quotes, so it is one function and not two.
function shown(v: Value) -> str
return match v
.number n => n.v.to_str()
.text x => x.s
.truth t => t.b.to_str()
.nothing => "nothing"
# The name of a value's kind, for a message that has to say what it got.
function kind_of(v: Value) -> str
return match v
.number _ => "a number"
.text _ => "a string"
.truth _ => "a boolean"
.nothing => "nothing"
# A condition must BE a boolean — there is no truthiness, which is the rule
# Heroes itself keeps (spec § Control flow: *`if cond` takes only `bool`*), and
# the little language keeps it for the same reason: `if 0` reads differently to
# different people.
function condition_of(v: Value) -> bool?
return match v
.truth t => ok(t.b)
.number _ => not_a_condition(v)
.text _ => not_a_condition(v)
.nothing => not_a_condition(v)
function not_a_condition(v: Value) -> bool?
return fail(
code: ERR_TYPE,
msg: "a condition must be `true` or `false`, and this one is " + kind_of(v) + " (`" + shown(v) + "`)"
)
# The number in a value, or a refusal naming what stood there.
function number_of(v: Value) -> i64?
return match v
.number n => ok(n.v)
.text _ => wanted(what: "a number", v: v)
.truth _ => wanted(what: "a number", v: v)
.nothing => wanted(what: "a number", v: v)
function text_of(v: Value) -> str?
return match v
.text x => ok(x.s)
.number _ => wanted(what: "a string", v: v)
.truth _ => wanted(what: "a string", v: v)
.nothing => wanted(what: "a string", v: v)
# The one refusal `number_of` and `text_of` hand back, written ONCE: `A`
# appears only in the result, and each caller's declared result says what it
# is — `i64?` in `number_of`, `str?` in `text_of`. **This is the function that
# found a compiler defect**: written exactly like this, `heroes
# check` accepted it at exit 0 and `heroes build` died at exit 134 with a bare
# `assert failed: false`, because inference read the arguments only and left
# `A` as a silent error type all the way to the emitter. It was rewritten as two
# concrete functions for a few hours (defect 006), and it is one generic
# again because panel 105 ruled, the same evening, that a type parameter the
# arguments leave unbound takes the type the context asks for — the rule `fail`,
# `[]` and `nullptr` already followed. A call with no context, `x = wanted(...)`,
# is refused at the call.
function wanted<A>(what: str, v: Value) -> A?
return fail(code: ERR_TYPE, msg: "expected " + what + ", found " + kind_of(v) + " (`" + shown(v) + "`)")
test "every value prints, including the one that is nothing"
assert shown(.number(v: 42)) == "42"
assert shown(.number(v: 0 - 1)) == "-1"
assert shown(.text(s: "hi")) == "hi"
assert shown(.text(s: "")) == ""
assert shown(.truth(b: true)) == "true"
assert shown(.truth(b: false)) == "false"
assert shown(.nothing) == "nothing"
test "every kind has a name a message can use"
assert kind_of(.number(v: 1)) == "a number"
assert kind_of(.text(s: "x")) == "a string"
assert kind_of(.truth(b: true)) == "a boolean"
assert kind_of(.nothing) == "nothing"
test "a condition is a boolean and nothing else is one"
assert condition_of(.truth(b: true)).must()
assert !condition_of(.truth(b: false)).must()
assert condition_of(.number(v: 1)).is_err()
assert condition_of(.number(v: 0)).is_err()
assert condition_of(.text(s: "yes")).is_err()
assert condition_of(.nothing).is_err()
test "the refusal says what it wanted and what it found"
said = condition_of(.number(v: 0))
assert said.is_err()
assert message_of_bool(said) == "a condition must be `true` or `false`, and this one is a number (`0`)"
test "a number is taken out of a number and refused out of everything else"
assert number_of(.number(v: 7)).must() == 7
assert number_of(.text(s: "7")).is_err()
assert number_of(.truth(b: true)).is_err()
assert number_of(.nothing).is_err()
assert text_of(.text(s: "a")).must() == "a"
assert text_of(.number(v: 1)).is_err()
test "a fresh frame holds nothing and is not going anywhere"
f = fresh()
assert len(f.vars) == 0
assert len(f.fns) == 0
assert f.lines.len() == 0
assert !f.returned
assert !f.stopping
assert f.code == 0
assert f.depth == 0
assert f.result == .nothing
function message_of_bool(said: bool?) -> str
return match said
.ok _ => "nothing was refused"
.err e => e.msg
syn/cursor.hero · 172 righe
# A place in a token list, and the four questions a parser asks of one.
#
# Part of `examples/interpreter/` — the root `main.hero` carries the program's
# argument. `syn/expr.hero` and `syn/parse.hero` both hold one of these and
# nothing else in the program knows what a token index is.
#
# WHY THIS IS ITS OWN MODULE. Everything here is four lines long and the
# temptation is to inline it into the parser, which is how a parser ends up
# with `c.at @ c.at + 1` written twenty times and one of them wrong. The
# interesting part is `expected`: it is the only function in the program that
# turns *the parser wanted something else* into a sentence a reader can act on,
# so it is written once, and the sentence names both what was wanted and what
# stood there (design.md §4.17 is the compiler's own rule for this and it is
# the right rule for a script too).
#
# THE CURSOR NEVER RUNS OFF THE END, and that is a property of the SCANNER
# rather than a check here: `lex/scan.hero` always ends its output with exactly
# one `.finish`, asserted by its own last test, so `here` on the last index
# returns `.finish` forever and no parser loop can index past the array. A
# guard here would be a second answer to a question already answered, and the
# comment is what stops somebody adding one.
use lex/token
constant ERR_PARSE: str
"parse"
record Cursor
tokens: [token.Token]
at: i64
function over(tokens: [token.Token]) -> Cursor
return Cursor(tokens: tokens, at: 0)
# The token the cursor stands on. `.finish` once the list is spent, because the
# scanner guarantees a `.finish` and never more than one.
function here(c: Cursor) -> token.Token
if c.at >= c.tokens.len()
return .finish
return c.tokens[c.at]
# The token after this one, for the two places the grammar needs to look ahead:
# telling `name = value` from `name(args)`.
function after(c: Cursor) -> token.Token
if c.at + 1 >= c.tokens.len()
return .finish
return c.tokens[c.at + 1]
# Take the token the cursor stands on and step past it.
function bump(@c: Cursor) -> token.Token
taken = here(c)
if c.at < c.tokens.len()
c.at @ c.at + 1
return taken
# Step past the token if it is the one wanted, and say whether it was.
function accepted(@c: Cursor, want: token.Token) -> bool
if here(c) == want
_ = bump(@c)
return true
return false
# Step past the token wanted, or refuse naming both sides.
function expected(@c: Cursor, want: token.Token) -> ()?
if accepted(@c, want: want)
return ok()
return fail(
code: ERR_PARSE,
msg: "expected `" + token.shown(want) + "`, found `" + token.shown(here(c)) + "`"
)
# A name, or a refusal saying what stood there instead.
function name_here(@c: Cursor) -> str?
return match bump(@c)
.number n => fail(code: ERR_PARSE, msg: "expected a name, found the number `" + n.value.to_str() + "`")
.text x => fail(code: ERR_PARSE, msg: "expected a name, found the string `" + x.value + "`")
.name n => ok(n.value)
.kw_let | .kw_print | .kw_if | .kw_else | .kw_while => keyword_taken(t: .kw_let, said: "a keyword")
.kw_fn | .kw_return | .kw_exit | .kw_end => keyword_taken(t: .kw_fn, said: "a keyword")
.kw_true | .kw_false => keyword_taken(t: .kw_true, said: "a keyword")
.plus | .minus | .star | .slash | .percent => keyword_taken(t: .plus, said: "an operator")
.equals | .less | .less_equal | .greater | .greater_equal => keyword_taken(t: .equals, said: "an operator")
.equal_equal | .bang_equal | .bang | .ampersands | .bars => keyword_taken(t: .bang, said: "an operator")
.lparen | .rparen | .comma => keyword_taken(t: .lparen, said: "punctuation")
.newline => fail(code: ERR_PARSE, msg: "expected a name, found the end of the line")
.finish => fail(code: ERR_PARSE, msg: "expected a name, found the end of the file")
# The refusal `name_here`'s keyword and operator arms share. Grouping the arms
# with `|` costs the exact token — the arms would each have to name it — so the
# message says the CLASS, which is what a reader needs: *a keyword cannot be a
# name* is the whole diagnosis.
function keyword_taken(t: token.Token, said: str) -> str?
_ = t
return fail(code: ERR_PARSE, msg: "expected a name, found " + said)
# Step past however many line ends stand here. A block may hold blank lines and
# the parser should not have to know.
function skipped_lines(@c: Cursor)
while here(c) == .newline
_ = bump(@c)
# Whether the cursor is spent.
function done(c: Cursor) -> bool
return here(c) == .finish
test "a cursor walks the list and then stands on the end for ever"
c: Cursor @= over([.kw_print, .number(value: 1), .newline, .finish])
assert here(c) == .kw_print
assert after(c) == .number(value: 1)
assert bump(@c) == .kw_print
assert bump(@c) == .number(value: 1)
assert bump(@c) == .newline
assert here(c) == .finish
assert bump(@c) == .finish
assert bump(@c) == .finish
assert done(c)
test "the cursor on an empty list is done, and reading it is safe"
empty: [token.Token] = []
c: Cursor @= over(empty)
assert done(c)
assert here(c) == .finish
assert after(c) == .finish
assert bump(@c) == .finish
test "accepted steps only when it matches"
c: Cursor @= over([.kw_if, .newline, .finish])
assert !accepted(@c, want: .kw_while)
assert here(c) == .kw_if
assert accepted(@c, want: .kw_if)
assert here(c) == .newline
test "expected names both sides when it refuses"
c: Cursor @= over([.kw_if, .finish])
assert !expected(@c, want: .kw_if).is_err()
said = expected(@c, want: .kw_end)
assert said.is_err()
assert refusal_message(said) == "expected `end`, found `end of file`"
test "a name is taken, and everything that is not one is refused by class"
c: Cursor @= over([.name(value: "total"), .number(value: 3), .kw_let, .plus, .newline, .finish])
assert name_here(@c).must() == "total"
assert refusal_message_of_name(name_here(@c)) == "expected a name, found the number `3`"
assert refusal_message_of_name(name_here(@c)) == "expected a name, found a keyword"
assert refusal_message_of_name(name_here(@c)) == "expected a name, found an operator"
assert refusal_message_of_name(name_here(@c)) == "expected a name, found the end of the line"
assert refusal_message_of_name(name_here(@c)) == "expected a name, found the end of the file"
test "blank lines are stepped over together"
c: Cursor @= over([.newline, .newline, .newline, .kw_print, .finish])
skipped_lines(@c)
assert here(c) == .kw_print
# And on something that is not a line end it does nothing at all.
skipped_lines(@c)
assert here(c) == .kw_print
function refusal_message(said: ()?) -> str
return match said
.ok _ => "nothing was refused"
.err e => e.msg
function refusal_message_of_name(said: str?) -> str
return match said
.ok _ => "nothing was refused"
.err e => e.msg
syn/expr.hero · 216 righe
# Tokens into an expression tree, by climbing one precedence level per
# function.
#
# Part of `examples/interpreter/` — the root `main.hero` carries the program's
# argument. **This module's name collides with `run/expr.hero` on purpose**,
# and the collision is the honest one a front end has: this file is *how an
# expression is read*, that one is *what an expression means*, and they are two
# jobs with one noun. A module that needs both must rename one at its `use`
# line, which is `use syn/expr as parse_expr` in `main.hero` — the language
# refuses two bindings of one name in a file (spec § Files and layout) and
# `examples/pipeline/` is the corpus's other program that cannot be written
# without `as`.
#
# WHY ONE FUNCTION PER LEVEL, and not a table of precedences. Seven small
# functions that each call the next are longer than one loop over a table, and
# they are what a reader can check against the grammar line by line: `summed`
# calls `scaled` and never the other way, so `2 + 3 * 4` cannot come out
# wrong without one of these seven reading wrong. The table version is the same
# program with the shape hidden in data, and the shape is the thing worth
# reading. `examples/calculator/` is the corpus's other recursive-descent
# parser and it is written the same way, one level per function.
#
# THE LEVELS, weakest binding first — the same order as the spec's own table
# for Heroes, minus the bitwise row this little language does not have:
#
# || && comparison + - * / % unary - ! call and literal
use lex/token
use syn/cursor
use syn/tree
constant ERR_PARSE: str
"parse"
# One expression, however deep. The entry point every caller uses.
function parsed(@c: cursor.Cursor, @t: tree.Tree) -> i64?
return either(@c, @t)
function either(@c: cursor.Cursor, @t: tree.Tree) -> i64?
left: i64 @= both(@c, @t)?
while cursor.here(c) == .bars
_ = cursor.bump(@c)
right = both(@c, @t)?
left @ tree.added_expr(@t, .binary(op: .either, left: left, right: right))
return ok(left)
function both(@c: cursor.Cursor, @t: tree.Tree) -> i64?
left: i64 @= compared(@c, @t)?
while cursor.here(c) == .ampersands
_ = cursor.bump(@c)
right = compared(@c, @t)?
left @ tree.added_expr(@t, .binary(op: .both, left: left, right: right))
return ok(left)
# Comparisons do NOT chain: `a < b < c` is a parse error rather than a
# left-nested pair that compares a bool with a number. This is one place where
# the little language is deliberately stricter than C, and the reason is the
# reason Heroes gives for the same choice — a program whose meaning a reader
# guesses wrong is worse than one that does not compile.
function compared(@c: cursor.Cursor, @t: tree.Tree) -> i64?
left = summed(@c, @t)?
op = comparison_of(cursor.here(c))
if op.is_err()
return ok(left)
_ = cursor.bump(@c)
right = summed(@c, @t)?
made = tree.added_expr(@t, .binary(op: op.must(), left: left, right: right))
second = comparison_of(cursor.here(c))
if !second.is_err()
return fail(
code: ERR_PARSE,
msg: "`" + token.shown(cursor.here(c)) + "` cannot follow a comparison — write it as two comparisons joined by `&&`"
)
return ok(made)
function summed(@c: cursor.Cursor, @t: tree.Tree) -> i64?
left: i64 @= scaled(@c, @t)?
while cursor.here(c) == .plus || cursor.here(c) == .minus
op: tree.Op @= .add
if cursor.bump(@c) == .minus
op @ .subtract
right = scaled(@c, @t)?
left @ tree.added_expr(@t, .binary(op: op, left: left, right: right))
return ok(left)
function scaled(@c: cursor.Cursor, @t: tree.Tree) -> i64?
left: i64 @= unary(@c, @t)?
while is_scaling(cursor.here(c))
op: tree.Op @= .multiply
taken = cursor.bump(@c)
if taken == .slash
op @ .divide
if taken == .percent
op @ .remainder
right = unary(@c, @t)?
left @ tree.added_expr(@t, .binary(op: op, left: left, right: right))
return ok(left)
# `- -x` and `!!x` are both legal and both nest, which is why this function
# calls itself rather than calling `primary` once.
function unary(@c: cursor.Cursor, @t: tree.Tree) -> i64?
if cursor.here(c) == .minus
_ = cursor.bump(@c)
on = unary(@c, @t)?
return ok(tree.added_expr(@t, .negate(on: on)))
if cursor.here(c) == .bang
_ = cursor.bump(@c)
on = unary(@c, @t)?
return ok(tree.added_expr(@t, .invert(on: on)))
return primary(@c, @t)
# A literal, a name, a call, or a parenthesised expression — and this is where
# the recursion turns around: `(` sends the parser back to `parsed`, which is
# how a 500-deep nest of parentheses becomes 500 turns through all seven of
# these functions. `main.hero` has that case and it must PASS, which is what
# makes it a positive witness for the runtime's stack guard rather than a test
# of it.
function primary(@c: cursor.Cursor, @t: tree.Tree) -> i64?
return match cursor.bump(@c)
.number n => ok(tree.added_expr(@t, .number(value: n.value)))
.text x => ok(tree.added_expr(@t, .text(value: x.value)))
.kw_true => ok(tree.added_expr(@t, .truth(value: true)))
.kw_false => ok(tree.added_expr(@t, .truth(value: false)))
.name n => named(@c, @t, word: n.value)
.lparen => grouped(@c, @t)
.kw_let | .kw_print | .kw_if | .kw_else | .kw_while => not_a_value(said: "a keyword")
.kw_fn | .kw_return | .kw_exit | .kw_end => not_a_value(said: "a keyword")
.plus | .star | .slash | .percent => not_a_value(said: "an operator with nothing on its left")
.equals | .less | .less_equal | .greater | .greater_equal => not_a_value(
said: "an operator with nothing on its left"
)
.equal_equal | .bang_equal | .ampersands | .bars => not_a_value(said: "an operator with nothing on its left")
.minus | .bang => not_a_value(said: "a sign with nothing after it")
.rparen => not_a_value(said: "a `)` with no `(`")
.comma => not_a_value(said: "a `,` outside an argument list")
.newline => not_a_value(said: "the end of the line")
.finish => not_a_value(said: "the end of the file")
function not_a_value(said: str) -> i64?
return fail(code: ERR_PARSE, msg: "expected a value, found " + said)
# A name, and then the one place this grammar looks ahead: a `(` right after a
# name makes it a call, and anything else leaves it a plain name.
function named(@c: cursor.Cursor, @t: tree.Tree, word: str) -> i64?
if cursor.here(c) != .lparen
return ok(tree.added_expr(@t, .name(value: word)))
_ = cursor.bump(@c)
# `given` and not `args`: `args` is a built-in of the language, so the
# name is taken everywhere (`error[builtin_name_taken]`) even for a local.
given: [i64] @= []
if cursor.here(c) == .rparen
_ = cursor.bump(@c)
return ok(tree.added_expr(@t, .call(name: word, args: given)))
# One argument, then as many as there are commas. Written this way rather
# than as a `while true` holding the `return`, because the flow checker
# does not read `while true` as *never falls through*: the loop form is
# `error[missing_return]` unless a second `return` follows it, and an
# unreachable `return` is a line a reader has to explain.
given @ given.push(parsed(@c, @t)?)
while cursor.accepted(@c, want: .comma)
given @ given.push(parsed(@c, @t)?)
cursor.expected(@c, want: .rparen)?
return ok(tree.added_expr(@t, .call(name: word, args: given)))
function grouped(@c: cursor.Cursor, @t: tree.Tree) -> i64?
inside = parsed(@c, @t)?
cursor.expected(@c, want: .rparen)?
return ok(inside)
function is_scaling(t: token.Token) -> bool
return t == .star || t == .slash || t == .percent
# The operator a comparison token spells, or a refusal — used as a TEST rather
# than as a conversion, which is why it returns a fallible instead of an
# `Op?`-shaped pair with a bool.
function comparison_of(t: token.Token) -> tree.Op?
if t == .less
return ok(.less)
if t == .less_equal
return ok(.less_equal)
if t == .greater
return ok(.greater)
if t == .greater_equal
return ok(.greater_equal)
if t == .equal_equal
return ok(.equal)
if t == .bang_equal
return ok(.unequal)
return fail(code: "not_a_comparison", msg: "`" + token.shown(t) + "` is not a comparison")
syn/parse.hero · 211 righe
# Statements, blocks and a whole program — the layer above `syn/expr.hero`.
#
# Part of `examples/interpreter/` — the root `main.hero` carries the program's
# argument. This module owns the line-oriented shape of the little language:
# every statement begins with a keyword or is an expression, every statement
# ends at the end of its line, and every block ends at `end`.
#
# WHERE THE GENERICS CROSS A `use`. `util/each.hero` declares four templates
# and this file instantiates two of them at `str` — `joined` for the parameter
# list a duplicate-parameter refusal has to quote, and `every` for the check
# that a `fn`'s parameters are all names. `run/eval.hero` instantiates the same
# templates at `run/value.hero`'s `Value`. That is the shape the corpus was
# measured to be missing: three programs declared generics and
# **none instantiated one across a module boundary**, so monomorphisation had
# never been asked for two layouts from two callers that share nothing.
#
# AND WHERE THE LANGUAGE STOPS. The duplicate-parameter check below is a plain
# loop and not `each.counted`, because the predicate would have to know which
# name it is looking for — and Heroes has no closures (design.md Part 7 item 1,
# still unruled). A generic over a `(function(A) -> bool)` can therefore only
# ever be handed a TOP-LEVEL function, which is why every predicate this
# program passes is one: `is_a_name`, `itself`, `token.opens_a_value`. That is
# not a complaint, it is the count `M-closures-verdict` is owed, taken from a
# program rather than from a feeling.
use lex/scan
use lex/token
use syn/cursor
use syn/expr
use syn/tree
use util/each
constant ERR_PARSE: str
"parse"
# A whole program: scanned, then read statement by statement until the tokens
# run out.
function program(source: str) -> tree.Tree?
tokens = scan.scanned(source)?
c: cursor.Cursor @= cursor.over(tokens)
t: tree.Tree @= tree.empty()
top: [i64] @= []
cursor.skipped_lines(@c)
while !cursor.done(c)
top @ top.push(statement(@c, @t)?)
cursor.skipped_lines(@c)
t.top @ top
return ok(t)
# One statement. The keyword decides, and a line that begins with none of them
# is an expression — which is how a bare call like `greet("world")` stands as a
# statement.
function statement(@c: cursor.Cursor, @t: tree.Tree) -> i64?
if cursor.accepted(@c, want: .kw_let)
return let_statement(@c, @t)
if cursor.accepted(@c, want: .kw_print)
value = expr.parsed(@c, @t)?
line_end(@c)?
return ok(tree.added_stmt(@t, .say(value: value)))
if cursor.accepted(@c, want: .kw_if)
return if_statement(@c, @t)
if cursor.accepted(@c, want: .kw_while)
return while_statement(@c, @t)
if cursor.accepted(@c, want: .kw_fn)
return fn_statement(@c, @t)
if cursor.accepted(@c, want: .kw_return)
value = expr.parsed(@c, @t)?
line_end(@c)?
return ok(tree.added_stmt(@t, .give(value: value)))
if cursor.accepted(@c, want: .kw_exit)
code = expr.parsed(@c, @t)?
line_end(@c)?
return ok(tree.added_stmt(@t, .stop(code: code)))
if cursor.here(c) == .kw_end || cursor.here(c) == .kw_else
return fail(
code: ERR_PARSE,
msg: "`" + token.shown(cursor.here(c)) + "` closes a block that was never opened"
)
value = expr.parsed(@c, @t)?
line_end(@c)?
return ok(tree.added_stmt(@t, .evaluate(value: value)))
function let_statement(@c: cursor.Cursor, @t: tree.Tree) -> i64?
name = cursor.name_here(@c)?
cursor.expected(@c, want: .equals)?
value = expr.parsed(@c, @t)?
line_end(@c)?
return ok(tree.added_stmt(@t, .bind(name: name, value: value)))
function if_statement(@c: cursor.Cursor, @t: tree.Tree) -> i64?
condition = expr.parsed(@c, @t)?
line_end(@c)?
then = block(@c, @t)?
otherwise: [i64] @= []
if cursor.accepted(@c, want: .kw_else)
line_end(@c)?
otherwise @ block(@c, @t)?
cursor.expected(@c, want: .kw_end)?
line_end(@c)?
return ok(tree.added_stmt(@t, .when(condition: condition, then: then, otherwise: otherwise)))
function while_statement(@c: cursor.Cursor, @t: tree.Tree) -> i64?
condition = expr.parsed(@c, @t)?
line_end(@c)?
body = block(@c, @t)?
cursor.expected(@c, want: .kw_end)?
line_end(@c)?
return ok(tree.added_stmt(@t, .while_loop(condition: condition, body: body)))
function fn_statement(@c: cursor.Cursor, @t: tree.Tree) -> i64?
name = cursor.name_here(@c)?
cursor.expected(@c, want: .lparen)?
params = parameters(@c)?
line_end(@c)?
body = block(@c, @t)?
cursor.expected(@c, want: .kw_end)?
line_end(@c)?
return ok(tree.added_stmt(@t, .define(name: name, params: params, body: body)))
# A parameter list, refusing a repeated name — and quoting the whole list when
# it does, which is `each.joined` instantiated at `str` from this module.
function parameters(@c: cursor.Cursor) -> [str]?
names: [str] @= []
if cursor.accepted(@c, want: .rparen)
return ok(names)
names @ names.push(cursor.name_here(@c)?)
while cursor.accepted(@c, want: .comma)
names @ names.push(cursor.name_here(@c)?)
cursor.expected(@c, want: .rparen)?
if repeats(names)
return fail(
code: ERR_PARSE,
msg: "a parameter is named twice in `(" + each.joined(names, itself, ", ") + ")`"
)
if !each.every(names, is_a_name)
return fail(code: ERR_PARSE, msg: "a parameter may not be the empty name")
return ok(names)
# Statements until the block's end — `end` or `else`, neither of which this
# function consumes: the caller does, because only the caller knows which of
# the two it is willing to see.
function block(@c: cursor.Cursor, @t: tree.Tree) -> [i64]?
made: [i64] @= []
cursor.skipped_lines(@c)
while !closes_a_block(cursor.here(c))
made @ made.push(statement(@c, @t)?)
cursor.skipped_lines(@c)
if cursor.done(c)
return fail(code: ERR_PARSE, msg: "the file ends inside a block that no `end` closes")
return ok(made)
function closes_a_block(t: token.Token) -> bool
return t == .kw_end || t == .kw_else || t == .finish
# A statement ends at the end of its line, or at the end of the file. Anything
# else left on the line is a mistake worth naming, because the commonest one is
# a second statement written after the first.
function line_end(@c: cursor.Cursor) -> ()?
if cursor.here(c) == .finish
return ok()
if cursor.accepted(@c, want: .newline)
return ok()
return fail(
code: ERR_PARSE,
msg: "a statement ends at the end of its line, and `" + token.shown(cursor.here(c)) + "` follows this one"
)
# Whether any name in `names` appears twice. A plain loop and not a generic
# walk, for the reason the module header gives: the predicate would have to
# know which name it is hunting, and this language has no closures to carry it.
function repeats(names: [str]) -> bool
i: i64 @= 0
while i < names.len()
j: i64 @= i + 1
while j < names.len()
if names[i] == names[j]
return true
j @ j + 1
i @ i + 1
return false
function is_a_name(word: str) -> bool
return word.len() > 0
function itself(word: str) -> str
return word
syn/tree.hero · 291 righe
# The shapes a parsed program has, and the arena that holds them.
#
# Part of `examples/interpreter/` — the root `main.hero` carries the program's
# argument. This module declares the vocabulary and the storage; `syn/expr.hero`
# and `syn/parse.hero` fill it, `run/` walks it, and nobody outside builds a
# node without going through `added_expr`/`added_stmt`.
#
# WHY AN ARENA AND NOT A RECURSIVE VARIANT. Both work — `examples/binarytrees/`
# is a recursive `variant` and `examples/tree/` is an arena — and the choice
# here is the one a real front end makes, for a reason this program can
# demonstrate: a node is referred to from two places (a `while`'s test is read
# once per turn of the loop) and the parser builds children before parents, so
# an index is what the two want. It also makes the depth test cheap: a
# 500-deep parenthesised expression is 500 entries in one array rather than 500
# nested values that copy on every read.
#
# THE TWO ARENAS ARE SEPARATE on purpose. An expression index and a statement
# index are both `i64`, and putting them in one array would make them
# interchangeable to the type system while remaining different in fact. Two
# arrays cannot stop somebody passing one for the other either — that would
# want two distinct types, which this language does not have a cheap way to
# spell — but it does mean every `t.exprs[id]` and `t.stmts[id]` says which
# kind of index it just used, and a mistake is then visible in the line rather
# than only at run time.
# An operator with two sides. A variant rather than a `str`, so that the
# evaluator's `match` is exhaustive and a new operator cannot be added to the
# scanner without breaking every place that has to handle it.
variant Op
add
subtract
multiply
divide
remainder
less
less_equal
greater
greater_equal
equal
unequal
both
either
# One expression. Children are indices into `Tree.exprs`.
variant Expr
number
value: i64
text
value: str
truth
value: bool
name
value: str
negate
on: i64
invert
on: i64
binary
op: Op
left: i64
right: i64
call
name: str
args: [i64]
# One statement. `then`, `otherwise` and `body` are lists of statement indices;
# `value` and `condition` are expression indices.
#
# `condition` and not the obvious `test`, because `test` opens a test block in
# Heroes and a field may not take it: `error[expected_field]: expected a field
# name, found `test``, met on this file's first compile. A keyword list is a
# language's smallest surface and this is where a program meets it.
variant Stmt
bind
name: str
value: i64
say
value: i64
when
condition: i64
then: [i64]
otherwise: [i64]
while_loop
condition: i64
body: [i64]
give
value: i64
stop
code: i64
define
name: str
params: [str]
body: [i64]
evaluate
value: i64
# Every node of one program, and the top-level statements in order.
record Tree
exprs: [Expr]
stmts: [Stmt]
top: [i64]
function empty() -> Tree
no_exprs: [Expr] = []
no_stmts: [Stmt] = []
no_top: [i64] = []
return Tree(exprs: no_exprs, stmts: no_stmts, top: no_top)
# Put an expression in the arena and hand back its index. The `@` parameter is
# the language's answer to *this function changes the thing you passed it*, and
# the index is returned rather than read back off the record, because a caller
# that computes `len(t.exprs) - 1` itself is a caller that will one day compute
# it after another push.
function added_expr(@t: Tree, e: Expr) -> i64
t.exprs @ t.exprs.push(e)
return t.exprs.len() - 1
function added_stmt(@t: Tree, s: Stmt) -> i64
t.stmts @ t.stmts.push(s)
return t.stmts.len() - 1
# How deep the expression at `id` nests. The 500-deep case in `main.hero`'s
# tests reads this, and so does the guard in `run/expr.hero`.
function expr_depth(t: Tree, id: i64) -> i64
return match t.exprs[id]
.number _ | .text _ | .truth _ | .name _ => 1
.negate u => 1 + expr_depth(t, u.on)
.invert u => 1 + expr_depth(t, u.on)
.binary b => 1 + deeper(a: expr_depth(t, b.left), b: expr_depth(t, b.right))
.call c => 1 + deepest_of(t, c.args)
function deeper(a: i64, b: i64) -> i64
if a > b
return a
return b
function deepest_of(t: Tree, ids: [i64]) -> i64
most: i64 @= 0
for id in ids
most @ deeper(a: most, b: expr_depth(t, id))
return most
# The operator a token spells, for the two tokens per operator that the
# scanner produces. `syn/expr.hero` asks this; keeping it here means the
# mapping and the `Op` cases are in one file and a new case cannot be added
# without meeting its spelling.
function shown_op(op: Op) -> str
return match op
.add => "+"
.subtract => "-"
.multiply => "*"
.divide => "/"
.remainder => "%"
.less => "<"
.less_equal => "<="
.greater => ">"
.greater_equal => ">="
.equal => "=="
.unequal => "!="
.both => "&&"
.either => "||"
# An expression written back out as text, parenthesised at every step so that
# the rendering says what the TREE holds rather than what the source said.
# `main.hero`'s tests read this: it is how a parse is checked without asserting
# on indices.
function shown_expr(t: Tree, id: i64) -> str
return match t.exprs[id]
.number n => n.value.to_str()
.text x => "\"" + x.value + "\""
.truth b => b.value.to_str()
.name n => n.value
.negate u => "(-" + shown_expr(t, u.on) + ")"
.invert u => "(!" + shown_expr(t, u.on) + ")"
.binary b => "(" + shown_expr(t, b.left) + " " + shown_op(b.op) + " " + shown_expr(t, b.right) + ")"
.call c => c.name + "(" + shown_args(t, c.args) + ")"
function shown_args(t: Tree, ids: [i64]) -> str
pieces: [str] @= []
for id in ids
pieces @ pieces.push(shown_expr(t, id))
return join(pieces, ", ")
test "an arena hands back the index it just filled, and it is the last one"
t: Tree @= empty()
one = added_expr(@t, .number(value: 1))
two = added_expr(@t, .number(value: 2))
assert one == 0
assert two == 1
assert t.exprs.len() == 2
sum = added_expr(@t, .binary(op: .add, left: one, right: two))
assert sum == 2
assert shown_expr(t, sum) == "(1 + 2)"
test "the two arenas count separately"
t: Tree @= empty()
e = added_expr(@t, .number(value: 7))
s = added_stmt(@t, .say(value: e))
assert e == 0
assert s == 0
assert t.exprs.len() == 1
assert t.stmts.len() == 1
test "every operator has a spelling, and no two share one"
ops: [Op] = [
.add
.subtract
.multiply
.divide
.remainder
.less
.less_equal
.greater
.greater_equal
.equal
.unequal
.both
.either
]
seen: {str: i64} @= {}
for op in ops
written = shown_op(op)
assert written.len() > 0
assert seen[written].is_err()
seen[written] @ 1
assert len(seen) == 13
test "a rendered expression says what the tree holds, not what the source said"
# The rendering is fully parenthesised on purpose: `1 + 2 * 3` and
# `(1 + 2) * 3` are different trees and must be different text, or the
# parser's tests would pass on either.
t: Tree @= empty()
one = added_expr(@t, .number(value: 1))
two = added_expr(@t, .number(value: 2))
three = added_expr(@t, .number(value: 3))
product = added_expr(@t, .binary(op: .multiply, left: two, right: three))
left_first = added_expr(@t, .binary(op: .add, left: one, right: product))
assert shown_expr(t, left_first) == "(1 + (2 * 3))"
sum = added_expr(@t, .binary(op: .add, left: one, right: two))
right_first = added_expr(@t, .binary(op: .multiply, left: sum, right: three))
assert shown_expr(t, right_first) == "((1 + 2) * 3)"
test "a call renders its arguments in order, and no arguments is empty parentheses"
t: Tree @= empty()
a = added_expr(@t, .number(value: 1))
b = added_expr(@t, .name(value: "x"))
two_args = added_expr(@t, .call(name: "f", args: [a, b]))
assert shown_expr(t, two_args) == "f(1, x)"
none: [i64] = []
no_args = added_expr(@t, .call(name: "now", args: none))
assert shown_expr(t, no_args) == "now()"
test "depth counts the nesting and a leaf is one"
t: Tree @= empty()
leaf = added_expr(@t, .number(value: 1))
assert expr_depth(t, leaf) == 1
once = added_expr(@t, .negate(on: leaf))
assert expr_depth(t, once) == 2
twice = added_expr(@t, .invert(on: once))
assert expr_depth(t, twice) == 3
# A binary node takes the deeper of its two sides, not their sum.
wide = added_expr(@t, .binary(op: .add, left: leaf, right: twice))
assert expr_depth(t, wide) == 4
test "a thousand nodes deep is a number and not a crash"
# Built by hand here; `main.hero` builds the same shape through the PARSER,
# which is the case panel 104's stack guard is a positive witness for.
t: Tree @= empty()
at: i64 @= added_expr(@t, .number(value: 0))
i: i64 @= 0
while i < 999
at @ added_expr(@t, .negate(on: at))
i @ i + 1
assert t.exprs.len() == 1000
assert expr_depth(t, at) == 1000
test "the empty tree is empty in all three of its parts"
t = empty()
assert t.exprs.len() == 0
assert t.stmts.len() == 0
assert t.top.len() == 0
util/each.hero · 120 righe
# Four generic walks over an array, declared here and instantiated from two
# other directories.
#
# Part of `examples/interpreter/` — the root `main.hero` carries the program's
# argument. **This module exists to be used from elsewhere**, and that is its
# point rather than a side effect. Measured over `examples/`: generics were
# declared in three programs (`calculator` 4, `deck` 5,
# `gallery` 3) and **instantiated across a `use` by none of them**. A generic
# that only its own file calls has never been asked the question
# monomorphisation exists to answer — one template, two type arguments, two
# emitted functions, from two different modules.
#
# Here `syn/parse.hero` instantiates them at `token.Token` and `run/eval.hero`
# at `value.Value`, so every function below is emitted twice with different
# layouts, from callers that share nothing.
#
# WHAT IS NOT HERE, and why the list is four rather than seven: `map`, `filter`
# and `fold` are the spec's own (§ Built-ins, *written in Heroes*), so
# redeclaring them would measure the corpus's ability to shadow a built-in
# rather than its ability to carry a generic. These four are the ones the
# interpreter actually needs and the spec does not have.
# How many of `xs` satisfy `fits`.
function counted<A>(xs: [A], fits: (function(A) -> bool)) -> i64
total: i64 @= 0
for one in xs
if fits(one)
total @ total + 1
return total
# The first of `xs` that satisfies `fits`, or `fallback` if none does.
#
# `fallback` and not a `A?`: a generic cannot say `ok(x)` for a type parameter
# without the caller knowing what failure means here, and every caller in this
# program has a natural nothing — the `.finish` token, the unit value. Handing
# it in keeps this function total.
function first_of<A>(xs: [A], fits: (function(A) -> bool), fallback: A) -> A
for one in xs
if fits(one)
return one
return fallback
# `xs`, each rendered by `shown`, joined by `sep`. The one place this program
# turns a list of anything into a line of text.
function joined<A>(xs: [A], shown: (function(A) -> str), sep: str) -> str
pieces: [str] @= []
for one in xs
pieces @ pieces.push(shown(one))
return join(pieces, sep)
# Whether every one of `xs` satisfies `fits` — `all` in the spec's list is the
# same idea, and this one is here because it takes the SAME shape as the three
# above, which is what makes the four readable as a family.
function every<A>(xs: [A], fits: (function(A) -> bool)) -> bool
for one in xs
if !fits(one)
return false
return true
# Two predicates over `i64`, used only by this module's own tests — the
# instantiations that matter are in `syn/` and `run/`.
function is_even(n: i64) -> bool
return n % 2 == 0
function is_negative(n: i64) -> bool
return n < 0
function as_text(n: i64) -> str
return n.to_str()
test "counted counts, and counts nothing in nothing"
assert counted([1, 2, 3, 4, 5], is_even) == 2
assert counted([2, 4], is_even) == 2
assert counted([1, 3], is_even) == 0
empty: [i64] = []
assert counted(empty, is_even) == 0
test "first_of finds the first and not merely one"
assert first_of([1, 3, 4, 6], is_even, 0 - 1) == 4
assert first_of([2, 4, 6], is_even, 0 - 1) == 2
assert first_of([1, 3, 5], is_even, 0 - 1) == 0 - 1
empty: [i64] = []
assert first_of(empty, is_even, 99) == 99
test "joined renders and joins, and an empty list is an empty line"
assert joined([1, 2, 3], as_text, ", ") == "1, 2, 3"
assert joined([7], as_text, ", ") == "7"
empty: [i64] = []
assert joined(empty, as_text, ", ") == ""
assert joined([1, 2], as_text, "") == "12"
test "every is true of nothing, which is the answer that surprises people"
empty: [i64] = []
assert every(empty, is_even)
assert every([2, 4, 6], is_even)
assert !every([2, 3, 6], is_even)
assert !every([1], is_negative)
test "the same template at str, which is the second instantiation in this file"
# The two that matter cross a `use` and live in `syn/` and `run/`. This one
# is here so that the module's own tests also cover more than one type
# argument — a template tested at one type is a template whose
# substitution nobody has checked.
words = ["alpha", "be", "gamma"]
assert counted(words, is_short) == 1
assert first_of(words, is_short, "none") == "be"
assert joined(words, itself, "-") == "alpha-be-gamma"
assert !every(words, is_short)
function is_short(word: str) -> bool
return word.len() < 3
function itself(word: str) -> str
return word
Input
# Everything the little language has, in one script.
# Run by `examples/interpreter/main.hero`, whose `main.expected` is this
# script's output.
let greeting = "hello"
let subject = "world"
print greeting + ", " + subject
# Arithmetic, with the precedence the parser climbs.
print 2 + 3 * 4
print (2 + 3) * 4
print 17 / 5
print 17 % 5
print -7 / 2
print -7 % 2
# Comparisons and the two operators that short-circuit.
print 3 < 4
print 3 == 4
print true && false
print false || true
print !false
# A loop that counts, and a variable that is re-let.
let n = 1
let total = 0
while n <= 5
let total = total + n
let n = n + 1
end
print total
# A branch, both ways.
if total > 10
print "big"
else
print "small"
end
if total < 10
print "unreachable"
else
print "the else arm"
end
# Functions, including one that calls another and one that recurses.
fn double(x)
return x * 2
end
fn quadruple(x)
return double(double(x))
end
fn factorial(n)
if n <= 1
return 1
end
return n * factorial(n - 1)
end
print double(21)
print quadruple(3)
print factorial(10)
# A function with no return gives nothing back, and nothing prints as nothing.
fn shout(word)
print word + "!"
end
shout("listen")
print shout("again")
# The script ends itself, which is the interpreter calling the language's own
# `exit` — see `main.hero`.
exit 0
impronta sha256 83eb711d8fd2 · 14 file · index.json
Successivo: json. Tutti gli esempi. ← Precedente: calculator
Eseguine uno: i quattro comandi. Impara il linguaggio, oppure leggi la specifica completa in un solo file.