/*
 * expr.cpp: Expression parser.
 */

/*
 * To make it really easy to add new operators at strange levels of
 * precedence in future, I think I'll implement an operator-
 * precedence parser on this occasion. This is a sort of informal
 * combination of a shift-reduce and recursive approach.
 *
 * The central function expects to read a stream of 'operator'
 * tokens and 'atom' nonterminals from the input. An operator token
 * comes straight from the lexer, and comes with tags saying what
 * the precedence and associativity of that operator is when it's
 * binary, and what its precedence is when unary. An atom token is
 * either a literal or identifier straight from the lexer, or a
 * function call, or something in parentheses; in the latter two
 * cases, we recurse into subfunctions to do the hard work, and when
 * we come back we've got something we can treat as atomic for the
 * purposes of this particular interleaving of operators and atoms.
 *
 * Within the central function, we pile up our operator and atom
 * nonterminals on a stack. Before shifting any given operator, we
 * may choose to perform one or more 'reduce' operations which
 * convert several of these symbols into one: a unary reduce
 * converts an operator and an atom into an atom, and a binary
 * reduce turns A,O,A into A. When we see end-of-string (or closing
 * parenthesis or function-call-argument-terminating comma, if we
 * have ourselves been called recursively), we perform reduces until
 * we have only one symbol left, and return that to our caller.
 *
 * So the question is, when do we shift and when do we reduce?
 *
 * We can trivially identify unary operators as soon as we see them:
 * they're precisely the operators not preceded by an atom (either
 * following another operator, or at the start of the string). So we
 * always know whether a binary or a unary reduce is an available
 * option.
 *
 * We do a reduce if the operator we're about to shift has lower
 * precedence than the one that would be involved in the reduce. (If
 * we have "1+2" and see *, we don't reduce the 1+2 because the
 * 2*something will take precedence; but if we have "1*2" and see +,
 * then we know that's equivalent to 3 plus whatever it is.) Ties
 * are broken by associativity: if the two operators have the same
 * precedence, we reduce if that precedence level is
 * right-associative and otherwise we shift. This applies to both
 * binary and unary reduces.
 *
 * (I'm assuming here, incidentally, that all operators at the same
 * precedence level have the same associativity and the same arity.
 * This is easily enough enforced at setup time by defining the
 * precedence levels to store associativity and arity in their
 * bottom two bits.)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include <limits.h>

#include <string>
#include <map>
#include <memory>
#include <vector>
using std::string;
using std::map;
using std::vector;
using std::shared_ptr;
using std::make_shared;

#include "spigot.h"
#include "funcs.h"
#include "expr.h"
#include "error.h"
#include "io.h"

#define TOKENS(X) \
    X(ERROR) \
    X(EOS) \
    X(LPAR) \
    X(RPAR) \
    X(LBRACE) \
    X(RBRACE) \
    X(COMMA) \
    X(SEMICOLON) \
    X(OPERATOR) \
    X(IDENTIFIER) \
    X(LET) \
    X(IN) \
    X(EQUALS) \
    X(NUMBER) \
    X(SPIGOT)

#define TOKEN_ENUM_DEF(x) TOK_##x,
enum tokentype { TOKENS(TOKEN_ENUM_DEF) TOK_MAX };
#define TOKEN_NAME_DEF(x) #x,
static const char *toknames[] = { TOKENS(TOKEN_NAME_DEF) };

inline bool isidstart(char c)
{
    return c && (isalpha((unsigned char)c) || c == '_');
}
inline bool isidchar(char c)
{
    return c && (isalnum((unsigned char)c) || c == '_');
}

class ExprNodeLookup {
    vector<const ExprNodeType *> queue;
    bool initialised { false };

    map<string, const ExprNodeType *> all_names;
    vector<const ExprOperator *> vec_ops_punct;
    vector<const ExprConstOrFunction *> vec_fns_normal;
    vector<const ExprConstOrFunction *> vec_fns_debug;
    map<string, const ExprOperator *> map_ops_id;
    map<string, const ExprOperator *> map_ops_other;

    static ExprNodeLookup the_instance;
    static ExprNodeLookup &setup_instance();
    static const ExprNodeLookup &runtime_instance();
    void initialise();

  public:
    static void register_node(const ExprNodeType *);
    static const vector<const ExprConstOrFunction *> &fns_normal();
    static const vector<const ExprConstOrFunction *> &fns_debug();
    static const vector<const ExprOperator *> &ops_punct();
    static const ExprOperator *get_op_id(const string &key);
    static const ExprOperator *get_op_homograph(const ExprOperator *op);
};

ExprNodeLookup &ExprNodeLookup::setup_instance()
{
    static ExprNodeLookup instance;
    return instance;
}

const ExprNodeLookup &ExprNodeLookup::runtime_instance()
{
    auto &instance = setup_instance();
    if (!instance.initialised)
        instance.initialise();
    return instance;
}

const vector<const ExprConstOrFunction *> &ExprNodeLookup::fns_normal()
{
    return runtime_instance().vec_fns_normal;
}

const vector<const ExprConstOrFunction *> &ExprNodeLookup::fns_debug()
{
    return runtime_instance().vec_fns_debug;
}

const vector<const ExprOperator *> &ExprNodeLookup::ops_punct()
{
    return runtime_instance().vec_ops_punct;
}

const ExprOperator *ExprNodeLookup::get_op_id(const string &key)
{
    auto &m = runtime_instance().map_ops_id;
    auto it = m.find(key);
    if (it == m.end())
        return nullptr;
    return it->second;
}

const ExprOperator *ExprNodeLookup::get_op_homograph(
    const ExprOperator *op)
{
    auto &m = runtime_instance().map_ops_other;
    auto it = m.find(op->name());
    if (it == m.end())
        return nullptr;
    return it->second;
}

void ExprNodeLookup::initialise()
{
    for (auto *node: queue) {
        bool expect_new_name = true;

        if (auto cf = dynamic_cast<const ExprConstOrFunction *>(node)) {
            auto &v = node->debug_only ? vec_fns_debug : vec_fns_normal;
            v.push_back(cf);
        }

        if (auto op = dynamic_cast<const ExprOperator *>(node)) {
            assert(!node->debug_only);

            auto name = op->name();
            auto it = all_names.find(name);
            if (it != all_names.end()) {
                const auto *prev = it->second;

                if (((dynamic_cast<const ExprUnaryPrefixOperator *>(node)) &&
                     (dynamic_cast<const ExprBinaryOperator *>(prev))) ||
                    ((dynamic_cast<const ExprUnaryPrefixOperator *>(prev)) &&
                     (dynamic_cast<const ExprBinaryOperator *>(node)))) {
                    map_ops_other[name] = op;
                    expect_new_name = false;
                }
            } else {
                if (isidstart(name[0]))
                    map_ops_id[name] = op;
                else
                    vec_ops_punct.push_back(op);
            }
        }

        if (expect_new_name) {
            assert(all_names.find(node->name()) == all_names.end());
            all_names[node->name()] = node;
        }
    }

    initialised = true;
}

void ExprNodeLookup::register_node(const ExprNodeType *node)
{
    setup_instance().queue.push_back(node);
}

ExprNodeType::ExprNodeType(const string &aname)
    : name_str{aname}
{
    ExprNodeLookup::register_node(this);
}

const string &ExprNodeType::name() const
{
    return name_str;
}

const char *ExprNodeType::c_name() const
{
    return name_str.c_str();
}

Spigot ExprNodeTypeNullary::evaluatev(argvector<Spigot> &args) const
{
    assert(args.size() == 0);
    return evaluate();
}

Spigot ExprNodeTypeUnary::evaluatev(argvector<Spigot> &args) const
{
    assert(args.size() == 1);
    return evaluate(move(args[0]));
}

Spigot ExprNodeTypeBinary::evaluatev(argvector<Spigot> &args) const
{
    assert(args.size() == 2);
    return evaluate(move(args[0]), move(args[1]));
}

ExprOperator::ExprOperator(const string &aname, Prec aprec)
    : ExprNodeType{aname}, precedence{aprec}
{
}

Prec ExprOperator::prec() const
{
    return precedence;
}

int ExprConstant::arity() const
{
    return 0;
}

int ExprUnaryFunction::arity() const
{
    return 1;
}

int ExprBinaryFunction::arity() const
{
    return 2;
}

int ExprVariadicFunction::arity() const
{
    return -1;
}

class UnaryPrefixOpFnWrapper : public ExprUnaryPrefixOperator {
    using Fn = Spigot (*)(Spigot x);
    Fn fn;

    virtual Spigot evaluate(Spigot x) const override;
  public:
    UnaryPrefixOpFnWrapper(const string &aname, Prec aprec, Fn afn);
};

UnaryPrefixOpFnWrapper::UnaryPrefixOpFnWrapper(
    const string &aname, Prec aprec, Fn afn)
    : ExprUnaryPrefixOperator(aname, aprec), fn{afn}
{
}

Spigot UnaryPrefixOpFnWrapper::evaluate(Spigot x) const
{
    return fn(move(x));
}

class UnarySuffixOpFnWrapper : public ExprUnarySuffixOperator {
    using Fn = Spigot (*)(Spigot x);
    Fn fn;

    virtual Spigot evaluate(Spigot x) const override;
  public:
    UnarySuffixOpFnWrapper(const string &aname, Prec aprec, Fn afn);
};

UnarySuffixOpFnWrapper::UnarySuffixOpFnWrapper(
    const string &aname, Prec aprec, Fn afn)
    : ExprUnarySuffixOperator(aname, aprec), fn{afn}
{
}

Spigot UnarySuffixOpFnWrapper::evaluate(Spigot x) const
{
    return fn(move(x));
}

class BinaryOpFnWrapper : public ExprBinaryOperator {
    using Fn = Spigot (*)(Spigot x, Spigot y);
    Fn fn;
    bool right_assoc;

    virtual Spigot evaluate(Spigot x, Spigot y) const override;
    virtual bool right_associative() const override;
  public:
    BinaryOpFnWrapper(const string &aname, Prec aprec, Fn afn,
                      bool aright = false);
};

BinaryOpFnWrapper::BinaryOpFnWrapper(const string &aname, Prec aprec, Fn afn,
                                     bool aright)
    : ExprBinaryOperator(aname, aprec), fn{afn}, right_assoc{aright}
{
}

bool BinaryOpFnWrapper::right_associative() const
{
    return right_assoc;
}

Spigot BinaryOpFnWrapper::evaluate(Spigot x, Spigot y) const
{
    return fn(move(x), move(y));
}


ConstantFnWrapper::ConstantFnWrapper(const string &aname, Fn afn)
    : ExprConstant(aname), fn{afn}
{
}

Spigot ConstantFnWrapper::evaluate() const
{
    return fn();
}

UnaryFnWrapper::UnaryFnWrapper(const string &aname, Fn afn)
    : ExprUnaryFunction(aname), fn{afn}
{
}

Spigot UnaryFnWrapper::evaluate(Spigot x) const
{
    return fn(move(x));
}

BinaryFnWrapper::BinaryFnWrapper(const string &aname, Fn afn)
    : ExprBinaryFunction(aname), fn{afn}
{
}

Spigot BinaryFnWrapper::evaluate(Spigot x, Spigot y) const
{
    return fn(move(x), move(y));
}

VariadicFnWrapper::VariadicFnWrapper(const string &aname, Fn afn)
    : ExprVariadicFunction(aname), fn{afn}
{
}

Spigot VariadicFnWrapper::evaluatev(argvector<Spigot> &args) const
{
    return fn(args);
}

ExprNodeTypeDebug::ExprNodeTypeDebug()
{
    debug_only = true;
}

bool enable_debug_fns = false;

void FnCallSyntax::enforce_trivial(const string &name) const
{
    if (!semicolons.empty())
        throw expr_error("unexpected semicolon in argument list for '",
                         name, "'");
}

enum class Prec {
    Additive,
    Multiplicative,
    SignPrefix,
    Power,
    Factorial,
};

static const BinaryOpFnWrapper
add_op{"+", Prec::Additive, spigot_add},
sub_op{"-", Prec::Additive, spigot_sub};

static const BinaryOpFnWrapper
mul_op{"*", Prec::Multiplicative, spigot_mul},
div_op{"/", Prec::Multiplicative, spigot_div},
mod_op_punct{"%", Prec::Multiplicative, spigot_mod},
mod_op_named{"mod", Prec::Multiplicative, spigot_mod},
rem_op{"rem", Prec::Multiplicative, spigot_rem};

static const UnaryPrefixOpFnWrapper
neg_op{"-", Prec::SignPrefix, spigot_neg},
unaryplus_op{"+", Prec::SignPrefix, spigot_identity};

static const BinaryOpFnWrapper
pow_op_caret{"^", Prec::Power, spigot_pow, true},
pow_op_doublestar{"**", Prec::Power, spigot_pow, true};

static const UnarySuffixOpFnWrapper
factorial_op{"!", Prec::Factorial, spigot_factorial};

vector<ExprFunction> expr_functions()
{
    vector<ExprFunction> toret;

    for (const ExprConstOrFunction *fn: ExprNodeLookup::fns_normal())
        toret.push_back(ExprFunction{fn->name(), fn->arity()});

    return toret;
}

class token {
  public:
    tokentype type;
    string text;
    const ExprOperator *op;
    bigint n, d;
    Spigot spigot;

    token();
    token(tokentype atype);
    token(tokentype atype, const char *str, int len = -1);
    token(tokentype atype, const ExprOperator *aop);
    token(tokentype atype, bigint an, bigint ad);
    token(tokentype atype, Spigot spig);
    token(const token &x);
    token &operator=(const token &x);
    void debug(void);
};

inline token::token()
    : type(TOK_ERROR), op(nullptr)
{
}

inline token::token(tokentype atype)
    : type(atype), op(nullptr)
{
}

inline token::token(tokentype atype, const char *str, int len)
    : type(atype), op(nullptr)
{
    if (len < 0)
        len = strlen(str);
    text = string(str, len);
}
inline token::token(tokentype atype, const ExprOperator *aop)
    : type(atype), op(aop)
{
}

inline token::token(tokentype atype, bigint an, bigint ad)
    : type(atype), op(nullptr), n(an), d(ad)
{
}

inline token::token(tokentype atype, Spigot spig)
    : type(atype), op(nullptr), spigot(move(spig))
{
}

inline token::token(const token &x)
    : type(x.type), text(x.text), op(x.op), n(x.n), d(x.d),
      spigot(x.spigot ? x.spigot->clone() : nullptr)
{
}

inline token &token::operator=(const token &x)
{
    type = x.type;
    op = x.op;
    text = x.text;
    n = x.n;
    d = x.d;
    spigot = x.spigot ? x.spigot->clone() : nullptr;
    return *this;
}

void token::debug(void)
{
    printf("%s", toknames[type]);
    if (type == TOK_OPERATOR) {
        printf(" op '%s'", op->c_name());
    } else if (type == TOK_IDENTIFIER) {
        printf(" '%s'", text.c_str());
    } else if (type == TOK_NUMBER) {
        putchar(' ');
        bigint_print(stdout, n);
        putchar('/');
        bigint_print(stdout, d);
    } else if (type == TOK_SPIGOT) {
        printf(" %p", spigot.get());
    }
    putchar('\n');
}

inline bool strczmatch(int lenc, const char *strc, const char *strz)
{
    int lenz = strlen(strz);
    return lenc == lenz && !memcmp(strc, strz, lenc);
}

class Lexer {
    FileAccessContext &fac;

    static inline int frombase(char c);
    static string parse_filename(const char *q, int *lenused);

  public:
    token currtok;
    const char *p;

    Lexer(FileAccessContext &afac, const char *string);
    void advance();
};

Lexer::Lexer(FileAccessContext &afac, const char *string)
    : fac(afac), currtok(), p(string)
{
    advance();
}

inline int Lexer::frombase(char c)
{
    // This translates an alphanumeric out of any base up to 36.
    // Return value is from 0 to 35 for success, or 36 for
    // failure, so you can easily vet the answer as being within a
    // smaller base.
    return (c >= '0' && c <= '9' ? c - '0' :
            c >= 'A' && c <= 'Z' ? c - ('A'-10) :
            c >= 'a' && c <= 'z' ? c - ('a'-10) : 36);
}

string Lexer::parse_filename(const char *q, int *lenused)
{
    string ret;
    const char *qorig = q;

    if (*q == '"' || *q == '\'') {
        char quote = *q++;
        while (*q != quote || q[1] == quote) {
            if (!*q) {
                throw expr_error("unexpected end of expression in quoted"
                                 " filename string");
            }
            if (*q == quote)
                q++;
            ret.push_back(*q);
            q++;
        }
        q++;                       /* eat closing quote */
    } else {
        while (*q && !isspace((unsigned char)*q)) {
            ret.push_back(*q);
            q++;
        }
    }

    if (lenused) *lenused = q - qorig;
    return ret;
}

void Lexer::advance()
{
    while (*p && isspace((unsigned char)*p)) p++;

    if (!*p) {
        currtok = token(TOK_EOS);
        return;
    }
    if (*p == '(') {
        currtok = token(TOK_LPAR);
        p++;
        return;
    }
    if (*p == ')') {
        currtok = token(TOK_RPAR);
        p++;
        return;
    }
    if (*p == '{') {
        currtok = token(TOK_LBRACE);
        p++;
        return;
    }
    if (*p == '}') {
        currtok = token(TOK_RBRACE);
        p++;
        return;
    }
    if (*p == ',') {
        currtok = token(TOK_COMMA);
        p++;
        return;
    }
    if (*p == ';') {
        currtok = token(TOK_SEMICOLON);
        p++;
        return;
    }
    if (*p == '=') {
        currtok = token(TOK_EQUALS);
        p++;
        return;
    }

    /*
     * Match non-identifier-shaped operators by maximal munch.
     */
    size_t opmaxlen = 0;
    const ExprOperator *op = nullptr;
    for (const ExprOperator *iop: ExprNodeLookup::ops_punct()) {
        const string &name = iop->name();
        auto oplen = name.size();
        if (oplen > opmaxlen && !strncmp(p, name.c_str(), oplen)) {
            opmaxlen = oplen;
            op = iop;
        }
    }
    if (op) {
        currtok = token(TOK_OPERATOR, op);
        p += opmaxlen;
        return;
    }

    int base_override = 0;
    if (isidstart(*p)) {

        const char *q = p;
        while (isidchar(*p)) p++;

        const string keyword_str(q, p-q);
        const char *keyword = keyword_str.c_str();
        int param, pos;

        if (*p == ':') {
            /*
             * Various special cases of keywords followed by
             * colons.
             */

            if (!strcmp(keyword, "ieee")) {
                /*
                 * Special case: "ieee:" followed by a 4-, 8-, 16-
                 * or 32-digit hex number is treated as an IEEE
                 * half, single, double or quad precision
                 * (respectively) floating-point bit pattern in
                 * hex, which is expanded into a rational and
                 * presented as TOK_NUMBER. If there's a following
                 * '.' plus extra hex digits, those extend the
                 * precision in the obvious way.
                 *
                 * So count and collect the hex digits.
                 */
                bigint value = 0;
                int ndigits = 0, expbits, sign = 1;
                p++;
                while (*p && isxdigit((unsigned char)*p)) {
                    if (ndigits >= 32) {
                        throw expr_error("expected 4, 8, 16 or 32 hex "
                                         "digits after 'ieee:'");
                    }
                    int val = frombase(*p);
                    assert(val < 16);
                    value *= 16;
                    value += val;
                    ndigits++;
                    p++;
                }
                switch (ndigits) {
                  case 4: expbits = 5; break;
                  case 8: expbits = 8; break;
                  case 16: expbits = 11; break;
                  case 32: expbits = 15; break;
                  default:
                    throw expr_error("expected 4, 8, 16 or 32 hex digits"
                                     " after 'ieee:'");
                }
                if (*p == '.') {
                    p++;
                    while (*p && isxdigit((unsigned char)*p)) {
                        int val = frombase(*p);
                        assert(val < 16);
                        value *= 16;
                        value += val;
                        ndigits++;
                        p++;
                    }
                }
                int exponent = value / bigint_power(2, 4*ndigits-expbits-1);
                value %= bigint_power(2, 4*ndigits-expbits-1);
                if (exponent & (1 << expbits))
                    sign = -1;
                exponent &= (1 << expbits)-1;
                if (exponent == (1 << expbits)-1) {
                    throw expr_error("IEEE ",
                                     value == 0 ? "infinity" : "NaN",
                                     " not supported");
                }
                if (exponent == 0) {
                    exponent = 1;
                } else {
                    value += bigint_power(2, 4*ndigits-expbits-1);
                }
                exponent -= (1 << (expbits-1))-1;   /* unbias exponent */
                exponent -= 4*ndigits-expbits-1; /* turn into int * 2^e */
                if (exponent > 0)
                    currtok = token(TOK_NUMBER, sign * value *
                                    bigint_power(2, exponent), 1);
                else
                    currtok = token(TOK_NUMBER, sign * value,
                                    bigint_power(2, -exponent));
                return;

            } else if (sscanf(keyword, "base%d%n", &param, &pos) > 0 &&
                       pos == (int)strlen(keyword)) {
                /*
                 * Prefixes such as base2: or base19: permit input
                 * in an arbitrary number base between 2 and 36.
                 * We don't process these here; we fall through to
                 * the main literal-processing code below, having
                 * set a base override variable.
                 */
                if (param < 2 || param > 36) {
                    throw expr_error("base in keyword '", keyword,
                                     ":' should be between 2 and 36"
                                     " inclusive");
                }
                base_override = param;
                assert(*p == ':');
                p++;               // advance past the colon
            } else if (!strcmp(keyword, "cfracfile") ||
                       !strcmp(keyword, "cfracxfile") ||
                       (sscanf(keyword, "base%dfile%n", &param, &pos)>0 &&
                        pos == (int)strlen(keyword)) ||
                       (sscanf(keyword, "base%dxfile%n", &param, &pos)>0 &&
                        pos == (int)strlen(keyword))) {
                bool cfrac = (keyword[0] == 'c');
                bool exact = (keyword[strlen(keyword)-5] == 'x');

                if (!cfrac && (param < 2 || param > 36)) {
                    throw expr_error("base in keyword '", keyword,
                                     ":' should be between 2 and 36"
                                     " inclusive");
                }

                /*
                 * We expect to see a file name here, which
                 * we'll open and read from at spigot evaluation
                 * time. Depending on which keyword was used
                 * above, we'll expect it to contain either
                 * continued fraction terms or digits in some
                 * number base.
                 */
                p++;

                int lenused;
                string filename = parse_filename(p, &lenused);
                p += lenused;

                if (*q == 'c') {
                    currtok = token(
                        TOK_SPIGOT,
                        spigot_cfracfile(fac, filename.c_str(), exact));
                } else {
                    currtok = token(
                        TOK_SPIGOT,
                        spigot_basefile(fac, param,
                                        filename.c_str(), exact));
                }
                return;
            } else if (!strcmp(keyword, "cfracfd") ||
                       (sscanf(keyword, "base%dfd%n", &param, &pos) > 0 &&
                        pos == (int)strlen(keyword))) {
                /*
                 * We expect to see an fd number here, from
                 * which we'll read continued fraction terms or
                 * a base representation in the same way as
                 * above.
                 */
                int fd;

                p++;
                fd = atoi(p);
                p += strspn(p, "0123456789");

                bool cfrac = (keyword[0] == 'c');

                if (!cfrac && (param < 2 || param > 36)) {
                    throw expr_error("base in keyword '", keyword,
                                     ":' should be between 2 and 36"
                                     " inclusive");
                }

                if (cfrac) {
                    currtok = token(TOK_SPIGOT, spigot_cfracfd(fac, fd));
                } else {
                    int base = atoi(q+4);
                    currtok = token(TOK_SPIGOT,
                                    spigot_basefd(fac, base, fd));
                }
                return;
            } else {
                throw expr_error("unrecognised prefix keyword '",
                                 string(q, p), ":'");
            }
        } else if (!strcmp(keyword, "cfracstdin") ||
                   !strcmp(keyword, "cfracxstdin") ||
                   (sscanf(keyword, "base%dstdin%n", &param, &pos)>0 &&
                    pos == (int)strlen(keyword)) ||
                   (sscanf(keyword, "base%dxstdin%n", &param, &pos)>0 &&
                    pos == (int)strlen(keyword))) {
            /*
             * These keywords are self-contained and require no
             * suffixing colon, but spiritually they belong with
             * all the file: and fd: stuff above, so I handle them
             * in a separate else-if branch from the scope lookup
             * below. (Also it would be a pain to insert all of
             * base2stdin, ..., base36stdin into the function
             * list.)
             */
            bool cfrac = (keyword[0] == 'c');

            if (!cfrac && (param < 2 || param > 36)) {
                throw expr_error("base in keyword '", keyword,
                                 ":' should be between 2 and 36 inclusive");
            }

            if (cfrac) {
                currtok = token(TOK_SPIGOT, spigot_cfracstdin(fac));
            } else {
                int base = atoi(q+4);
                currtok = token(TOK_SPIGOT, spigot_basestdin(fac, base));
            }
            return;
        } else {
            /*
             * Identifiers not prefixed by a colon must be checked
             * against keywords we know, and failing that,
             * returned as TOK_IDENTIFIER.
             */
            if (auto op = ExprNodeLookup::get_op_id(keyword_str)) {
                currtok = token(TOK_OPERATOR, op);
                return;
            }
            if (strczmatch(p-q, q, "let")) {
                currtok = token(TOK_LET);
            } else if (strczmatch(p-q, q, "in")) {
                currtok = token(TOK_IN);
            } else {
                currtok = token(TOK_IDENTIFIER, q, p-q);
            }
            return;
        }
    }

    if (*p == '.' || frombase(*p) < (base_override ? base_override : 10)) {
        int base, expbase;
        int expmarker;
        bool seendot = false;
        bigint n = 0, d = 1;

        if (base_override) {
            /*
             * We saw a baseN: prefix above, which means we're
             * expecting a number in that base, with no exponent
             * suffix.
             */
            base = base_override;
            expbase = 0;
            expmarker = UCHAR_MAX + 1; // avoid ever matching below
        } else if (*p == '0' && tolower((unsigned char)p[1]) == 'x') {
            /*
             * Hex literal.
             */
            p += 2;
            base = 16;
            expbase = 2;
            expmarker = 'p';
        } else {
            /*
             * Decimal literal.
             */
            base = expbase = 10;
            expmarker = 'e';
        }

        while (*p == '.' || (*p && frombase(*p) < base)) {
            if (*p == '.') {
                if (!seendot) {
                    seendot = true;
                    p++;
                    continue;
                } else {
                    throw expr_error("two dots in numeric literal");
                }
            } else {
                int val = frombase(*p);
                n *= base;
                n += val;
                if (seendot)
                    d *= base;
                p++;
            }
        }

        if (*p && tolower((unsigned char)*p) == expmarker) {
            int exponent = 0;
            bool expneg = false;
            p++;
            if (*p == '-' || *p == '+') {
                expneg = (*p == '-');
                p++;
            }
            while (*p && isdigit((unsigned char)*p)) {
                exponent = 10 * exponent + frombase(*p);
                p++;
            }

            bigint mult = bigint_power(expbase, exponent);
            if (expneg)
                d *= mult;
            else
                n *= mult;
        }

        currtok = token(TOK_NUMBER, n, d);
        return;
    }

    throw expr_error("unrecognised token");
}

// Uniquely identify every function defined in an expression, no
// matter where in the tree and nesting structure.
typedef int FunctionID;

struct FnArgHolder {
    argvector<Spigot> args;
    FunctionID fnid;
    FnArgHolder *parent;
    FnArgHolder(FunctionID afnid, FnArgHolder *aparent)
        : fnid(afnid), parent(aparent) {}
    void add_arg(Spigot arg) {
        args.push_back(move(arg));
    }
    Spigot lookup(FunctionID afnid, int argindex) {
        if (afnid == fnid) {
            assert(0 <= argindex);
            assert(argindex < (int)args.size());
            return args[argindex]->clone();
        } else {
            assert(parent);
            return parent->lookup(afnid, argindex);
        }
    }
};

class ASTNode {
  public:
    virtual ~ASTNode() = default;
    virtual Spigot evaluate(FnArgHolder *fnargs) = 0;
};

class ASTSpigot : public ASTNode {
    Spigot spigot;
    virtual Spigot evaluate(FnArgHolder *) override;
  public:
    ASTSpigot(Spigot aspigot);
};

ASTSpigot::ASTSpigot(Spigot aspigot)
    : spigot(move(aspigot))
{
}

Spigot ASTSpigot::evaluate(FnArgHolder *)
{
    return spigot->clone();
}

class ASTOp : public ASTNode {
    const ExprNodeType *op;
    argvector<shared_ptr<ASTNode>> args;

    virtual Spigot evaluate(FnArgHolder *fnargs) override;

  public:
    ASTOp(const ExprNodeType *aop,
          const argvector<shared_ptr<ASTNode>> &aargs);
};

ASTOp::ASTOp(const ExprNodeType *aop,
             const argvector<shared_ptr<ASTNode>> &aargs)
    : op(aop), args(aargs)
{
}

Spigot ASTOp::evaluate(FnArgHolder *fnargs)
{
    argvector<Spigot> sargs;
    sargs.syntax = args.syntax;
    for (int i = 0; i < (int)args.size(); ++i)
        sargs.push_back(args[i]->evaluate(fnargs));
    return op->evaluatev(sargs);
}

class ASTFnArg : public ASTNode {
    FunctionID fnid;
    int argindex;

    virtual Spigot evaluate(FnArgHolder *fnargs) override;

  public:
    ASTFnArg(FunctionID afnid, int aargindex);
};

ASTFnArg::ASTFnArg(FunctionID afnid, int aargindex)
    : fnid(afnid), argindex(aargindex)
{
}

Spigot ASTFnArg::evaluate(FnArgHolder *fnargs)
{
    return fnargs->lookup(fnid, argindex);
}

class ASTFnCall : public ASTNode {
    shared_ptr<ASTNode> fnbody;
    FunctionID fnid;
    argvector<shared_ptr<ASTNode>> args;

    virtual Spigot evaluate(FnArgHolder *fnargs) override;

  public:
    ASTFnCall(shared_ptr<ASTNode> afnbody, FunctionID afnid,
              const argvector<shared_ptr<ASTNode>> &aargs);
};

ASTFnCall::ASTFnCall(shared_ptr<ASTNode> afnbody, FunctionID afnid,
                     const argvector<shared_ptr<ASTNode>> &aargs)
    : fnbody(afnbody), fnid(afnid), args(aargs)
{
}

Spigot ASTFnCall::evaluate(FnArgHolder *fnargs)
{
    FnArgHolder newfnargs(fnid, fnargs);
    for (int i = 0; i < (int)args.size(); ++i)
        newfnargs.add_arg(args[i]->evaluate(fnargs));
    return fnbody->evaluate(&newfnargs);
}

class Definition {
  public:
    virtual ~Definition() = default;
    virtual bool is_function() = 0;
    virtual void check_syntax(size_t nargs, const FnCallSyntax &syntax);
    virtual shared_ptr<ASTNode> make_call(
        const argvector<shared_ptr<ASTNode>> &aargs) = 0;
};

void Definition::check_syntax(size_t nargs, const FnCallSyntax &syntax)
{
}

class BuiltinDefinition : public Definition {
    const ExprConstOrFunction *cf;

    virtual bool is_function() override;
    virtual void check_syntax(
        size_t nargs, const FnCallSyntax &syntax) override;
    virtual shared_ptr<ASTNode> make_call(
        const argvector<shared_ptr<ASTNode>> &args) override;

  public:
    BuiltinDefinition(const ExprConstOrFunction *acf);
};

BuiltinDefinition::BuiltinDefinition(const ExprConstOrFunction *acf)
    : cf(acf)
{
}

bool BuiltinDefinition::is_function()
{
    return cf->arity() != 0;
}

static void throw_nargs_error(size_t expected, size_t got, const string &name)
{
    throw expr_error("expected ", expected, " arguments for function '",
                     name, "', found ", got);
}

void BuiltinDefinition::check_syntax(
    size_t nargs, const FnCallSyntax &syntax)
{
    if (cf->arity() == -1 || (size_t)cf->arity() == nargs)
        return; // number of arguments is ok

    throw_nargs_error(cf->arity(), nargs, cf->name());
}

shared_ptr<ASTNode> BuiltinDefinition::make_call(
    const argvector<shared_ptr<ASTNode>> &args)
{
    return make_shared<ASTOp>(cf, args);
}

class Variable : public Definition {
    shared_ptr<ASTNode> value;
    virtual bool is_function() override;
    virtual shared_ptr<ASTNode> make_call(
        const argvector<shared_ptr<ASTNode>> &) override;

  public:
    Variable(shared_ptr<ASTNode> avalue);
};

Variable::Variable(shared_ptr<ASTNode> avalue)
    : value(avalue)
{
}

bool Variable::is_function()
{
    return false;
}

shared_ptr<ASTNode> Variable::make_call(
    const argvector<shared_ptr<ASTNode>> &)
{
    return value;
}

class UserDefinedFunction : public Definition {
    string name;
    size_t nargs;
    shared_ptr<ASTNode> fnbody;
    FunctionID fnid;

    virtual bool is_function() override;
    virtual void check_syntax(
        size_t nargs, const FnCallSyntax &syntax) override;
    virtual shared_ptr<ASTNode> make_call(
        const argvector<shared_ptr<ASTNode>> &args) override;

  public:
    UserDefinedFunction(const string &aname, size_t anargs,
                        shared_ptr<ASTNode> afnbody, FunctionID afnid);
};

UserDefinedFunction::UserDefinedFunction(
    const string &aname, size_t anargs,
    shared_ptr<ASTNode> afnbody, FunctionID afnid)
    : name(aname), nargs(anargs), fnbody(afnbody), fnid(afnid)
{
}

bool UserDefinedFunction::is_function()
{
    return true;
}

void UserDefinedFunction::check_syntax(
    size_t nargs_got, const FnCallSyntax &syntax)
{
    syntax.enforce_trivial(name);
    if (nargs != nargs_got)
        throw_nargs_error(nargs, nargs_got, name);
}

shared_ptr<ASTNode> UserDefinedFunction::make_call(
    const argvector<shared_ptr<ASTNode>> &args)
{
    return make_shared<ASTFnCall>(fnbody, fnid, args);
}

class Scope {
  protected:
    shared_ptr<Scope> parent;
    virtual shared_ptr<Definition> lookup_here(const char *varname) = 0;
  public:
    Scope(shared_ptr<Scope> aparent);
    virtual ~Scope() = default;
    shared_ptr<Definition> lookup(const char *varname);
};

Scope::Scope(shared_ptr<Scope> aparent)
    : parent(aparent)
{
}

shared_ptr<Definition> Scope::lookup(const char *varname)
{
    shared_ptr<Definition> ret = lookup_here(varname);
    if (!ret && parent)
        ret = parent->lookup(varname);
    return ret;
}

class DictScope : public Scope {
    using Scope::Scope;
  protected:
    map<string, shared_ptr<Definition>> names;
    virtual shared_ptr<Definition> lookup_here(const char *varname) override;
};

shared_ptr<Definition> DictScope::lookup_here(const char *varname)
{
    map<string, shared_ptr<Definition>>::iterator it;
    it = names.find(varname);
    if (it != names.end())
        return it->second;
    else
        return nullptr;
}

class GlobalScopeWrapper : public DictScope {
    GlobalScope *gs;
    virtual shared_ptr<Definition> lookup_here(const char *varname) override;
  public:
    GlobalScopeWrapper(GlobalScope *ags);
};

GlobalScopeWrapper::GlobalScopeWrapper(GlobalScope *ags)
    : DictScope(nullptr), gs(ags)
{
    for (const ExprConstOrFunction *cf: ExprNodeLookup::fns_normal())
        names[cf->name()] = make_shared<BuiltinDefinition>(cf);
    if (enable_debug_fns)
        for (const ExprConstOrFunction *cf: ExprNodeLookup::fns_debug())
            names[cf->name()] = make_shared<BuiltinDefinition>(cf);
}

shared_ptr<Definition> GlobalScopeWrapper::lookup_here(const char *varname)
{
    if (gs) {
        Spigot spig = gs->lookup(varname);
        if (spig)
            return make_shared<Variable>(
                make_shared<ASTSpigot>(move(spig)));
    }
    return DictScope::lookup_here(varname);
}

class LetScope : public DictScope {
    using DictScope::DictScope;
  public:
    void add_var(const string &varname, shared_ptr<ASTNode> node);
    void add_fn(const string &fnname, size_t nargs,
                shared_ptr<ASTNode> node, FunctionID fnid);
};

void LetScope::add_var(const string &varname, shared_ptr<ASTNode> node)
{
    names[varname] = make_shared<Variable>(node);
}

void LetScope::add_fn(const string &fnname, size_t nargs,
                      shared_ptr<ASTNode> node, FunctionID fnid)
{
    names[fnname] = make_shared<UserDefinedFunction>(
        fnname, nargs, node, fnid);
}

class FnArgScope : public DictScope {
    size_t nargs;
    FunctionID fnid;
  public:
    FnArgScope(shared_ptr<Scope> parent, FunctionID afnid);
    void add_arg(const string &argname);
    size_t n_args() const;
};

FnArgScope::FnArgScope(shared_ptr<Scope> parent, FunctionID afnid)
    : DictScope(parent), nargs(0), fnid(afnid)
{
}

void FnArgScope::add_arg(const string &argname)
{
    if (names.find(argname) != names.end())
        throw expr_error("parameter name '", argname,
                         "' repeated in function definition");
    names[argname] = make_shared<Variable>(
        make_shared<ASTFnArg>(fnid, nargs++));
}

size_t FnArgScope::n_args() const
{
    return nargs;
}

struct stack {
    shared_ptr<ASTNode> atom;
    const ExprOperator *op;
};

void parse_recursive(Lexer &lexer, struct stack *stack,
                     shared_ptr<Scope> scope, FunctionID *curr_fnid)
{
    struct stack *sp = stack;
    const ExprOperator *op;

    while (1) {
        if (lexer.currtok.type == TOK_EOS ||
            lexer.currtok.type == TOK_COMMA ||
            lexer.currtok.type == TOK_SEMICOLON ||
            lexer.currtok.type == TOK_IN ||
            lexer.currtok.type == TOK_RPAR ||
            lexer.currtok.type == TOK_RBRACE ||
            lexer.currtok.type == TOK_OPERATOR) {
            /*
             * These are all the types of token which might require
             * reducing before we process them.
             */
            if (lexer.currtok.type == TOK_OPERATOR) {
                op = lexer.currtok.op;
                lexer.advance();

              got_op:
                /*
                 * Distinguish binary from unary prefix operators: a
                 * unary prefix operator is any operator appearing
                 * directly after another operator or at
                 * start-of-expression.
                 */
                if (sp == stack || !sp[-1].atom) {
                    /*
                     * This should be a unary operator. If the lexer has
                     * returned us a binary operator with the same
                     * spelling, find the right one instead.
                     */
                    if (dynamic_cast<const ExprBinaryOperator *>(op))
                        if (auto iop = ExprNodeLookup::get_op_homograph(op))
                            if (auto unop = dynamic_cast<
                                const ExprUnaryPrefixOperator *>(iop))
                                op = unop;

                    /*
                     * And if that didn't work, we have a syntax error.
                     */
                    if (!dynamic_cast<const ExprUnaryPrefixOperator *>(op)) {
                        throw expr_error("expected unary operator");
                    }

                    /*
                     * If it did, though, unary prefix operators get
                     * unconditionally shifted.
                     */
                    sp->atom = nullptr;
                    sp->op = op;
                    sp++;
                    continue;
                } else {
                    /*
                     * This should be either a binary operator, or a
                     * unary suffix operator.
                     *
                     * There must never be a binary *and* a unary
                     * suffix operator that have the same spelling. A
                     * language containing both a binary/prefix
                     * ambiguity and a binary/suffix ambiguity is
                     * ambiguous overall, because if BP can be either
                     * binary or prefix, and BS can be either binary
                     * or suffix, then an expression like ID BS BP ID
                     * could be parsed with either operator being the
                     * binary one. And in this language there
                     * definitely _are_ binary/prefix ambiguous
                     * operators ('-' and '+'), so there cannot be
                     * binary/suffix ambiguous ones.
                     *
                     * (Conversely, if a language does _not_ have both
                     * kinds of ambiguous operator, then there's no
                     * problem: whichever kind of unary operator is
                     * unambiguous - in this case, suffix ones - can
                     * be trivially distinguished, reduced, and
                     * removed from consideration, leaving the rest of
                     * the expression with only the normal problem of
                     * telling unary from binary '-'.)
                     */

                    if (dynamic_cast<const ExprUnaryPrefixOperator *>(op))
                        if (auto iop = ExprNodeLookup::get_op_homograph(op))
                            if (auto binop = dynamic_cast<
                                const ExprBinaryOperator *>(iop))
                                op = binop;

                    if (!dynamic_cast<const ExprBinaryOperator *>(op) &&
                        !dynamic_cast<const ExprUnarySuffixOperator *>(op)) {
                        throw expr_error("expected binary operator");
                    }
                }
            } else {
                op = nullptr; /* this is not actually an operator */
            }

            /*
             * Before we shift (or terminate), reduce any higher-
             * priority operators already on our stack.
             */
            while (1) {
                if (sp - stack < 2)
                    break;             /* run out of candidate reduces */
                if (!sp[-1].atom)
                    throw expr_error("subexpression ends after operator");
                assert(!sp[-2].atom);  // because we expect atoms,ops alternate
                if (op) {
                    if (op->prec() > sp[-2].op->prec())
                        break;   /* new operator is higher-priority */
                    auto binop = dynamic_cast<const ExprBinaryOperator *>(op);
                    if (binop && binop->right_associative() &&
                        op->prec() == sp[-2].op->prec())
                        break;  /* equal-prec but right-associative */
                }

                /*
                 * Now we know we want to reduce. Split into unary
                 * and binary cases.
                 */
                vector<shared_ptr<ASTNode>> args;
                shared_ptr<ASTNode> ret;
                if (auto unop = dynamic_cast<const ExprUnaryOperator *>(
                        sp[-2].op)) {
                    args.push_back(sp[-1].atom);
                    ret = make_shared<ASTOp>(unop, args);
                    sp--;
                } else if (auto binop =
                           dynamic_cast<const ExprBinaryOperator *>(
                               sp[-2].op)) {
                    assert(sp - stack >= 3);
                    assert(sp[-3].atom);
                    args.push_back(sp[-3].atom);
                    args.push_back(sp[-1].atom);
                    ret = make_shared<ASTOp>(binop, args);
                    sp -= 2;
                } else {
                    assert(false && "Weird operator on stack");
                }
                sp[-1].atom = ret;
            }

            /*
             * Now we can shift the new operator, or terminate
             * the parse, depending.
             */
            if (op) {
                if (dynamic_cast<const ExprUnarySuffixOperator *>(op)) {
                    /*
                     * If it's a suffix unary operator, then we don't
                     * shift it; instead, we just apply it immediately
                     * to modify the atom at the top of the stack.
                     */
                    assert(sp[-1].atom);
                    vector<shared_ptr<ASTNode>> args;
                    args.push_back(sp[-1].atom);
                    sp[-1].atom = make_shared<ASTOp>(op, args);
                } else {
                    sp->atom = nullptr;
                    sp->op = op;
                    sp++;
                }
                continue;
            } else {
                if (sp != stack+1 || !sp[-1].atom) {
                    if (lexer.currtok.type == TOK_EOS)
                        throw expr_error("unexpected end of expression");
                    else if (lexer.currtok.type == TOK_COMMA)
                        throw expr_error("unexpected ','");
                    else if (lexer.currtok.type == TOK_IN)
                        throw expr_error("unexpected 'in'");
                    else
                        throw expr_error("unexpected ')'");
                }
                return;
            }
        }

        /*
         * If we get here, it means we're about to parse an atom.
         * 
         * One silly special case: if the previous thing on the
         * stack is also an atom, we pretend there was a
         * multiplication sign in between.
         */
        if (sp > stack && sp[-1].atom) {
            op = &mul_op;
            goto got_op;
        }

        if (lexer.currtok.type == TOK_NUMBER) {
            sp->atom = make_shared<ASTSpigot>
                (spigot_rational(lexer.currtok.n, lexer.currtok.d));
            sp->op = nullptr;
            sp++;
            lexer.advance();
            continue;
        }

        if (lexer.currtok.type == TOK_SPIGOT) {
            sp->atom = make_shared<ASTSpigot>(move(lexer.currtok.spigot));
            sp->op = nullptr;
            sp++;
            lexer.advance();
            continue;
        }

        if (lexer.currtok.type == TOK_IDENTIFIER) {
            string id = lexer.currtok.text;
            argvector<shared_ptr<ASTNode>> args;

            auto def = scope->lookup(lexer.currtok.text.c_str());
            if (!def)
                throw expr_error("unrecognised identifier '",
                                 lexer.currtok.text, "'");
            lexer.advance();

            if (def->is_function()) {
                /*
                 * This is a function call, so collect its
                 * arguments.
                 */
                if (lexer.currtok.type != TOK_LPAR) {
                    throw expr_error("expected '(' after function name '",
                                     id, "'");
                }
                lexer.advance();

                // Parse initial semicolons appearing before any argument
                while (lexer.currtok.type == TOK_SEMICOLON) {
                    args.syntax.semicolons.push_back(args.size());
                    lexer.advance();
                }

                while (1) {
                    parse_recursive(lexer, sp, scope, curr_fnid);
                    args.push_back(sp->atom);

                    if (lexer.currtok.type == TOK_RPAR) {
                        lexer.advance();
                        break;
                    } else if (lexer.currtok.type == TOK_COMMA) {
                        lexer.advance();
                    } else if (lexer.currtok.type == TOK_SEMICOLON) {
                        // Parse any nonzero number of semicolons, and
                        // regard them as an argument separator.
                        do {
                            args.syntax.semicolons.push_back(args.size());
                            lexer.advance();
                        } while (lexer.currtok.type == TOK_SEMICOLON);

                        // But it's syntactically legitimate to then
                        // see the closing paren.
                        if (lexer.currtok.type == TOK_RPAR) {
                            lexer.advance();
                            break;
                        }
                    } else {
                        throw expr_error("expected ',', ';' or ')'");
                    }
                }

                def->check_syntax(args.size(), args.syntax);
            }

            sp->atom = def->make_call(args);
            sp->op = nullptr;
            sp++;
            continue;
        }

        if (lexer.currtok.type == TOK_LPAR) {
            lexer.advance();
            parse_recursive(lexer, sp, scope, curr_fnid);
            sp++;
            if (lexer.currtok.type != TOK_RPAR) {
                throw expr_error("expected ')'");
            }
            lexer.advance();
            continue;
        }

        if (lexer.currtok.type == TOK_LBRACE) {
            if (!(sp > stack && !sp[-1].atom && sp[-1].op == &pow_op_caret))
                throw expr_error("'{' is only valid after the ^ operator");
            lexer.advance();
            parse_recursive(lexer, sp, scope, curr_fnid);
            sp++;
            if (lexer.currtok.type != TOK_RBRACE) {
                throw expr_error("expected '}'");
            }
            lexer.advance();
            continue;
        }

        if (lexer.currtok.type == TOK_LET) {
            auto ls = make_shared<LetScope>(scope);
            lexer.advance();
            while (1) {
                if (lexer.currtok.type != TOK_IDENTIFIER)
                    throw expr_error("expected identifier in let clause");
                string name = lexer.currtok.text;
                lexer.advance();
                auto this_fnid = *curr_fnid;
                auto fas = make_shared<FnArgScope>(ls, this_fnid);
                bool is_function = false;
                if (lexer.currtok.type == TOK_LPAR) {
                    (*curr_fnid)++;
                    is_function = true;
                    lexer.advance();
                    while (1) {
                        if (lexer.currtok.type != TOK_IDENTIFIER)
                            throw expr_error("expected identifier in "
                                             "function parameter list");
                        fas->add_arg(lexer.currtok.text);
                        lexer.advance();
                        if (lexer.currtok.type == TOK_COMMA) {
                            lexer.advance();
                            continue;
                        } else if (lexer.currtok.type == TOK_RPAR) {
                            lexer.advance();
                            break;
                        } else {
                            throw expr_error("expected ',' or ')' after "
                                             "identifier in "
                                             "function parameter list");
                        }
                    }
                }
                if (lexer.currtok.type != TOK_EQUALS)
                    throw expr_error("expected '=' after identifier in"
                                     " let clause");
                lexer.advance();
                parse_recursive(lexer, sp, fas, curr_fnid);
                if (is_function) {
                    ls->add_fn(name, fas->n_args(), sp[0].atom, this_fnid);
                } else {
                    ls->add_var(name, sp[0].atom);
                }
                if (lexer.currtok.type == TOK_IN) {
                    lexer.advance();
                    break;
                } else if (lexer.currtok.type == TOK_COMMA) {
                    lexer.advance();
                    continue;
                } else {
                    throw expr_error("expected ',' or 'in' after definition"
                                     " in let clause");
                }
            }
            parse_recursive(lexer, sp, ls, curr_fnid);
            sp++;
            continue;
        }

        throw expr_error("unrecognised token");
    }
}

Spigot expr_parse(const char *expr, GlobalScope *globalscope,
                  FileAccessContext &fac)
{
    Lexer lexer(fac, expr);
    auto gsw = make_shared<GlobalScopeWrapper>(globalscope);
    FunctionID curr_fnid = 0;

    vector<stack> stackv(strlen(expr));
    parse_recursive(lexer, stackv.data(), gsw, &curr_fnid);
    // Most parse errors will have thrown by now, but need one last check here
    if (lexer.currtok.type != TOK_EOS)
        throw expr_error("expected end of string");
    return stackv[0].atom->evaluate(nullptr);
}

class NullFileAccessContext : public FileAccessContext {
    virtual unique_ptr<FileOpener> file_opener(const string &) override;
    virtual unique_ptr<FileOpener> fd_opener(int) override;
    virtual unique_ptr<FileOpener> stdin_opener() override;
};

unique_ptr<FileOpener> NullFileAccessContext::file_opener(const string &)
{
    throw io_error("file keywords not permitted in this context");
}

unique_ptr<FileOpener> NullFileAccessContext::fd_opener(int)
{
    throw io_error("fd keywords not permitted in this context");
}

unique_ptr<FileOpener> NullFileAccessContext::stdin_opener()
{
    throw io_error("stdin keywords not permitted in this context");
}

Spigot literal_parse(const char *expr)
{
    NullFileAccessContext nfac;
    Lexer lexer(nfac, expr);
    bigint n = 1, d = 1;

    if (lexer.currtok.type == TOK_OPERATOR &&
        (lexer.currtok.op == &neg_op || lexer.currtok.op == &sub_op)) {
        lexer.advance();
        n = -n;
    }

    if (lexer.currtok.type != TOK_NUMBER)
        throw expr_error("expected numeric literal");
    n *= lexer.currtok.n;
    d *= lexer.currtok.d;
    lexer.advance();

    if (lexer.currtok.type == TOK_OPERATOR && lexer.currtok.op == &div_op) {
        lexer.advance();
        if (lexer.currtok.type != TOK_NUMBER)
            throw expr_error("expected numeric literal after '/'");
        n *= lexer.currtok.d;
        d *= lexer.currtok.n;
        lexer.advance();
    }

    if (lexer.currtok.type != TOK_EOS)
        throw expr_error("expected end of string");

    return spigot_rational(n, d);
}
