Heroes / Esempi / rpn

examples/rpn/

Una calcolatrice in notazione polacca inversa, dove gli operatori seguono i numeri.

Il programma usa uno stack, una sequenza da cui si preleva per primo l’ultimo valore inserito. Prima di dividere controlla che il divisore sia diverso da zero, così può restituire un errore leggibile. Senza questo controllo, una divisione intera per zero interromperebbe il programma.

Eseguilo

$ heroes run examples/rpn/main.hero
3 4 + = 7
3 4 + 2 * = 14
5 1 2 + 4 * + 3 - = 14
-7 3 / = -2
7 -3 / = -2
true
true
true
true
true

exit 0 · 10 righe

Il programma

# Reverse Polish, evaluated on a stack — the shortest program in this corpus
# whose failures are all interesting.
#
# Corpus and regression material (CLAUDE.md §9). It closes no measured form
# gap; `examples/calculator/` already parses infix. What this adds is the
# opposite shape: no grammar, no precedence, no parentheses, and therefore
# nothing to get wrong except the STACK — which is where every RPN bug is.
# The three ways it breaks are all arity mistakes, and none of them is a
# syntax error, so a language that only checked syntax would accept all
# three.
#
# WHY IT IS WORTH A PROGRAM. `spec:155` makes integer division by zero an
# ABORT, not a failure — so an evaluator that reads `4 0 /` from user input
# must check before it divides, or the process ends. That is one line, it is
# easy to leave out, and leaving it out is invisible until somebody types a
# zero. The check is here and it has its own case.

variant Token
    number
        value: i64
    plus
    minus
    times
    divide

function tokens_of(text: str) -> [Token]?
    out: [Token] @= []

    for word in split_words(text)
        one = token_of(word)?
        out @ out.push(one)

    return ok(out)

function token_of(word: str) -> Token?
    if word == "+"
        return ok(.plus)

    if word == "-"
        return ok(.minus)

    if word == "*"
        return ok(.times)

    if word == "/"
        return ok(.divide)
    return ok(.number(value: number_of(word)?))

# A number, read by hand because the language ships no `str` to `i64`: the
# conversions in `spec:62-68` all go the other way. Nine lines, and the sign
# is the half a first draft forgets.
function number_of(word: str) -> i64?
    if word.len() == 0
        return fail(code: "empty", msg: "an empty word is not a number")
    negative: bool @= false
    total: i64 @= 0
    digits: i64 @= 0
    first: bool @= true

    for ch in word.chars()
        if first && ch == "-"
            negative @ true
            first @ false
            continue
        first @ false
        d = digit_of(ch)?
        total @ total * 10 + d
        digits @ digits + 1

    if digits == 0
        return fail(code: "not_a_number", msg: "`" + word + "` has no digits in it")

    return if negative
        ok(0 - total)
    else
        ok(total)

function digit_of(ch: str) -> i64?
    digits = "0123456789"
    i: i64 @= 0

    while i < 10
        if digits.slice(from: i, to: i + 1) == ch
            return ok(i)
        i @ i + 1

    return fail(code: "not_a_digit", msg: "`" + ch + "` is not a digit")

function split_words(text: str) -> [str]
    out: [str] @= []
    piece: str @= ""

    for ch in text.chars()
        if ch == " "
            if piece.len() > 0
                out @ out.push(piece)
            piece @ ""
        else
            piece @ piece + ch

    if piece.len() > 0
        out @ out.push(piece)

    return out

function evaluate(text: str) -> i64?
    stack: [i64] @= []

    for one in tokens_of(text)?
        match one
            .number n => stack @ stack.push(n.value)
            .plus | .minus | .times | .divide =>
                if stack.len() < 2
                    return fail(code: "too_few", msg: "an operator with fewer than two numbers under it")
                right = stack[stack.len() - 1]
                left = stack[stack.len() - 2]
                stack @ stack.slice(from: 0, to: stack.len() - 2)
                stack @ stack.push(applied(one, left: left, right: right)?)

    if stack.len() == 0
        return fail(code: "empty", msg: "nothing to evaluate")

    if stack.len() > 1
        return fail(code: "too_many", msg: "numbers left over: an operator is missing")

    return ok(stack[0])

function applied(op: Token, left: i64, right: i64) -> i64?
    return match op
        .plus  => ok(left + right)
        .minus => ok(left - right)
        .times => ok(left * right)
        .divide =>
            # `spec:155`: integer division by zero ABORTS. A program reading
            # its input from anywhere must check first, or a typed zero ends
            # the process rather than producing a diagnostic.
            if right == 0
                fail(code: "divide_by_zero", msg: "cannot divide by zero")
            else
                ok(left / right)
        .number _ => fail(code: "not_an_operator", msg: "a number is not an operator")

function main()
    for one in ["3 4 +", "3 4 + 2 *", "5 1 2 + 4 * + 3 -", "-7 3 /", "7 -3 /"]
        print(one + " = " + evaluate(one).must().to_str())

    # The three arity failures, none of which is a syntax error.
    print(evaluate("3 +").is_err())
    print(evaluate("3 4").is_err())
    print(evaluate("").is_err())

    # And the one the abort rule forces a check for.
    print(evaluate("4 0 /").is_err())
    print(evaluate("4 0 x").is_err())

test "the ordinary case, and the one everybody uses as an example"
    assert evaluate("3 4 +").must() == 7
    assert evaluate("3 4 + 2 *").must() == 14
    assert evaluate("5 1 2 + 4 * + 3 -").must() == 14
    assert evaluate("2").must() == 2

test "the four operators mean what they mean, and order is not commutative"
    assert evaluate("7 3 -").must() == 4
    assert evaluate("3 7 -").must() == 0 - 4
    assert evaluate("7 3 /").must() == 2
    assert evaluate("3 7 /").must() == 0
    assert evaluate("7 3 *").must() == 21

test "division truncates toward zero, on both signs"
    # `spec:156` says so, and an RPN evaluator is where a reader meets it.
    assert evaluate("-7 3 /").must() == 0 - 2
    assert evaluate("7 -3 /").must() == 0 - 2
    assert evaluate("-7 -3 /").must() == 2

test "dividing by zero is a failure and not an abort"
    # The whole reason `applied` has a branch: without it the process ends.
    assert evaluate("4 0 /").is_err()
    assert evaluate("0 0 /").is_err()
    assert !evaluate("4 2 /").is_err()

test "the three arity failures are three different codes"
    # Too few numbers under an operator.
    assert evaluate("3 +").is_err()
    assert evaluate("+").is_err()

    # Numbers left over, which means a missing operator.
    assert evaluate("3 4").is_err()
    assert evaluate("1 2 3 +").is_err()

    # Nothing at all.
    assert evaluate("").is_err()
    assert evaluate("   ").is_err()

test "a word that is neither a number nor an operator fails"
    assert evaluate("3 4 x").is_err()
    assert evaluate("hello").is_err()
    assert evaluate("3 4.5 +").is_err()
    assert evaluate("- +").is_err()

test "a negative number is a number and a minus is an operator"
    # The one ambiguity RPN has, and it is resolved by position: a `-` with
    # digits after it is a number, a `-` alone is an operator.
    assert evaluate("-3").must() == 0 - 3
    assert evaluate("-3 -4 +").must() == 0 - 7
    assert evaluate("3 -4 -").must() == 7
    assert number_of("-42").must() == 0 - 42
    assert number_of("42").must() == 42
    assert number_of("-").is_err()

test "reading a number by hand agrees with itself on the boundaries"
    assert number_of("0").must() == 0
    assert number_of("-0").must() == 0
    assert number_of("1000000").must() == 1000000
    assert number_of("").is_err()
    assert number_of("1a").is_err()
    assert number_of("a1").is_err()
examples/rpn/main.hero, 216 righe

impronta sha256 6a2cf3091574 · 2 file · index.json

Successivo: assembler. Tutti gli esempi. ← Precedente: template

Eseguine uno: i quattro comandi. Impara il linguaggio, oppure leggi la specifica completa in un solo file.