/*
  femtoLisp

  a compact interpreter for a minimal lisp/scheme dialect

  characteristics:
  * lexical scope, lisp-1
  * unrestricted macros
  * data types: 30-bit integer, symbol, pair, vector, char, string, table
      iostream, procedure, low-level data types
  * case-sensitive
  * simple compacting copying garbage collector
  * Scheme-style varargs (dotted formal argument lists)
  * "human-readable" bytecode with self-hosted compiler

  extra features:
  * circular structure can be printed and read
  * #. read macro for eval-when-read and readably printing builtins
  * read macros for backquote
  * symbol character-escaping printer
  * exceptions
  * gensyms (can be usefully read back in, too)
  * #| multiline comments |#, lots of other lexical syntax
  * generic compare function, cyclic equal
  * cvalues system providing C data types and a C FFI
  * constructor notation for nicely printing arbitrary values

  by Jeff Bezanson (C) 2009
  Distributed under the BSD License
*/

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <assert.h>
#include <ctype.h>
#include <wctype.h>
#include <sys/types.h>
#include <locale.h>
#include <limits.h>
#include <errno.h>
#include <libgen.h> // defines dirname

#include "platform.h"
#include "libsupport.h"
#include "flisp.h"
#include "opcodes.h"

#ifdef __cplusplus
extern "C" {
#endif

static const char *const builtin_names[] =
    { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
      NULL, NULL, NULL, NULL,
      // predicates
      "eq?", "eqv?", "equal?", "atom?", "not", "null?", "boolean?", "symbol?",
      "number?", "bound?", "pair?", "builtin?", "vector?", "fixnum?",
      "function?",

      // lists
      "cons", "list", "car", "cdr", "set-car!", "set-cdr!",

      // execution
      "apply",

      // arithmetic
      "+", "-", "*", "/", "div0", "=", "<", "compare",

      // sequences
      "vector", "aref", "aset!",
      "", "", "" };

#define ANYARGS -10000

static const short builtin_arg_counts[] =
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
      2, ANYARGS, 1, 1, 2, 2,
      -2,
      ANYARGS, -1, ANYARGS, -1, 2,  2, 2, 2,
      ANYARGS, 2, 3 };

#define PUSH(fl_ctx, v) (fl_ctx->Stack[fl_ctx->SP++] = (v))
#define POP(fl_ctx)   (fl_ctx->Stack[--fl_ctx->SP])
#define POPN(fl_ctx, n) (fl_ctx->SP-=(n))

static value_t apply_cl(fl_context_t *fl_ctx, uint32_t nargs);
static value_t *alloc_words(fl_context_t *fl_ctx, int n);
static value_t relocate(fl_context_t *fl_ctx, value_t v);

typedef struct _fl_readstate_t {
    htable_t backrefs;
    htable_t gensyms;
    value_t source;
    struct _fl_readstate_t *prev;
} fl_readstate_t;

static void free_readstate(fl_readstate_t *rs)
{
    htable_free(&rs->backrefs);
    htable_free(&rs->gensyms);
}

// error utilities ------------------------------------------------------------

#define FL_TRY(fl_ctx)                           \
  fl_exception_context_t _ctx; int l__tr, l__ca; \
  _ctx.sp=fl_ctx->SP; _ctx.frame=fl_ctx->curr_frame; _ctx.rdst=fl_ctx->readstate; _ctx.prev=fl_ctx->exc_ctx; \
  _ctx.ngchnd = fl_ctx->N_GCHND; fl_ctx->exc_ctx = &_ctx;                                    \
  if (!fl_setjmp(_ctx.buf)) \
    for (l__tr=1; l__tr; l__tr=0, (void)(fl_ctx->exc_ctx=fl_ctx->exc_ctx->prev))

#define FL_CATCH(fl_ctx)                                                \
    else                                                                \
        for(l__ca=1; l__ca; l__ca=0,                                    \
                fl_ctx->lasterror=fl_ctx->NIL,fl_ctx->throwing_frame=0,fl_ctx->SP=_ctx.sp,fl_ctx->curr_frame=_ctx.frame)

void fl_savestate(fl_context_t *fl_ctx, fl_exception_context_t *_ctx)
{
    _ctx->sp = fl_ctx->SP;
    _ctx->frame = fl_ctx->curr_frame;
    _ctx->rdst = fl_ctx->readstate;
    _ctx->prev = fl_ctx->exc_ctx;
    _ctx->ngchnd = fl_ctx->N_GCHND;
}

void fl_restorestate(fl_context_t *fl_ctx, fl_exception_context_t *_ctx)
{
    fl_ctx->lasterror = fl_ctx->NIL;
    fl_ctx->throwing_frame = 0;
    fl_ctx->SP = _ctx->sp;
    fl_ctx->curr_frame = _ctx->frame;
}

void fl_raise(fl_context_t *fl_ctx, value_t e)
{
    fl_ctx->lasterror = e;
    // unwind read state
    while (fl_ctx->readstate != (fl_readstate_t*)fl_ctx->exc_ctx->rdst) {
        free_readstate(fl_ctx->readstate);
        fl_ctx->readstate = fl_ctx->readstate->prev;
    }
    if (fl_ctx->throwing_frame == 0)
        fl_ctx->throwing_frame = fl_ctx->curr_frame;
    fl_ctx->N_GCHND = fl_ctx->exc_ctx->ngchnd;
    fl_exception_context_t *thisctx = fl_ctx->exc_ctx;
    if (fl_ctx->exc_ctx->prev)   // don't throw past toplevel
        fl_ctx->exc_ctx = fl_ctx->exc_ctx->prev;
    fl_longjmp(thisctx->buf, 1);
}

static value_t make_error_msg(fl_context_t *fl_ctx, const char *format, va_list args)
{
    char msgbuf[512];
    size_t len = vsnprintf(msgbuf, sizeof(msgbuf), format, args);
    return string_from_cstrn(fl_ctx, msgbuf, len);
}

void lerrorf(fl_context_t *fl_ctx, value_t e, const char *format, ...)
{
    va_list args;
    PUSH(fl_ctx, e);
    va_start(args, format);
    value_t msg = make_error_msg(fl_ctx, format, args);
    va_end(args);

    e = POP(fl_ctx);
    fl_raise(fl_ctx, fl_list2(fl_ctx, e, msg));
}

void lerror(fl_context_t *fl_ctx, value_t e, const char *msg)
{
    PUSH(fl_ctx, e);
    value_t m = cvalue_static_cstring(fl_ctx, msg);
    e = POP(fl_ctx);
    fl_raise(fl_ctx, fl_list2(fl_ctx, e, m));
}

void type_error(fl_context_t *fl_ctx, const char *fname, const char *expected, value_t got)
{
    fl_raise(fl_ctx, fl_listn(fl_ctx, 4, fl_ctx->TypeError, symbol(fl_ctx, fname), symbol(fl_ctx, expected), got));
}

void bounds_error(fl_context_t *fl_ctx, const char *fname, value_t arr, value_t ind)
{
    fl_raise(fl_ctx, fl_listn(fl_ctx, 4, fl_ctx->BoundsError, symbol(fl_ctx, fname), arr, ind));
}

// safe cast operators --------------------------------------------------------

#define isstring(v) fl_isstring(fl_ctx, v)
#define SAFECAST_OP(type,ctype,cnvt)                                    \
    ctype to##type(fl_context_t *fl_ctx, value_t v, const char *fname)  \
    {                                                                   \
        if (is##type(v))                                                \
            return (ctype)cnvt(v);                                      \
        type_error(fl_ctx, fname, #type, v);                            \
    }
SAFECAST_OP(cons,  cons_t*,  ptr)
SAFECAST_OP(symbol,symbol_t*,ptr)
SAFECAST_OP(fixnum,fixnum_t, numval)
SAFECAST_OP(string,char*,    cvalue_data)
#undef isstring

// symbol table ---------------------------------------------------------------

int fl_is_keyword_name(const char *str, size_t len)
{
    return len>1 && ((str[0] == ':' || str[len-1] == ':') && str[1] != '\0');
}

#define CHECK_ALIGN8(p) assert((((uintptr_t)(p))&0x7)==0 && "flisp requires malloc to return 8-aligned pointers")

static symbol_t *mk_symbol(const char *str)
{
    symbol_t *sym;
    size_t len = strlen(str);

    sym = (symbol_t*)malloc((offsetof(symbol_t,name)+len+1+7)&-8);
    // TODO: if sym == NULL
    CHECK_ALIGN8(sym);
    sym->left = sym->right = NULL;
    sym->flags = 0;
    if (fl_is_keyword_name(str, len)) {
        value_t s = tagptr(sym, TAG_SYM);
        setc(s, s);
        sym->flags |= 0x2;
    }
    else {
        sym->binding = UNBOUND;
    }
    sym->type = NULL;
    sym->dlcache = NULL;
    sym->hash = memhash32(str, len)^0xAAAAAAAA;
    strcpy(&sym->name[0], str);
    return sym;
}

static symbol_t **symtab_lookup(symbol_t **ptree, const char *str)
{
    int x;

    while (*ptree != NULL) {
        x = strcmp(str, (*ptree)->name);
        if (x == 0)
            return ptree;
        if (x < 0)
            ptree = &(*ptree)->left;
        else
            ptree = &(*ptree)->right;
    }
    return ptree;
}

value_t symbol(fl_context_t *fl_ctx, const char *str)
{
    symbol_t **pnode = symtab_lookup(&fl_ctx->symtab, str);
    if (*pnode == NULL)
        *pnode = mk_symbol(str);
    return tagptr(*pnode, TAG_SYM);
}

value_t fl_gensym(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
#ifdef MEMDEBUG2
    fl_ctx->gsnameno = 1-fl_ctx->gsnameno;
    char *n = uint2str(fl_ctx->gsname[fl_ctx->gsnameno]+1, sizeof(fl_ctx->gsname[0])-1, fl_ctx->gensym_ctr++, 10);
    *(--n) = 'g';
    return tagptr(mk_symbol(n), TAG_SYM);
#else
    argcount(fl_ctx, "gensym", nargs, 0);
    (void)args;
    gensym_t *gs = (gensym_t*)alloc_words(fl_ctx, sizeof(gensym_t)/sizeof(void*));
    gs->id = fl_ctx->gensym_ctr++;
    gs->binding = UNBOUND;
    gs->isconst = 0;
    gs->type = NULL;
    return tagptr(gs, TAG_SYM);
#endif
}

int fl_isgensym(fl_context_t *fl_ctx, value_t v)
{
    return isgensym(fl_ctx, v);
}

static value_t fl_gensymp(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    argcount(fl_ctx, "gensym?", nargs, 1);
    return isgensym(fl_ctx, args[0]) ? fl_ctx->T : fl_ctx->F;
}

char *symbol_name(fl_context_t *fl_ctx, value_t v)
{
#ifndef MEMDEBUG2
    if (ismanaged(fl_ctx, v)) {
        gensym_t *gs = (gensym_t*)ptr(v);
        fl_ctx->gsnameno = 1-fl_ctx->gsnameno;
        char *n = uint2str(fl_ctx->gsname[fl_ctx->gsnameno]+1, sizeof(fl_ctx->gsname[0])-1, gs->id, 10);
        *(--n) = 'g';
        return n;
    }
#else
    (void)fl_ctx;
#endif
    return ((symbol_t*)ptr(v))->name;
}

// conses ---------------------------------------------------------------------

#ifdef MEMDEBUG2
#define GC_INTERVAL 100000
#endif

void gc(fl_context_t *fl_ctx, int mustgrow);

static value_t mk_cons(fl_context_t *fl_ctx)
{
    cons_t *c;

#ifdef MEMDEBUG2
    if (fl_ctx->n_allocd > GC_INTERVAL)
        gc(fl_ctx, 0);
    c = (cons_t*)((void**)malloc(3*sizeof(void*)) + 1);
    // TODO: if c == NULL
    CHECK_ALIGN8(c);
    ((void**)c)[-1] = fl_ctx->tochain;
    fl_ctx->tochain = c;
    fl_ctx->n_allocd += sizeof(cons_t);
#else
    if (__unlikely(fl_ctx->curheap > fl_ctx->lim))
        gc(fl_ctx, 0);
    c = (cons_t*)fl_ctx->curheap;
    fl_ctx->curheap += sizeof(cons_t);
#endif
    return tagptr(c, TAG_CONS);
}

static value_t *alloc_words(fl_context_t *fl_ctx, int n)
{
    value_t *first;

    assert(n > 0);
    n = LLT_ALIGN(n, 2);   // only allocate multiples of 2 words
#ifdef MEMDEBUG2
    if (fl_ctx->n_allocd > GC_INTERVAL)
        gc(fl_ctx, 0);
    first = (value_t*)malloc((n+1)*sizeof(value_t)) + 1;
    // TODO: if first == NULL
    CHECK_ALIGN8(first);
    first[-1] = (value_t)fl_ctx->tochain;
    fl_ctx->tochain = first;
    fl_ctx->n_allocd += (n*sizeof(value_t));
#else
    if (__unlikely((value_t*)fl_ctx->curheap > ((value_t*)fl_ctx->lim)+2-n)) {
        gc(fl_ctx, 0);
        while ((value_t*)fl_ctx->curheap > ((value_t*)fl_ctx->lim)+2-n) {
            gc(fl_ctx, 1);
        }
    }
    first = (value_t*)fl_ctx->curheap;
    fl_ctx->curheap += (n*sizeof(value_t));
#endif
    return first;
}

// allocate n consecutive conses
#ifndef MEMDEBUG2
#define cons_reserve(fl_ctx, n) tagptr(alloc_words(fl_ctx, (n)*2), TAG_CONS)
#endif

#ifndef MEMDEBUG2
#define cons_index(fl_ctx, c)  (((cons_t*)ptr(c))-((cons_t*)fl_ctx->fromspace))
#endif

#ifdef MEMDEBUG2
#define ismarked(fl_ctx, c)    ((((value_t*)ptr(c))[-1]&1) != 0)
#define mark_cons(fl_ctx, c)   ((((value_t*)ptr(c))[-1]) |= 1)
#define unmark_cons(fl_ctx, c) ((((value_t*)ptr(c))[-1]) &= (~(value_t)1))
#else
#define ismarked(fl_ctx, c)    bitvector_get(fl_ctx->consflags, cons_index(fl_ctx, c))
#define mark_cons(fl_ctx, c)   bitvector_set(fl_ctx->consflags, cons_index(fl_ctx, c), 1)
#define unmark_cons(fl_ctx, c) bitvector_set(fl_ctx->consflags, cons_index(fl_ctx, c), 0)
#endif

value_t alloc_vector(fl_context_t *fl_ctx, size_t n, int init)
{
    if (n == 0) return fl_ctx->the_empty_vector;
    value_t *c = alloc_words(fl_ctx, n+1);
    value_t v = tagptr(c, TAG_VECTOR);
    vector_setsize(v, n);
    if (init) {
        unsigned int i;
        for(i=0; i < n; i++)
            vector_elt(v, i) = FL_UNSPECIFIED(fl_ctx);
    }
    return v;
}

// cvalues --------------------------------------------------------------------

#include "cvalues.c"
#include "types.c"

// print ----------------------------------------------------------------------

static int isnumtok(fl_context_t *fl_ctx, char *tok, value_t *pval);
static inline int symchar(char c);

#include "print.c"

// collector ------------------------------------------------------------------

void fl_gc_handle(fl_context_t *fl_ctx, value_t *pv)
{
    if (fl_ctx->N_GCHND >= FL_N_GC_HANDLES)
        lerror(fl_ctx, fl_ctx->OutOfMemoryError, "out of gc handles");
    fl_ctx->GCHandleStack[fl_ctx->N_GCHND++] = pv;
}

void fl_free_gc_handles(fl_context_t *fl_ctx, uint32_t n)
{
    assert(fl_ctx->N_GCHND >= n);
    fl_ctx->N_GCHND -= n;
}

value_t relocate_lispvalue(fl_context_t *fl_ctx, value_t v)
{
    return relocate(fl_ctx, v);
}

static void trace_globals(fl_context_t *fl_ctx, symbol_t *root)
{
    while (root != NULL) {
        if (root->binding != UNBOUND)
            root->binding = relocate(fl_ctx, root->binding);
        trace_globals(fl_ctx, root->left);
        root = root->right;
    }
}

static value_t relocate(fl_context_t *fl_ctx, value_t v)
{
    value_t a, d, nc, first, *pcdr;
    uintptr_t t = tag(v);

    if (t == TAG_CONS) {
        // iterative implementation allows arbitrarily long cons chains
        pcdr = &first;
        do {
            if ((a=car_(v)) == TAG_FWD) {
                *pcdr = cdr_(v);
                return first;
            }
#ifdef MEMDEBUG2
            *pcdr = nc = mk_cons(fl_ctx);
#else
            *pcdr = nc = tagptr((cons_t*)fl_ctx->curheap, TAG_CONS);
            fl_ctx->curheap += sizeof(cons_t);
#endif
            d = cdr_(v);
            car_(v) = TAG_FWD; cdr_(v) = nc;
            if ((tag(a)&3) == 0 || !ismanaged(fl_ctx, a))
                car_(nc) = a;
            else
                car_(nc) = relocate(fl_ctx, a);
            pcdr = &cdr_(nc);
            v = d;
        } while (iscons(v));
        *pcdr = (d==fl_ctx->NIL) ? fl_ctx->NIL : relocate(fl_ctx, d);
        return first;
    }

    if ((t&3) == 0 || !ismanaged(fl_ctx, v)) return v;
    if (isforwarded(v)) return forwardloc(v);

    if (t == TAG_VECTOR) {
        // N.B.: 0-length vectors secretly have space for a first element
        size_t i, sz = vector_size(v);
        if (vector_elt(v,-1) & 0x1) {
            // grown vector
            nc = relocate(fl_ctx, vector_elt(v,0));
            forward(v, nc);
        }
        else {
            nc = tagptr(alloc_words(fl_ctx, sz+1), TAG_VECTOR);
            vector_setsize(nc, sz);
            a = vector_elt(v,0);
            forward(v, nc);
            if (sz > 0) {
                vector_elt(nc,0) = relocate(fl_ctx, a);
                for(i=1; i < sz; i++) {
                    a = vector_elt(v,i);
                    if ((tag(a)&3) == 0 || !ismanaged(fl_ctx, a))
                        vector_elt(nc,i) = a;
                    else
                        vector_elt(nc,i) = relocate(fl_ctx, a);
                }
            }
        }
        return nc;
    }
    else if (t == TAG_CPRIM) {
        cprim_t *pcp = (cprim_t*)ptr(v);
        size_t nw = CPRIM_NWORDS-1+NWORDS(cp_class(pcp)->size);
        cprim_t *ncp = (cprim_t*)alloc_words(fl_ctx, nw);
        while (nw--)
            ((value_t*)ncp)[nw] = ((value_t*)pcp)[nw];
        nc = tagptr(ncp, TAG_CPRIM);
        forward(v, nc);
        return nc;
    }
    else if (t == TAG_CVALUE) {
        return cvalue_relocate(fl_ctx, v);
    }
    else if (t == TAG_FUNCTION) {
        function_t *fn = (function_t*)ptr(v);
        function_t *nfn = (function_t*)alloc_words(fl_ctx, 4);
        nfn->bcode = fn->bcode;
        nfn->vals = fn->vals;
        nc = tagptr(nfn, TAG_FUNCTION);
        forward(v, nc);
        nfn->env = relocate(fl_ctx, fn->env);
        nfn->vals = relocate(fl_ctx, nfn->vals);
        nfn->bcode = relocate(fl_ctx, nfn->bcode);
        nfn->name = fn->name;
        return nc;
    }
    else if (t == TAG_SYM) {
        gensym_t *gs = (gensym_t*)ptr(v);
        gensym_t *ng = (gensym_t*)alloc_words(fl_ctx, sizeof(gensym_t)/sizeof(void*));
        ng->id = gs->id;
        ng->binding = gs->binding;
        ng->isconst = 0;
        nc = tagptr(ng, TAG_SYM);
        forward(v, nc);
        if (ng->binding != UNBOUND)
            ng->binding = relocate(fl_ctx, ng->binding);
        return nc;
    }
    return v;
}

void gc(fl_context_t *fl_ctx, int mustgrow)
{
    void *temp;
    uint32_t i, f, top;
    fl_readstate_t *rs;
#ifdef MEMDEBUG2
    temp = fl_ctx->tochain;
    fl_ctx->tochain = NULL;
    fl_ctx->n_allocd = -100000000000LL;
#else
    size_t hsz = fl_ctx->gc_grew ? fl_ctx->heapsize*2 : fl_ctx->heapsize;
#ifdef MEMDEBUG
    fl_ctx->tospace = LLT_ALLOC(hsz);
#endif
    fl_ctx->curheap = fl_ctx->tospace;
    fl_ctx->lim = fl_ctx->curheap + hsz - sizeof(cons_t);
#endif

    if (fl_ctx->throwing_frame > fl_ctx->curr_frame) {
        top = fl_ctx->throwing_frame - 3;
        f = fl_ctx->Stack[fl_ctx->throwing_frame-3];
    }
    else {
        top = fl_ctx->SP;
        f = fl_ctx->curr_frame;
    }
    while (1) {
        for (i=f; i < top; i++)
            fl_ctx->Stack[i] = relocate(fl_ctx, fl_ctx->Stack[i]);
        if (f == 0) break;
        top = f - 3;
        f = fl_ctx->Stack[f-3];
    }
    for (i=0; i < fl_ctx->N_GCHND; i++)
        *fl_ctx->GCHandleStack[i] = relocate(fl_ctx, *fl_ctx->GCHandleStack[i]);
    trace_globals(fl_ctx, fl_ctx->symtab);
    relocate_typetable(fl_ctx);
    rs = fl_ctx->readstate;
    while (rs) {
        for(i=0; i < rs->backrefs.size; i++)
            rs->backrefs.table[i] = (void*)relocate(fl_ctx, (value_t)rs->backrefs.table[i]);
        for(i=0; i < rs->gensyms.size; i++)
            rs->gensyms.table[i] = (void*)relocate(fl_ctx, (value_t)rs->gensyms.table[i]);
        rs->source = relocate(fl_ctx, rs->source);
        rs = rs->prev;
    }
    fl_ctx->lasterror = relocate(fl_ctx, fl_ctx->lasterror);
    fl_ctx->memory_exception_value = relocate(fl_ctx, fl_ctx->memory_exception_value);
    fl_ctx->the_empty_vector = relocate(fl_ctx, fl_ctx->the_empty_vector);

    sweep_finalizers(fl_ctx);

#ifdef MEMDEBUG2
    while (temp != NULL) {
        void *next = ((void**)temp)[-1];
        free(&((void**)temp)[-1]);
        temp = next;
    }
    fl_ctx->n_allocd = 0;
#else
#ifdef VERBOSEGC
    printf("GC: found %d/%d live conses\n",
           (fl_ctx->curheap-fl_ctx->tospace)/sizeof(cons_t), fl_ctx->heapsize/sizeof(cons_t));
#endif

    temp = fl_ctx->tospace;
    fl_ctx->tospace = fl_ctx->fromspace;
    fl_ctx->fromspace = (unsigned char*)temp;

    // if we're using > 80% of the space, resize tospace so we have
    // more space to fill next time. if we grew tospace last time,
    // grow the other half of the heap this time to catch up.
    if (fl_ctx->gc_grew || mustgrow
#ifdef MEMDEBUG
        // GC more often
        || ((fl_ctx->lim-fl_ctx->curheap) < (int)(fl_ctx->heapsize/128))
#else
        || ((fl_ctx->lim-fl_ctx->curheap) < (int)(fl_ctx->heapsize/5))
#endif
        ) {
        temp = LLT_REALLOC(fl_ctx->tospace, fl_ctx->heapsize*2);
        if (temp == NULL)
            fl_raise(fl_ctx, fl_ctx->memory_exception_value);
        fl_ctx->tospace = (unsigned char*)temp;
        if (fl_ctx->gc_grew) {
            fl_ctx->heapsize*=2;
            temp = bitvector_resize(fl_ctx->consflags, 0, fl_ctx->heapsize/sizeof(cons_t), 1);
            if (temp == NULL)
                fl_raise(fl_ctx, fl_ctx->memory_exception_value);
            fl_ctx->consflags = (uint32_t*)temp;
        }
        fl_ctx->gc_grew = !fl_ctx->gc_grew;
    }
#ifdef MEMDEBUG
    LLT_FREE(fl_ctx->tospace);
#endif
    if ((value_t*)fl_ctx->curheap > ((value_t*)fl_ctx->lim)-2) {
        // all data was live; gc again and grow heap.
        // but also always leave at least 4 words available, so a closure
        // can be allocated without an extra check.
        gc(fl_ctx, 0);
    }
#endif
}

static void grow_stack(fl_context_t *fl_ctx)
{
    size_t newsz = fl_ctx->N_STACK + (fl_ctx->N_STACK>>1);
    value_t *ns = (value_t*)realloc(fl_ctx->Stack, newsz*sizeof(value_t));
    if (ns == NULL)
        lerror(fl_ctx, fl_ctx->OutOfMemoryError, "stack overflow");
    fl_ctx->Stack = ns;
    fl_ctx->N_STACK = newsz;
}

// utils ----------------------------------------------------------------------

// apply function with n args on the stack
static value_t _applyn(fl_context_t *fl_ctx, uint32_t n)
{
    value_t f = fl_ctx->Stack[fl_ctx->SP-n-1];
    uint32_t saveSP = fl_ctx->SP;
    value_t v;
    if (iscbuiltin(fl_ctx, f)) {
        v = ((builtin_t*)ptr(f))[3](fl_ctx, &fl_ctx->Stack[fl_ctx->SP-n], n);
    }
    else if (isfunction(f)) {
        v = apply_cl(fl_ctx, n);
    }
    else if (isbuiltin(f)) {
        value_t tab = symbol_value(fl_ctx->builtins_table_sym);
        fl_ctx->Stack[fl_ctx->SP-n-1] = vector_elt(tab, uintval(f));
        v = apply_cl(fl_ctx, n);
    }
    else {
        type_error(fl_ctx, "apply", "function", f);
    }
    fl_ctx->SP = saveSP;
    return v;
}

value_t fl_apply(fl_context_t *fl_ctx, value_t f, value_t l)
{
    value_t v = l;
    uint32_t n = fl_ctx->SP;

    PUSH(fl_ctx, f);
    while (iscons(v)) {
        if (fl_ctx->SP >= fl_ctx->N_STACK)
            grow_stack(fl_ctx);
        PUSH(fl_ctx, car_(v));
        v = cdr_(v);
    }
    n = fl_ctx->SP - n - 1;
    v = _applyn(fl_ctx, n);
    POPN(fl_ctx, n+1);
    return v;
}

value_t fl_applyn(fl_context_t *fl_ctx, uint32_t n, value_t f, ...)
{
    va_list ap;
    va_start(ap, f);
    size_t i;

    PUSH(fl_ctx, f);
    while (fl_ctx->SP+n > fl_ctx->N_STACK)
        grow_stack(fl_ctx);
    for(i=0; i < n; i++) {
        value_t a = va_arg(ap, value_t);
        PUSH(fl_ctx, a);
    }
    value_t v = _applyn(fl_ctx, n);
    POPN(fl_ctx, n+1);
    va_end(ap);
    return v;
}

value_t fl_listn(fl_context_t *fl_ctx, size_t n, ...)
{
    va_list ap;
    va_start(ap, n);
    uint32_t si = fl_ctx->SP;
    size_t i;

    while (fl_ctx->SP+n > fl_ctx->N_STACK)
        grow_stack(fl_ctx);
    for(i=0; i < n; i++) {
        value_t a = va_arg(ap, value_t);
        PUSH(fl_ctx, a);
    }
#ifdef MEMDEBUG2
    si = fl_ctx->SP-1;
    value_t l = fl_ctx->NIL;
    for(i=0; i < n; i++) {
        l = fl_cons(fl_ctx, fl_ctx->Stack[si--], l);
    }
    POPN(fl_ctx, n);
    va_end(ap);
    return l;
#else
    cons_t *c = (cons_t*)alloc_words(fl_ctx, n*2);
    cons_t *l = c;
    for(i=0; i < n; i++) {
        c->car = fl_ctx->Stack[si++];
        c->cdr = tagptr(c+1, TAG_CONS);
        c++;
    }
    (c-1)->cdr = fl_ctx->NIL;
    POPN(fl_ctx, n);
    va_end(ap);
    return tagptr(l, TAG_CONS);
#endif
}

value_t fl_list2(fl_context_t *fl_ctx, value_t a, value_t b)
{
    PUSH(fl_ctx, a);
    PUSH(fl_ctx, b);
#ifdef MEMDEBUG2
    fl_ctx->Stack[fl_ctx->SP-1] = fl_cons(fl_ctx, b, fl_ctx->NIL);
    a = fl_cons(fl_ctx, fl_ctx->Stack[fl_ctx->SP-2], fl_ctx->Stack[fl_ctx->SP-1]);
    POPN(fl_ctx, 2);
    return a;
#else
    cons_t *c = (cons_t*)alloc_words(fl_ctx, 4);
    b = POP(fl_ctx);
    a = POP(fl_ctx);
    c[0].car = a;
    c[0].cdr = tagptr(c+1, TAG_CONS);
    c[1].car = b;
    c[1].cdr = fl_ctx->NIL;
    return tagptr(c, TAG_CONS);
#endif
}

value_t fl_cons(fl_context_t *fl_ctx, value_t a, value_t b)
{
    PUSH(fl_ctx, a);
    PUSH(fl_ctx, b);
    value_t c = mk_cons(fl_ctx);
    cdr_(c) = POP(fl_ctx);
    car_(c) = POP(fl_ctx);
    return c;
}

int fl_isnumber(fl_context_t *fl_ctx, value_t v)
{
    if (isfixnum(v)) return 1;
    if (iscprim(v)) {
        cprim_t *c = (cprim_t*)ptr(v);
        return c->type != fl_ctx->wchartype;
    }
    return 0;
}

// read -----------------------------------------------------------------------

#include "read.c"

// equal ----------------------------------------------------------------------

#include "equal.c"

// eval -----------------------------------------------------------------------

#define list(fl_ctx, a,n) _list(fl_ctx, (a), (n), 0)

static value_t _list(fl_context_t *fl_ctx, value_t *args, uint32_t nargs, int star)
{
    cons_t *c;
    int i;
    value_t v;
#ifdef MEMDEBUG2
    value_t n;
    i = nargs-1;
    if (star) {
        n = mk_cons(fl_ctx);
        c = (cons_t*)ptr(n);
        c->car = args[i-1];
        c->cdr = args[i];
        i -= 2;
        v = n;
    }
    else {
        v = fl_ctx->NIL;
    }
    PUSH(fl_ctx, v);
    for(; i >= 0; i--) {
        n = mk_cons(fl_ctx);
        c = (cons_t*)ptr(n);
        c->car = args[i];
        c->cdr = fl_ctx->Stack[fl_ctx->SP-1];
        fl_ctx->Stack[fl_ctx->SP-1] = n;
    }
    v = POP(fl_ctx);
#else
    v = cons_reserve(fl_ctx, nargs);
    c = (cons_t*)ptr(v);
    for(i=0; i < nargs; i++) {
        c->car = args[i];
        c->cdr = tagptr(c+1, TAG_CONS);
        c++;
    }
    if (star)
        (c-2)->cdr = (c-1)->car;
    else
        (c-1)->cdr = fl_ctx->NIL;
#endif
    return v;
}

static value_t copy_list(fl_context_t *fl_ctx, value_t L)
{
    if (!iscons(L))
        return fl_ctx->NIL;
    PUSH(fl_ctx, fl_ctx->NIL);
    PUSH(fl_ctx, L);
    value_t *plcons = &fl_ctx->Stack[fl_ctx->SP-2];
    value_t *pL = &fl_ctx->Stack[fl_ctx->SP-1];
    value_t c;
    c = mk_cons(fl_ctx); PUSH(fl_ctx, c);  // save first cons
    car_(c) = car_(*pL);
    cdr_(c) = fl_ctx->NIL;
    *plcons = c;
    *pL = cdr_(*pL);
    while (iscons(*pL)) {
        c = mk_cons(fl_ctx);
        car_(c) = car_(*pL);
        cdr_(c) = fl_ctx->NIL;
        cdr_(*plcons) = c;
        *plcons = c;
        *pL = cdr_(*pL);
    }
    c = POP(fl_ctx);  // first cons
    POPN(fl_ctx, 2);
    return c;
}

static value_t do_trycatch(fl_context_t *fl_ctx)
{
    uint32_t saveSP = fl_ctx->SP;
    value_t v;
    value_t thunk = fl_ctx->Stack[fl_ctx->SP-2];
    fl_ctx->Stack[fl_ctx->SP-2] = fl_ctx->Stack[fl_ctx->SP-1];
    fl_ctx->Stack[fl_ctx->SP-1] = thunk;

    FL_TRY(fl_ctx) {
        v = apply_cl(fl_ctx, 0);
    }
    FL_CATCH(fl_ctx) {
        v = fl_ctx->Stack[saveSP-2];
        PUSH(fl_ctx, v);
        PUSH(fl_ctx, fl_ctx->lasterror);
        v = apply_cl(fl_ctx, 1);
    }
    fl_ctx->SP = saveSP;
    return v;
}

/*
  argument layout on stack is
  |--required args--|--opt args--|--kw args--|--rest args...
*/
static uint32_t process_keys(fl_context_t *fl_ctx, value_t kwtable,
                             uint32_t nreq, uint32_t nkw, uint32_t nopt,
                             uint32_t bp, uint32_t nargs, int va)
{
    uintptr_t n;
    uint32_t extr = nopt+nkw;
    uint32_t ntot = nreq+extr;
    value_t *args = (value_t*)alloca(extr*sizeof(value_t));
    value_t v;
    uint32_t i, a = 0, nrestargs;
    value_t s1 = fl_ctx->Stack[fl_ctx->SP-1];
    value_t s3 = fl_ctx->Stack[fl_ctx->SP-3];
    value_t s4 = fl_ctx->Stack[fl_ctx->SP-4];
    if (nargs < nreq)
        lerror(fl_ctx, fl_ctx->ArgError, "apply: too few arguments");
    for (i=0; i < extr; i++) args[i] = UNBOUND;
    for (i=nreq; i < nargs; i++) {
        v = fl_ctx->Stack[bp+i];
        if (issymbol(v) && iskeyword((symbol_t*)ptr(v)))
            break;
        if (a >= nopt)
            goto no_kw;
        args[a++] = v;
    }
    if (i >= nargs) goto no_kw;
    // now process keywords
    n = vector_size(kwtable)/2;
    do {
        i++;
        if (i >= nargs)
            lerrorf(fl_ctx, fl_ctx->ArgError, "keyword %s requires an argument",
                    symbol_name(fl_ctx, v));
        value_t hv = fixnum(((symbol_t*)ptr(v))->hash);
        uintptr_t x = 2*(labs(numval(hv)) % n);
        if (vector_elt(kwtable, x) == v) {
            uintptr_t idx = numval(vector_elt(kwtable, x+1));
            assert(idx < nkw);
            idx += nopt;
            if (args[idx] == UNBOUND) {
                // if duplicate key, keep first value
                args[idx] = fl_ctx->Stack[bp+i];
            }
        }
        else {
            lerrorf(fl_ctx, fl_ctx->ArgError, "unsupported keyword %s", symbol_name(fl_ctx, v));
        }
        i++;
        if (i >= nargs) break;
        v = fl_ctx->Stack[bp+i];
    } while (issymbol(v) && iskeyword((symbol_t*)ptr(v)));
 no_kw:
    nrestargs = nargs - i;
    if (!va && nrestargs > 0)
        lerror(fl_ctx, fl_ctx->ArgError, "apply: too many arguments");
    nargs = ntot + nrestargs;
    if (nrestargs)
        memmove(&fl_ctx->Stack[bp+ntot], &fl_ctx->Stack[bp+i], nrestargs*sizeof(value_t));
    memcpy(&fl_ctx->Stack[bp+nreq], args, extr*sizeof(value_t));
    fl_ctx->SP = bp + nargs;
    assert(fl_ctx->SP < fl_ctx->N_STACK-4);
    PUSH(fl_ctx, s4);
    PUSH(fl_ctx, s3);
    PUSH(fl_ctx, nargs);
    PUSH(fl_ctx, s1);
    fl_ctx->curr_frame = fl_ctx->SP;
    return nargs;
}

#if BYTE_ORDER == BIG_ENDIAN
#define GET_INT32(a)                            \
    ((int32_t)                                  \
    ((((int32_t)a[0])<<0)  |                    \
     (((int32_t)a[1])<<8)  |                    \
     (((int32_t)a[2])<<16) |                    \
     (((int32_t)a[3])<<24)))
#define GET_INT16(a)                            \
    ((int16_t)                                  \
    ((((int16_t)a[0])<<0)  |                    \
     (((int16_t)a[1])<<8)))
#define PUT_INT32(a,i) jl_store_unaligned_i32((void*)a,
    (uint32_t)bswap_32((int32_t)(i)))
#else
#define GET_INT32(a) (int32_t)jl_load_unaligned_i32((void*)a)
#define GET_INT16(a) (int16_t)jl_load_unaligned_i16((void*)a)
#define PUT_INT32(a,i) jl_store_unaligned_i32((void*)a, (uint32_t)(i))
#endif
#define SWAP_INT32(a) (*(int32_t*)(a) = bswap_32(*(int32_t*)(a)))
#define SWAP_INT16(a) (*(int16_t*)(a) = bswap_16(*(int16_t*)(a)))

#ifdef USE_COMPUTED_GOTO
#define OP(x) L_##x:
#define NEXT_OP goto *vm_labels[*ip++]
#else
#define OP(x) case x:
#define NEXT_OP goto next_op
#endif

/*
  stack on entry: <func>  <nargs args...>
  caller's responsibility:
  - put the stack in this state
  - provide arg count
  - respect tail position
  - restore fl_ctx->SP

  callee's responsibility:
  - check arg counts
  - allocate vararg array
  - push closed env, set up new environment
*/
JL_EXTENSION static value_t apply_cl(fl_context_t *fl_ctx, uint32_t nargs)
{
    VM_LABELS;
    VM_APPLY_LABELS;
    uint32_t top_frame = fl_ctx->curr_frame;
    // frame variables
    uint32_t n=0;
    uint32_t bp;
    const uint8_t *ip;
    fixnum_t s, hi;

    // temporary variables (not necessary to preserve across calls)
#ifndef USE_COMPUTED_GOTO
    uint32_t op;
#endif
    uint32_t i;
    symbol_t *sym;
#define fl_apply_c fl_ctx->apply_c
#define fl_apply_pv fl_ctx->apply_pv
#define fl_apply_accum fl_ctx->apply_accum
#define fl_apply_func fl_ctx->apply_func
#define fl_apply_v fl_ctx->apply_v
#define fl_apply_e fl_ctx->apply_e

 apply_cl_top:
    fl_apply_func = fl_ctx->Stack[fl_ctx->SP-nargs-1];
    ip = (uint8_t*)cv_data((cvalue_t*)ptr(fn_bcode(fl_apply_func)));
#ifndef MEMDEBUG2
    assert(!ismanaged(fl_ctx, (uintptr_t)ip));
#endif
    while (fl_ctx->SP+GET_INT32(ip) > fl_ctx->N_STACK) {
        grow_stack(fl_ctx);
    }
    ip += 4;

    bp = fl_ctx->SP-nargs;
    PUSH(fl_ctx, fn_env(fl_apply_func));
    PUSH(fl_ctx, fl_ctx->curr_frame);
    PUSH(fl_ctx, nargs);
    fl_ctx->SP++;//PUSH(fl_ctx, 0); //ip
    fl_ctx->curr_frame = fl_ctx->SP;

    {
#ifdef USE_COMPUTED_GOTO
    {
        NEXT_OP;
#else
    next_op:
        op = *ip++;
    dispatch:
        switch (op) {
#endif
        OP(OP_ARGC)
            n = *ip++;
        do_argc:
            if (nargs != n) {
                if (nargs > n)
                    lerror(fl_ctx, fl_ctx->ArgError, "apply: too many arguments");
                else
                    lerror(fl_ctx, fl_ctx->ArgError, "apply: too few arguments");
            }
            NEXT_OP;
        OP(OP_VARGC)
            i = *ip++;
        do_vargc:
            s = (fixnum_t)nargs - (fixnum_t)i;
            if (s > 0) {
                fl_apply_v = list(fl_ctx, &fl_ctx->Stack[bp+i], s);
                fl_ctx->Stack[bp+i] = fl_apply_v;
                if (s > 1) {
                    fl_ctx->Stack[bp+i+1] = fl_ctx->Stack[bp+nargs+0];
                    fl_ctx->Stack[bp+i+2] = fl_ctx->Stack[bp+nargs+1];
                    fl_ctx->Stack[bp+i+3] = i+1;
                    fl_ctx->Stack[bp+i+4] = 0;
                    fl_ctx->SP =  bp+i+5;
                    fl_ctx->curr_frame = fl_ctx->SP;
                }
            }
            else if (s < 0) {
                lerror(fl_ctx, fl_ctx->ArgError, "apply: too few arguments");
            }
            else {
                fl_ctx->SP++;
                fl_ctx->Stack[fl_ctx->SP-2] = i+1;
                fl_ctx->Stack[fl_ctx->SP-3] = fl_ctx->Stack[fl_ctx->SP-4];
                fl_ctx->Stack[fl_ctx->SP-4] = fl_ctx->Stack[fl_ctx->SP-5];
                fl_ctx->Stack[fl_ctx->SP-5] = fl_ctx->NIL;
                fl_ctx->curr_frame = fl_ctx->SP;
            }
            nargs = i+1;
            NEXT_OP;
        OP(OP_LARGC)
            n = GET_INT32(ip); ip+=4;
            goto do_argc;
        OP(OP_LVARGC)
            i = GET_INT32(ip); ip+=4;
            goto do_vargc;
        OP(OP_BRBOUND)
            i = GET_INT32(ip); ip+=4;
            fl_apply_v = fl_ctx->Stack[bp+i];
            if (fl_apply_v != UNBOUND) PUSH(fl_ctx, fl_ctx->T);
            else PUSH(fl_ctx, fl_ctx->F);
            NEXT_OP;
        OP(OP_DUP) fl_ctx->SP++; fl_ctx->Stack[fl_ctx->SP-1] = fl_ctx->Stack[fl_ctx->SP-2]; NEXT_OP;
        OP(OP_POP) POPN(fl_ctx, 1); NEXT_OP;
        OP(OP_TCALL)
            n = *ip++;  // nargs
        do_tcall:
            fl_apply_func = fl_ctx->Stack[fl_ctx->SP-n-1];
            if (tag(fl_apply_func) == TAG_FUNCTION) {
                if (fl_apply_func > (N_BUILTINS<<3)) {
                    fl_ctx->curr_frame = fl_ctx->Stack[fl_ctx->curr_frame-3];
                    for(s=-1; s < (fixnum_t)n; s++)
                        fl_ctx->Stack[bp+s] = fl_ctx->Stack[fl_ctx->SP-n+s];
                    fl_ctx->SP = bp+n;
                    nargs = n;
                    goto apply_cl_top;
                }
                else {
                    i = uintval(fl_apply_func);
                    if (i <= OP_ASET) {
                        s = builtin_arg_counts[i];
                        if (s >= 0)
                            argcount(fl_ctx, builtin_names[i], n, s);
                        else if (s != ANYARGS && (signed)n < -s)
                            argcount(fl_ctx, builtin_names[i], n, -s);
                        // remove function arg
                        for(s=fl_ctx->SP-n-1; s < (int)fl_ctx->SP-1; s++)
                            fl_ctx->Stack[s] = fl_ctx->Stack[s+1];
                        fl_ctx->SP--;
#ifdef USE_COMPUTED_GOTO
                        if (i == OP_APPLY)
                            goto apply_tapply;
                        goto *vm_apply_labels[i];
#else
                        switch (i) {
                        case OP_LIST:   goto apply_list;
                        case OP_VECTOR: goto apply_vector;
                        case OP_APPLY:  goto apply_tapply;
                        case OP_ADD:    goto apply_add;
                        case OP_SUB:    goto apply_sub;
                        case OP_MUL:    goto apply_mul;
                        case OP_DIV:    goto apply_div;
                        default:
                            op = (uint8_t)i;
                            goto dispatch;
                        }
#endif
                    }
                }
            }
            else if (iscbuiltin(fl_ctx, fl_apply_func)) {
                s = fl_ctx->SP;
                fl_apply_v = ((builtin_t)(uintptr_t)(((void**)ptr(fl_apply_func))[3]))(fl_ctx, &fl_ctx->Stack[fl_ctx->SP-n], n);
                fl_ctx->SP = s-n;
                fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
                NEXT_OP;
            }
            type_error(fl_ctx, "apply", "function", fl_apply_func);
        // WARNING: repeated code ahead
        OP(OP_CALL)
            n = *ip++;  // nargs
        do_call:
            fl_apply_func = fl_ctx->Stack[fl_ctx->SP-n-1];
            if (tag(fl_apply_func) == TAG_FUNCTION) {
                if (fl_apply_func > (N_BUILTINS<<3)) {
                    fl_ctx->Stack[fl_ctx->curr_frame-1] = (uintptr_t)ip;
                    nargs = n;
                    goto apply_cl_top;
                }
                else {
                    i = uintval(fl_apply_func);
                    if (i <= OP_ASET) {
                        s = builtin_arg_counts[i];
                        if (s >= 0)
                            argcount(fl_ctx, builtin_names[i], n, s);
                        else if (s != ANYARGS && (signed)n < -s)
                            argcount(fl_ctx, builtin_names[i], n, -s);
                        // remove function arg
                        for(s=fl_ctx->SP-n-1; s < (int)fl_ctx->SP-1; s++)
                            fl_ctx->Stack[s] = fl_ctx->Stack[s+1];
                        fl_ctx->SP--;
#ifdef USE_COMPUTED_GOTO
                        goto *vm_apply_labels[i];
#else
                        switch (i) {
                        case OP_LIST:   goto apply_list;
                        case OP_VECTOR: goto apply_vector;
                        case OP_APPLY:  goto apply_apply;
                        case OP_ADD:    goto apply_add;
                        case OP_SUB:    goto apply_sub;
                        case OP_MUL:    goto apply_mul;
                        case OP_DIV:    goto apply_div;
                        default:
                            op = (uint8_t)i;
                            goto dispatch;
                        }
#endif
                    }
                }
            }
            else if (iscbuiltin(fl_ctx, fl_apply_func)) {
                s = fl_ctx->SP;
                fl_apply_v = ((builtin_t)(uintptr_t)(((void**)ptr(fl_apply_func))[3]))(fl_ctx, &fl_ctx->Stack[fl_ctx->SP-n], n);
                fl_ctx->SP = s-n;
                fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
                NEXT_OP;
            }
            type_error(fl_ctx, "apply", "function", fl_apply_func);
        OP(OP_TCALLL) n = GET_INT32(ip); ip+=4; goto do_tcall;
        OP(OP_CALLL)  n = GET_INT32(ip); ip+=4; goto do_call;
        OP(OP_JMP) ip += (intptr_t)GET_INT16(ip); NEXT_OP;
        OP(OP_BRF)
            fl_apply_v = POP(fl_ctx);
            if (fl_apply_v == fl_ctx->F) ip += (intptr_t)GET_INT16(ip);
            else ip += 2;
            NEXT_OP;
        OP(OP_BRT)
            fl_apply_v = POP(fl_ctx);
            if (fl_apply_v != fl_ctx->F) ip += (intptr_t)GET_INT16(ip);
            else ip += 2;
            NEXT_OP;
        OP(OP_JMPL) ip += (intptr_t)GET_INT32(ip); NEXT_OP;
        OP(OP_BRFL)
            fl_apply_v = POP(fl_ctx);
            if (fl_apply_v == fl_ctx->F) ip += (intptr_t)GET_INT32(ip);
            else ip += 4;
            NEXT_OP;
        OP(OP_BRTL)
            fl_apply_v = POP(fl_ctx);
            if (fl_apply_v != fl_ctx->F) ip += (intptr_t)GET_INT32(ip);
            else ip += 4;
            NEXT_OP;
        OP(OP_BRNE)
            if (fl_ctx->Stack[fl_ctx->SP-2] != fl_ctx->Stack[fl_ctx->SP-1]) ip += (intptr_t)GET_INT16(ip);
            else ip += 2;
            POPN(fl_ctx, 2);
            NEXT_OP;
        OP(OP_BRNEL)
            if (fl_ctx->Stack[fl_ctx->SP-2] != fl_ctx->Stack[fl_ctx->SP-1]) ip += (intptr_t)GET_INT32(ip);
            else ip += 4;
            POPN(fl_ctx, 2);
            NEXT_OP;
        OP(OP_BRNN)
            fl_apply_v = POP(fl_ctx);
            if (fl_apply_v != fl_ctx->NIL) ip += (intptr_t)GET_INT16(ip);
            else ip += 2;
            NEXT_OP;
        OP(OP_BRNNL)
            fl_apply_v = POP(fl_ctx);
            if (fl_apply_v != fl_ctx->NIL) ip += (intptr_t)GET_INT32(ip);
            else ip += 4;
            NEXT_OP;
        OP(OP_BRN)
            fl_apply_v = POP(fl_ctx);
            if (fl_apply_v == fl_ctx->NIL) ip += (intptr_t)GET_INT16(ip);
            else ip += 2;
            NEXT_OP;
        OP(OP_BRNL)
            fl_apply_v = POP(fl_ctx);
            if (fl_apply_v == fl_ctx->NIL) ip += (intptr_t)GET_INT32(ip);
            else ip += 4;
            NEXT_OP;
        OP(OP_RET)
            fl_apply_v = POP(fl_ctx);
            fl_ctx->SP = fl_ctx->curr_frame;
            fl_ctx->curr_frame = fl_ctx->Stack[fl_ctx->SP-3];
            if (fl_ctx->curr_frame == top_frame) return fl_apply_v;
            fl_ctx->SP -= (4+nargs);
            ip = (uint8_t*)fl_ctx->Stack[fl_ctx->curr_frame-1];
            nargs        = fl_ctx->Stack[fl_ctx->curr_frame-2];
            bp           = fl_ctx->curr_frame - 4 - nargs;
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;

        OP(OP_EQ)
            fl_ctx->Stack[fl_ctx->SP-2] = ((fl_ctx->Stack[fl_ctx->SP-2] == fl_ctx->Stack[fl_ctx->SP-1]) ? fl_ctx->T : fl_ctx->F);
            POPN(fl_ctx, 1); NEXT_OP;
        OP(OP_EQV)
            if (fl_ctx->Stack[fl_ctx->SP-2] == fl_ctx->Stack[fl_ctx->SP-1]) {
                fl_apply_v = fl_ctx->T;
            }
            else if (!leafp(fl_ctx->Stack[fl_ctx->SP-2]) || !leafp(fl_ctx->Stack[fl_ctx->SP-1])) {
                fl_apply_v = fl_ctx->F;
            }
            else {
                fl_apply_v = (compare_(fl_ctx, fl_ctx->Stack[fl_ctx->SP-2], fl_ctx->Stack[fl_ctx->SP-1], 1)==0 ? fl_ctx->T : fl_ctx->F);
            }
            fl_ctx->Stack[fl_ctx->SP-2] = fl_apply_v; POPN(fl_ctx, 1);
            NEXT_OP;
        OP(OP_EQUAL)
            if (fl_ctx->Stack[fl_ctx->SP-2] == fl_ctx->Stack[fl_ctx->SP-1]) {
                fl_apply_v = fl_ctx->T;
            }
            else {
                fl_apply_v = (compare_(fl_ctx, fl_ctx->Stack[fl_ctx->SP-2], fl_ctx->Stack[fl_ctx->SP-1], 1)==0 ? fl_ctx->T : fl_ctx->F);
            }
            fl_ctx->Stack[fl_ctx->SP-2] = fl_apply_v; POPN(fl_ctx, 1);
            NEXT_OP;
        OP(OP_PAIRP)
            fl_ctx->Stack[fl_ctx->SP-1] = (iscons(fl_ctx->Stack[fl_ctx->SP-1]) ? fl_ctx->T : fl_ctx->F); NEXT_OP;
        OP(OP_ATOMP)
            fl_ctx->Stack[fl_ctx->SP-1] = (iscons(fl_ctx->Stack[fl_ctx->SP-1]) ? fl_ctx->F : fl_ctx->T); NEXT_OP;
        OP(OP_NOT)
            fl_ctx->Stack[fl_ctx->SP-1] = ((fl_ctx->Stack[fl_ctx->SP-1]==fl_ctx->F) ? fl_ctx->T : fl_ctx->F); NEXT_OP;
        OP(OP_NULLP)
            fl_ctx->Stack[fl_ctx->SP-1] = ((fl_ctx->Stack[fl_ctx->SP-1]==fl_ctx->NIL) ? fl_ctx->T : fl_ctx->F); NEXT_OP;
        OP(OP_BOOLEANP)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
            fl_ctx->Stack[fl_ctx->SP-1] = ((fl_apply_v == fl_ctx->T || fl_apply_v == fl_ctx->F) ? fl_ctx->T:fl_ctx->F); NEXT_OP;
        OP(OP_SYMBOLP)
            fl_ctx->Stack[fl_ctx->SP-1] = (issymbol(fl_ctx->Stack[fl_ctx->SP-1]) ? fl_ctx->T : fl_ctx->F); NEXT_OP;
        OP(OP_NUMBERP)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
            fl_ctx->Stack[fl_ctx->SP-1] = (fl_isnumber(fl_ctx, fl_apply_v) ? fl_ctx->T:fl_ctx->F); NEXT_OP;
        OP(OP_FIXNUMP)
            fl_ctx->Stack[fl_ctx->SP-1] = (isfixnum(fl_ctx->Stack[fl_ctx->SP-1]) ? fl_ctx->T : fl_ctx->F); NEXT_OP;
        OP(OP_BOUNDP)
            sym = tosymbol(fl_ctx, fl_ctx->Stack[fl_ctx->SP-1], "bound?");
            fl_ctx->Stack[fl_ctx->SP-1] = ((sym->binding == UNBOUND) ? fl_ctx->F : fl_ctx->T);
            NEXT_OP;
        OP(OP_BUILTINP)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
        fl_ctx->Stack[fl_ctx->SP-1] = (isbuiltin(fl_apply_v) || iscbuiltin(fl_ctx, fl_apply_v)) ? fl_ctx->T : fl_ctx->F;
            NEXT_OP;
        OP(OP_FUNCTIONP)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
            fl_ctx->Stack[fl_ctx->SP-1] = ((tag(fl_apply_v)==TAG_FUNCTION &&
                            (uintval(fl_apply_v)<=OP_ASET || fl_apply_v>(N_BUILTINS<<3))) ||
                                 iscbuiltin(fl_ctx, fl_apply_v)) ? fl_ctx->T : fl_ctx->F;
            NEXT_OP;
        OP(OP_VECTORP)
            fl_ctx->Stack[fl_ctx->SP-1] = (isvector(fl_ctx->Stack[fl_ctx->SP-1]) ? fl_ctx->T : fl_ctx->F); NEXT_OP;

        OP(OP_CONS)
#ifdef MEMDEBUG2
            fl_apply_c = (cons_t*)ptr(mk_cons(fl_ctx));
#else
            if (fl_ctx->curheap > fl_ctx->lim)
                gc(fl_ctx, 0);
            fl_apply_c = (cons_t*)fl_ctx->curheap;
            fl_ctx->curheap += sizeof(cons_t);
#endif
            fl_apply_c->car = fl_ctx->Stack[fl_ctx->SP-2];
            fl_apply_c->cdr = fl_ctx->Stack[fl_ctx->SP-1];
            fl_ctx->Stack[fl_ctx->SP-2] = tagptr(fl_apply_c, TAG_CONS);
            POPN(fl_ctx, 1); NEXT_OP;
        OP(OP_CAR)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
            if (!iscons(fl_apply_v)) type_error(fl_ctx, "car", "cons", fl_apply_v);
            fl_ctx->Stack[fl_ctx->SP-1] = car_(fl_apply_v);
            NEXT_OP;
        OP(OP_CDR)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
            if (!iscons(fl_apply_v)) type_error(fl_ctx, "cdr", "cons", fl_apply_v);
            fl_ctx->Stack[fl_ctx->SP-1] = cdr_(fl_apply_v);
            NEXT_OP;
        OP(OP_CADR)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
            if (!iscons(fl_apply_v)) type_error(fl_ctx, "cdr", "cons", fl_apply_v);
            fl_apply_v = cdr_(fl_apply_v);
            if (!iscons(fl_apply_v)) type_error(fl_ctx, "car", "cons", fl_apply_v);
            fl_ctx->Stack[fl_ctx->SP-1] = car_(fl_apply_v);
            NEXT_OP;
        OP(OP_SETCAR)
            car(fl_ctx, fl_ctx->Stack[fl_ctx->SP-2]) = fl_ctx->Stack[fl_ctx->SP-1];
            POPN(fl_ctx, 1); NEXT_OP;
        OP(OP_SETCDR)
            cdr(fl_ctx, fl_ctx->Stack[fl_ctx->SP-2]) = fl_ctx->Stack[fl_ctx->SP-1];
            POPN(fl_ctx, 1); NEXT_OP;
        OP(OP_LIST)
            n = *ip++;
        apply_list:
            if (n > 0) {
                fl_apply_v = list(fl_ctx, &fl_ctx->Stack[fl_ctx->SP-n], n);
                POPN(fl_ctx, n);
                PUSH(fl_ctx, fl_apply_v);
            }
            else {
                PUSH(fl_ctx, fl_ctx->NIL);
            }
            NEXT_OP;

        OP(OP_TAPPLY)
            n = *ip++;
        apply_tapply:
            fl_apply_v = POP(fl_ctx);     // arglist
            n = fl_ctx->SP-(n-2);  // n-2 == # leading arguments not in the list
            while (iscons(fl_apply_v)) {
                if (fl_ctx->SP >= fl_ctx->N_STACK)
                    grow_stack(fl_ctx);
                PUSH(fl_ctx, car_(fl_apply_v));
                fl_apply_v = cdr_(fl_apply_v);
            }
            n = fl_ctx->SP-n;
            goto do_tcall;
        OP(OP_APPLY)
            n = *ip++;
        apply_apply:
            fl_apply_v = POP(fl_ctx);     // arglist
            n = fl_ctx->SP-(n-2);  // n-2 == # leading arguments not in the list
            while (iscons(fl_apply_v)) {
                if (fl_ctx->SP >= fl_ctx->N_STACK)
                    grow_stack(fl_ctx);
                PUSH(fl_ctx, car_(fl_apply_v));
                fl_apply_v = cdr_(fl_apply_v);
            }
            n = fl_ctx->SP-n;
            goto do_call;

        OP(OP_ADD)
            n = *ip++;
        apply_add:
            s = 0;
            i = fl_ctx->SP-n;
            for (; i < fl_ctx->SP; i++) {
                if (isfixnum(fl_ctx->Stack[i])) {
                    s += numval(fl_ctx->Stack[i]);
                    if (!fits_fixnum(s)) {
                        i++;
                        goto add_ovf;
                    }
                }
                else {
                add_ovf:
                    fl_apply_v = fl_add_any(fl_ctx, &fl_ctx->Stack[i], fl_ctx->SP-i, s);
                    break;
                }
            }
            if (i==fl_ctx->SP)
                fl_apply_v = fixnum(s);
            POPN(fl_ctx, n);
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;
        OP(OP_ADD2)
            if (bothfixnums(fl_ctx->Stack[fl_ctx->SP-1], fl_ctx->Stack[fl_ctx->SP-2])) {
                s = numval(fl_ctx->Stack[fl_ctx->SP-1]) + numval(fl_ctx->Stack[fl_ctx->SP-2]);
                if (fits_fixnum(s))
                    fl_apply_v = fixnum(s);
                else
                    fl_apply_v = mk_ptrdiff(fl_ctx, s);
            }
            else {
                fl_apply_v = fl_add_any(fl_ctx, &fl_ctx->Stack[fl_ctx->SP-2], 2, 0);
            }
            POPN(fl_ctx, 1);
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;
        OP(OP_SUB)
            n = *ip++;
        apply_sub:
            if (n == 2) goto do_sub2;
            if (n == 1) goto do_neg;
            i = fl_ctx->SP-n;
            // we need to pass the full arglist on to fl_add_any
            // so it can handle rest args properly
            PUSH(fl_ctx, fl_ctx->Stack[i]);
            fl_ctx->Stack[i] = fixnum(0);
            fl_ctx->Stack[i+1] = fl_neg(fl_ctx, fl_add_any(fl_ctx, &fl_ctx->Stack[i], n, 0));
            fl_ctx->Stack[i] = POP(fl_ctx);
            fl_apply_v = fl_add_any(fl_ctx, &fl_ctx->Stack[i], 2, 0);
            POPN(fl_ctx, n);
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;
        OP(OP_NEG)
        do_neg:
            if (isfixnum(fl_ctx->Stack[fl_ctx->SP-1]))
                fl_ctx->Stack[fl_ctx->SP-1] = fixnum(-numval(fl_ctx->Stack[fl_ctx->SP-1]));
            else
                fl_ctx->Stack[fl_ctx->SP-1] = fl_neg(fl_ctx, fl_ctx->Stack[fl_ctx->SP-1]);
            NEXT_OP;
        OP(OP_SUB2)
        do_sub2:
            if (bothfixnums(fl_ctx->Stack[fl_ctx->SP-2], fl_ctx->Stack[fl_ctx->SP-1])) {
                s = numval(fl_ctx->Stack[fl_ctx->SP-2]) - numval(fl_ctx->Stack[fl_ctx->SP-1]);
                if (fits_fixnum(s))
                    fl_apply_v = fixnum(s);
                else
                    fl_apply_v = mk_ptrdiff(fl_ctx, s);
            }
            else {
                fl_ctx->Stack[fl_ctx->SP-1] = fl_neg(fl_ctx, fl_ctx->Stack[fl_ctx->SP-1]);
                fl_apply_v = fl_add_any(fl_ctx, &fl_ctx->Stack[fl_ctx->SP-2], 2, 0);
            }
            POPN(fl_ctx, 1);
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;
        OP(OP_MUL)
            n = *ip++;
        apply_mul:
            fl_apply_accum = 1;
            i = fl_ctx->SP-n;
            for (; i < fl_ctx->SP; i++) {
                if (isfixnum(fl_ctx->Stack[i])) {
                    fl_apply_accum *= numval(fl_ctx->Stack[i]);
                }
                else {
                    fl_apply_v = fl_mul_any(fl_ctx, &fl_ctx->Stack[i], fl_ctx->SP-i, fl_apply_accum);
                    break;
                }
            }
            if (i == fl_ctx->SP) {
                if (fits_fixnum(fl_apply_accum))
                    fl_apply_v = fixnum(fl_apply_accum);
                else
                    fl_apply_v = return_from_int64(fl_ctx, fl_apply_accum);
            }
            POPN(fl_ctx, n);
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;
        OP(OP_DIV)
            n = *ip++;
        apply_div:
            i = fl_ctx->SP-n;
            if (n == 1) {
                fl_ctx->Stack[fl_ctx->SP-1] = fl_div2(fl_ctx, fixnum(1), fl_ctx->Stack[i]);
            }
            else {
                if (n > 2) {
                    PUSH(fl_ctx, fl_ctx->Stack[i]);
                    fl_ctx->Stack[i] = fixnum(1);
                    fl_ctx->Stack[i+1] = fl_mul_any(fl_ctx, &fl_ctx->Stack[i], n, 1);
                    fl_ctx->Stack[i] = POP(fl_ctx);
                }
                fl_apply_v = fl_div2(fl_ctx, fl_ctx->Stack[i], fl_ctx->Stack[i+1]);
                POPN(fl_ctx, n);
                PUSH(fl_ctx, fl_apply_v);
            }
            NEXT_OP;
        OP(OP_IDIV)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-2]; fl_apply_e = fl_ctx->Stack[fl_ctx->SP-1];
            if (bothfixnums(fl_apply_v, fl_apply_e)) {
                if (fl_apply_e==0) DivideByZeroError(fl_ctx);
                fl_apply_v = fixnum(numval(fl_apply_v) / numval(fl_apply_e));
            }
            else
                fl_apply_v = fl_idiv2(fl_ctx, fl_apply_v, fl_apply_e);
            POPN(fl_ctx, 1);
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;
        OP(OP_NUMEQ)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-2]; fl_apply_e = fl_ctx->Stack[fl_ctx->SP-1];
            if (bothfixnums(fl_apply_v, fl_apply_e))
                fl_apply_v = (fl_apply_v == fl_apply_e) ? fl_ctx->T : fl_ctx->F;
            else
                fl_apply_v = (!numeric_compare(fl_ctx,fl_apply_v,fl_apply_e,1,0,"=")) ? fl_ctx->T : fl_ctx->F;
            POPN(fl_ctx, 1);
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;
        OP(OP_LT)
            if (bothfixnums(fl_ctx->Stack[fl_ctx->SP-2], fl_ctx->Stack[fl_ctx->SP-1])) {
                fl_apply_v = (numval(fl_ctx->Stack[fl_ctx->SP-2]) < numval(fl_ctx->Stack[fl_ctx->SP-1])) ? fl_ctx->T : fl_ctx->F;
            }
            else {
                fl_apply_v = (numval(fl_compare(fl_ctx, fl_ctx->Stack[fl_ctx->SP-2], fl_ctx->Stack[fl_ctx->SP-1])) < 0) ?
                    fl_ctx->T : fl_ctx->F;
            }
            POPN(fl_ctx, 1);
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;
        OP(OP_COMPARE)
            fl_ctx->Stack[fl_ctx->SP-2] = compare_(fl_ctx, fl_ctx->Stack[fl_ctx->SP-2], fl_ctx->Stack[fl_ctx->SP-1], 0);
            POPN(fl_ctx, 1);
            NEXT_OP;

        OP(OP_VECTOR)
            n = *ip++;
        apply_vector:
            fl_apply_v = alloc_vector(fl_ctx, n, 0);
            if (n) {
                memcpy(&vector_elt(fl_apply_v,0), &fl_ctx->Stack[fl_ctx->SP-n], n*sizeof(value_t));
                POPN(fl_ctx, n);
            }
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;

        OP(OP_AREF)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-2];
            if (isvector(fl_apply_v)) {
                fl_apply_e = fl_ctx->Stack[fl_ctx->SP-1];
                if (isfixnum(fl_apply_e))
                    i = numval(fl_apply_e);
                else
                    i = (uint32_t)tosize(fl_ctx, fl_apply_e, "aref");
                if ((unsigned)i >= vector_size(fl_apply_v))
                    bounds_error(fl_ctx, "aref", fl_apply_v, fl_apply_e);
                fl_apply_v = vector_elt(fl_apply_v, i);
            }
            else if (isarray(fl_apply_v)) {
                fl_apply_v = cvalue_array_aref(fl_ctx, &fl_ctx->Stack[fl_ctx->SP-2]);
            }
            else {
                type_error(fl_ctx, "aref", "sequence", fl_apply_v);
            }
            POPN(fl_ctx, 1);
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;
        OP(OP_ASET)
            fl_apply_e = fl_ctx->Stack[fl_ctx->SP-3];
            if (isvector(fl_apply_e)) {
                i = tofixnum(fl_ctx, fl_ctx->Stack[fl_ctx->SP-2], "aset!");
                if ((unsigned)i >= vector_size(fl_apply_e))
                    bounds_error(fl_ctx, "aset!", fl_apply_v, fl_ctx->Stack[fl_ctx->SP-1]);
                vector_elt(fl_apply_e, i) = (fl_apply_v=fl_ctx->Stack[fl_ctx->SP-1]);
            }
            else if (isarray(fl_apply_e)) {
                fl_apply_v = cvalue_array_aset(fl_ctx, &fl_ctx->Stack[fl_ctx->SP-3]);
            }
            else {
                type_error(fl_ctx, "aset!", "sequence", fl_apply_e);
            }
            POPN(fl_ctx, 2);
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;
        OP(OP_FOR)
            s  = tofixnum(fl_ctx, fl_ctx->Stack[fl_ctx->SP-3], "for");
            hi = tofixnum(fl_ctx, fl_ctx->Stack[fl_ctx->SP-2], "for");
            //f = fl_ctx->Stack[fl_ctx->SP-1];
            fl_apply_v = FL_UNSPECIFIED(fl_ctx);
            fl_ctx->SP += 2;
            n = fl_ctx->SP;
            for(; s <= hi; s++) {
                fl_ctx->Stack[fl_ctx->SP-2] = fl_ctx->Stack[fl_ctx->SP-3];
                fl_ctx->Stack[fl_ctx->SP-1] = fixnum(s);
                fl_apply_v = apply_cl(fl_ctx, 1);
                fl_ctx->SP = n;
            }
            POPN(fl_ctx, 4);
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;

        OP(OP_LOADT) PUSH(fl_ctx, fl_ctx->T); NEXT_OP;
        OP(OP_LOADF) PUSH(fl_ctx, fl_ctx->F); NEXT_OP;
        OP(OP_LOADNIL) PUSH(fl_ctx, fl_ctx->NIL); NEXT_OP;
        OP(OP_LOAD0) PUSH(fl_ctx, fixnum(0)); NEXT_OP;
        OP(OP_LOAD1) PUSH(fl_ctx, fixnum(1)); NEXT_OP;
        OP(OP_LOADI8) s = (int8_t)*ip++; PUSH(fl_ctx, fixnum(s)); NEXT_OP;
        OP(OP_LOADV)
            fl_apply_v = fn_vals(fl_ctx->Stack[bp-1]);
            assert(*ip < vector_size(fl_apply_v));
            fl_apply_v = vector_elt(fl_apply_v, *ip); ip++;
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;
        OP(OP_LOADVL)
            fl_apply_v = fn_vals(fl_ctx->Stack[bp-1]);
            fl_apply_v = vector_elt(fl_apply_v, GET_INT32(ip)); ip+=4;
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;
        OP(OP_LOADGL)
            fl_apply_v = fn_vals(fl_ctx->Stack[bp-1]);
            fl_apply_v = vector_elt(fl_apply_v, GET_INT32(ip)); ip+=4;
            goto do_loadg;
        OP(OP_LOADG)
            fl_apply_v = fn_vals(fl_ctx->Stack[bp-1]);
            assert(*ip < vector_size(fl_apply_v));
            fl_apply_v = vector_elt(fl_apply_v, *ip); ip++;
        do_loadg:
            assert(issymbol(fl_apply_v));
            sym = (symbol_t*)ptr(fl_apply_v);
            if (sym->binding == UNBOUND)
                fl_raise(fl_ctx, fl_list2(fl_ctx, fl_ctx->UnboundError, fl_apply_v));
            PUSH(fl_ctx, sym->binding);
            NEXT_OP;

        OP(OP_SETGL)
            fl_apply_v = fn_vals(fl_ctx->Stack[bp-1]);
            fl_apply_v = vector_elt(fl_apply_v, GET_INT32(ip)); ip+=4;
            goto do_setg;
        OP(OP_SETG)
            fl_apply_v = fn_vals(fl_ctx->Stack[bp-1]);
            assert(*ip < vector_size(fl_apply_v));
            fl_apply_v = vector_elt(fl_apply_v, *ip); ip++;
        do_setg:
            assert(issymbol(fl_apply_v));
            sym = (symbol_t*)ptr(fl_apply_v);
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
            if (!isconstant(sym))
                sym->binding = fl_apply_v;
            NEXT_OP;

        OP(OP_LOADA)
            i = *ip++;
            fl_apply_v = fl_ctx->Stack[bp+i];
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;
        OP(OP_LOADA0)
            fl_apply_v = fl_ctx->Stack[bp];
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;
        OP(OP_LOADA1)
            fl_apply_v = fl_ctx->Stack[bp+1];
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;
        OP(OP_LOADAL)
            i = GET_INT32(ip); ip+=4;
            fl_apply_v = fl_ctx->Stack[bp+i];
            PUSH(fl_ctx, fl_apply_v);
            NEXT_OP;
        OP(OP_SETA)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
            i = *ip++;
            fl_ctx->Stack[bp+i] = fl_apply_v;
            NEXT_OP;
        OP(OP_SETAL)
            fl_apply_v = fl_ctx->Stack[fl_ctx->SP-1];
            i = GET_INT32(ip); ip+=4;
            fl_ctx->Stack[bp+i] = fl_apply_v;
            NEXT_OP;

        OP(OP_BOX)
            i = *ip++;
            fl_apply_v = mk_cons(fl_ctx);
            car_(fl_apply_v) = fl_ctx->Stack[bp+i];
            cdr_(fl_apply_v) = fl_ctx->NIL;
            fl_ctx->Stack[bp+i] = fl_apply_v;
            NEXT_OP;
        OP(OP_BOXL)
            i = GET_INT32(ip); ip+=4;
            fl_apply_v = mk_cons(fl_ctx);
            car_(fl_apply_v) = fl_ctx->Stack[bp+i];
            cdr_(fl_apply_v) = fl_ctx->NIL;
            fl_ctx->Stack[bp+i] = fl_apply_v;
            NEXT_OP;

        OP(OP_SHIFT)
            i = *ip++;
            fl_ctx->Stack[fl_ctx->SP-1-i] = fl_ctx->Stack[fl_ctx->SP-1];
            fl_ctx->SP -= i;
            NEXT_OP;

        OP(OP_LOADC)
            i = *ip++;
            fl_apply_v = fl_ctx->Stack[bp+nargs];
            assert(isvector(fl_apply_v));
            assert(i < vector_size(fl_apply_v));
            PUSH(fl_ctx, vector_elt(fl_apply_v, i));
            NEXT_OP;

        OP(OP_LOADC0)
            PUSH(fl_ctx, vector_elt(fl_ctx->Stack[bp+nargs], 0));
            NEXT_OP;
        OP(OP_LOADC1)
            PUSH(fl_ctx, vector_elt(fl_ctx->Stack[bp+nargs], 1));
            NEXT_OP;

        OP(OP_LOADCL)
            i = GET_INT32(ip); ip+=4;
            fl_apply_v = fl_ctx->Stack[bp+nargs];
            PUSH(fl_ctx, vector_elt(fl_apply_v, i));
            NEXT_OP;

        OP(OP_CLOSURE)
            n = *ip++;
            assert(n > 0);
            fl_apply_pv = alloc_words(fl_ctx, n + 1);
            fl_apply_v = tagptr(fl_apply_pv, TAG_VECTOR);
            fl_apply_pv[0] = fixnum(n);
            i = 1;
            do {
                fl_apply_pv[i] = fl_ctx->Stack[fl_ctx->SP-n + i-1];
                i++;
            } while (i<=n);
            POPN(fl_ctx, n);
            PUSH(fl_ctx, fl_apply_v);
#ifdef MEMDEBUG2
            fl_apply_pv = alloc_words(fl_ctx, 4);
#else
            if ((value_t*)fl_ctx->curheap > ((value_t*)fl_ctx->lim)-2)
                gc(fl_ctx, 0);
            fl_apply_pv = (value_t*)fl_ctx->curheap;
            fl_ctx->curheap += (4*sizeof(value_t));
#endif
            fl_apply_e = fl_ctx->Stack[fl_ctx->SP-2];  // closure to copy
            assert(isfunction(fl_apply_e));
            fl_apply_pv[0] = ((value_t*)ptr(fl_apply_e))[0];
            fl_apply_pv[1] = ((value_t*)ptr(fl_apply_e))[1];
            fl_apply_pv[2] = fl_ctx->Stack[fl_ctx->SP-1];  // env
            fl_apply_pv[3] = ((value_t*)ptr(fl_apply_e))[3];
            POPN(fl_ctx, 1);
            fl_ctx->Stack[fl_ctx->SP-1] = tagptr(fl_apply_pv, TAG_FUNCTION);
            NEXT_OP;

        OP(OP_TRYCATCH)
            fl_apply_v = do_trycatch(fl_ctx);
            POPN(fl_ctx, 1);
            fl_ctx->Stack[fl_ctx->SP-1] = fl_apply_v;
            NEXT_OP;

        OP(OP_OPTARGS)
            i = GET_INT32(ip); ip+=4;
            n = GET_INT32(ip); ip+=4;
            if (nargs < i)
                lerror(fl_ctx, fl_ctx->ArgError, "apply: too few arguments");
            if ((int32_t)n > 0) {
                if (nargs > n)
                    lerror(fl_ctx, fl_ctx->ArgError, "apply: too many arguments");
            }
            else n = -n;
            if (n > nargs) {
                n -= nargs;
                fl_ctx->SP += n;
                fl_ctx->Stack[fl_ctx->SP-1] = fl_ctx->Stack[fl_ctx->SP-n-1];
                fl_ctx->Stack[fl_ctx->SP-2] = nargs+n;
                fl_ctx->Stack[fl_ctx->SP-3] = fl_ctx->Stack[fl_ctx->SP-n-3];
                fl_ctx->Stack[fl_ctx->SP-4] = fl_ctx->Stack[fl_ctx->SP-n-4];
                fl_ctx->curr_frame = fl_ctx->SP;
                for(i=0; i < n; i++) {
                    fl_ctx->Stack[bp+nargs+i] = UNBOUND;
                }
                nargs += n;
            }
            NEXT_OP;
        OP(OP_KEYARGS)
            fl_apply_v = fn_vals(fl_ctx->Stack[bp-1]);
            fl_apply_v = vector_elt(fl_apply_v, 0);
            i = GET_INT32(ip); ip+=4;
            n = GET_INT32(ip); ip+=4;
            s = GET_INT32(ip); ip+=4;
            nargs = process_keys(fl_ctx, fl_apply_v, i, n, llabs(s)-(i+n), bp, nargs, s<0);
            NEXT_OP;

#ifndef USE_COMPUTED_GOTO
        default:
            goto dispatch;
#endif
        }
    }
#ifdef USE_COMPUTED_GOTO
    return UNBOUND;  // not reached
#else
    goto dispatch;
#endif
}

static uint32_t compute_maxstack(uint8_t *code, size_t len, int bswap)
{
    uint8_t *ip = code+4, *end = code+len;
    uint8_t op;
    uint32_t i, n, sp = 0, maxsp = 0;

    while (1) {
        if ((int32_t)sp > (int32_t)maxsp) maxsp = sp;
        if (ip >= end) break;
        op = *ip++;
        switch (op) {
        case OP_ARGC:
            n = *ip++;
            break;
        case OP_VARGC:
            n = *ip++;
            sp += (n+2);
            break;
        case OP_LARGC:
            if (bswap) SWAP_INT32(ip);
            n = GET_INT32(ip); ip+=4;
            break;
        case OP_LVARGC:
            if (bswap) SWAP_INT32(ip);
            n = GET_INT32(ip); ip+=4;
            sp += (n+2);
            break;
        case OP_OPTARGS:
            if (bswap) SWAP_INT32(ip);
            i = GET_INT32(ip); ip+=4;
            if (bswap) SWAP_INT32(ip);
            n = abs(GET_INT32(ip)); ip+=4;
            sp += (n-i);
            break;
        case OP_KEYARGS:
            if (bswap) SWAP_INT32(ip);
            i = GET_INT32(ip); ip+=4;
            if (bswap) SWAP_INT32(ip);
            n = GET_INT32(ip); ip+=4;
            if (bswap) SWAP_INT32(ip);
            n = abs(GET_INT32(ip)); ip+=4;
            sp += (n-i);
            break;
        case OP_BRBOUND:
            if (bswap) SWAP_INT32(ip);
            ip+=4;
            sp++;
            break;

        case OP_TCALL: case OP_CALL:
            n = *ip++;  // nargs
            sp -= n;
            break;
        case OP_TCALLL: case OP_CALLL:
            if (bswap) SWAP_INT32(ip);
            n = GET_INT32(ip); ip+=4;
            sp -= n;
            break;
        case OP_JMP:
            if (bswap) SWAP_INT16(ip);
            ip += 2; break;
        case OP_JMPL:
            if (bswap) SWAP_INT32(ip);
            ip += 4; break;
        case OP_BRF: case OP_BRT:
            if (bswap) SWAP_INT16(ip);
            ip+=2;
            sp--;
            break;
        case OP_BRFL: case OP_BRTL:
            if (bswap) SWAP_INT32(ip);
            ip += 4;
            sp--;
            break;
        case OP_BRNE:
            if (bswap) SWAP_INT16(ip);
            ip += 2;
            sp -= 2;
            break;
        case OP_BRNEL:
            if (bswap) SWAP_INT32(ip);
            ip += 4;
            sp -= 2;
            break;
        case OP_BRNN: case OP_BRN:
            if (bswap) SWAP_INT16(ip);
            ip += 2;
            sp--;
            break;
        case OP_BRNNL: case OP_BRNL:
            if (bswap) SWAP_INT32(ip);
            ip += 4;
            sp--;
            break;
        case OP_RET: sp--; break;

        case OP_CONS: case OP_SETCAR: case OP_SETCDR: case OP_POP:
        case OP_EQ: case OP_EQV: case OP_EQUAL: case OP_ADD2: case OP_SUB2:
        case OP_IDIV: case OP_NUMEQ: case OP_LT: case OP_COMPARE:
        case OP_AREF: case OP_TRYCATCH:
            sp--;
            break;

        case OP_PAIRP: case OP_ATOMP: case OP_NOT: case OP_NULLP:
        case OP_BOOLEANP: case OP_SYMBOLP: case OP_NUMBERP: case OP_FIXNUMP:
        case OP_BOUNDP: case OP_BUILTINP: case OP_FUNCTIONP: case OP_VECTORP:
        case OP_NOP: case OP_CAR: case OP_CDR: case OP_NEG:
            break;

        case OP_TAPPLY: case OP_APPLY:
            n = *ip++;
            sp -= (n-1);
            break;

        case OP_LIST: case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV:
        case OP_VECTOR:
            n = *ip++;
            sp -= (n-1);
            break;
        case OP_CLOSURE:
            n = *ip++;
            sp -= n;
            break;
        case OP_SHIFT:
            n = *ip++;
            sp -= n;
            break;

        case OP_ASET:
            sp -= 2;
            break;
        case OP_FOR:
            if (sp+2 > maxsp) maxsp = sp+2;
            sp -=2;
            break;

        case OP_LOADT: case OP_LOADF: case OP_LOADNIL: case OP_LOAD0:
        case OP_LOAD1: case OP_LOADA0: case OP_LOADA1: case OP_DUP:
        case OP_LOADC0: case OP_LOADC1:
            sp++;
            break;

        case OP_LOADI8: case OP_LOADV: case OP_LOADG: case OP_LOADA:
            ip++;
            sp++;
            break;
        case OP_LOADVL: case OP_LOADGL: case OP_LOADAL:
            if (bswap) SWAP_INT32(ip);
            ip+=4;
            sp++;
            break;

        case OP_SETG: case OP_SETA: case OP_BOX:
            ip++;
            break;
        case OP_SETGL: case OP_SETAL: case OP_BOXL:
            if (bswap) SWAP_INT32(ip);
            ip+=4;
            break;

        case OP_LOADC: ip+=1; sp++; break;
        case OP_LOADCL:
            if (bswap) SWAP_INT32(ip);
            ip+=4;
            sp++; break;
        }
    }
    return maxsp+4;
}

// top = top frame pointer to start at
static value_t _stacktrace(fl_context_t *fl_ctx, uint32_t top)
{
    uint32_t bp, sz;
    value_t v, lst = fl_ctx->NIL;
    fl_gc_handle(fl_ctx, &lst);
    while (top > 0) {
        sz = fl_ctx->Stack[top-2]+1;
        bp = top-4-sz;
        v = alloc_vector(fl_ctx, sz, 0);
        memcpy(&vector_elt(v,0), &fl_ctx->Stack[bp], sz*sizeof(value_t));
        lst = fl_cons(fl_ctx, v, lst);
        top = fl_ctx->Stack[top-3];
    }
    fl_free_gc_handles(fl_ctx, 1);
    return lst;
}

// builtins -------------------------------------------------------------------

void assign_global_builtins(fl_context_t *fl_ctx, const builtinspec_t *b)
{
    while (b->name != NULL) {
        setc(symbol(fl_ctx, b->name), cbuiltin(fl_ctx, b->name, b->fptr));
        b++;
    }
}

static value_t fl_function(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    if (nargs == 1 && issymbol(args[0]))
        return fl_builtin(fl_ctx, args, nargs);
    if (nargs < 2 || nargs > 4)
        argcount(fl_ctx, "function", nargs, 2);
    if (!fl_isstring(fl_ctx, args[0]))
        type_error(fl_ctx, "function", "string", args[0]);
    if (!isvector(args[1]))
        type_error(fl_ctx, "function", "vector", args[1]);
    cvalue_t *arr = (cvalue_t*)ptr(args[0]);
    cv_pin(fl_ctx, arr);
    char *data = (char*)cv_data(arr);
    int swap = 0;
    if ((uint8_t)data[4] >= N_OPCODES) {
        // read syntax, shifted 48 for compact text representation
        size_t i, sz = cv_len(arr);
        for(i=0; i < sz; i++)
            data[i] -= 48;
    }
    else {
#if BYTE_ORDER == BIG_ENDIAN
        swap = 1;
#endif
    }
    uint32_t ms = compute_maxstack((uint8_t*)data, cv_len(arr), swap);
    PUT_INT32(data, ms);
    function_t *fn = (function_t*)alloc_words(fl_ctx, 4);
    value_t fv = tagptr(fn, TAG_FUNCTION);
    fn->bcode = args[0];
    fn->vals = args[1];
    fn->env = fl_ctx->NIL;
    fn->name = fl_ctx->LAMBDA;
    if (nargs > 2) {
        if (issymbol(args[2])) {
            fn->name = args[2];
            if (nargs > 3)
                fn->env = args[3];
        }
        else {
            fn->env = args[2];
            if (nargs > 3) {
                if (!issymbol(args[3]))
                    type_error(fl_ctx, "function", "symbol", args[3]);
                fn->name = args[3];
            }
        }
        if (isgensym(fl_ctx, fn->name))
            lerror(fl_ctx, fl_ctx->ArgError, "function: name should not be a gensym");
    }
    return fv;
}

static value_t fl_function_code(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    argcount(fl_ctx, "function:code", nargs, 1);
    value_t v = args[0];
    if (!isclosure(v)) type_error(fl_ctx, "function:code", "function", v);
    return fn_bcode(v);
}
static value_t fl_function_vals(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    argcount(fl_ctx, "function:vals", nargs, 1);
    value_t v = args[0];
    if (!isclosure(v)) type_error(fl_ctx, "function:vals", "function", v);
    return fn_vals(v);
}
static value_t fl_function_env(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    argcount(fl_ctx, "function:env", nargs, 1);
    value_t v = args[0];
    if (!isclosure(v)) type_error(fl_ctx, "function:env", "function", v);
    return fn_env(v);
}
static value_t fl_function_name(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    argcount(fl_ctx, "function:name", nargs, 1);
    value_t v = args[0];
    if (!isclosure(v)) type_error(fl_ctx, "function:name", "function", v);
    return fn_name(v);
}

value_t fl_copylist(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    argcount(fl_ctx, "copy-list", nargs, 1);
    return copy_list(fl_ctx, args[0]);
}

value_t fl_append(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    if (nargs == 0)
        return fl_ctx->NIL;
    value_t first=fl_ctx->NIL, lst, lastcons=fl_ctx->NIL;
    fl_gc_handle(fl_ctx, &first);
    fl_gc_handle(fl_ctx, &lastcons);
    uint32_t i=0;
    while (1) {
        lst = args[i++];
        if (i >= nargs) break;
        if (iscons(lst)) {
            lst = copy_list(fl_ctx, lst);
            if (first == fl_ctx->NIL)
                first = lst;
            else
                cdr_(lastcons) = lst;
#ifdef MEMDEBUG2
            lastcons = lst;
            while (cdr_(lastcons) != fl_ctx->NIL)
                lastcons = cdr_(lastcons);
#else
            lastcons = tagptr((((cons_t*)fl_ctx->curheap)-1), TAG_CONS);
#endif
        }
        else if (lst != fl_ctx->NIL) {
            type_error(fl_ctx, "append", "cons", lst);
        }
    }
    if (first == fl_ctx->NIL)
        first = lst;
    else
        cdr_(lastcons) = lst;
    fl_free_gc_handles(fl_ctx, 2);
    return first;
}

value_t fl_liststar(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    if (nargs == 1) return args[0];
    else if (nargs == 0) argcount(fl_ctx, "list*", nargs, 1);
    return _list(fl_ctx, args, nargs, 1);
}

value_t fl_stacktrace(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    (void)args;
    argcount(fl_ctx, "stacktrace", nargs, 0);
    return _stacktrace(fl_ctx, fl_ctx->throwing_frame ? fl_ctx->throwing_frame : fl_ctx->curr_frame);
}

value_t fl_map1(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    if (nargs < 2)
        lerror(fl_ctx, fl_ctx->ArgError, "map: too few arguments");
    if (!iscons(args[1])) return fl_ctx->NIL;
    value_t v;
    uint32_t first, last, argSP = args-fl_ctx->Stack;
    assert(args >= fl_ctx->Stack && argSP < fl_ctx->N_STACK);
    if (nargs == 2) {
        if (fl_ctx->SP+4 > fl_ctx->N_STACK) grow_stack(fl_ctx);
        PUSH(fl_ctx, fl_ctx->Stack[argSP]);
        PUSH(fl_ctx, car_(fl_ctx->Stack[argSP+1]));
        v = _applyn(fl_ctx, 1);
        POPN(fl_ctx, 2);
        PUSH(fl_ctx, v);
        v = mk_cons(fl_ctx);
        car_(v) = POP(fl_ctx); cdr_(v) = fl_ctx->NIL;
        PUSH(fl_ctx, v);
        PUSH(fl_ctx, v);
        first = fl_ctx->SP-2;
        last = fl_ctx->SP-1;
        fl_ctx->Stack[argSP+1] = cdr_(fl_ctx->Stack[argSP+1]);
        while (iscons(fl_ctx->Stack[argSP+1])) {
            PUSH(fl_ctx, fl_ctx->Stack[argSP]);
            PUSH(fl_ctx, car_(fl_ctx->Stack[argSP+1]));
            v = _applyn(fl_ctx, 1);
            POPN(fl_ctx, 2);
            PUSH(fl_ctx, v);
            v = mk_cons(fl_ctx);
            car_(v) = POP(fl_ctx); cdr_(v) = fl_ctx->NIL;
            cdr_(fl_ctx->Stack[last]) = v;
            fl_ctx->Stack[last] = v;
            fl_ctx->Stack[argSP+1] = cdr_(fl_ctx->Stack[argSP+1]);
        }
        POPN(fl_ctx, 2);
    }
    else {
        size_t i;
        while (fl_ctx->SP+nargs+1 > fl_ctx->N_STACK) grow_stack(fl_ctx);
        PUSH(fl_ctx, fl_ctx->Stack[argSP]);
        for(i=1; i < nargs; i++) {
            PUSH(fl_ctx, car(fl_ctx, fl_ctx->Stack[argSP+i]));
            fl_ctx->Stack[argSP+i] = cdr_(fl_ctx->Stack[argSP+i]);
        }
        v = _applyn(fl_ctx, nargs-1);
        POPN(fl_ctx, nargs);
        PUSH(fl_ctx, v);
        v = mk_cons(fl_ctx);
        car_(v) = POP(fl_ctx); cdr_(v) = fl_ctx->NIL;
        PUSH(fl_ctx, v);
        PUSH(fl_ctx, v);
        first = fl_ctx->SP-2;
        last = fl_ctx->SP-1;
        while (iscons(fl_ctx->Stack[argSP+1])) {
            PUSH(fl_ctx, fl_ctx->Stack[argSP]);
            for(i=1; i < nargs; i++) {
                PUSH(fl_ctx, car(fl_ctx, fl_ctx->Stack[argSP+i]));
                fl_ctx->Stack[argSP+i] = cdr_(fl_ctx->Stack[argSP+i]);
            }
            v = _applyn(fl_ctx, nargs-1);
            POPN(fl_ctx, nargs);
            PUSH(fl_ctx, v);
            v = mk_cons(fl_ctx);
            car_(v) = POP(fl_ctx); cdr_(v) = fl_ctx->NIL;
            cdr_(fl_ctx->Stack[last]) = v;
            fl_ctx->Stack[last] = v;
        }
        POPN(fl_ctx, 2);
    }
    return fl_ctx->Stack[first];
}

value_t fl_foreach(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
    if (nargs != 2)
        lerror(fl_ctx, fl_ctx->ArgError, "for-each: expected 2 arguments");
    uint32_t argSP = args-fl_ctx->Stack;
    assert(args >= fl_ctx->Stack && argSP < fl_ctx->N_STACK);
    if (fl_ctx->SP+2 > fl_ctx->N_STACK) grow_stack(fl_ctx);
    PUSH(fl_ctx, fl_ctx->T);
    PUSH(fl_ctx, fl_ctx->T);
    while (iscons(fl_ctx->Stack[argSP+1])) {
        fl_ctx->Stack[fl_ctx->SP-2] = fl_ctx->Stack[argSP];
        fl_ctx->Stack[fl_ctx->SP-1] = car_(fl_ctx->Stack[argSP+1]);
        _applyn(fl_ctx, 1);
        fl_ctx->Stack[argSP+1] = cdr_(fl_ctx->Stack[argSP+1]);
    }
    POPN(fl_ctx, 2);
    return fl_ctx->T;
}

static const builtinspec_t core_builtin_info[] = {
    { "function", fl_function },
    { "function:code", fl_function_code },
    { "function:vals", fl_function_vals },
    { "function:env", fl_function_env },
    { "function:name", fl_function_name },
    { "stacktrace", fl_stacktrace },
    { "gensym", fl_gensym },
    { "gensym?", fl_gensymp },
    { "hash", fl_hash },
    { "copy-list", fl_copylist },
    { "append", fl_append },
    { "list*", fl_liststar },
    { "map", fl_map1 },
    { "for-each", fl_foreach },
    { NULL, NULL }
};

// initialization -------------------------------------------------------------

extern void builtins_init(fl_context_t *fl_ctx);
extern void comparehash_init(fl_context_t *fl_ctx);

static void lisp_init(fl_context_t *fl_ctx, size_t initial_heapsize)
{
    int i;

    libsupport_init();

    fl_ctx->SP = 0;
    fl_ctx->curr_frame = 0;
    fl_ctx->N_GCHND = 0;
    fl_ctx->readstate = NULL;
    fl_ctx->gensym_ctr = 0;
    fl_ctx->gsnameno = 0;

#ifdef MEMDEBUG2
    fl_ctx->tochain = NULL;
    fl_ctx->n_allocd = 0;
#endif

    fl_ctx->heapsize = initial_heapsize;

    fl_ctx->fromspace = (unsigned char*)LLT_ALLOC(fl_ctx->heapsize);
#ifdef MEMDEBUG
    fl_ctx->tospace   = NULL;
#else
    fl_ctx->tospace   = (unsigned char*)LLT_ALLOC(fl_ctx->heapsize);
#endif
    fl_ctx->curheap = fl_ctx->fromspace;
    fl_ctx->lim = fl_ctx->curheap+fl_ctx->heapsize-sizeof(cons_t);
    fl_ctx->consflags = bitvector_new(fl_ctx->heapsize/sizeof(cons_t), 1);
    fl_print_init(fl_ctx);
    comparehash_init(fl_ctx);
    fl_ctx->N_STACK = 262144;
    fl_ctx->Stack = (value_t*)malloc(fl_ctx->N_STACK*sizeof(value_t));
    // TODO: if fl_ctx->Stack == NULL
    CHECK_ALIGN8(fl_ctx->Stack);

    fl_ctx->NIL = builtin(OP_THE_EMPTY_LIST);
    fl_ctx->T = builtin(OP_BOOL_CONST_T);
    fl_ctx->F = builtin(OP_BOOL_CONST_F);
    fl_ctx->FL_EOF = builtin(OP_EOF_OBJECT);
    fl_ctx->LAMBDA = symbol(fl_ctx, "lambda");        fl_ctx->FUNCTION = symbol(fl_ctx, "function");
    fl_ctx->QUOTE = symbol(fl_ctx, "quote");          fl_ctx->TRYCATCH = symbol(fl_ctx, "trycatch");
    fl_ctx->BACKQUOTE = symbol(fl_ctx, "quasiquote");       fl_ctx->COMMA = symbol(fl_ctx, "unquote");
    fl_ctx->COMMAAT = symbol(fl_ctx, "unquote-splicing");   fl_ctx->COMMADOT = symbol(fl_ctx, "unquote-nsplicing");
    fl_ctx->IOError = symbol(fl_ctx, "io-error");     fl_ctx->ParseError = symbol(fl_ctx, "parse-error");
    fl_ctx->TypeError = symbol(fl_ctx, "type-error"); fl_ctx->ArgError = symbol(fl_ctx, "arg-error");
    fl_ctx->UnboundError = symbol(fl_ctx, "unbound-error");
    fl_ctx->KeyError = symbol(fl_ctx, "key-error");   fl_ctx->OutOfMemoryError = symbol(fl_ctx, "memory-error");
    fl_ctx->BoundsError = symbol(fl_ctx, "bounds-error");
    fl_ctx->DivideError = symbol(fl_ctx, "divide-error");
    fl_ctx->EnumerationError = symbol(fl_ctx, "enumeration-error");
    fl_ctx->pairsym = symbol(fl_ctx, "pair");
    fl_ctx->symbolsym = symbol(fl_ctx, "symbol");     fl_ctx->fixnumsym = symbol(fl_ctx, "fixnum");
    fl_ctx->vectorsym = symbol(fl_ctx, "vector");     fl_ctx->builtinsym = symbol(fl_ctx, "builtin");
    fl_ctx->booleansym = symbol(fl_ctx, "boolean");   fl_ctx->nullsym = symbol(fl_ctx, "null");
    fl_ctx->definesym = symbol(fl_ctx, "define");     fl_ctx->defmacrosym = symbol(fl_ctx, "define-macro");
    fl_ctx->forsym = symbol(fl_ctx, "for");
    fl_ctx->setqsym = symbol(fl_ctx, "set!");         fl_ctx->evalsym = symbol(fl_ctx, "eval");
    fl_ctx->vu8sym = symbol(fl_ctx, "vu8");           fl_ctx->fnsym = symbol(fl_ctx, "fn");
    fl_ctx->nulsym = symbol(fl_ctx, "nul");           fl_ctx->alarmsym = symbol(fl_ctx, "alarm");
    fl_ctx->backspacesym = symbol(fl_ctx, "backspace"); fl_ctx->tabsym = symbol(fl_ctx, "tab");
    fl_ctx->linefeedsym = symbol(fl_ctx, "linefeed"); fl_ctx->vtabsym = symbol(fl_ctx, "vtab");
    fl_ctx->pagesym = symbol(fl_ctx, "page");         fl_ctx->returnsym = symbol(fl_ctx, "return");
    fl_ctx->escsym = symbol(fl_ctx, "esc");           fl_ctx->spacesym = symbol(fl_ctx, "space");
    fl_ctx->deletesym = symbol(fl_ctx, "delete");     fl_ctx->newlinesym = symbol(fl_ctx, "newline");
    fl_ctx->tsym = symbol(fl_ctx, "t"); fl_ctx->Tsym = symbol(fl_ctx, "T");
    fl_ctx->fsym = symbol(fl_ctx, "f"); fl_ctx->Fsym = symbol(fl_ctx, "F");
    set(fl_ctx->printprettysym=symbol(fl_ctx, "*print-pretty*"), fl_ctx->T);
    set(fl_ctx->printreadablysym=symbol(fl_ctx, "*print-readably*"), fl_ctx->T);
    set(fl_ctx->printwidthsym=symbol(fl_ctx, "*print-width*"), fixnum(fl_ctx->SCR_WIDTH));
    set(fl_ctx->printlengthsym=symbol(fl_ctx, "*print-length*"), fl_ctx->F);
    set(fl_ctx->printlevelsym=symbol(fl_ctx, "*print-level*"), fl_ctx->F);
    fl_ctx->builtins_table_sym = symbol(fl_ctx, "*builtins*");
    fl_ctx->lasterror = fl_ctx->NIL;
    i = 0;
    for (i=OP_EQ; i <= OP_ASET; i++) {
        setc(symbol(fl_ctx, builtin_names[i]), builtin(i));
    }
    setc(symbol(fl_ctx, "eq"), builtin(OP_EQ));
    setc(symbol(fl_ctx, "procedure?"), builtin(OP_FUNCTIONP));
    setc(symbol(fl_ctx, "top-level-bound?"), builtin(OP_BOUNDP));

#if defined(_OS_LINUX_)
    set(symbol(fl_ctx, "*os-name*"), symbol(fl_ctx, "linux"));
#elif defined(_OS_WINDOWS_)
    set(symbol(fl_ctx, "*os-name*"), symbol(fl_ctx, "win32"));
#elif defined(_OS_DARWIN_)
    set(symbol(fl_ctx, "*os-name*"), symbol(fl_ctx, "macos"));
#else
    set(symbol(fl_ctx, "*os-name*"), symbol(fl_ctx, "unknown"));
#endif

    fl_ctx->jl_sym = symbol(fl_ctx, "julia_value");
    fl_ctx->jl_char_sym = symbol(fl_ctx, "julia_char");

    fl_ctx->the_empty_vector = tagptr(alloc_words(fl_ctx, 1), TAG_VECTOR);
    vector_setsize(fl_ctx->the_empty_vector, 0);

    cvalues_init(fl_ctx);

    char exename[1024];
    size_t exe_size = sizeof(exename) / sizeof(exename[0]);
    if ( uv_exepath(exename, &exe_size) == 0 ) {
        setc(symbol(fl_ctx, "*install-dir*"), cvalue_static_cstring(fl_ctx, strdup(dirname(exename))));
    }

    fl_ctx->memory_exception_value = fl_list2(fl_ctx, fl_ctx->OutOfMemoryError,
                                              cvalue_static_cstring(fl_ctx, "out of memory"));

    assign_global_builtins(fl_ctx, core_builtin_info);

    fl_read_init(fl_ctx);

    builtins_init(fl_ctx);
}

// top level ------------------------------------------------------------------

value_t fl_toplevel_eval(fl_context_t *fl_ctx, value_t expr)
{
    return fl_applyn(fl_ctx, 1, symbol_value(fl_ctx->evalsym), expr);
}

extern void fl_init_julia_extensions(fl_context_t *fl_ctx);

void fl_init(fl_context_t *fl_ctx, size_t initial_heapsize)
{
    lisp_init(fl_ctx, initial_heapsize);
    fl_init_julia_extensions(fl_ctx);
}

int fl_load_system_image_str(fl_context_t *fl_ctx, char *str, size_t len)
{
    value_t img = cvalue(fl_ctx, fl_ctx->iostreamtype, sizeof(ios_t));
    ios_t *pi = value2c(ios_t*, img);
    ios_static_buffer(pi, str, len);

    return fl_load_system_image(fl_ctx, img);
}

int fl_load_system_image(fl_context_t *fl_ctx, value_t sys_image_iostream)
{
    value_t e;
    int saveSP;
    symbol_t *sym;

    PUSH(fl_ctx, sys_image_iostream);
    saveSP = fl_ctx->SP;
    FL_TRY(fl_ctx) {
        while (1) {
            e = fl_read_sexpr(fl_ctx, fl_ctx->Stack[fl_ctx->SP-1]);
            if (ios_eof(value2c(ios_t*,fl_ctx->Stack[fl_ctx->SP-1]))) break;
            if (isfunction(e)) {
                // stage 0 format: series of thunks
                PUSH(fl_ctx, e);
                (void)_applyn(fl_ctx, 0);
                fl_ctx->SP = saveSP;
            }
            else {
                // stage 1 format: list alternating symbol/value
                while (iscons(e)) {
                    sym = tosymbol(fl_ctx, car_(e), "bootstrap");
                    e = cdr_(e);
                    (void)tocons(fl_ctx, e, "bootstrap");
                    sym->binding = car_(e);
                    e = cdr_(e);
                }
                break;
            }
        }
    }
    FL_CATCH(fl_ctx) {
        ios_puts("fatal error during bootstrap:\n", ios_stderr);
        fl_print(fl_ctx, ios_stderr, fl_ctx->lasterror);
        ios_putc('\n', ios_stderr);
        return 1;
    }
    ios_close(value2c(ios_t*,fl_ctx->Stack[fl_ctx->SP-1]));
    POPN(fl_ctx, 1);
    return 0;
}

#ifdef __cplusplus
}
#endif
