Goose

17_calc.goose

// An interactive calculator: reads a line, tokenizes it, parses it by
// recursive descent straight to a value, and prints the result. Variables
// are kept in a dictionary, so `x = 3 * (2 + y)` works. The tokens, the
// cursor and the line are locals of `evaluate`, and the tokenizer and the
// recursive-descent functions are nested inside it, using those as free
// variables instead of passing them around. Errors are the interesting
// part: a bad token, a missing parenthesis or a division by zero can
// happen deep inside the recursion, and each one is a single
// `return ... from evaluate`, which unwinds every parse frame in between
// and lands the message at the one place that reports it.
//
// Build and run (from this directory):
//   goose -o calc.c 17_calc.goose && cl calc.c && calc < data/calc.stdin
// or run it without redirection and type expressions at the prompt.

import std;
import dictionary;
import math;
import os;

enum Token {
    Num { v: f64 },
    Name { at: i32, len: i32 },          // a slice of the line, by position
    Op { c: u8 },                        // + - * / ( ) = ^
    End,
}

var vars = dictionary<u8[..16], f64> {};

// The message for a stray operator, built outside the parsing cycle (a
// function in a recursive cycle cannot own a builder) and constructed
// straight into the error result.
fn unexpected(c: u8) -> u8[>..] {
    var msg: u8[>..] = [];
    format(msg, "unexpected '");
    msg.push(c);                         // the byte itself, where str(c) would print its number
    msg.push('\'');
    return msg;
}

// One line: `name = expr` or `expr`. Every deep failure returns here.
fn evaluate(line: u8[:]) -> f64, u8[] {
    // Tokens are fixed-mode enum values in a plain array: 9 bytes each,
    // indexable, and the parser walks them with one cursor. The array is a
    // fresh local per line, gone when evaluate returns.
    var tokens: Token[>..] = [];
    var pos = 0;

    fn tokenize() {
        var i = 0;
        while i < line.len {
            let c = line[i];
            if c == ' ' || c == '\t' { i++; continue; }
            if (c >= '0' && c <= '9') || c == '.' {
                let start = i;
                while i < line.len && ((line[i] >= '0' && line[i] <= '9') || line[i] == '.') { i++; }
                let v, ok = parse_flt(line[start..i]);
                guard ok else { return 0.0, str("bad number '", line[start..i], "'") from evaluate; }
                tokens.push(Token.Num { v: v });
                continue;
            }
            if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
                let start = i;
                while i < line.len && ((line[i] >= 'a' && line[i] <= 'z') || (line[i] >= 'A' && line[i] <= 'Z')) { i++; }
                guard i - start <= 16 else { return 0.0, "name too long" from evaluate; }
                tokens.push(Token.Name { at: start as i32, len: (i - start) as i32 });
                continue;
            }
            guard find("+-*/()=^", line[i..i + 1]) >= 0 else {
                return 0.0, str("unexpected character '", line[i..i + 1], "'") from evaluate;
            }
            tokens.push(Token.Op { c: c });
            i++;
        }
        tokens.push(Token.End);
    }

    // The token under the cursor, and tests for an operator character.
    fn peek_op(c: u8) -> bool { match tokens[pos] { Op o => o.c == c, _ => false } }
    fn peek_op_at(i: i64, c: u8) -> bool { match tokens[i] { Op o => o.c == c, _ => false } }
    fn peek_end() -> bool { match tokens[pos] { End => true, _ => false } }

    fn name_key(t: Token.Name) -> u8[..16] {
        var k: u8[..16] = [];
        k.append(line[t.at..t.at + t.len]);
        return k;
    }

    // expr := term (('+' | '-') term)*
    recursive fn parse_expr() -> f64 {
        var v = parse_term();
        loop {
            if peek_op('+') { pos++; v += parse_term(); }
            else if peek_op('-') { pos++; v -= parse_term(); }
            else { break; }
        }
        return v;
    }

    // term := power (('*' | '/') power)*
    recursive fn parse_term() -> f64 {
        var v = parse_power();
        loop {
            if peek_op('*') { pos++; v *= parse_power(); }
            else if peek_op('/') {
                pos++;
                let d = parse_power();
                guard d != 0.0 else { return 0.0, "division by zero" from evaluate; }
                v /= d;
            }
            else { break; }
        }
        return v;
    }

    // power := unary ('^' power)?     (right associative)
    recursive fn parse_power() -> f64 {
        let base = parse_unary();
        if peek_op('^') { pos++; return pow(base, parse_power()); }
        return base;
    }

    // unary := '-' unary | '(' expr ')' | number | name
    recursive fn parse_unary() -> f64 {
        if peek_op('-') { pos++; return 0.0 - parse_unary(); }
        if peek_op('(') {
            pos++;
            let v = parse_expr();
            guard peek_op(')') else { return 0.0, "expected ')'" from evaluate; }
            pos++;
            return v;
        }
        let t = tokens[pos];
        pos++;
        match t {
            Num n => n.v,
            Name nm => {
                let v = vars.get(name_key(nm));
                guard v else { return 0.0, str("unknown variable '", line[nm.at..nm.at + nm.len], "'") from evaluate; }
                v
            },
            Op o => { return 0.0, unexpected(o.c) from evaluate; },
            End => { return 0.0, "unexpected end of input" from evaluate; },
        }
    }

    tokenize();
    // An assignment is a name followed by '='.
    if tokens.len > 2 {
        match tokens[0] {
            Name nm => if peek_op_at(1, '=') {
                pos = 2;
                let v = parse_expr();
                guard peek_end() else { return 0.0, "trailing input" from evaluate; }
                vars.insert(name_key(nm), v);
                return v, "";
            },
            _ => {},
        }
    }
    let v = parse_expr();
    guard peek_end() else { return 0.0, "trailing input" from evaluate; }
    return v, "";
}

fn main() {
    vars.insert("pi", PI);
    vars.insert("e", 2.718281828459045);
    print("calc: expressions, `name = expr`, `quit`");
    var line: u8[>..] = [];
    loop {
        write_stdout("> ");
        flush_stdout();
        line.clear();                         // nothing points into it here
        guard read_line(line) else { break; } // false at the end of input
        let t = trim(line);
        if t.len == 0 { continue; }
        if t == "quit" { break; }
        let v, err = evaluate(line);
        if err.len > 0 { print("error: ", err); continue; }
        var out: u8[>..] = [];
        format_flt(out, v, 6);
        // Trim trailing zeros and a dangling point, so 3 prints as 3.
        while out.len > 1 && out[out.len - 1] == '0' { out.pop(); }
        if out[out.len - 1] == '.' { out.pop(); }
        print(out);
    }
    print("bye");
}