Errors
An error is a failure a correct program can meet anyway, because the world decided. A -> T! says a call may raise one; try passes it up and catch handles it here.
func parse_port(text: String) -> Int!:
let n = parse_int(text) else error(f"not a number: {text}")
if n < 1 or n > 65535:
error(f"port out of range: {n}")
return n
func main() -> !:
print(str(try parse_port("8080")))
print(str(parse_port("nope") catch -1))
print(str(parse_port("99999") catch -1))8080 -1 -1
try propagates#
try releases what this frame owns and leaves — exactly what return does, with one terminator changed. It needs a caller that said !.
func inner(n: Int) -> Int!:
if n == 0:
error("inner refuses zero")
return 100 / n
func middle(n: Int) -> Int!:
return try inner(n) + 1
func outer(n: Int) -> Int!:
return try middle(n) * 2
func main() -> !:
print(str(try outer(5)))
print(str(outer(0) catch -1))42 -1
Forgetting to say which you meant is a compile error. There is no spelling that ignores the outcome.
func risky() -> Int!:
error("no")
func main():
risky()luce: compile failed
main.luc:5:5: risky can fail: write 'try risky(…)' to pass the error on, or 'risky(…) catch …' to handle it [luce.sema.fallible]
risky()
^~~~~~~catch has two forms#
catch EXPR supplies a value. catch: opens a handler block, and it guards exactly one call — there is never a question about which statement failed.
import std.files
func main() -> !:
let text = files.read("absent.txt") catch "(default contents)"
print(text)
files.write("/nowhere/x", "data") catch:
print("the write did not land")
var greeting = "unset"
greeting = files.read("absent.txt") catch:
greeting = "(new file)"
print(greeting)(default contents) the write did not land (new file)
An uncaught error#
Out of main() -> ! it ends the run, and loom prints the words and the one place it was raised. One line, not a stack: a trap is a bug and the stack is its diagnosis, but an error is news and where it came from is the news.
func check(value: Int) -> Int!:
if value > 100:
error(f"{value} is too large")
return value
func main() -> !:
print(str(try check(50)))
print(str(try check(500)))50
loom: error: 500 is too large [user_error]
raised in check (main.luc:3:9)The worked example#
programs/calc.luc in the repository is a recursive-descent calculator. Every way it can be defeated is a way the user defeated it, so the parser says -> Step! and raises with error(...); try carries the failure up through four frames of recursion without a single if written for it.
# A recursive-descent expression calculator: structs as parser state,
# recursion, checked integer arithmetic — and the worked example for
# errors (docs/FAILURE.md).
#
# loom run calc.lc "2 + 3 * (10 - 4)" one expression, then exit
# loom run calc.lc a REPL: type, blank line to quit
#
# Every way this parser can be defeated is a way the *user* defeated
# it, not a way the program is wrong: a correct calculator given
# "2 + )" still has nothing to compute. So the parser says `-> Step!`
# and raises with `error(...)`; `try` carries the failure up through
# four frames of recursion without a single `if` written for it, and
# `main() -> !` hands what is left to loom, which prints the words and
# the line they were raised on.
#
# Compare what this file said before: `trap(...)`, four times, for
# conditions no caller could handle and every one of which ended the
# program with "trap:" in front of a message about the user's typing.
struct Step:
value: Int
at: Int
struct Scan:
func skip_spaces(text: String, at: Int) -> Int:
var here = at
while here < len(text) and text.byte_at(here) == 32:
here = here + 1
return here
func number(text: String, at: Int) -> Step!:
var stop = at
while stop < len(text) and text.byte_at(stop) >= 48 and text.byte_at(stop) <= 57:
stop = stop + 1
if stop == at:
error("expected a number at position " + str(at))
let digits = text[at:stop]
return Step(value = parse_int(digits) else error("not a number: " + digits), at = stop)
struct Parse:
# expression = term (("+" | "-") term)*
func expression(text: String, at: Int) -> Step!:
var left = try Parse.term(text, at)
var here = Scan.skip_spaces(text, left.at)
while here < len(text) and (text.byte_at(here) == 43 or text.byte_at(here) == 45):
let operator = text.byte_at(here)
let right = try Parse.term(text, here + 1)
if operator == 43:
left = Step(value = left.value + right.value, at = right.at)
else:
left = Step(value = left.value - right.value, at = right.at)
here = Scan.skip_spaces(text, left.at)
return left
# term = factor (("*" | "/" | "%") factor)*
func term(text: String, at: Int) -> Step!:
var left = try Parse.factor(text, at)
var here = Scan.skip_spaces(text, left.at)
while here < len(text) and (text.byte_at(here) == 42 or text.byte_at(here) == 47 or text.byte_at(here) == 37):
let operator = text.byte_at(here)
let right = try Parse.factor(text, here + 1)
if operator == 42:
left = Step(value = left.value * right.value, at = right.at)
elif operator == 47:
left = Step(value = left.value / right.value, at = right.at)
else:
left = Step(value = left.value % right.value, at = right.at)
here = Scan.skip_spaces(text, left.at)
return left
# factor = number | "(" expression ")" | "-" factor
func factor(text: String, at: Int) -> Step!:
let here = Scan.skip_spaces(text, at)
if here < len(text) and text.byte_at(here) == 45:
let inner = try Parse.factor(text, here + 1)
return Step(value = 0 - inner.value, at = inner.at)
if here < len(text) and text.byte_at(here) == 40:
let inner = try Parse.expression(text, here + 1)
let close = Scan.skip_spaces(text, inner.at)
if close >= len(text) or text.byte_at(close) != 41:
error("expected ) at position " + str(close))
return Step(value = inner.value, at = close + 1)
return try Scan.number(text, here)
# Evaluate one line and print what it came to. Fallible, so every
# `error(...)` four frames down arrives here whole.
func evaluate(text: String) -> !:
let result = try Parse.expression(text, 0)
let rest = Scan.skip_spaces(text, result.at)
if rest != len(text):
error("unexpected character at position " + str(rest))
print(text + " = " + str(result.value))
# The interactive loop. `read_line` answers `String?`, so end of input
# — a pipe running dry, Ctrl-D at a terminal — is `none` and ends the
# session; an empty line ends it too, because a person pressing return
# at a prompt has said "nothing more".
#
# A bad expression must not end the loop: `evaluate(...) catch:` says
# so on stderr and asks again, which is the whole difference between a
# calculator and a program that exits when you mistype. What it
# cannot yet do is repeat the reason — `catch` has no binding form, so
# the words the parser raised are discarded here and only the one-shot
# path below ever prints them.
func repl():
print("calc — an expression a line, blank line to quit")
var running = true
while running:
let typed = read_line("calc> ")
if typed == none or typed == "":
running = false
else:
evaluate(typed) catch:
print_error("cannot compute: " + typed)
func main() -> !:
if arg_count() == 0:
repl()
return
try evaluate(arg(0))2 + 3 * (10 - 4) = 20
Give it something it cannot parse and the same program says so, at the position it gave up:
# A recursive-descent expression calculator: structs as parser state,
# recursion, checked integer arithmetic — and the worked example for
# errors (docs/FAILURE.md).
#
# loom run calc.lc "2 + 3 * (10 - 4)" one expression, then exit
# loom run calc.lc a REPL: type, blank line to quit
#
# Every way this parser can be defeated is a way the *user* defeated
# it, not a way the program is wrong: a correct calculator given
# "2 + )" still has nothing to compute. So the parser says `-> Step!`
# and raises with `error(...)`; `try` carries the failure up through
# four frames of recursion without a single `if` written for it, and
# `main() -> !` hands what is left to loom, which prints the words and
# the line they were raised on.
#
# Compare what this file said before: `trap(...)`, four times, for
# conditions no caller could handle and every one of which ended the
# program with "trap:" in front of a message about the user's typing.
struct Step:
value: Int
at: Int
struct Scan:
func skip_spaces(text: String, at: Int) -> Int:
var here = at
while here < len(text) and text.byte_at(here) == 32:
here = here + 1
return here
func number(text: String, at: Int) -> Step!:
var stop = at
while stop < len(text) and text.byte_at(stop) >= 48 and text.byte_at(stop) <= 57:
stop = stop + 1
if stop == at:
error("expected a number at position " + str(at))
let digits = text[at:stop]
return Step(value = parse_int(digits) else error("not a number: " + digits), at = stop)
struct Parse:
# expression = term (("+" | "-") term)*
func expression(text: String, at: Int) -> Step!:
var left = try Parse.term(text, at)
var here = Scan.skip_spaces(text, left.at)
while here < len(text) and (text.byte_at(here) == 43 or text.byte_at(here) == 45):
let operator = text.byte_at(here)
let right = try Parse.term(text, here + 1)
if operator == 43:
left = Step(value = left.value + right.value, at = right.at)
else:
left = Step(value = left.value - right.value, at = right.at)
here = Scan.skip_spaces(text, left.at)
return left
# term = factor (("*" | "/" | "%") factor)*
func term(text: String, at: Int) -> Step!:
var left = try Parse.factor(text, at)
var here = Scan.skip_spaces(text, left.at)
while here < len(text) and (text.byte_at(here) == 42 or text.byte_at(here) == 47 or text.byte_at(here) == 37):
let operator = text.byte_at(here)
let right = try Parse.factor(text, here + 1)
if operator == 42:
left = Step(value = left.value * right.value, at = right.at)
elif operator == 47:
left = Step(value = left.value / right.value, at = right.at)
else:
left = Step(value = left.value % right.value, at = right.at)
here = Scan.skip_spaces(text, left.at)
return left
# factor = number | "(" expression ")" | "-" factor
func factor(text: String, at: Int) -> Step!:
let here = Scan.skip_spaces(text, at)
if here < len(text) and text.byte_at(here) == 45:
let inner = try Parse.factor(text, here + 1)
return Step(value = 0 - inner.value, at = inner.at)
if here < len(text) and text.byte_at(here) == 40:
let inner = try Parse.expression(text, here + 1)
let close = Scan.skip_spaces(text, inner.at)
if close >= len(text) or text.byte_at(close) != 41:
error("expected ) at position " + str(close))
return Step(value = inner.value, at = close + 1)
return try Scan.number(text, here)
# Evaluate one line and print what it came to. Fallible, so every
# `error(...)` four frames down arrives here whole.
func evaluate(text: String) -> !:
let result = try Parse.expression(text, 0)
let rest = Scan.skip_spaces(text, result.at)
if rest != len(text):
error("unexpected character at position " + str(rest))
print(text + " = " + str(result.value))
# The interactive loop. `read_line` answers `String?`, so end of input
# — a pipe running dry, Ctrl-D at a terminal — is `none` and ends the
# session; an empty line ends it too, because a person pressing return
# at a prompt has said "nothing more".
#
# A bad expression must not end the loop: `evaluate(...) catch:` says
# so on stderr and asks again, which is the whole difference between a
# calculator and a program that exits when you mistype. What it
# cannot yet do is repeat the reason — `catch` has no binding form, so
# the words the parser raised are discarded here and only the one-shot
# path below ever prints them.
func repl():
print("calc — an expression a line, blank line to quit")
var running = true
while running:
let typed = read_line("calc> ")
if typed == none or typed == "":
running = false
else:
evaluate(typed) catch:
print_error("cannot compute: " + typed)
func main() -> !:
if arg_count() == 0:
repl()
return
try evaluate(arg(0))loom: error: expected a number at position 4 [user_error]
raised in Scan.number (calc.luc:36:13)