#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include "wl.h"

/////////////////////////////////////////////////////////////////////////
// BASIC
/////////////////////////////////////////////////////////////////////////

typedef struct {
    char *ptr;
    int   len;
} String;

typedef struct {
    char *buf;
    int   cap;
    bool  yes;
} Error;

#define S(X) (String) { (X), SIZEOF(X)-1 }

#ifdef _WIN32
#define LLD "lld"
#define LLU "llu"
#else
#define LLD "ld"
#define LLU "lu"
#endif

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
#define SIZEOF(X) (int) sizeof(X)
#define ALIGNOF(X) (int) _Alignof(X)

#ifndef NDEBUG
#define UNREACHABLE __builtin_trap()
#define ASSERT(X) if (!(X)) __builtin_trap();
#else
#define UNREACHABLE {}
#define ASSERT(X) {}
#endif

static bool is_space(char c)
{
    return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}

static bool is_digit(char c)
{
    return c >= '0' && c <= '9';
}

static bool is_alpha(char c)
{
    return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}

static bool is_printable(char c)
{
    return c >= ' ' && c <= '~';
}

static bool is_hex_digit(char c)
{
    return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}

static char to_lower(char c)
{
    if (c >= 'A' && c <= 'Z')
        return c - 'A' + 'a';
    return c;
}

static int hex_digit_to_int(char c)
{
    if (c >= 'a' && c <= 'f')
        return c - 'a' + 10;

    if (c >= 'A' && c <= 'F')
        return c - 'A' + 10;

    return c - '0';
}

static bool streq(String a, String b)
{
    if (a.len != b.len)
        return false;
    for (int i = 0; i < a.len; i++)
        if (a.ptr[i] != b.ptr[i])
            return false;
    return true;
}

static bool streqcase(String a, String b)
{
    if (a.len != b.len)
        return false;
    for (int i = 0; i < a.len; i++)
        if (to_lower(a.ptr[i]) != to_lower(b.ptr[i]))
            return false;
    return true;
}


#define REPORT(err, fmt, ...) report((err), __FILE__, __LINE__, fmt, ## __VA_ARGS__)
static void report(Error *err, char *file, int line, char *fmt, ...)
{
    if (err->yes) return;

    if (err->cap > 0) {

        va_list args;
        va_start(args, fmt);
        int len = vsnprintf(err->buf, err->cap, fmt, args);
        va_end(args);
        ASSERT(len >= 0);

        if (err->cap > len) {
            int ret = snprintf(err->buf + len, err->cap - len,
                " (reported at %s:%d)", file, line);
            ASSERT(ret >= 0);
            len += ret;
        }

        if (len > err->cap)
            len = err->cap-1;
        err->buf[len] = '\0';
    }

    err->yes = true;
}

/////////////////////////////////////////////////////////////////////////
// ARENA
/////////////////////////////////////////////////////////////////////////

static void *alloc(WL_Arena *a, int len, int align)
{
    int pad = -(intptr_t) (a->ptr + a->cur) & (align-1);
    if (a->len - a->cur < len + pad)
        return NULL;
    void *ret = a->ptr + a->cur + pad;
    a->cur += pad + len;
    return ret;
}

static bool grow_alloc(WL_Arena *a, char *p, int new_len)
{
    int new_cur = (p - a->ptr) + new_len;
    if (new_cur > a->len)
        return false;
    a->cur = new_cur;
    return true;
}

static String copystr(String s, WL_Arena *a)
{
    char *p = alloc(a, s.len, 1);
    if (p == NULL)
        return (String) { NULL, 0 };
    memcpy(p, s.ptr, s.len);
    return (String) { p, s.len };
}

/////////////////////////////////////////////////////////////////////////
// WRITER
/////////////////////////////////////////////////////////////////////////

typedef struct {
    char *dst;
    int   cap;
    int   len;
} Writer;

static void write_raw_mem(Writer *w, void *ptr, int len)
{
    if (w->cap > w->len) {
        int cpy = MIN(w->cap - w->len, len);
        if (ptr && w->dst)
            memcpy(w->dst + w->len, ptr, cpy);
    }
    w->len += len;
}

static void write_raw_u8 (Writer *w, uint8_t  x) { write_raw_mem(w, &x, SIZEOF(x)); }
static void write_raw_u16(Writer *w, uint16_t x) { write_raw_mem(w, &x, SIZEOF(x)); }
static void write_raw_u32(Writer *w, uint32_t x) { write_raw_mem(w, &x, SIZEOF(x)); }
static void write_raw_u64(Writer *w, uint64_t x) { write_raw_mem(w, &x, SIZEOF(x)); }
static void write_raw_s8 (Writer *w, int8_t   x) { write_raw_mem(w, &x, SIZEOF(x)); }
static void write_raw_s16(Writer *w, int16_t  x) { write_raw_mem(w, &x, SIZEOF(x)); }
static void write_raw_s32(Writer *w, int32_t  x) { write_raw_mem(w, &x, SIZEOF(x)); }
static void write_raw_s64(Writer *w, int64_t  x) { write_raw_mem(w, &x, SIZEOF(x)); }
static void write_raw_f32(Writer *w, float    x) { write_raw_mem(w, &x, SIZEOF(x)); }
static void write_raw_f64(Writer *w, double   x) { write_raw_mem(w, &x, SIZEOF(x)); }

static void write_text(Writer *w, String str)
{
    write_raw_mem(w, str.ptr, str.len);
}

static void write_text_s64(Writer *w, int64_t n)
{
    int len;
    if (w->len < w->cap)
        len = snprintf(w->dst + w->len, w->cap - w->len, "%" LLD, n);
    else
        len = snprintf(NULL, 0, "%" LLD, n);
    ASSERT(len >= 0);
    w->len += len;
}

static void write_text_f64(Writer *w, double n)
{
    int len;
    if (w->len < w->cap)
        len = snprintf(w->dst + w->len, w->cap - w->len, "%2.2f", n);
    else
        len = snprintf(NULL, 0, "%2.2f", n);
    ASSERT(len >= 0);
    w->len += len;
}

static void patch_mem(Writer *w, void *src, int off, int len)
{
    ASSERT(off + len <= w->len);
    if (off < w->cap) {
        int cpy = MIN(w->cap - off, len);
        memcpy(w->dst + off, src, cpy);
    }
}

/////////////////////////////////////////////////////////////////////////
// PARSER
/////////////////////////////////////////////////////////////////////////

typedef struct {
    char *src;
    int   len;
    int   cur;
} Scanner;

typedef enum {
    TOKEN_END,
    TOKEN_ERROR,
    TOKEN_IDENT,
    TOKEN_KWORD_IF,
    TOKEN_KWORD_ELSE,
    TOKEN_KWORD_WHILE,
    TOKEN_KWORD_FOR,
    TOKEN_KWORD_IN,
    TOKEN_KWORD_PROCEDURE,
    TOKEN_KWORD_LET,
    TOKEN_KWORD_NONE,
    TOKEN_KWORD_TRUE,
    TOKEN_KWORD_FALSE,
    TOKEN_KWORD_INCLUDE,
    TOKEN_KWORD_LEN,
    TOKEN_VALUE_FLOAT,
    TOKEN_VALUE_INT,
    TOKEN_VALUE_STR,
    TOKEN_OPER_EQL,
    TOKEN_OPER_NQL,
    TOKEN_OPER_LSS,
    TOKEN_OPER_GRT,
    TOKEN_OPER_ADD,
    TOKEN_OPER_SUB,
    TOKEN_OPER_MUL,
    TOKEN_OPER_DIV,
    TOKEN_OPER_MOD,
    TOKEN_OPER_ASS,
    TOKEN_OPER_SHOVEL,
    TOKEN_PAREN_OPEN,
    TOKEN_PAREN_CLOSE,
    TOKEN_BRACKET_OPEN,
    TOKEN_BRACKET_CLOSE,
    TOKEN_CURLY_OPEN,
    TOKEN_CURLY_CLOSE,
    TOKEN_DOT,
    TOKEN_COMMA,
    TOKEN_COLON,
    TOKEN_DOLLAR,
    TOKEN_NEWLINE,
} TokType;

typedef struct {
    TokType type;
    int64_t ival;
    double  fval;
    String  sval;
} Token;

typedef enum {
    NODE_PROCEDURE_DECL,
    NODE_PROCEDURE_ARG,
    NODE_PROCEDURE_CALL,
    NODE_VAR_DECL,
    NODE_COMPOUND,
    NODE_GLOBAL,
    NODE_IFELSE,
    NODE_FOR,
    NODE_WHILE,
    NODE_INCLUDE,
    NODE_SELECT,
    NODE_NESTED,
    NODE_OPER_LEN,
    NODE_OPER_POS,
    NODE_OPER_NEG,
    NODE_OPER_ASS,
    NODE_OPER_EQL,
    NODE_OPER_NQL,
    NODE_OPER_LSS,
    NODE_OPER_GRT,
    NODE_OPER_ADD,
    NODE_OPER_SUB,
    NODE_OPER_MUL,
    NODE_OPER_DIV,
    NODE_OPER_MOD,
    NODE_OPER_SHOVEL,
    NODE_VALUE_INT,
    NODE_VALUE_FLOAT,
    NODE_VALUE_STR,
    NODE_VALUE_NONE,
    NODE_VALUE_TRUE,
    NODE_VALUE_FALSE,
    NODE_VALUE_VAR,
    NODE_VALUE_SYSVAR,
    NODE_VALUE_HTML,
    NODE_VALUE_ARRAY,
    NODE_VALUE_MAP,
} NodeType;

typedef struct Node Node;
struct Node {
    NodeType type;
    Node *next;

    Node *key;

    Node *left;
    Node *right;

    Node *child;

    uint64_t ival;
    double   fval;
    String   sval;

    String html_tag;
    Node*  html_attr;
    Node*  html_child;
    bool   html_body;

    Node *if_cond;
    Node *if_branch1;
    Node *if_branch2;

    Node *while_cond;
    Node *while_body;

    String for_var1;
    String for_var2;
    Node*  for_set;

    String proc_name;
    Node*  proc_args;
    Node*  proc_body;

    String var_name;
    Node*  var_value;

    String include_path;
    Node*  include_next;
    Node*  include_root;
};

typedef struct {
    Node *node;
    Node *includes;
    int   errlen;
} ParseResult;

typedef struct {
    Scanner   s;
    WL_Arena*    arena;
    char*     errbuf;
    int       errmax;
    int       errlen;
    Node*     include_head;
    Node**    include_tail;
} Parser;

static bool consume_str(Scanner *s, String x)
{
    if (x.len == 0)
        return false;

    if (x.len > s->len - s->cur)
        return false;

    for (int i = 0; i < x.len; i++)
        if (s->src[s->cur+i] != x.ptr[i])
            return false;

    s->cur += x.len;
    return true;
}

static void write_token(Writer *w, Token token)
{
    switch (token.type) {

        default                 : write_text(w, S("???"));       break;
        case TOKEN_END          : write_text(w, S("<EOF>"));     break;
        case TOKEN_ERROR        : write_text(w, S("<ERROR>"));   break;
        case TOKEN_IDENT        : write_text(w, token.sval);     break;
        case TOKEN_KWORD_IF     : write_text(w, S("if"));        break;
        case TOKEN_KWORD_ELSE   : write_text(w, S("else"));      break;
        case TOKEN_KWORD_WHILE  : write_text(w, S("while"));     break;
        case TOKEN_KWORD_FOR    : write_text(w, S("for"));       break;
        case TOKEN_KWORD_IN     : write_text(w, S("in"));        break;
        case TOKEN_KWORD_PROCEDURE: write_text(w, S("procedure")); break;
        case TOKEN_KWORD_LET    : write_text(w, S("let"));       break;
        case TOKEN_KWORD_NONE   : write_text(w, S("none"));      break;
        case TOKEN_KWORD_TRUE   : write_text(w, S("true"));      break;
        case TOKEN_KWORD_FALSE  : write_text(w, S("false"));     break;
        case TOKEN_KWORD_INCLUDE: write_text(w, S("include"));   break;
        case TOKEN_KWORD_LEN    : write_text(w, S("len"));       break;
        case TOKEN_VALUE_FLOAT  : write_text_f64(w, token.fval); break;
        case TOKEN_VALUE_INT    : write_text_s64(w, token.ival); break;
        case TOKEN_OPER_ASS     : write_text(w, S("="));         break;
        case TOKEN_OPER_EQL     : write_text(w, S("=="));        break;
        case TOKEN_OPER_NQL     : write_text(w, S("!="));        break;
        case TOKEN_OPER_LSS     : write_text(w, S("<"));         break;
        case TOKEN_OPER_GRT     : write_text(w, S(">"));         break;
        case TOKEN_OPER_ADD     : write_text(w, S("+"));         break;
        case TOKEN_OPER_SUB     : write_text(w, S("-"));         break;
        case TOKEN_OPER_MUL     : write_text(w, S("*"));         break;
        case TOKEN_OPER_DIV     : write_text(w, S("/"));         break;
        case TOKEN_OPER_MOD     : write_text(w, S("%"));         break;
        case TOKEN_OPER_SHOVEL  : write_text(w, S("<<"));        break;
        case TOKEN_PAREN_OPEN   : write_text(w, S("("));         break;
        case TOKEN_PAREN_CLOSE  : write_text(w, S(")"));         break;
        case TOKEN_BRACKET_OPEN : write_text(w, S("["));         break;
        case TOKEN_BRACKET_CLOSE: write_text(w, S("]"));         break;
        case TOKEN_CURLY_OPEN   : write_text(w, S("{"));         break;
        case TOKEN_CURLY_CLOSE  : write_text(w, S("}"));         break;
        case TOKEN_DOT          : write_text(w, S("."));         break;
        case TOKEN_COMMA        : write_text(w, S(","));         break;
        case TOKEN_COLON        : write_text(w, S(":"));         break;
        case TOKEN_DOLLAR       : write_text(w, S("$"));         break;
        case TOKEN_NEWLINE      : write_text(w, S("\\n"));       break;

        case TOKEN_VALUE_STR:
        write_text(w, S("\""));
        write_text(w, token.sval); // TODO: Escape
        write_text(w, S("\""));
        break;

    }
}

static void parser_report(Parser *p, char *fmt, ...)
{
    if (p->errmax == 0 || p->errlen > 0)
        return;

    int line = 1;
    int cur = 0;
    while (cur < p->s.cur) {
        if (p->s.src[cur] == '\n')
            line++;
        cur++;
    }

    int len = snprintf(p->errbuf, p->errmax, "Error (line %d): ", line);
    ASSERT(len >= 0);

    va_list args;
    va_start(args, fmt);
    int ret = vsnprintf(p->errbuf + len, p->errmax - len, fmt, args);
    va_end(args);
    ASSERT(ret >= 0);
    len += ret;

    p->errlen = len;
}

static Node *alloc_node(Parser *p)
{
    Node *n = alloc(p->arena, sizeof(Node), _Alignof(Node));
    if (n == NULL) {
        parser_report(p, "Out of memory");
        return NULL;
    }

    return n;
}

static Token next_token(Parser *p)
{
    for (;;) {
        while (p->s.cur < p->s.len && is_space(p->s.src[p->s.cur]))
            p->s.cur++;

        if (!consume_str(&p->s, S("<!--")))
            break;

        while (p->s.cur < p->s.len) {
            if (consume_str(&p->s, S("-->")))
                break;
            p->s.cur++;
        }
    }

    if (p->s.cur == p->s.len)
        return (Token) { .type=TOKEN_END };
    char c = p->s.src[p->s.cur];

    if (is_alpha(c) || c == '_') {

        int start = p->s.cur;
        do
            p->s.cur++;
        while (p->s.cur < p->s.len && (is_alpha(p->s.src[p->s.cur]) || is_digit(p->s.src[p->s.cur]) || p->s.src[p->s.cur] == '_'));

        String kword = {
            p->s.src + start,
            p->s.cur - start
        };

        if (streq(kword, S("if")))      return (Token) { .type=TOKEN_KWORD_IF      };
        if (streq(kword, S("else")))    return (Token) { .type=TOKEN_KWORD_ELSE    };
        if (streq(kword, S("while")))   return (Token) { .type=TOKEN_KWORD_WHILE   };
        if (streq(kword, S("for")))     return (Token) { .type=TOKEN_KWORD_FOR     };
        if (streq(kword, S("in")))      return (Token) { .type=TOKEN_KWORD_IN      };
        if (streq(kword, S("procedure"))) return (Token) { .type=TOKEN_KWORD_PROCEDURE };
        if (streq(kword, S("let")))     return (Token) { .type=TOKEN_KWORD_LET     };
        if (streq(kword, S("none")))    return (Token) { .type=TOKEN_KWORD_NONE    };
        if (streq(kword, S("true")))    return (Token) { .type=TOKEN_KWORD_TRUE    };
        if (streq(kword, S("false")))   return (Token) { .type=TOKEN_KWORD_FALSE   };
        if (streq(kword, S("include"))) return (Token) { .type=TOKEN_KWORD_INCLUDE };
        if (streq(kword, S("len")))     return (Token) { .type=TOKEN_KWORD_LEN     };

        return (Token) { .type=TOKEN_IDENT, .sval=kword };
    }

    if (is_digit(c)) {

        int peek = p->s.cur;
        do
            peek++;
        while (peek < p->s.len && is_digit(p->s.src[peek]));

        if (p->s.len - peek > 1 && p->s.src[peek] == '.' && is_digit(p->s.src[peek+1])) {

            double buf = 0;
            do {
                int d = p->s.src[p->s.cur++] - '0';
                buf = buf * 10 + d;
            } while (p->s.cur < p->s.len && p->s.src[p->s.cur] != '.');

            p->s.cur++;

            double q = 1;
            do {
                int d = p->s.src[p->s.cur++] - '0';
                q /= 10;
                buf += q * d;
            } while (p->s.cur < p->s.len && is_digit(p->s.src[p->s.cur]));

            return (Token) { .type=TOKEN_VALUE_FLOAT, .fval=buf };

        } else {

            uint64_t buf = 0;
            do {
                int d = p->s.src[p->s.cur++] - '0';
                if (buf > (UINT64_MAX - d) / 10) {
                    parser_report(p, "Integer literal overflow");
                    return (Token) { .type=TOKEN_ERROR };
                }
                buf = buf * 10 + d;
            } while (p->s.cur < p->s.len && is_digit(p->s.src[p->s.cur]));

            return (Token) { .type=TOKEN_VALUE_INT, .ival=buf };
        }
    }

    if (c == '\'' || c == '"') {

        char f = c;
        p->s.cur++;

        char *buf = NULL;
        int   len = 0;

        for (;;) {

            int substr_off = p->s.cur;

            while (p->s.cur < p->s.len && is_printable(p->s.src[p->s.cur]) && p->s.src[p->s.cur] != f && p->s.src[p->s.cur] != '\\')
                p->s.cur++;

            int substr_len = p->s.cur - substr_off;

            if (buf == NULL)
                buf = alloc(p->arena, substr_len+1, 1);
            else
                if (!grow_alloc(p->arena, buf, len + substr_len+1))
                    buf = NULL;

            if (buf == NULL) {
                parser_report(p, "Out of memory");
                return (Token) { .type=TOKEN_ERROR };
            }

            if (substr_len > 0) {
                memcpy(
                    buf + len,
                    p->s.src + substr_off,
                    p->s.cur - substr_off
                );
                len += substr_len;
            }

            if (p->s.cur == p->s.len) {
                parser_report(p, "String literal wasn't closed");
                return (Token) { .type=TOKEN_ERROR };
            }

            if (!is_printable(p->s.src[p->s.cur])) {
                parser_report(p, "Invalid byte in string literal");
                return (Token) { .type=TOKEN_ERROR };
            }

            if (p->s.src[p->s.cur] == f)
                break;

            p->s.cur++;
            if (p->s.cur == p->s.len) {
                parser_report(p, "Missing special character after escape character \\");
                return (Token) { .type=TOKEN_ERROR };
            }

            switch (p->s.src[p->s.cur]) {
                case 'n':  buf[len++] = '\n'; break;
                case 't':  buf[len++] = '\t'; break;
                case 'r':  buf[len++] = '\r'; break;
                case '"':  buf[len++] = '"';  break;
                case '\'': buf[len++] = '\''; break;
                case '\\': buf[len++] = '\\'; break;

                case 'x':
                {
                    if (p->s.len - p->s.cur < 3
                        || !is_hex_digit(p->s.src[p->s.cur+1])
                        || !is_hex_digit(p->s.src[p->s.cur+2]))
                        return (Token) { .type=TOKEN_ERROR };
                    buf[len++]
                        = (hex_digit_to_int(p->s.src[p->s.cur+1]) << 4)
                        | (hex_digit_to_int(p->s.src[p->s.cur+2]) << 0);
                    p->s.cur += 2;
                }
                break;

                default:
                parser_report(p, "Invalid character after escape character \\");
                return (Token) { .type=TOKEN_ERROR };
            }

            p->s.cur++;
        }

        p->s.cur++;
        return (Token) { .type=TOKEN_VALUE_STR, .sval=(String) { .ptr=buf, .len=len } };
    }

    if (consume_str(&p->s, S("<<"))) return (Token) { .type=TOKEN_OPER_SHOVEL };
    if (consume_str(&p->s, S("=="))) return (Token) { .type=TOKEN_OPER_EQL };
    if (consume_str(&p->s, S("!="))) return (Token) { .type=TOKEN_OPER_NQL };
    if (consume_str(&p->s, S("<")))  return (Token) { .type=TOKEN_OPER_LSS };
    if (consume_str(&p->s, S(">")))  return (Token) { .type=TOKEN_OPER_GRT };
    if (consume_str(&p->s, S("+")))  return (Token) { .type=TOKEN_OPER_ADD };
    if (consume_str(&p->s, S("-")))  return (Token) { .type=TOKEN_OPER_SUB };
    if (consume_str(&p->s, S("*")))  return (Token) { .type=TOKEN_OPER_MUL };
    if (consume_str(&p->s, S("/")))  return (Token) { .type=TOKEN_OPER_DIV };
    if (consume_str(&p->s, S("%")))  return (Token) { .type=TOKEN_OPER_MOD };
    if (consume_str(&p->s, S("=")))  return (Token) { .type=TOKEN_OPER_ASS };

    if (consume_str(&p->s, S("(")))  return (Token) { .type=TOKEN_PAREN_OPEN };
    if (consume_str(&p->s, S(")")))  return (Token) { .type=TOKEN_PAREN_CLOSE };
    if (consume_str(&p->s, S("[")))  return (Token) { .type=TOKEN_BRACKET_OPEN };
    if (consume_str(&p->s, S("]")))  return (Token) { .type=TOKEN_BRACKET_CLOSE };
    if (consume_str(&p->s, S("{")))  return (Token) { .type=TOKEN_CURLY_OPEN };
    if (consume_str(&p->s, S("}")))  return (Token) { .type=TOKEN_CURLY_CLOSE };
    if (consume_str(&p->s, S(".")))  return (Token) { .type=TOKEN_DOT };
    if (consume_str(&p->s, S(",")))  return (Token) { .type=TOKEN_COMMA };
    if (consume_str(&p->s, S(":")))  return (Token) { .type=TOKEN_COLON };
    if (consume_str(&p->s, S("$")))  return (Token) { .type=TOKEN_DOLLAR };

    parser_report(p, "Invalid character '%c'", c);
    return (Token) { .type=TOKEN_ERROR };
}

static Token next_token_or_newline(Parser *p)
{
    int peek = p->s.cur;
    while (peek < p->s.len && is_space(p->s.src[peek]) && p->s.src[peek] != '\n')
        peek++;

    if (peek < p->s.len && p->s.src[peek] == '\n') {
        p->s.cur = peek+1;
        return (Token) { .type=TOKEN_NEWLINE };
    }

    return next_token(p);
}

enum {
    IGNORE_GRT = 1 << 0,
    IGNORE_LSS = 1 << 1,
    IGNORE_DIV = 1 << 2,
};

static Node *parse_stmt(Parser *p, int opflags);
static Node *parse_expr(Parser *p, int opflags);

static Node *parse_html(Parser *p)
{
    // NOTE: The first < was already consumed

    Token t = next_token(p);
    if (t.type != TOKEN_IDENT) {
        parser_report(p, "HTML tag doesn't start with a name");
        return NULL;
    }
    String tagname = t.sval;

    Node *attr_head;
    Node **attr_tail = &attr_head;

    bool no_body = false;
    Scanner *s = &p->s;
    for (;;) {

        int off = s->cur;

        bool quotes = false;
        while (s->cur < s->len && s->src[s->cur] != '\\' && (quotes || (s->src[s->cur] != '/' && s->src[s->cur] != '>'))) {
            if (s->src[s->cur] == '"')
                quotes = !quotes;
            s->cur++;
        }

        if (s->cur > off) {

            Node *child = alloc_node(p);
            if (child == NULL)
                return NULL;

            child->type = NODE_VALUE_STR;
            child->sval = (String) { p->s.src + off, p->s.cur - off };

            *attr_tail = child;
            attr_tail = &child->next;
        }

        if (s->cur == s->len) {
            ASSERT(0); // TODO
        }
        s->cur++;

        if (s->src[s->cur-1] == '>')
            break;

        if (s->src[s->cur-1] == '/') {
            while (s->cur < s->len && is_space(s->src[s->cur]))
                s->cur++;
            if (s->cur == s->len || s->src[s->cur] != '>') {
                ASSERT(0); // TODO
            }
            s->cur++;
            no_body = true;
            break;
        }

        ASSERT(s->src[s->cur-1] == '\\');

        Node *child = parse_stmt(p, IGNORE_GRT | IGNORE_DIV);
        if (child == NULL)
            return NULL;

        *attr_tail = child;
        attr_tail = &child->next;
    }

    *attr_tail = NULL;

    Node *child_head;
    Node **child_tail = &child_head;

    if (no_body == false)
        for (;;) {

            int off = s->cur;

            while (s->cur < s->len && s->src[s->cur] != '\\' && s->src[s->cur] != '<')
                s->cur++;

            if (s->cur > off) {

                Node *child = alloc_node(p);
                if (child == NULL)
                    return NULL;

                child->type = NODE_VALUE_STR;
                child->sval = (String) { p->s.src + off, p->s.cur - off };

                *child_tail = child;
                child_tail = &child->next;
            }

            if (s->cur == s->len) {
                ASSERT(0); // TODO
            }
            s->cur++;

            if (s->src[s->cur-1] == '<') {

                Scanner saved = *s;
                t = next_token(p);
                if (t.type == TOKEN_OPER_DIV) {

                    t = next_token(p);
                    if (t.type != TOKEN_IDENT) {
                        ASSERT(0); // TODO
                    }
                    String closing_tagname = t.sval;

                    if (!streq(closing_tagname, tagname)) {
                        ASSERT(0); // TODO
                    }

                    t = next_token(p);
                    if (t.type != TOKEN_OPER_GRT) {
                        ASSERT(0);
                    }

                    break;
                }

                *s = saved;

                Node *child = parse_html(p);
                if (child == NULL)
                    return NULL;

                *child_tail = child;
                child_tail = &child->next;

            } else {

                Node *child = parse_stmt(p, IGNORE_LSS);
                if (child == NULL)
                    return NULL;

                *child_tail = child;
                child_tail = &child->next;
            }
        }

    *child_tail = NULL;

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = NODE_VALUE_HTML;
    parent->html_tag   = tagname;
    parent->html_attr  = attr_head;
    parent->html_child = child_head;
    parent->html_body  = !no_body;

    return parent;
}

static Node *parse_array(Parser *p)
{
    // Left bracket already consumed

    Node *head;
    Node **tail = &head;

    Scanner saved = p->s;
    Token t = next_token(p);
    if (t.type != TOKEN_BRACKET_CLOSE) {

        p->s = saved;

        for (;;) {

            Node *child = parse_expr(p, 0);
            if (child == NULL)
                return NULL;

            *tail = child;
            tail = &child->next;

            saved = p->s;
            t = next_token(p);
            if (t.type == TOKEN_COMMA) {
                saved = p->s;
                t = next_token(p);
            }

            if (t.type == TOKEN_BRACKET_CLOSE)
                break;

            p->s = saved;
        }
    }

    *tail = NULL;

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = NODE_VALUE_ARRAY;
    parent->child  = head;

    return parent;
}

static Node *parse_map(Parser *p)
{
    // Left bracket already consumed

    Node *head;
    Node **tail = &head;

    Scanner saved = p->s;
    Token t = next_token(p);
    if (t.type != TOKEN_CURLY_CLOSE) {

        p->s = saved;

        for (;;) {

            Node *key;

            saved = p->s;
            t = next_token(p);
            if (t.type == TOKEN_IDENT) {
   
                key = alloc_node(p);
                if (key == NULL)
                    return NULL;

                key->type = NODE_VALUE_STR;
                key->sval = t.sval;

            } else {

                p->s = saved;
                key = parse_expr(p, 0);
                if (key == NULL)
                    return NULL;
            }

            t = next_token(p);
            if (t.type != TOKEN_COLON) {
                parser_report(p, "Missing ':' after key inside map literal");
                return NULL;
            }

            Node *child = parse_expr(p, 0);
            if (child == NULL)
                return NULL;
            child->key = key;

            *tail = child;
            tail = &child->next;

            saved = p->s;
            t = next_token(p);
            if (t.type == TOKEN_COMMA) {
                saved = p->s;
                t = next_token(p);
            }

            if (t.type == TOKEN_CURLY_CLOSE)
                break;

            p->s = saved;
        }
    }

    *tail = NULL;

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = NODE_VALUE_MAP;
    parent->child  = head;

    return parent;
}

static int precedence(Token t, int flags)
{
    switch (t.type) {

        case TOKEN_OPER_ASS:
        return 1;

        case TOKEN_OPER_SHOVEL:
        if (flags & IGNORE_LSS)
            return -1;
        return 1;

        case TOKEN_OPER_EQL:
        case TOKEN_OPER_NQL:
        return 2;

        case TOKEN_OPER_LSS:
        if (flags & IGNORE_LSS)
            return -1;
        return 2;

        case TOKEN_OPER_GRT:
        if (flags & IGNORE_GRT)
            return -1;
        return 2;

        case TOKEN_OPER_ADD:
        case TOKEN_OPER_SUB:
        return 3;

        case TOKEN_OPER_MUL:
        case TOKEN_OPER_MOD:
        return 4;

        case TOKEN_OPER_DIV:
        if (flags & IGNORE_DIV)
            return -1;
        return 4;

        default:
        break;
    }

    return -1;
}

static bool right_associative(Token t)
{
    return t.type == TOKEN_OPER_ASS;
}

static Node *parse_atom(Parser *p)
{
    Token t = next_token(p);

    Node *ret;
    switch (t.type) {
        case TOKEN_OPER_ADD:
        {
            Node *child = parse_atom(p);
            if (child == NULL)
                return NULL;

            Node *parent = alloc_node(p);
            if (parent == NULL)
                return NULL;

            parent->type = NODE_OPER_POS;
            parent->left = child;

            ret = parent;
        }
        break;

        case TOKEN_OPER_SUB:
        {
            Node *child = parse_atom(p);
            if (child == NULL)
                return NULL;

            Node *parent = alloc_node(p);
            if (parent == NULL)
                return NULL;

            parent->type = NODE_OPER_NEG;
            parent->left = child;

            ret = parent;
        }
        break;

        case TOKEN_KWORD_LEN:
        {
            Node *child = parse_atom(p);
            if (child == NULL)
                return NULL;

            Node *parent = alloc_node(p);
            if (parent == NULL)
                return NULL;

            parent->type = NODE_OPER_LEN;
            parent->left = child;

            ret = parent;
        }
        break;

        case TOKEN_IDENT:
        {
            Node *node = alloc_node(p);
            if (node == NULL)
                return NULL;

            node->type = NODE_VALUE_VAR;
            node->sval = t.sval;

            ret = node;
        }
        break;

        case TOKEN_VALUE_INT:
        {
            Node *node = alloc_node(p);
            if (node == NULL)
                return NULL;

            node->type = NODE_VALUE_INT;
            node->ival = t.ival;

            ret = node;
        }
        break;

        case TOKEN_VALUE_FLOAT:
        {
            Node *node = alloc_node(p);
            if (node == NULL)
                return NULL;

            node->type = NODE_VALUE_FLOAT;
            node->fval = t.fval;

            ret = node;
        }
        break;

        case TOKEN_VALUE_STR:
        {
            Node *node = alloc_node(p);
            if (node == NULL)
                return NULL;

            node->type = NODE_VALUE_STR;
            node->sval = t.sval;

            ret = node;
        }
        break;

        case TOKEN_KWORD_NONE:
        {
            Node *node = alloc_node(p);
            if (node == NULL)
                return NULL;

            node->type = NODE_VALUE_NONE;
            node->sval = t.sval;

            ret = node;
        }
        break;

        case TOKEN_KWORD_TRUE:
        {
            Node *node = alloc_node(p);
            if (node == NULL)
                return NULL;

            node->type = NODE_VALUE_TRUE;
            node->sval = t.sval;

            ret = node;
        }
        break;
        case TOKEN_KWORD_FALSE:
        {
            Node *node = alloc_node(p);
            if (node == NULL)
                return NULL;

            node->type = NODE_VALUE_FALSE;
            node->sval = t.sval;

            ret = node;
        }
        break;

        case TOKEN_OPER_LSS:
        {
            Node *node = parse_html(p);
            if (node == NULL)
                return NULL;

            ret = node;
        }
        break;

        case TOKEN_PAREN_OPEN:
        {
            Node *node = parse_expr(p, 0);
            if (node == NULL)
                return NULL;

            Token t = next_token(p);
            if (t.type != TOKEN_PAREN_CLOSE) {
                parser_report(p, "Missing ')' after expression");
                return NULL;
            }

            Node *parent = alloc_node(p);
            if (parent == NULL)
                return NULL;

            parent->type = NODE_NESTED;
            parent->left = node;

            ret = parent;
        }
        break;

        case TOKEN_BRACKET_OPEN:
        {
            Node *node = parse_array(p);
            if (node == NULL)
                return NULL;

            ret = node;
        }
        break;

        case TOKEN_CURLY_OPEN:
        {
            Node *node = parse_map(p);
            if (node == NULL)
                return NULL;

            ret = node;
        }
        break;

        case TOKEN_DOLLAR:
        {
            t = next_token(p);
            if (t.type != TOKEN_IDENT) {
                parser_report(p, "Missing identifier after '$'");
                return NULL;
            }

            Node *node = alloc_node(p);
            if (node == NULL)
                return NULL;

            node->type = NODE_VALUE_SYSVAR;
            node->sval = t.sval;

            ret = node;
        }
        break;

        default:
        {
            parser_report(p, "Invalid token inside expression");
        }
        return NULL;
    }

    for (;;) {
        Scanner saved = p->s;
        t = next_token(p);
        if (t.type == TOKEN_DOT) {

            t = next_token(p);
            if (t.type != TOKEN_IDENT) {
                parser_report(p, "Invalid token after '.' where an identifier was expected");
                return NULL;
            }

            Node *child = alloc_node(p);
            if (child == NULL)
                return NULL;

            child->type = NODE_VALUE_STR;
            child->sval = t.sval;

            Node *parent = alloc_node(p);
            if (parent == NULL)
                return NULL;

            parent->type = NODE_SELECT;
            parent->left = ret;
            parent->right = child;

            ret = parent;

        } else if (t.type == TOKEN_BRACKET_OPEN) {

            Node *child = parse_expr(p, 0);
            if (child == NULL)
                return NULL;

            t = next_token(p);
            if (t.type != TOKEN_BRACKET_CLOSE) {
                parser_report(p, "Missing token ']'");
                return NULL;
            }

            Node *parent = alloc_node(p);
            if (parent == NULL)
                return NULL;

            parent->type = NODE_SELECT;
            parent->left = ret;
            parent->right = child;

            ret = parent;

        } else if (t.type == TOKEN_PAREN_OPEN && (ret->type == NODE_VALUE_VAR || ret->type == NODE_VALUE_SYSVAR)) {

            Node *arg_head;
            Node **arg_tail = &arg_head;

            Scanner saved = p->s;
            t = next_token(p);
            if (t.type != TOKEN_PAREN_CLOSE) {

                p->s = saved;

                for (;;) {

                    Node *argval = parse_expr(p, 0);
                    if (argval == NULL)
                        return NULL;

                    *arg_tail = argval;
                    arg_tail = &argval->next;

                    t = next_token(p);
                    if (t.type == TOKEN_PAREN_CLOSE)
                        break;

                    if (t.type != TOKEN_COMMA) {
                        parser_report(p, "Expected ',' after argument in procedure call");
                        return NULL;
                    }
                }
            }

            *arg_tail = NULL;

            Node *parent = alloc_node(p);
            if (parent == NULL)
                return NULL;

            parent->type = NODE_PROCEDURE_CALL;
            parent->left = ret;
            parent->right = arg_head;

            ret = parent;

        } else {
            p->s = saved;
            break;
        }
    }

    return ret;
}

static Node *parse_expr_inner(Parser *p, Node *left, int min_prec, int flags)
{
    for (;;) {

        Scanner saved = p->s;
        Token t1 = next_token_or_newline(p);
        if (precedence(t1, flags) < min_prec) {
            p->s = saved;
           break;
        }

        Node *right = parse_atom(p);
        if (right == NULL)
            return NULL;

        for (;;) {

            saved = p->s;
            Token t2 = next_token_or_newline(p);
            int p1 = precedence(t1, flags);
            int p2 = precedence(t2, flags);
            p->s = saved;

            if (p2 < 0)
                break;

            if (p2 <= p1 && (p1 != p2 || !right_associative(t2)))
                break;

            right = parse_expr_inner(p, right, p1 + (p2 > p1), flags);
            if (right == NULL)
                return NULL;
        }

        Node *parent = alloc_node(p);
        if (parent == NULL)
            return NULL;

        parent->left = left;
        parent->right = right;

        switch (t1.type) {
            case TOKEN_OPER_ASS: parent->type = NODE_OPER_ASS; break;
            case TOKEN_OPER_EQL: parent->type = NODE_OPER_EQL; break;
            case TOKEN_OPER_NQL: parent->type = NODE_OPER_NQL; break;
            case TOKEN_OPER_LSS: parent->type = NODE_OPER_LSS; break;
            case TOKEN_OPER_GRT: parent->type = NODE_OPER_GRT; break;
            case TOKEN_OPER_ADD: parent->type = NODE_OPER_ADD; break;
            case TOKEN_OPER_SUB: parent->type = NODE_OPER_SUB; break;
            case TOKEN_OPER_MUL: parent->type = NODE_OPER_MUL; break;
            case TOKEN_OPER_DIV: parent->type = NODE_OPER_DIV; break;
            case TOKEN_OPER_MOD: parent->type = NODE_OPER_MOD; break;
            case TOKEN_OPER_SHOVEL: parent->type = NODE_OPER_SHOVEL; break;
            default:
            parser_report(p, "Operator not implemented");
            return NULL;
        }

        left = parent;
    }

    return left;
}

static Node *parse_expr(Parser *p, int flags)
{
    Node *left = parse_atom(p);
    if (left == NULL)
        return NULL;

    return parse_expr_inner(p, left, 0, flags);
}

static Node *parse_expr_stmt(Parser *p, int opflags)
{
    Node *e = parse_expr(p, opflags);
    if (e == NULL)
        return NULL;

    return e;
}

static Node *parse_ifelse_stmt(Parser *p, int opflags)
{
    Token t = next_token(p);
    if (t.type != TOKEN_KWORD_IF) {
        parser_report(p, "Missing 'if' keyword before if statement");
        return NULL;
    }

    Node *cond = parse_expr(p, 0);
    if (cond == NULL)
        return NULL;

    t = next_token(p);
    if (t.type != TOKEN_COLON) {
        parser_report(p, "Missing ':' after if condition");
        return NULL;
    }

    Node *if_stmt = parse_stmt(p, opflags);
    if (if_stmt == NULL)
        return NULL;

    Scanner saved = p->s;
    t = next_token(p);

    Node *else_stmt = NULL;
    if (t.type == TOKEN_KWORD_ELSE) {

        else_stmt = parse_stmt(p, opflags);
        if (else_stmt == NULL)
            return NULL;

    } else {
        p->s = saved;
    }

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = NODE_IFELSE;
    parent->if_cond = cond;
    parent->if_branch1 = if_stmt;
    parent->if_branch2 = else_stmt;

    return parent;
}

static Node *parse_for_stmt(Parser *p, int opflags)
{
    Token t = next_token(p);
    if (t.type != TOKEN_KWORD_FOR) {
        parser_report(p, "Missing 'for' keyword at the start of a for statement");
        return NULL;
    }

    t = next_token(p);
    if (t.type != TOKEN_IDENT) {
        parser_report(p, "Missing iteraion variable name in for statement");
        return NULL;
    }
    String var1 = t.sval;

    t = next_token(p);

    String var2 = S("");
    if (t.type == TOKEN_COMMA) {

        t = next_token(p);
        if (t.type != TOKEN_IDENT) {
            parser_report(p, "Missing iteration variable name after ',' in for statement");
            return NULL;
        }
        var2 = t.sval;

        t = next_token(p);
    }

    if (t.type != TOKEN_KWORD_IN) {
        parser_report(p, "Missing 'in' keyword after iteration variable name in for statement");
        return NULL;
    }

    Node *set = parse_expr(p, 0);
    if (set == NULL)
        return NULL;

    t = next_token(p);
    if (t.type != TOKEN_COLON) {
        parser_report(p, "Missing ':' after for statement set expression");
        return NULL;
    }

    Node *body = parse_stmt(p, opflags);
    if (body == NULL)
        return NULL;

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = NODE_FOR;
    parent->left = body;
    parent->for_var1 = var1;
    parent->for_var2 = var2;
    parent->for_set  = set;

    return parent;
}

static Node *parse_while_stmt(Parser *p, int opflags)
{
    Token t = next_token(p);
    if (t.type != TOKEN_KWORD_WHILE) {
        parser_report(p, "Missing keyword 'while' at the start of a while statement");
        return NULL;
    }

    Node *cond = parse_expr(p, 0);
    if (cond == NULL)
        return NULL;

    t = next_token(p);
    if (t.type != TOKEN_COLON) {
        parser_report(p, "Missing token ':' after while statement condition");
        return NULL;
    }

    Node *stmt = parse_stmt(p, opflags);
    if (stmt == NULL)
        return NULL;

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = NODE_WHILE;
    parent->left = stmt;
    parent->while_cond = cond;
    parent->while_body = stmt;

    return parent;
}

static Node *parse_compound_stmt(Parser *p, bool global)
{
    if (!global) {
        Token t = next_token(p);
        if (t.type != TOKEN_CURLY_OPEN) {
            parser_report(p, "Missing '{' at the start of a compound statement");
            return NULL;
        }
    }

    Node *head;
    Node **tail = &head;

    for (;;) {

        Scanner saved = p->s;
        Token t = next_token(p);
        if (!global) {
            if (t.type == TOKEN_CURLY_CLOSE)
                break;
        } else {
            if (t.type == TOKEN_END)
                break;
        }
        p->s = saved;

        Node *node = parse_stmt(p, 0);
        if (node == NULL)
            return NULL;

        *tail = node;
        tail = &node->next;
    }

    *tail = NULL;

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = global ? NODE_GLOBAL : NODE_COMPOUND;
    parent->left = head;

    return parent;
}

static Node *parse_proc_decl(Parser *p, int opflags)
{
    Token t = next_token(p);
    if (t.type != TOKEN_KWORD_PROCEDURE) {
        parser_report(p, "Missing keyword 'procedure' at the start of a procedure declaration");
        return NULL;
    }

    t = next_token(p);
    if (t.type != TOKEN_IDENT) {
        parser_report(p, "Missing procedure name after 'procedure' keyword");
        return NULL;
    }
    String name = t.sval;

    t = next_token(p);
    if (t.type != TOKEN_PAREN_OPEN) {
        parser_report(p, "Missing '(' after procedure name in declaration");
        return NULL;
    }

    Node *arg_head;
    Node **arg_tail = &arg_head;

    Scanner saved = p->s;
    t = next_token(p);
    if (t.type != TOKEN_PAREN_CLOSE) {
        p->s = saved;

        for (;;) {

            t = next_token(p);
            if (t.type != TOKEN_IDENT) {
                parser_report(p, "Missing argument name in procedure declaration");
                return NULL;
            }
            String argname = t.sval;

            Node *node = alloc_node(p);
            if (node == NULL)
                return NULL;

            node->type = NODE_PROCEDURE_ARG;
            node->sval = argname;

            *arg_tail = node;
            arg_tail = &node->next;

            Scanner saved = p->s;
            t = next_token(p);
            if (t.type == TOKEN_COMMA) {
                saved = p->s;
                t = next_token(p);
            }

            if (t.type == TOKEN_PAREN_CLOSE)
                break;
            p->s = saved;
        }
    }

    *arg_tail = NULL;

    Node *body = parse_stmt(p, opflags);
    if (body == NULL)
        return NULL;

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = NODE_PROCEDURE_DECL;
    parent->proc_name = name;
    parent->proc_args = arg_head;
    parent->proc_body = body;

    return parent;
}

static Node *parse_var_decl(Parser *p, int opflags)
{
    Token t = next_token(p);
    if (t.type != TOKEN_KWORD_LET) {
        parser_report(p, "Missing keyword 'let' at the start of a variable declaration");
        return NULL;
    }

    t = next_token(p);
    if (t.type != TOKEN_IDENT) {
        parser_report(p, "Missing variable name after 'let' keyword");
        return NULL;
    }
    String name = t.sval;

    Scanner saved = p->s;
    t = next_token(p);

    Node *value;
    if (t.type == TOKEN_OPER_ASS) {

        value = parse_expr(p, opflags);
        if (value == NULL)
            return NULL;

    } else {
        p->s = saved;
        value = NULL;
    }

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = NODE_VAR_DECL;
    parent->var_name = name;
    parent->var_value = value;

    return parent;
}

static Node *parse_include_stmt(Parser *p)
{
    Token t = next_token(p);
    if (t.type != TOKEN_KWORD_INCLUDE) {
        parser_report(p, "Missing keyword 'include' at the start of an include statement");
        return NULL;
    }

    t = next_token(p);
    if (t.type != TOKEN_VALUE_STR) {
        parser_report(p, "Missing file path string after 'include' keyword");
        return NULL;
    }
    String path = t.sval;

    Node *parent = alloc_node(p);
    if (parent == NULL)
        return NULL;

    parent->type = NODE_INCLUDE;
    parent->include_path = path;
    parent->include_root = NULL;

    *p->include_tail = parent;
    p->include_tail = &parent->include_next;

    return parent;
}

static Node *parse_stmt(Parser *p, int opflags)
{
    Scanner saved = p->s;
    Token t = next_token(p);
    p->s = saved;

    switch (t.type) {

        case TOKEN_KWORD_INCLUDE:
        return parse_include_stmt(p);

        case TOKEN_KWORD_PROCEDURE:
        return parse_proc_decl(p, opflags);

        case TOKEN_KWORD_LET:
        return parse_var_decl(p, opflags);

        case TOKEN_KWORD_IF:
        return parse_ifelse_stmt(p, opflags);

        case TOKEN_KWORD_WHILE:
        return parse_while_stmt(p, opflags);

        case TOKEN_KWORD_FOR:
        return parse_for_stmt(p, opflags);

        case TOKEN_CURLY_OPEN:
        return parse_compound_stmt(p, false);

        default:
        break;
    }

    return parse_expr_stmt(p, opflags);
}

static void write_node(Writer *w, Node *node)
{
    switch (node->type) {

        case NODE_VALUE_NONE : write_text(w, S("none")); break;
        case NODE_VALUE_TRUE : write_text(w, S("true")); break;
        case NODE_VALUE_FALSE: write_text(w, S("false")); break;

        case NODE_NESTED:
        write_text(w, S("(nested "));
        write_node(w, node->left);
        write_text(w, S(")"));
        break;

        case NODE_COMPOUND:
        {
            write_text(w, S("(compound "));
            Node *cur = node->left;
            while (cur) {
                write_node(w, cur);
                cur = cur->next;
                if (cur)
                    write_text(w, S(" "));
            }
            write_text(w, S(")"));
        }
        break;

        case NODE_GLOBAL:
        {
            write_text(w, S("(global "));
            Node *cur = node->left;
            while (cur) {
                write_node(w, cur);
                cur = cur->next;
                if (cur)
                    write_text(w, S(" "));
            }
            write_text(w, S(")"));
        }
        break;

        case NODE_OPER_LEN:
        write_text(w, S("(len "));
        write_node(w, node->left);
        write_text(w, S(")"));
        break;

        case NODE_OPER_POS:
        write_text(w, S("(+"));
        write_node(w, node->left);
        write_text(w, S(")"));
        break;

        case NODE_OPER_NEG:
        write_text(w, S("("));
        write_text(w, S("-"));
        write_node(w, node->left);
        write_text(w, S(")"));
        break;

        case NODE_OPER_ASS:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S("="));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_OPER_EQL:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S("=="));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_OPER_NQL:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S("!="));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_OPER_LSS:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S("<"));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_OPER_GRT:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S(">"));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_OPER_ADD:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S("+"));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_OPER_SUB:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S("-"));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_OPER_MUL:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S("*"));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_OPER_DIV:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S("/"));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_OPER_MOD:
        write_text(w, S("("));
        write_node(w, node->left);
        write_text(w, S("%%"));
        write_node(w, node->right);
        write_text(w, S(")"));
        break;

        case NODE_VALUE_INT:
        write_text_s64(w, node->ival);
        break;

        case NODE_VALUE_FLOAT:
        write_text_f64(w, node->fval);
        break;

        case NODE_VALUE_STR:
        write_text(w, S("\""));
        write_text(w, node->sval);
        write_text(w, S("\""));
        break;

        case NODE_VALUE_VAR:
        write_text(w, node->sval);
        break;

        case NODE_VALUE_SYSVAR:
        write_text(w, S("$"));
        write_text(w, node->sval);
        break;

        case NODE_IFELSE:
        write_text(w, S("(if "));
        write_node(w, node->if_cond);
        write_text(w, S(" "));
        write_node(w, node->if_branch1);
        if (node->if_branch2) {
            write_text(w, S(" else "));
            write_node(w, node->if_branch2);
        }
        write_text(w, S(")"));
        break;

        case NODE_WHILE:
        write_text(w, S("(while "));
        write_node(w, node->while_cond);
        write_text(w, S(" "));
        write_node(w, node->while_body);
        write_text(w, S(")"));
        break;

        case NODE_VALUE_HTML:
        {
            write_text(w, S("(html "));
            write_text(w, node->html_tag);

            Node *child = node->html_child;
            while (child) {
                write_text(w, S(" "));
                write_node(w, child);
                child = child->next;
            }

            write_text(w, S(")"));
        }
        break;

        case NODE_FOR:
        write_text(w, S("(for "));
        write_text(w, node->for_var1);
        if (node->for_var2.len > 0) {
            write_text(w, S(", "));
            write_text(w, node->for_var2);
        }
        write_text(w, S(" in "));
        write_node(w, node->for_set);
        write_text(w, S(": "));
        write_node(w, node->left);
        break;

        case NODE_SELECT:
        write_node(w, node->left);
        write_text(w, S("["));
        write_node(w, node->right);
        write_text(w, S("]"));
        break;

        case NODE_VALUE_ARRAY:
        {
            write_text(w, S("["));
            Node *child = node->child;
            while (child) {
                write_node(w, child);
                write_text(w, S(", "));
                child = child->next;
            }
            write_text(w, S("]"));
        }
        break;

        case NODE_VALUE_MAP:
        {
            write_text(w, S("{"));
            Node *child = node->child;
            while (child) {
                write_node(w, child->key);
                write_text(w, S(": "));
                write_node(w, child);
                write_text(w, S(", "));
                child = child->next;
            }
            write_text(w, S("}"));
        }
        break;

        case NODE_PROCEDURE_DECL:
        {
            write_text(w, S("(proc "));
            write_text(w, node->proc_name);
            write_text(w, S("("));
            Node *arg = node->proc_args;
            while (arg) {
                write_node(w, arg);
                arg = arg->next;
                if (arg)
                    write_text(w, S(", "));
            }
            write_text(w, S(")"));
            write_node(w, node->proc_body);
        }
        break;

        case NODE_PROCEDURE_ARG:
        write_text(w, node->sval);
        break;

        case NODE_PROCEDURE_CALL:
        {
            write_node(w, node->left);
            write_text(w, S("("));
            Node *arg = node->right;
            while (arg) {
                write_node(w, arg);
                arg = arg->next;
                if (arg)
                    write_text(w, S(", "));
            }
            write_text(w, S(")"));
        }
        break;

        case NODE_VAR_DECL:
        write_text(w, S("(let "));
        write_text(w, node->var_name);
        if (node->var_value) {
            write_text(w, S(" = "));
            write_node(w, node->var_value);
        }
        write_text(w, S(")"));
        break;

        case NODE_INCLUDE:
        write_text(w, S("include \""));
        write_text(w, node->include_path);
        write_text(w, S("\""));
        break;
    }
}

static ParseResult parse(String src, WL_Arena *arena, char *errbuf, int errmax)
{
    Parser p = {
        .s={ src.ptr, src.len, 0 },
        .arena=arena,
        .errbuf=errbuf,
        .errmax=errmax,
        .errlen=0,
    };

    p.include_tail = &p.include_head;

    Node *node = parse_compound_stmt(&p, true);
    if (node == NULL)
        return (ParseResult) { .node=NULL, .includes=NULL, .errlen=p.errlen };

    *p.include_tail = NULL;
    return (ParseResult) { .node=node, .includes=p.include_head, .errlen=-1 };
}

/////////////////////////////////////////////////////////////////////////
// CODEGEN
/////////////////////////////////////////////////////////////////////////

enum {
    OPCODE_NOPE,
    OPCODE_JUMP,
    OPCODE_JIFP,
    OPCODE_OUTPUT,
    OPCODE_SYSVAR,
    OPCODE_SYSCALL,
    OPCODE_CALL,
    OPCODE_RET,
    OPCODE_GROUP,
    OPCODE_PACK,
    OPCODE_GPOP,
    OPCODE_FOR,
    OPCODE_EXIT,
    OPCODE_VARS,
    OPCODE_POP,
    OPCODE_SETV,
    OPCODE_PUSHV,
    OPCODE_PUSHI,
    OPCODE_PUSHF,
    OPCODE_PUSHS,
    OPCODE_PUSHA,
    OPCODE_PUSHM,
    OPCODE_PUSHN,
    OPCODE_PUSHT,
    OPCODE_PUSHFL,
    OPCODE_LEN,
    OPCODE_NEG,
    OPCODE_EQL,
    OPCODE_NQL,
    OPCODE_LSS,
    OPCODE_GRT,
    OPCODE_ADD,
    OPCODE_SUB,
    OPCODE_MUL,
    OPCODE_DIV,
    OPCODE_MOD,
    OPCODE_APPEND,
    OPCODE_INSERT1,
    OPCODE_INSERT2,
    OPCODE_SELECT,
};

typedef struct UnpatchedCall UnpatchedCall;
struct UnpatchedCall {
    UnpatchedCall *next;
    String         name;
    int            off;
};

typedef enum {
    SYMBOL_VARIABLE,
    SYMBOL_PROCEDURE,
} SymbolType;

typedef struct {
    SymbolType type;
    String     name;
    bool       cnst;
    int        off;
} Symbol;

typedef enum {
    SCOPE_IF,
    SCOPE_ELSE,
    SCOPE_FOR,
    SCOPE_WHILE,
    SCOPE_PROC,
    SCOPE_COMPOUND,
    SCOPE_GLOBAL,
    SCOPE_ASSIGNMENT,
} ScopeType;

typedef struct {
    ScopeType type;
    int idx_syms;
    int max_vars;
    UnpatchedCall *calls;
} Scope;

#define MAX_SYMBOLS 1024
#define MAX_SCOPES 128
#define MAX_UNPATCHED_CALLS 32

typedef struct {

    Writer code;
    Writer data;

    int num_scopes;
    Scope scopes[MAX_SCOPES];

    int num_syms;
    Symbol syms[MAX_SYMBOLS];

    UnpatchedCall *free_list_calls;
    UnpatchedCall calls[MAX_UNPATCHED_CALLS];

    bool  err;
    char *errmsg;
    int   errcap;

    int data_off;

} Codegen;

static void cg_report(Codegen *cg, char *fmt, ...)
{
    if (cg->err) return;

    va_list args;
    va_start(args, fmt);
    int len = vsnprintf(cg->errmsg, cg->errcap, fmt, args);
    va_end(args);

    if (len > cg->errcap)
        len = cg->errcap-1;

    cg->errmsg[len] = '\0';
    cg->err = true;
}

static int cg_write_u8(Codegen *cg, uint8_t x)
{
    if (cg->err) return -1;

    int off = cg->code.len;
    write_raw_u8(&cg->code, x);
    return off;
}

static int cg_write_u32(Codegen *cg, uint32_t x)
{
    if (cg->err) return -1;

    int off = cg->code.len;
    write_raw_u32(&cg->code, x);
    return off;
}

static int cg_write_s64(Codegen *cg, int64_t x)
{
    if (cg->err) return -1;

    int off = cg->code.len;
    write_raw_s64(&cg->code, x);
    return off;
}

static int cg_write_f64(Codegen *cg, double x)
{
    if (cg->err) return -1;

    int off = cg->code.len;
    write_raw_f64(&cg->code, x);
    return off;
}

static void cg_write_str(Codegen *cg, String x)
{
    if (cg->err) return;

    int off = cg->data.len;
    write_text(&cg->data, x);
    write_raw_u32(&cg->code, off);
    write_raw_u32(&cg->code, x.len);
}

static void cg_patch_u8(Codegen *cg, int off, uint8_t x)
{
    if (cg->err) return;

    patch_mem(&cg->code, &x, off, SIZEOF(x));
}

static void cg_patch_u32(Codegen *cg, int off, uint32_t x)
{
    if (cg->err) return;

    patch_mem(&cg->code, &x, off, SIZEOF(x));
}

static uint32_t cg_current_offset(Codegen *cg)
{
    return cg->code.len;
}

int count_nodes(Node *head)
{
    int n = 0;
    Node *node = head;
    while (node) {
        n++;
        node = node->next;
    }
    return n;
}

static Scope *parent_scope(Codegen *cg)
{
    ASSERT(cg->num_scopes > 0);

    int parent = cg->num_scopes-1;
    while (cg->scopes[parent].type != SCOPE_PROC && cg->scopes[parent].type != SCOPE_GLOBAL)
        parent--;

    return &cg->scopes[parent];
}

static bool inside_assignment(Codegen *cg)
{
    ASSERT(cg->num_scopes > 0);

    int parent = cg->num_scopes-1;
    while (cg->scopes[parent].type != SCOPE_PROC
        && cg->scopes[parent].type != SCOPE_GLOBAL
        && cg->scopes[parent].type != SCOPE_ASSIGNMENT)
        parent--;

    return cg->scopes[parent].type == SCOPE_ASSIGNMENT;
}

static int count_function_vars(Codegen *cg)
{
    int n = 0;
    Scope *scope = parent_scope(cg);
    for (int i = scope->idx_syms; i < cg->num_syms; i++)
        if (cg->syms[i].type == SYMBOL_VARIABLE)
            n++;
    return n;
}

static Symbol *cg_find_symbol(Codegen *cg, String name, bool local)
{
    if (cg->err) return NULL;

    if (name.len == 0) return NULL;
    ASSERT(cg->num_scopes > 0);
    Scope *scope = local ? &cg->scopes[cg->num_scopes-1] : parent_scope(cg);
    for (int i = cg->num_syms-1; i >= scope->idx_syms; i--)
        if (streq(cg->syms[i].name, name))
            return &cg->syms[i];
    return NULL;
}

static int cg_declare_variable(Codegen *cg, String name, bool cnst)
{
    if (cg->err) return -1;

    Symbol *sym = cg_find_symbol(cg, name, true);
    if (sym) {
        cg_report(cg, "Variable declared twice");
        return -1;
    }

    if (cg->num_syms == MAX_SYMBOLS) {
        cg_report(cg, "Symbol count limit reached");
        return -1;
    }

    int off = count_function_vars(cg);

    Scope *parent = parent_scope(cg);
    parent->max_vars = MAX(parent->max_vars, off+1);

    cg->syms[cg->num_syms++] = (Symbol) {
        .type = SYMBOL_VARIABLE,
        .name = name,
        .cnst = cnst,
        .off  = off,
    };
    return off;
}

static void cg_declare_procedure(Codegen *cg, String name, int off)
{
    if (cg->err) return;

    Symbol *sym = cg_find_symbol(cg, name, true);
    if (sym) {
        cg_report(cg, "Procedure declared twice");
        return;
    }

    if (cg->num_syms == MAX_SYMBOLS) {
        cg_report(cg, "Symbol count limit reached");
        return;
    }

    cg->syms[cg->num_syms++] = (Symbol) {
        .type = SYMBOL_PROCEDURE,
        .name = name,
        .cnst = true,
        .off  = off,
    };
}

static void cg_push_scope(Codegen *cg, ScopeType type)
{
    if (cg->err) return;

    if (cg->num_scopes == MAX_SCOPES) {
        cg_report(cg, "Scope limit reached");
        return;
    }

    Scope *scope = &cg->scopes[cg->num_scopes++];
    scope->type     = type;
    scope->idx_syms = cg->num_syms;
    scope->max_vars = 0;
    scope->calls    = NULL;
}

static void cg_pop_scope(Codegen *cg)
{
    if (cg->err) return;

    ASSERT(cg->num_scopes > 0);
    Scope *scope = &cg->scopes[cg->num_scopes-1];

    ASSERT(scope->type == SCOPE_PROC || scope->type == SCOPE_GLOBAL || scope->max_vars == 0);

    Scope *parent_scope = NULL;
    if (cg->num_scopes > 1)
        parent_scope = &cg->scopes[cg->num_scopes-2];

    while (scope->calls) {

        UnpatchedCall *call = scope->calls;
        scope->calls = call->next;

        ASSERT(call - cg->calls >= 0 && call - cg->calls < MAX_UNPATCHED_CALLS);

        Symbol *sym = cg_find_symbol(cg, call->name, true);

        if (sym == NULL) {
            if (parent_scope == NULL) {
                cg_report(cg, "Undefined function '%.*s'",
                    scope->calls->name.len,
                    scope->calls->name.ptr);
                    return;
                }
            call->next = parent_scope->calls;
            parent_scope->calls = call; 
            continue;
        }

        if (sym->type != SYMBOL_PROCEDURE) {
            cg_report(cg, "Symbol '%.*s' is not a procedure", call->name.len, call->name.ptr);
            return;
        }

        cg_patch_u32(cg, call->off, sym->off);

        call->next = cg->free_list_calls;
        cg->free_list_calls = call;

        // TODO: remove
        ASSERT(cg->scopes[cg->num_scopes-1].calls == NULL || (cg->scopes[cg->num_scopes-1].calls - cg->calls >= 0 && cg->scopes[cg->num_scopes-1].calls - cg->calls < MAX_UNPATCHED_CALLS));
    }

    cg->num_syms = scope->idx_syms;
    cg->num_scopes--;
}

static void cg_append_unpatched_call(Codegen *cg, String name, int p)
{
    if (cg->err) return;

    if (cg->free_list_calls == NULL) {
        cg_report(cg, "Out of memory");
        return;
    }
    UnpatchedCall *call = cg->free_list_calls;
    cg->free_list_calls = call->next;

    ASSERT(call - cg->calls >= 0 && call - cg->calls < MAX_UNPATCHED_CALLS);

    call->name = name;
    call->off  = p;
    call->next = NULL;

    ASSERT(cg->num_scopes > 0);
    Scope *scope = &cg->scopes[cg->num_scopes-1];

    call->next = scope->calls;
    scope->calls = call;
}

static bool cg_global_scope(Codegen *cg)
{
    Scope *scope = parent_scope(cg);
    return scope->type == SCOPE_GLOBAL;
}

static void cg_flush_pushs(Codegen *cg)
{
    if (cg->data_off != -1) {
        if (cg->data_off < cg->data.len) {
            cg_write_u8(cg, OPCODE_PUSHS);
            cg_write_u32(cg, cg->data_off);
            cg_write_u32(cg, cg->data.len - cg->data_off);
        }
        cg->data_off = -1;
    }
}

static int cg_write_opcode(Codegen *cg, uint8_t opcode)
{
    ASSERT(opcode != OPCODE_PUSHS);
    cg_flush_pushs(cg);
    return cg_write_u8(cg, opcode);
}

static void cg_write_pushs(Codegen *cg, String str, bool dont_group)
{
    if (dont_group) {
        cg_flush_pushs(cg);
        cg_write_u8(cg, OPCODE_PUSHS);
        cg_write_str(cg, str);
    } else {
        if (cg->data_off == -1)
            cg->data_off = cg->data.len;
        write_raw_mem(&cg->data, str.ptr, str.len);
    }
}

static void walk_node(Codegen *cg, Node *node);

static void walk_expr_node(Codegen *cg, Node *node, bool one)
{
    // TODO: remove
    ASSERT(cg->scopes[cg->num_scopes-1].calls == NULL || (cg->scopes[cg->num_scopes-1].calls - cg->calls >= 0 && cg->scopes[cg->num_scopes-1].calls - cg->calls < MAX_UNPATCHED_CALLS));

    switch (node->type) {

        case NODE_NESTED:
        walk_expr_node(cg, node->left, one);
        break;

        case NODE_OPER_LEN:
        walk_expr_node(cg, node->left, true);
        cg_write_opcode(cg, OPCODE_LEN);
        break;

        case NODE_OPER_POS:
        walk_expr_node(cg, node->left, one);
        break;

        case NODE_OPER_NEG:
        walk_expr_node(cg, node->left, true);
        cg_write_opcode(cg, OPCODE_NEG);
        break;

        case NODE_OPER_ASS:
        {
            Node *dst = node->left;
            Node *src = node->right;

            if (dst->type == NODE_VALUE_VAR) {

                String name = dst->sval;
                Symbol *sym = cg_find_symbol(cg, name, false);
                if (sym == NULL) {
                    cg_report(cg, "Write to undeclared variable");
                    return;
                }
                if (sym->type == SYMBOL_PROCEDURE) {
                    cg_report(cg, "Symbol is not a variable");
                    return;
                }
                if (sym->cnst) {
                    cg_report(cg, "Variable is constant");
                    return;
                }

                cg_push_scope(cg, SCOPE_ASSIGNMENT);
                walk_expr_node(cg, src, true);
                cg_pop_scope(cg);

                cg_write_opcode(cg, OPCODE_SETV);
                cg_write_u8(cg, sym->off);

                if (!one)
                    cg_write_opcode(cg, OPCODE_POP);

            } else if (dst->type == NODE_SELECT) {

                cg_push_scope(cg, SCOPE_ASSIGNMENT);
                walk_expr_node(cg, src, true);
                cg_pop_scope(cg);

                walk_expr_node(cg, dst->left,  true);
                walk_expr_node(cg, dst->right, true);
                cg_write_opcode(cg, OPCODE_INSERT2);

                if (!one)
                    cg_write_opcode(cg, OPCODE_POP);

            } else {

                cg_report(cg, "Assignment left side can't be assigned to");
                return;
            }
        }
        break;

        case NODE_OPER_SHOVEL:
        {
            walk_expr_node(cg, node->left, true);

            cg_push_scope(cg, SCOPE_ASSIGNMENT);
            walk_expr_node(cg, node->right, true);
            cg_pop_scope(cg);

            cg_write_opcode(cg, OPCODE_APPEND);
            if (!one)
                cg_write_opcode(cg, OPCODE_POP);
        }
        break;

        case NODE_OPER_EQL:
        walk_expr_node(cg, node->left, true);
        walk_expr_node(cg, node->right, true);
        cg_write_opcode(cg, OPCODE_EQL);
        break;

        case NODE_OPER_NQL:
        walk_expr_node(cg, node->left, true);
        walk_expr_node(cg, node->right, true);
        cg_write_opcode(cg, OPCODE_NQL);
        break;

        case NODE_OPER_LSS:
        walk_expr_node(cg, node->left, true);
        walk_expr_node(cg, node->right, true);
        cg_write_opcode(cg, OPCODE_LSS);
        break;

        case NODE_OPER_GRT:
        walk_expr_node(cg, node->left, true);
        walk_expr_node(cg, node->right, true);
        cg_write_opcode(cg, OPCODE_GRT);
        break;

        case NODE_OPER_ADD:
        walk_expr_node(cg, node->left, true);
        walk_expr_node(cg, node->right, true);
        cg_write_opcode(cg, OPCODE_ADD);
        break;

        case NODE_OPER_SUB:
        walk_expr_node(cg, node->left, true);
        walk_expr_node(cg, node->right, true);
        cg_write_opcode(cg, OPCODE_SUB);
        break;

        case NODE_OPER_MUL:
        walk_expr_node(cg, node->left, true);
        walk_expr_node(cg, node->right, true);
        cg_write_opcode(cg, OPCODE_MUL);
        break;

        case NODE_OPER_DIV:
        walk_expr_node(cg, node->left, true);
        walk_expr_node(cg, node->right, true);
        cg_write_opcode(cg, OPCODE_DIV);
        break;

        case NODE_OPER_MOD:
        walk_expr_node(cg, node->left, true);
        walk_expr_node(cg, node->right, true);
        cg_write_opcode(cg, OPCODE_MOD);
        break;

        case NODE_VALUE_INT:
        cg_write_opcode(cg, OPCODE_PUSHI);
        cg_write_s64(cg, node->ival);
        break;

        case NODE_VALUE_FLOAT:
        cg_write_opcode(cg, OPCODE_PUSHF);
        cg_write_f64(cg, node->fval);
        break;

        case NODE_VALUE_STR:
        cg_write_pushs(cg, node->sval, one);
        break;

        case NODE_VALUE_NONE:
        cg_write_opcode(cg, OPCODE_PUSHN);
        break;

        case NODE_VALUE_TRUE:
        cg_write_opcode(cg, OPCODE_PUSHT);
        break;

        case NODE_VALUE_FALSE:
        cg_write_opcode(cg, OPCODE_PUSHFL);
        break;

        case NODE_VALUE_VAR:
        {
            String name = node->sval;
            Symbol *sym = cg_find_symbol(cg, name, false);
            if (sym == NULL) {
                cg_report(cg, "Access to undeclared variable '%.*s'", name.len, name.ptr);
                return;
            }
            if (sym->type == SYMBOL_PROCEDURE) {
                cg_report(cg, "Symbol is not a variable");
                return;
            }

            cg_write_opcode(cg, OPCODE_PUSHV);
            cg_write_u8(cg, sym->off);
        }
        break;

        case NODE_VALUE_SYSVAR:
        cg_write_opcode(cg, OPCODE_SYSVAR);
        cg_write_str(cg, node->sval);
        break;

        case NODE_VALUE_HTML:
        {
            if (one)
                cg_write_opcode(cg, OPCODE_GROUP);

            cg_write_pushs(cg, S("<"), false);
            cg_write_pushs(cg, node->html_tag, false);

            Node *child = node->html_attr;
            while (child) {
                walk_node(cg, child);
                child = child->next;
            }

            if (!node->html_body) {
                cg_write_pushs(cg, S("/>"), false);
            } else {
                cg_write_pushs(cg, S(">"), false);
                Node *child = node->html_child;
                while (child) {
                    walk_node(cg, child);
                    child = child->next;
                }
                cg_write_pushs(cg, S("</"), false);
                cg_write_pushs(cg, node->html_tag, false);
                cg_write_pushs(cg, S(">"), false);
            }

            if (one)
                cg_write_opcode(cg, OPCODE_PACK);
        }
        break;

        case NODE_VALUE_ARRAY:
        {
            cg_write_opcode(cg, OPCODE_PUSHA);
            cg_write_u32(cg, count_nodes(node->child));

            Node *child = node->child;
            while (child) {
                walk_expr_node(cg, child, true);
                cg_write_opcode(cg, OPCODE_APPEND);
                child = child->next;
            }
        }
        break;

        case NODE_VALUE_MAP:
        {
            cg_write_opcode(cg, OPCODE_PUSHM);
            cg_write_u32(cg, count_nodes(node->child));

            Node *child = node->child;
            while (child) {
                walk_expr_node(cg, child, true);
                walk_expr_node(cg, child->key, true);
                cg_write_opcode(cg, OPCODE_INSERT1);
                child = child->next;
            }
        }
        break;

        case NODE_SELECT:
        {
            Node *set = node->left;
            Node *key = node->right;
            walk_expr_node(cg, set, true);
            walk_expr_node(cg, key, true);
            cg_write_opcode(cg, OPCODE_SELECT);
        }
        break;

        case NODE_PROCEDURE_CALL:
        {
            if (one)
                cg_write_opcode(cg, OPCODE_GROUP);

            int count = 0;
            Node *arg = node->right;
            while (arg) {
                walk_expr_node(cg, arg, true);
                count++;
                arg = arg->next;
            }

            Node *proc = node->left;
            if (proc->type == NODE_VALUE_VAR) {
                
                cg_write_opcode(cg, OPCODE_CALL);
                cg_write_u8(cg, count);
                int p = cg_write_u32(cg, 0);
                cg_append_unpatched_call(cg, proc->sval, p);

            } else {

                ASSERT(proc->type == NODE_VALUE_SYSVAR);
                cg_write_opcode(cg, OPCODE_SYSCALL);
                cg_write_u8(cg, count);
                cg_write_str(cg, proc->sval);
            }

            if (one)
                cg_write_opcode(cg, OPCODE_PACK);
        }
        break;

        default:
        UNREACHABLE;
    }
}

static void walk_node(Codegen *cg, Node *node)
{
    // TODO: remove
    ASSERT(cg->scopes[cg->num_scopes-1].calls == NULL || (cg->scopes[cg->num_scopes-1].calls - cg->calls >= 0 && cg->scopes[cg->num_scopes-1].calls - cg->calls < MAX_UNPATCHED_CALLS));

    switch (node->type) {

        case NODE_GLOBAL:
        for (Node *child = node->left;
            child; child = child->next) {
            walk_node(cg, child);
        }
        break;

        case NODE_COMPOUND:
        cg_push_scope(cg, SCOPE_COMPOUND);
        for (Node *child = node->left;
            child; child = child->next)
            walk_node(cg, child);
        cg_pop_scope(cg);
        break;

        case NODE_PROCEDURE_DECL:
        {
            cg_push_scope(cg, SCOPE_PROC);

            cg_write_opcode(cg, OPCODE_JUMP);
            int off0 = cg_write_u32(cg, 0);

            #define MAX_ARGS 128

            int num_args = 0;
            Node *args[MAX_ARGS];

            Node *arg = node->proc_args;
            while (arg) {
                if (num_args == MAX_ARGS) {
                    cg_report(cg, "Procedure argument limit reached");
                    return;
                }
                args[num_args++] = arg;
                arg = arg->next;
            }

            for (int i = num_args-1; i >= 0; i--)
                cg_declare_variable(cg, args[i]->sval, false);

            int off1 = cg_write_opcode(cg, OPCODE_VARS);
            int off2 = cg_write_u8(cg, 0);

            walk_node(cg, node->proc_body);
            cg_write_opcode(cg, OPCODE_RET);

            cg_patch_u8 (cg, off2, cg->scopes[cg->num_scopes-1].max_vars);
            cg_patch_u32(cg, off0, cg_current_offset(cg));

            cg_pop_scope(cg);

            cg_declare_procedure(cg, node->proc_name, off1);
        }
        break;

        case NODE_VAR_DECL:
        {
            int off = cg_declare_variable(cg, node->var_name, false);
            if (node->var_value) {
                cg_push_scope(cg, SCOPE_ASSIGNMENT);
                walk_expr_node(cg, node->var_value, true);
                cg_pop_scope(cg);
            } else
                cg_write_opcode(cg, OPCODE_PUSHN);
            cg_write_opcode(cg, OPCODE_SETV);
            cg_write_u8(cg, off);
            cg_write_opcode(cg, OPCODE_POP);
        }
        break;

        case NODE_IFELSE:
        {
            // If there is no else branch:
            //
            //   <cond>
            //   JIFP end
            //   <left>
            // end:
            //   ...
            //
            // If there is:
            //
            //   <cond>
            //   JIFP else
            //   <left>
            //   JUMP end
            // else:
            //   <right>
            // end:
            //   ...

            if (node->if_branch2) {

                walk_expr_node(cg, node->if_cond, true);

                cg_write_opcode(cg, OPCODE_JIFP);
                int p1 = cg_write_u32(cg, 0);

                cg_push_scope(cg, SCOPE_IF);
                walk_node(cg, node->if_branch1);
                cg_pop_scope(cg);

                cg_write_opcode(cg, OPCODE_JUMP);
                int p2 = cg_write_u32(cg, 0);

                cg_flush_pushs(cg);
                cg_patch_u32(cg, p1, cg_current_offset(cg));

                cg_push_scope(cg, SCOPE_ELSE);
                walk_node(cg, node->if_branch2);
                cg_pop_scope(cg);

                cg_flush_pushs(cg);
                cg_patch_u32(cg, p2, cg_current_offset(cg));

            } else {

                walk_expr_node(cg, node->if_cond, true);

                cg_write_opcode(cg, OPCODE_JIFP);
                int p1 = cg_write_u32(cg, 0);

                cg_push_scope(cg, SCOPE_IF);
                walk_node(cg, node->if_branch1);
                cg_pop_scope(cg);

                cg_flush_pushs(cg);
                cg_patch_u32(cg, p1, cg_current_offset(cg));
            }
        }
        break;

        case NODE_FOR:
        {
            cg_push_scope(cg, SCOPE_FOR);

            int var_1 = cg_declare_variable(cg, node->for_var1, false);
            int var_2 = cg_declare_variable(cg, node->for_var2, true);
            int var_3 = cg_declare_variable(cg, (String) { NULL, 0 }, true);

            walk_expr_node(cg, node->for_set, true);
            cg_write_opcode(cg, OPCODE_SETV);
            cg_write_u8(cg, var_3);
            cg_write_opcode(cg, OPCODE_POP);

            cg_write_opcode(cg, OPCODE_PUSHI);
            cg_write_s64(cg, -1);
            cg_write_opcode(cg, OPCODE_SETV);
            cg_write_u8(cg, var_2);
            cg_write_opcode(cg, OPCODE_POP);

            int start = cg_write_opcode(cg, OPCODE_FOR);
            cg_write_u8(cg, var_3);
            cg_write_u8(cg, var_1);
            cg_write_u8(cg, var_2);
            int p = cg_write_u32(cg, 0);

            walk_node(cg, node->left);

            cg_write_opcode(cg, OPCODE_JUMP);
            cg_write_u32(cg, start);

            cg_patch_u32(cg, p, cg_current_offset(cg));

            cg_pop_scope(cg);
        }
        break;

        case NODE_WHILE:
        {
            // start:
            //   <cond>
            //   JIFP end
            //   <body>
            //   JUMP start
            // end:
            //   ...

            int start = cg_current_offset(cg);

            walk_expr_node(cg, node->while_cond, true);

            cg_write_opcode(cg, OPCODE_JIFP);
            int p = cg_write_u32(cg, 0);

            cg_push_scope(cg, SCOPE_WHILE);
            walk_node(cg, node->left);
            cg_pop_scope(cg);

            cg_write_opcode(cg, OPCODE_JUMP);
            cg_write_u32(cg, start);

            cg_patch_u32(cg, p, cg_current_offset(cg));
        }
        break;

        case NODE_INCLUDE:
        walk_node(cg, node->include_root);
        break;

        default:
        walk_expr_node(cg, node, false);
        if (cg_global_scope(cg) && !inside_assignment(cg))
            cg_write_opcode(cg, OPCODE_OUTPUT);
        break;
    }
}

#define WL_MAGIC 0xFEEDBEEF

static int codegen(Node *node, char *dst, int cap, char *errmsg, int errcap)
{
    char *hdr;
    if (cap < SIZEOF(uint32_t) * 3)
        hdr = NULL;
    else {
        hdr = dst;
        dst += SIZEOF(uint32_t) * 3;
        cap -= SIZEOF(uint32_t) * 3;
    }

    Codegen cg = {
        .code = { dst,         cap/2, 0 },
        .data = { dst + cap/2, cap/2, 0 },
        .num_scopes = 0,
        .err = false,
        .errmsg = errmsg,
        .errcap = errcap,
        .data_off = -1,
    };

    cg.free_list_calls = cg.calls;
    for (int i = 0; i < MAX_UNPATCHED_CALLS-1; i++)
        cg.calls[i].next = &cg.calls[i+1];
    cg.calls[MAX_UNPATCHED_CALLS-1].next = NULL;

    cg_push_scope(&cg, SCOPE_GLOBAL);
    cg_write_opcode(&cg, OPCODE_VARS);
    int off = cg_write_u8(&cg, 0);
    walk_node(&cg, node);
    cg_write_opcode(&cg, OPCODE_EXIT);
    cg_patch_u8(&cg, off, cg.scopes[0].max_vars);
    cg_pop_scope(&cg);

    if (cg.err)
        return -1;

    if (hdr) {

        uint32_t magic = WL_MAGIC;
        uint32_t code_len = cg.code.len;
        uint32_t data_len = cg.data.len;
        memcpy(hdr + 0, &magic   , sizeof(uint32_t));
        memcpy(hdr + 4, &code_len, sizeof(uint32_t));
        memcpy(hdr + 8, &data_len, sizeof(uint32_t));

        if (cg.code.len + cg.data.len <= cap)
            memmove(dst + cg.code.len, dst + cap/2, cg.data.len);
    }

    return cg.code.len + cg.data.len + SIZEOF(uint32_t) * 3;
}

static int write_instr(Writer *w, char *src, int len, String data)
{
    if (len == 0)
        return -1;

    switch (src[0]) {

        uint8_t b0;
        uint8_t b1;
        uint8_t b2;
        uint32_t w0;
        uint32_t w1;
        int64_t i;
        double  d;

        case OPCODE_NOPE:
        write_text(w, S("NOPE\n"));
        return 1;

        case OPCODE_JUMP:
        if (len < 5) return -1;
        memcpy(&w0, src + 1, sizeof(uint32_t));
        write_text(w, S("JUMP "));
        write_text_s64(w, w0);
        write_text(w, S("\n"));
        return 5;

        case OPCODE_JIFP:
        if (len < 5) return -1;
        memcpy(&w0, src + 1, sizeof(uint32_t));
        write_text(w, S("JIFP "));
        write_text_s64(w, w0);
        write_text(w, S("\n"));
        return 5;

        case OPCODE_OUTPUT:
        write_text(w, S("OUTPUT\n"));
        return 1;

        case OPCODE_SYSVAR:
        if (len < 9) return -1;
        memcpy(&w0, src + 1, sizeof(uint32_t));
        memcpy(&w1, src + 5, sizeof(uint32_t));
        write_text(w, S("SYSVAR \""));
        write_text(w, (String) { data.ptr + w0, w1 });
        write_text(w, S("\"\n"));
        return 9;

        case OPCODE_SYSCALL:
        if (len < 10) return -1;
        memcpy(&b0, src + 1, sizeof(uint8_t));
        memcpy(&w0, src + 2, sizeof(uint32_t));
        memcpy(&w1, src + 6, sizeof(uint32_t));
        write_text(w, S("SYSCALL "));
        write_text_s64(w, b0);
        write_text(w, S(" \""));
        write_text(w, (String) { data.ptr + w0, w1 });
        write_text(w, S("\"\n"));
        return 10;

        case OPCODE_CALL:
        if (len < 6) return -1;
        memcpy(&b0, src + 1, sizeof(uint8_t));
        memcpy(&w0, src + 2, sizeof(uint32_t));
        write_text(w, S("CALL "));
        write_text_s64(w, b0);
        write_text(w, S(" "));
        write_text_s64(w, w0);
        write_text(w, S("\n"));
        return 6;

        case OPCODE_RET:
        write_text(w, S("RET\n"));
        return 1;

        case OPCODE_GROUP:
        write_text(w, S("GROUP\n"));
        return 1;

        case OPCODE_PACK:
        write_text(w, S("PACK\n"));
        return 1;

        case OPCODE_GPOP:
        write_text(w, S("GPOP\n"));
        return 1;

        case OPCODE_FOR:
        if (len < 8) return -1;
        memcpy(&b0, src + 1, sizeof(b0));
        memcpy(&b1, src + 2, sizeof(b1));
        memcpy(&b2, src + 3, sizeof(b2));
        memcpy(&w0, src + 4, sizeof(w0));
        write_text(w, S("FOR "));
        write_text_s64(w, b0);
        write_text(w, S(" "));
        write_text_s64(w, b1);
        write_text(w, S(" "));
        write_text_s64(w, b2);
        write_text(w, S(" "));
        write_text_s64(w, w0);
        write_text(w, S("\n"));
        return 8;

        case OPCODE_EXIT:
        write_text(w, S("EXIT\n"));
        return 1;

        case OPCODE_VARS:
        if (len < 2) return -1;
        memcpy(&b0, src + 1, sizeof(b0));
        write_text(w, S("VARS "));
        write_text_s64(w, b0);
        write_text(w, S("\n"));
        return 2;

        case OPCODE_POP:
        write_text(w, S("POP\n"));
        return 1;

        case OPCODE_SETV:
        if (len < 2) return -1;
        memcpy(&b0, src + 1, sizeof(uint8_t));
        write_text(w, S("SETV "));
        write_text_s64(w, b0);
        write_text(w, S("\n"));
        return 2;

        case OPCODE_PUSHV:
        if (len < 2) return -1;
        memcpy(&b0, src + 1, sizeof(uint8_t));
        write_text(w, S("PUSHV "));
        write_text_s64(w, b0);
        write_text(w, S("\n"));
        return 2;

        case OPCODE_PUSHI:
        if (len < 9) return -1;
        memcpy(&i, src + 1, sizeof(int64_t));
        write_text(w, S("PUSHI "));
        write_text_s64(w, i);
        write_text(w, S("\n"));
        return 9;

        case OPCODE_PUSHF:
        if (len < 9) return -1;
        memcpy(&d, src + 1, sizeof(double));
        write_text(w, S("PUSHF "));
        write_text_f64(w, d);
        write_text(w, S("\n"));
        return 9;

        case OPCODE_PUSHS:
        if (len < 9) return -1;
        memcpy(&w0, src + 1, sizeof(uint32_t));
        memcpy(&w1, src + 5, sizeof(uint32_t));
        write_text(w, S("PUSHS \""));
        write_text(w, (String) { data.ptr + w0, w1 });
        write_text(w, S("\"\n"));
        return 9;

        case OPCODE_PUSHA:
        if (len < 5) return -1;
        memcpy(&w0, src + 1, sizeof(w0));
        write_text(w, S("PUSHA "));
        write_text_s64(w, w0);
        write_text(w, S("\n"));
        return 5;

        case OPCODE_PUSHM:
        if (len < 5) return -1;
        memcpy(&w0, src + 1, sizeof(w0));
        write_text(w, S("PUSHM "));
        write_text_s64(w, w0);
        write_text(w, S("\n"));
        return 5;

        case OPCODE_PUSHN:
        write_text(w, S("PUSHN\n"));
        return 1;

        case OPCODE_PUSHT:
        write_text(w, S("PUSHT\n"));
        return 1;

        case OPCODE_PUSHFL:
        write_text(w, S("PUSHFL\n"));
        return 1;

        case OPCODE_LEN:
        write_text(w, S("LEN\n"));
        return 1;

        case OPCODE_NEG:
        write_text(w, S("NEG\n"));
        return 1;

        case OPCODE_EQL:
        write_text(w, S("EQL\n"));
        return 1;

        case OPCODE_NQL:
        write_text(w, S("NQL\n"));
        return 1;

        case OPCODE_LSS:
        write_text(w, S("LSS\n"));
        return 1;

        case OPCODE_GRT:
        write_text(w, S("GRT\n"));
        return 1;

        case OPCODE_ADD:
        write_text(w, S("ADD\n"));
        return 1;

        case OPCODE_SUB:
        write_text(w, S("SUB\n"));
        return 1;

        case OPCODE_MUL:
        write_text(w, S("MUL\n"));
        return 1;

        case OPCODE_DIV:
        write_text(w, S("DIV\n"));
        return 1;

        case OPCODE_MOD:
        write_text(w, S("MOD\n"));
        return 1;

        case OPCODE_APPEND:
        write_text(w, S("APPEND\n"));
        return 1;

        case OPCODE_INSERT1:
        write_text(w, S("INSERT1\n"));
        return 1;

        case OPCODE_INSERT2:
        write_text(w, S("INSERT2\n"));
        return 1;

        case OPCODE_SELECT:
        write_text(w, S("SELECT\n"));
        return 1;

        default:
        write_text(w, S("byte "));
        write_text_s64(w, src[0]);
        return 1;
    }

    return -1;
}

static int write_program(WL_Program program, char *dst, int cap)
{
    if (program.len < 3 * sizeof(uint32_t))
        return -1;

    uint32_t magic;
    uint32_t code_len;
    uint32_t data_len;

    memcpy(&magic   , program.ptr + 0, sizeof(uint32_t));
    memcpy(&code_len, program.ptr + 4, sizeof(uint32_t));
    memcpy(&data_len, program.ptr + 8, sizeof(uint32_t));

    if (magic != WL_MAGIC)
        return -1;

    if (code_len + data_len + 3 * sizeof(uint32_t) != program.len)
        return -1;

    String code = { program.ptr + 3 * sizeof(uint32_t)           , code_len };
    String data = { program.ptr + 3 * sizeof(uint32_t) + code_len, data_len };

    Writer w = { dst, cap, 0 };

    int cur = 0;
    while (cur < code.len) {
        write_text_s64(&w, cur);
        write_text(&w, S(": "));
        int ret = write_instr(&w, code.ptr + cur, code.len - cur, data);
        if (ret < 0) return -1;
        cur += ret;
    }

    return w.len;
}

void wl_dump_program(WL_Program program)
{
    char buf[1<<10];
    int len = write_program(program, buf, SIZEOF(buf));

    if (len < 0) {
        printf("Invalid program\n");
        return;
    }

    if (len > SIZEOF(buf)) {
        char *p = malloc(len+1);
        if (p == NULL) {
            printf("Out of memory\n");
            return;
        }
        write_program(program, p, len);
        p[len] = '\0';
        fwrite(p, 1, len, stdout);
    } else {
        fwrite(buf, 1, len, stdout);
    }
}

/////////////////////////////////////////////////////////////////////////
// COMPILER
/////////////////////////////////////////////////////////////////////////

#define FILE_LIMIT 128

typedef struct {
    String file;
    Node*  root;
    Node*  includes;
} CompiledFile;

struct WL_Compiler {

    WL_Arena*    arena;
    CompiledFile files[FILE_LIMIT];
    int          num_files;
    String       waiting_file;

    bool err;
    char msg[1<<8];
};

WL_Compiler *wl_compiler_init(WL_Arena *arena)
{
    WL_Compiler *compiler = alloc(arena, SIZEOF(WL_Compiler), _Alignof(WL_Compiler));
    if (compiler == NULL)
        return NULL;
    compiler->arena = arena;
    compiler->num_files = 0;
    compiler->waiting_file = (String) { NULL, 0 };
    compiler->err = false;
    return compiler;
}

WL_AddResult wl_compiler_add(WL_Compiler *compiler, WL_String content)
{
    if (compiler->err)
        return (WL_AddResult) { .type=WL_ADD_ERROR };

    ParseResult pres = parse((String) { content.ptr, content.len }, compiler->arena, compiler->msg, SIZEOF(compiler->msg));
    if (pres.node == NULL) {
        compiler->err = true;
        return (WL_AddResult) { .type=WL_ADD_ERROR };
    }

    CompiledFile compiled_file = {
        .file = compiler->waiting_file,
        .root = pres.node,
        .includes = pres.includes,
    };
    compiler->files[compiler->num_files++] = compiled_file;
    compiler->waiting_file = (String) { NULL, 0 };

    for (int i = 0; i < compiler->num_files; i++) {

        Node *include = compiler->files[i].includes;
        while (include) {

            ASSERT(include->type == NODE_INCLUDE);

            if (include->include_root == NULL) {
                for (int j = 0; j < compiler->num_files; j++) {
                    if (streq(include->include_path, compiler->files[j].file)) {
                        include->include_root = compiler->files[j].root;
                        break;
                    }
                }
            }

            if (include->include_root == NULL) {

                if (compiler->num_files == FILE_LIMIT) {
                    ASSERT(0); // TODO
                }

                // TODO: Make the path relative to the compiled file

                compiler->waiting_file = include->include_path;
                return (WL_AddResult) { .type=WL_ADD_AGAIN, .path={ include->include_path.ptr, include->include_path.len } };
            }

            include = include->include_next;
        }
    }

    return (WL_AddResult) { .type=WL_ADD_LINK };
}

int wl_compiler_link(WL_Compiler *compiler, WL_Program *program)
{
    if (compiler->err) return -1;

    if (compiler->num_files == 0 || compiler->waiting_file.len > 0) {
        int len = snprintf(compiler->msg, SIZEOF(compiler->msg), "Missing files in compilation unit");
        if (len > SIZEOF(compiler->msg))
            len = SIZEOF(compiler->msg)-1;
        compiler->msg[len] = '\0';
        compiler->err = true;
        return -1;
    }

    char *dst = compiler->arena->ptr + compiler->arena->cur;
    int   cap = compiler->arena->len - compiler->arena->cur;

    int len = codegen(compiler->files[0].root, dst, cap, compiler->msg, SIZEOF(compiler->msg));
    if (len < 0) {
        compiler->err = true;
        return -1;
    }
    if (len > cap) {
        int len = snprintf(compiler->msg, SIZEOF(compiler->msg), "Out of memory");
        if (len > SIZEOF(compiler->msg))
            len = SIZEOF(compiler->msg)-1;
        compiler->msg[len] = '\0';
        compiler->err = true;
        return -1;
    }

    *program = (WL_Program) { dst, len };

    compiler->arena->cur += len;
    return 0;
}

WL_String wl_compiler_error(WL_Compiler *compiler)
{
    return compiler->err
        ? (WL_String) { compiler->msg, strlen(compiler->msg) }
        : (WL_String) { NULL, 0 };
}

int wl_dump_ast(WL_Compiler *compiler, char *dst, int cap)
{
    Writer w = { dst, cap, 0 };
    for (int i = 0; i < compiler->num_files; i++) {
        write_text(&w, S("(file \""));
        write_text(&w, compiler->files[i].file);
        write_text(&w, S("\" "));
        write_node(&w, compiler->files[i].root);
        write_text(&w, S(")"));
    }
    return w.len;
}

/////////////////////////////////////////////////////////////////////////
// OBJECT MODEL
/////////////////////////////////////////////////////////////////////////

typedef enum {
    TYPE_NONE,
    TYPE_BOOL,
    TYPE_INT,
    TYPE_FLOAT,
    TYPE_STRING,
    TYPE_ARRAY,
    TYPE_MAP,
    TYPE_ERROR,
} Type;

#define TAG_ERROR 0
#define TAG_POSITIVE_INT 1
#define TAG_NEGATIVE_INT 2
#define TAG_BOOL 3
#define TAG_NONE 4
#define TAG_PTR  5

#define VALUE_NONE  ((0 << 3) | TAG_NONE)
#define VALUE_TRUE  ((0 << 3) | TAG_BOOL)
#define VALUE_FALSE ((1 << 3) | TAG_BOOL)
#define VALUE_ERROR ((0 << 3) | TAG_ERROR)

typedef uint64_t Value;

typedef struct Extension Extension;
struct Extension {
    Extension *next;
    int count;
    int capacity;
    Value vals[];
};

typedef struct {
    Type  type;
    int   count;
    int   capacity;
    Extension *ext;
    Value vals[];
} AggregateValue;

typedef struct {
    Type   type;
    double raw;
} FloatValue;

typedef struct {
    Type    type;
    int64_t raw;
} IntValue;

typedef struct {
    Type type;
    int  len;
    char data[];
} StringValue;

static int value_convert_to_str(Value v, char *dst, int cap);

static Type value_type(Value v)
{
    switch (v & 7) {
        case TAG_ERROR: return TYPE_ERROR;
        case TAG_POSITIVE_INT: return TYPE_INT;
        case TAG_NEGATIVE_INT: return TYPE_INT;
        case TAG_BOOL : return TYPE_BOOL;
        case TAG_NONE : return TYPE_NONE;
        case TAG_PTR  : return *(Type*) (v & ~(Value) 7); break;
    }
    return TAG_ERROR;
}

static int64_t value_to_s64(Value v)
{
    ASSERT(value_type(v) == TYPE_INT);

    if ((v & 7) == TAG_POSITIVE_INT)
        return (int64_t) (v >> 3);

    if ((v & 7) == TAG_NEGATIVE_INT)
        return (int64_t) ((v >> 3) | ((Value) 7 << 61));

    IntValue *p = (IntValue*) (v & ~(Value) 7);
    return p->raw;
}

static double value_to_f64(Value v)
{
    ASSERT(value_type(v) == TYPE_FLOAT);

    FloatValue *p = (FloatValue*) (v & ~(Value) 7);
    return p->raw;
}

static String value_to_str(Value v)
{
    ASSERT(value_type(v) == TYPE_STRING);

    StringValue *p = (StringValue*) (v & ~(Value) 7);
    return (String) { p->data, p->len };
}

/*

2 bits -> 2^2 = 4

00000   0    .
00001   1    .
00010   2    .
00011   3    .
00100   4    .
00101   5    .
00110   6    .
00111   7    .
01000   8
01001   9
01010   10
01011   11
01100   12
01101   13
01110   14
01111   15
10000  -16
10001  -15
10010  -14
10011  -13
10100  -12
10101  -11
10110  -10
10111  -9
11000  -8    .
11001  -7    .
11010  -6    .
11011  -5    .
11100  -4    .
11101  -3    .
11110  -2    .
11111  -1    .

*/

static Value value_from_s64(int64_t x, WL_Arena *arena, Error *err)
{
    Value v = (Value) x;
    Value upper3bits = v >> 61;

    if (upper3bits == 0)
        return (v << 3) | TAG_POSITIVE_INT;

    if (upper3bits == 7)
        return (v << 3) | TAG_NEGATIVE_INT;

    IntValue *p = alloc(arena, SIZEOF(IntValue), _Alignof(IntValue));
    if (p == NULL) {
        REPORT(err, "Out of memory");
        return VALUE_ERROR;
    }

    p->type = TYPE_INT;
    p->raw  = x;

    ASSERT(((Value) p & 7) == 0);
    return ((Value) p) | TAG_PTR;
}

static Value value_from_f64(double x, WL_Arena *arena, Error *err)
{
    FloatValue *v = alloc(arena, SIZEOF(FloatValue), _Alignof(FloatValue));
    if (v == NULL) {
        REPORT(err, "Out of memory");
        return VALUE_ERROR;
    }

    v->type = TYPE_FLOAT;
    v->raw  = x;

    ASSERT(((uintptr_t) v & 7) == 0);
    return ((Value) v) | TAG_PTR;
}

static Value value_from_str(String x, WL_Arena *arena, Error *err)
{
    StringValue *v = alloc(arena, SIZEOF(StringValue) + x.len, 8);
    if (v == NULL) {
        REPORT(err, "Out of memory");
        return VALUE_ERROR;
    }

    v->type = TYPE_STRING;
    v->len = x.len;
    memcpy(v->data, x.ptr, x.len);

    ASSERT(((uintptr_t) v & 7) == 0);
    return ((Value) v) | TAG_PTR;
}

static Value aggregate_empty(bool map, uint32_t cap, WL_Arena *arena, Error *err)
{
    AggregateValue *v = alloc(arena, SIZEOF(AggregateValue) + 2 * cap * SIZEOF(Value), MAX(_Alignof(AggregateValue), 8));
    if (v == NULL) {
        REPORT(err, "Out of memory");
        return VALUE_ERROR;
    }

    v->type = map ? TYPE_MAP : TYPE_ARRAY;
    v->count = 0;
    v->capacity = cap;
    v->ext = NULL;

    ASSERT(((uintptr_t) v & 7) == 0);
    return ((Value) v) | TAG_PTR;
}

static int64_t aggregate_length(AggregateValue *agg)
{
    int64_t n = agg->count;

    Extension *ext = agg->ext;
    while (ext) {
        n += ext->count;
        ext = ext->next;
    }

    return n;
}

static Value *aggregate_select_by_raw_index(AggregateValue *agg, int64_t idx)
{
    ASSERT(agg->type == TYPE_ARRAY || agg->type == TYPE_MAP);

    if (idx < 0 || idx >= aggregate_length(agg))
        return NULL;

    if (idx < agg->count)
        return &agg->vals[idx];

    idx -= agg->count;
    Extension *ext = agg->ext;
    while (ext) {
        if (idx < ext->count)
            return &ext->vals[idx];
        idx -= ext->count;
        ext = ext->next;
    }

    UNREACHABLE;
    return NULL;
}

static bool value_eql(Value a, Value b);

static Value *aggregate_select(AggregateValue *agg, Value key)
{
    if (agg->type == TYPE_MAP) {

        for (int i = 0; i < agg->count; i += 2)
            if (value_eql(agg->vals[i], key))
                return &agg->vals[i+1];

        Extension *ext = agg->ext;
        while (ext) {
            for (int i = 0; i < ext->count; i += 2)
                if (value_eql(ext->vals[i], key)) {
                    return &ext->vals[i+1];
                }
            ext = ext->next;
        }

        return NULL;
    
    } else {

        ASSERT(agg->type == TYPE_ARRAY);

        if (value_type(key) != TYPE_INT)
            return NULL;
        int64_t idx = value_to_s64(key);

        return aggregate_select_by_raw_index(agg, idx);
    }
}

static bool aggregate_append(AggregateValue *agg, Value v1, Value v2, WL_Arena *arena)
{
    if (agg->count < agg->capacity) {
        agg->vals[agg->count++] = v1;
        if (v2 != VALUE_ERROR)
            agg->vals[agg->count++] = v2;
        return true;
    }

    Extension *tail = agg->ext;
    if (tail)
        while (tail->next)
            tail = tail->next;

    Extension *ext;
    if (tail == NULL || tail->count == tail->capacity) {

        int cap = 8;
        ext = alloc(arena, SIZEOF(Extension) + cap * sizeof(Value), ALIGNOF(Extension));
        if (ext == NULL)
            return false;

        ext->count = 0;
        ext->capacity = cap;
        ext->next = NULL;

        if (tail)
            tail->next = ext;
        else
            agg->ext = ext;

    } else
        ext = tail;

    ext->vals[ext->count++] = v1;
    if (v2 != VALUE_ERROR)
        ext->vals[ext->count++] = v2;
    return true;
}

static Value value_empty_map(uint32_t cap, WL_Arena *arena, Error *err)
{
    return aggregate_empty(true, 2 * cap, arena, err);
}

static Value value_empty_array(uint32_t cap, WL_Arena *arena, Error *err)
{
    return aggregate_empty(false, cap, arena, err);
}

static int64_t value_length(Value set)
{
    ASSERT(value_type(set) == TYPE_MAP || value_type(set) == TYPE_ARRAY);
    AggregateValue *agg = (void*) (set & ~(Value) 7);
    int64_t len = aggregate_length(agg);
    if (agg->type == TYPE_MAP)
        len /= 2;
    return len;
}

static bool value_insert(Value set, Value key, Value val, WL_Arena *arena, Error *err)
{
    Type t = value_type(set);
    if (t != TYPE_MAP && t != TYPE_ARRAY) {
        REPORT(err, "Invalid insertion on non-map and non-array value");
        return false;
    }
    AggregateValue *agg = (void*) (set & ~(Value) 7);

    Value *dst = aggregate_select(agg, key);
    if (dst != NULL) {
        *dst = val;
        return true;
    }

    if (agg->type == TYPE_ARRAY && value_type(key) != TYPE_INT) {
        REPORT(err, "Invalid index used in array access");
        return false;
    }

    if (!aggregate_append(agg, key, val, arena)) {
        REPORT(err, "Out of memory");
        return false;
    }

    return true;
}

static Value value_select(Value set, Value key, Error *err)
{
    Type t = value_type(set);
    if (t != TYPE_MAP && t != TYPE_ARRAY) {
        REPORT(err, "Invalid selection from non-map and non-array value");
        return VALUE_ERROR;
    }
    AggregateValue *agg = (void*) (set & ~(Value) 7);

    Value *dst = aggregate_select(agg, key);
    if (dst) return *dst;

    if (agg->type == TYPE_ARRAY && value_type(key) != TYPE_INT) {
        REPORT(err, "Invalid index used in array access");
        return VALUE_ERROR;
    }

    char key_buf[1<<8];
    int key_len = value_convert_to_str(key, key_buf, SIZEOF(key_buf));
    if (key_len > SIZEOF(key_buf)-1)
        key_len = SIZEOF(key_buf)-1;
    key_buf[key_len] = '\0';

    char set_buf[1<<8];
    int set_len = value_convert_to_str(set, set_buf, SIZEOF(set_buf));
    if (set_len > SIZEOF(set_buf)-1)
        set_len = SIZEOF(set_buf)-1;
    set_buf[set_len] = '\0';

    REPORT(err, "Invalid key '%s' used in access to map '%s'", key_buf, set_buf);
    return VALUE_ERROR;
}

static Value value_select_by_index(Value set, int64_t idx, Error *err)
{
    Type t = value_type(set);
    if (t != TYPE_MAP && t != TYPE_ARRAY) {
        REPORT(err, "Invalid selection from non-map and non-array value");
        return VALUE_ERROR;
    }
    AggregateValue *agg = (void*) (set & ~(Value) 7);

    if (agg->type == TYPE_MAP)
        idx *= 2;

    Value *src = aggregate_select_by_raw_index(agg, idx);
    if (src == NULL) {
        REPORT(err, "Invalid selection from non-map and non-array value");
        return VALUE_ERROR;
    }

    return *src;
}

static bool value_append(Value set, Value val, WL_Arena *arena, Error *err)
{
    Type t = value_type(set);
    if (t != TYPE_ARRAY) {
        REPORT(err, "Invalid append on non-array value");
        return false;
    }
    AggregateValue *agg = (void*) (set & ~(Value) 7);

    if (!aggregate_append(agg, val, VALUE_ERROR, arena)) {
        REPORT(err, "Out of memory");
        return false;
    }

    return true;
}

static bool value_eql(Value a, Value b)
{
    Type t1 = value_type(a);
    Type t2 = value_type(b);

    if (t1 != t2)
        return false;

    switch (t1) {

        case TYPE_NONE:
        return true;

        case TYPE_BOOL:
        return a == b;

        case TYPE_INT:
        return value_to_s64(a) == value_to_s64(b);

        case TYPE_FLOAT:
        return value_to_f64(a) == value_to_f64(b);

        case TYPE_MAP:
        return false; // TODO

        case TYPE_ARRAY:
        return false; // TODO

        case TYPE_STRING:
        return streq(value_to_str(a), value_to_str(b));

        case TYPE_ERROR:
        return true;
    }

    return false;
}

static bool value_nql(Value a, Value b)
{
    return !value_eql(a, b);
}

#define TYPE_PAIR(X, Y) (((uint16_t) (X) << 16) | (uint16_t) (Y))

bool value_greater(Value a, Value b, Error *err)
{
    Type t1 = value_type(a);
    Type t2 = value_type(b);
    switch (TYPE_PAIR(t1, t2)) {
        case TYPE_PAIR(TYPE_INT  , TYPE_INT  ): return value_to_s64(a) > value_to_s64(b);
        case TYPE_PAIR(TYPE_INT  , TYPE_FLOAT): return value_to_s64(a) > value_to_f64(b);
        case TYPE_PAIR(TYPE_FLOAT, TYPE_INT  ): return value_to_f64(a) > value_to_s64(b);
        case TYPE_PAIR(TYPE_FLOAT, TYPE_FLOAT): return value_to_f64(a) > value_to_f64(b);
        default:break;
    }
    REPORT(err, "Invalid '>' operation on non-numeric type");
    return false;
}

bool value_lower(Value a, Value b, Error *err)
{
    Type t1 = value_type(a);
    Type t2 = value_type(b);
    switch (TYPE_PAIR(t1, t2)) {
        case TYPE_PAIR(TYPE_INT  , TYPE_INT  ): return value_to_s64(a) < value_to_s64(b);
        case TYPE_PAIR(TYPE_INT  , TYPE_FLOAT): return value_to_s64(a) < value_to_f64(b);
        case TYPE_PAIR(TYPE_FLOAT, TYPE_INT  ): return value_to_f64(a) < value_to_s64(b);
        case TYPE_PAIR(TYPE_FLOAT, TYPE_FLOAT): return value_to_f64(a) < value_to_f64(b);
        default:break;
    }
    REPORT(err, "Invalid '<' operation on non-numeric type");
    return false;
}

static Value value_neg(Value v, WL_Arena *arena, Error *err)
{
    Type t = value_type(v);
    if (t == TYPE_INT)
        return value_from_s64(-value_to_s64(v), arena, err); // TODO: overflow
    
    if (t == TYPE_FLOAT)
        return value_from_f64(-value_to_f64(v), arena, err);

    REPORT(err, "Invalid '-' operation on non-numeric type");
    return VALUE_ERROR;
}

static Value value_add(Value v1, Value v2, WL_Arena *arena, Error *err)
{
    Type t1 = value_type(v1);
    Type t2 = value_type(v2);

    Value r;
    switch (TYPE_PAIR(t1, t2)) {

        case TYPE_PAIR(TYPE_INT, TYPE_INT):
        {
            int64_t u = value_to_s64(v1);
            int64_t v = value_to_s64(v2);
            // TODO: check overflow and underflow
            r = value_from_s64(u + v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_INT, TYPE_FLOAT):
        {
            double u = (double) value_to_s64(v1);
            double v = value_to_f64(v2);
            r = value_from_f64(u + v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_FLOAT, TYPE_INT):
        {
            double u = value_to_f64(v1);
            double v = (double) value_to_s64(v2);
            r = value_from_f64(u + v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_FLOAT, TYPE_FLOAT):
        {
            double u = value_to_f64(v1);
            double v = value_to_f64(v2);
            // TODO: check overflow and underflow
            r = value_from_f64(u + v, arena, err);
        }
        break;

        default:
        REPORT(err, "Invalid operation '+' on non-numeric value");
        return VALUE_ERROR;
    }

    return r;
}

static Value value_sub(Value v1, Value v2, WL_Arena *arena, Error *err)
{
    Type t1 = value_type(v1);
    Type t2 = value_type(v2);

    Value r;
    switch (TYPE_PAIR(t1, t2)) {

        case TYPE_PAIR(TYPE_INT, TYPE_INT):
        {
            int64_t u = value_to_s64(v1);
            int64_t v = value_to_s64(v2);
            // TODO: check overflow and underflow
            r = value_from_s64(u - v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_INT, TYPE_FLOAT):
        {
            double u = (double) value_to_s64(v1);
            double v = value_to_f64(v2);
            r = value_from_f64(u - v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_FLOAT, TYPE_INT):
        {
            double u = value_to_f64(v1);
            double v = (double) value_to_s64(v2);
            r = value_from_f64(u - v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_FLOAT, TYPE_FLOAT):
        {
            double u = value_to_f64(v1);
            double v = value_to_f64(v2);
            // TODO: check overflow and underflow
            r = value_from_f64(u - v, arena, err);
        }
        break;

        default:
        REPORT(err, "Invalid operation '-' on non-numeric value");
        return VALUE_ERROR;
    }

    return r;
}

static Value value_mul(Value v1, Value v2, WL_Arena *arena, Error *err)
{
    Type t1 = value_type(v1);
    Type t2 = value_type(v2);

    Value r;
    switch (TYPE_PAIR(t1, t2)) {

        case TYPE_PAIR(TYPE_INT, TYPE_INT):
        {
            int64_t u = value_to_s64(v1);
            int64_t v = value_to_s64(v2);
            // TODO: check overflow and underflow
            r = value_from_s64(u * v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_INT, TYPE_FLOAT):
        {
            double u = (double) value_to_s64(v1);
            double v = value_to_f64(v2);
            r = value_from_f64(u * v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_FLOAT, TYPE_INT):
        {
            double u = value_to_f64(v1);
            double v = (double) value_to_s64(v2);
            r = value_from_f64(u * v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_FLOAT, TYPE_FLOAT):
        {
            double u = value_to_f64(v1);
            double v = value_to_f64(v2);
            // TODO: check overflow and underflow
            r = value_from_f64(u * v, arena, err);
        }
        break;

        default:
        REPORT(err, "Invalid operation '*' on non-numeric value");
        return VALUE_ERROR;
    }

    return r;
}

static Value value_div(Value v1, Value v2, WL_Arena *arena, Error *err)
{
    Type t1 = value_type(v1);
    Type t2 = value_type(v2);

    Value r;
    switch (TYPE_PAIR(t1, t2)) {

        case TYPE_PAIR(TYPE_INT, TYPE_INT):
        {
            // TODO: check division by 0

            int64_t u = value_to_s64(v1);
            int64_t v = value_to_s64(v2);
            r = value_from_s64(u / v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_INT, TYPE_FLOAT):
        {
            // TODO: check division by 0

            double u = (double) value_to_s64(v1);
            double v = value_to_f64(v2);
            r = value_from_f64(u / v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_FLOAT, TYPE_INT):
        {
            // TODO: check division by 0

            double u = value_to_f64(v1);
            double v = (double) value_to_s64(v2);
            r = value_from_f64(u / v, arena, err);
        }
        break;

        case TYPE_PAIR(TYPE_FLOAT, TYPE_FLOAT):
        {
            double u = value_to_f64(v1);
            double v = value_to_f64(v2);
            r = value_from_f64(u / v, arena, err);
        }
        break;

        default:
        REPORT(err, "Invalid operation '/' on non-numeric value");
        return VALUE_ERROR;
    }

    return r;
}

static Value value_mod(Value v1, Value v2, WL_Arena *arena, Error *err)
{
    Type t1 = value_type(v1);
    Type t2 = value_type(v2);

    if (t1 != TYPE_INT || t2 != TYPE_INT) {
        REPORT(err, "Invalid operation '%%' on non-integer value");
        return VALUE_ERROR;
    }

    int64_t u = value_to_s64(v1);
    int64_t v = value_to_s64(v2);
    Value r = value_from_s64(u % v, arena, err);
    return r;
}

static void value_convert_to_str_inner(Writer *w, Value v)
{
    Type t = value_type(v);
    switch (t) {

        case TYPE_NONE:
        break;

        case TYPE_BOOL:
        write_text(w, v == VALUE_TRUE ? S("true") : S("false"));
        break;

        case TYPE_INT:
        write_text_s64(w, value_to_s64(v));
        break;

        case TYPE_FLOAT:
        write_text_f64(w, value_to_f64(v));
        break;

        case TYPE_STRING:
        write_text(w, value_to_str(v));
        break;

        case TYPE_ARRAY:
        {
            AggregateValue *agg = (void*) (v & ~(Value) 7);
            for (int i = 0; i < agg->count; i++)
                value_convert_to_str_inner(w, agg->vals[i]);
            Extension *ext = agg->ext;
            while (ext) {
                for (int i = 0; i < ext->count; i++)
                    value_convert_to_str_inner(w, ext->vals[i]);
                ext = ext->next;
            }
        }
        break;

        case TYPE_MAP:
        {
            write_text(w, S("{"));
            AggregateValue *agg = (void*) (v & ~(Value) 7);
            for (int i = 0; i < agg->count; i += 2) {
                value_convert_to_str_inner(w, agg->vals[i+0]);
                write_text(w, S(": "));
                value_convert_to_str_inner(w, agg->vals[i+1]);
                if (i+2 < agg->count || agg->ext)
                    write_text(w, S(", "));
            }
            Extension *ext = agg->ext;
            while (ext) {
                for (int i = 0; i < ext->count; i += 2) {
                    value_convert_to_str_inner(w, ext->vals[i+0]);
                    write_text(w, S(": "));
                    value_convert_to_str_inner(w, ext->vals[i+1]);
                    if (i+2 < ext->count || ext->next)
                        write_text(w, S(", "));
                }
                ext = ext->next;
            }
            write_text(w, S("}"));
        }
        break;

        case TYPE_ERROR:
        break;
    }
}

static int value_convert_to_str(Value v, char *dst, int cap)
{
    Writer w = { dst, cap, 0};
    value_convert_to_str_inner(&w, v);
    return w.len;
}

#undef TYPE_PAIR

/////////////////////////////////////////////////////////////////////////
// RUNTIME
/////////////////////////////////////////////////////////////////////////

#define MAX_STACK 1024
#define MAX_FRAMES 1024
#define MAX_GROUPS 8

typedef struct {
    int retaddr;
    int varbase;
} Frame;

typedef enum {
    RUNTIME_BEGIN,
    RUNTIME_LOOP,
    RUNTIME_DONE,
    RUNTIME_ERROR,
    RUNTIME_OUTPUT,
    RUNTIME_SYSVAR,
    RUNTIME_SYSCALL,
} RuntimeState;

struct WL_Runtime {

    RuntimeState state;

    String code;
    String data;
    int off;

    int vars;
    int stack;
    Value values[MAX_STACK];

    int num_frames;
    Frame frames[MAX_FRAMES];

    int num_groups;
    int groups[MAX_GROUPS];

    WL_Arena *arena;

    char  msg[128];
    Error err;

    int stack_before_user;
    String str_for_user;
    int num_output;
    int cur_output;
    char buf[128];
};

WL_Runtime *wl_runtime_init(WL_Arena *arena, WL_Program program)
{
    if (program.len < 3 * sizeof(uint32_t))
        return NULL;

    uint32_t magic;
    uint32_t code_len;
    uint32_t data_len;

    memcpy(&magic   , program.ptr + 0, sizeof(uint32_t));
    memcpy(&code_len, program.ptr + 4, sizeof(uint32_t));
    memcpy(&data_len, program.ptr + 8, sizeof(uint32_t));

    if (magic != WL_MAGIC)
        return NULL;

    String code = { program.ptr + sizeof(uint32_t) * 3           , code_len };
    String data = { program.ptr + sizeof(uint32_t) * 3 + code_len, data_len };

    WL_Runtime *rt = alloc(arena, SIZEOF(WL_Runtime), ALIGNOF(WL_Runtime));
    if (rt == NULL)
        return NULL;

    *rt = (WL_Runtime) {
        .state      = RUNTIME_BEGIN,
        .code       = code,
        .data       = data,
        .off        = 0,
        .stack      = 0,
        .vars       = MAX_STACK-1,
        .num_frames = 0,
        .arena      = arena,
        .err        = { NULL, 0, false },
    };
    rt->err.buf = rt->msg;
    rt->err.cap = SIZEOF(rt->msg);

    rt->frames[rt->num_frames++] = (Frame) {
        .retaddr = 0,
        .varbase = rt->vars,
    };

    return rt;
}

WL_String wl_runtime_error(WL_Runtime *rt)
{
    return rt->err.yes
        ? (WL_String) { rt->msg, strlen(rt->msg) }
        : (WL_String) { NULL, 0 };
}

static void rt_read_mem(WL_Runtime *r, void *dst, int len)
{
    ASSERT(r->off + len <= r->code.len);
    memcpy(dst, r->code.ptr + r->off, len);
    r->off += len;
}

static uint8_t rt_read_u8(WL_Runtime *rt)
{
    ASSERT(rt->state == RUNTIME_LOOP);

    uint8_t x;
    rt_read_mem(rt, &x, SIZEOF(x));

    return x;
}

static uint32_t rt_read_u32(WL_Runtime *rt)
{
    ASSERT(rt->state == RUNTIME_LOOP);

    uint32_t x;
    rt_read_mem(rt, &x, SIZEOF(x));

    return x;
}

static int64_t rt_read_s64(WL_Runtime *rt)
{
    ASSERT(rt->state == RUNTIME_LOOP);

    int64_t x;
    rt_read_mem(rt, &x, SIZEOF(x));

    return x;
}

static double rt_read_f64(WL_Runtime *rt)
{
    ASSERT(rt->state == RUNTIME_LOOP);

    double x;
    rt_read_mem(rt, &x, SIZEOF(x));

    return x;
}

static String rt_read_str(WL_Runtime *rt)
{
    ASSERT(rt->state == RUNTIME_LOOP);
    uint32_t off = rt_read_u32(rt);
    uint32_t len = rt_read_u32(rt);
    ASSERT(off + len <= rt->data.len);
    return (String) { rt->data.ptr + off, len };
}

static Value *rt_variable(WL_Runtime *rt, uint8_t x)
{
    ASSERT(rt->num_frames > 0);

    Frame *frame = &rt->frames[rt->num_frames-1];

    ASSERT(frame->varbase - x >= 0
        && frame->varbase - x < MAX_STACK);

    return &rt->values[frame->varbase - x];
}

static int values_usage(WL_Runtime *rt)
{
    int num_vars = (MAX_STACK - rt->vars - 1);
    return rt->stack + num_vars;
}

static bool rt_check_stack(WL_Runtime *rt, int min)
{
    if (MAX_STACK - values_usage(rt) < min) {
        REPORT(&rt->err, "Out of stack");
        rt->state = RUNTIME_ERROR;
        return false;
    }
    return true;
}

static bool rt_push_frame(WL_Runtime *rt, uint8_t args)
{
    if (rt->num_frames == MAX_FRAMES) {
        REPORT(&rt->err, "Call stack limit reached");
        rt->state = RUNTIME_ERROR;
        return false;
    }

    if (MAX_STACK - values_usage(rt) < args) {
        REPORT(&rt->err, "Stack limit reached");
        rt->state = RUNTIME_ERROR;
        return false;
    }

    Frame *frame = &rt->frames[rt->num_frames++];
    frame->retaddr = rt->off;
    frame->varbase = rt->vars;

    for (int i = 0; i < args; i++)
        rt->values[rt->vars--] = rt->values[--rt->stack];

    return true;
}

static void rt_pop_frame(WL_Runtime *rt)
{
    ASSERT(rt->num_frames > 0);
    Frame *frame = &rt->frames[rt->num_frames-1];
    rt->off  = frame->retaddr;
    rt->vars = frame->varbase;
    rt->num_frames--;
}

static void rt_set_frame_vars(WL_Runtime *rt, uint8_t num)
{
    ASSERT(rt->num_frames > 0);
    Frame *frame = &rt->frames[rt->num_frames-1];
    int num_vars = frame->varbase - rt->vars;
    if (num_vars < num)
        for (int i = 0; i < num - num_vars; i++)
            rt->values[rt->vars - i] = VALUE_NONE;
    rt->vars = frame->varbase - num;
}

static void rt_push_group(WL_Runtime *rt)
{
    if (rt->num_groups == MAX_GROUPS) {
        REPORT(&rt->err, "Out of memory");
        rt->state = RUNTIME_ERROR;
        return;
    }
    rt->groups[rt->num_groups++] = rt->stack;
}

static void rt_pack_group(WL_Runtime *rt)
{
    if (!rt_check_stack(rt, 1))
        return;

    ASSERT(rt->num_groups > 0);
    int start = rt->groups[--rt->num_groups];
    int end = rt->stack;

    if (end - start > 1) {

        Value set = value_empty_array(end - start, rt->arena, &rt->err);
        if (set == VALUE_ERROR)
            return;

        for (int i = start; i < end; i++)
            if (!value_append(set, rt->values[i], rt->arena, &rt->err))
                return;

        rt->stack = start;
        rt->values[rt->stack++] = set;
    }
}

static void rt_pop_group(WL_Runtime *rt)
{
    ASSERT(rt->num_groups > 0);
    rt->stack = rt->groups[--rt->num_groups];
}

static void value_print(Value v)
{
    char buf[1<<8];
    int len = value_convert_to_str(v, buf, SIZEOF(buf));
    if (len < SIZEOF(buf))
        fwrite(buf, 1, len, stdout);
    else {
        len = SIZEOF(buf)-1;
        fwrite(buf, 1, len, stdout);
        fprintf(stdout, " [...]");
    }
    putc('\n', stdout);
    fflush(stdout);
}

static void step(WL_Runtime *rt)
{
    switch (rt_read_u8(rt)) {

        Type t;
        Value v1;
        Value v2;
        Value v3;
        uint32_t o;
        uint8_t  b1;
        uint8_t  b2;
        uint8_t  b3;
        int64_t  i;
        double   f;
        String   s;

        case OPCODE_NOPE:
        break;

        case OPCODE_JUMP:
        rt->off = rt_read_u32(rt);
        break;

        case OPCODE_JIFP:
        ASSERT(rt->stack > 0);
        o = rt_read_u32(rt);
        v1 = rt->values[--rt->stack];
        if (v1 == VALUE_FALSE)
            rt->off = o;
        else if (value_type(v1) != TYPE_BOOL) {
            REPORT(&rt->err, "Invalid non-boolean condition");
            rt->state = RUNTIME_ERROR;
            break;
        }
        break;

        case OPCODE_VARS:
        b1 = rt_read_u8(rt);
        rt_set_frame_vars(rt, b1);
        break;

        case OPCODE_OUTPUT:
        if (rt->stack > 0) {
            rt->cur_output = 0;
            rt->num_output = rt->stack;
            rt->state = RUNTIME_OUTPUT;
        }
        break;

        case OPCODE_SYSVAR:
        s = rt_read_str(rt);
        rt_push_frame(rt, 0);
        rt->stack_before_user = rt->stack;
        rt->str_for_user = s;
        rt->state = RUNTIME_SYSVAR;
        break;

        case OPCODE_SYSCALL:
        b1 = rt_read_u8(rt);
        s = rt_read_str(rt);
        rt_push_frame(rt, b1);
        rt->stack_before_user = rt->stack;
        rt->str_for_user = s;
        rt->state = RUNTIME_SYSCALL;
        break;

        case OPCODE_CALL:
        b1 = rt_read_u8(rt);
        o = rt_read_u32(rt);
        rt_push_frame(rt, b1);
        rt->off = o;
        break;

        case OPCODE_RET:
        rt_pop_frame(rt);
        break;

        case OPCODE_GROUP:
        rt_push_group(rt);
        break;

        case OPCODE_PACK:
        rt_pack_group(rt);
        break;

        case OPCODE_GPOP:
        rt_pop_group(rt);
        break;

        case OPCODE_FOR:
        b1 = rt_read_u8(rt);
        b2 = rt_read_u8(rt);
        b3 = rt_read_u8(rt);
        o  = rt_read_u32(rt);

        v1 = *rt_variable(rt, b3);
        ASSERT(value_type(v1) == TYPE_INT);
        i = value_to_s64(v1);

        v2 = *rt_variable(rt, b1);

        if (value_length(v2)-1 == i) {
            rt->off = o;
            break;
        }
        i++;

        v1 = value_select_by_index(v2, i, &rt->err);
        if (v1 == VALUE_ERROR) break;

        *rt_variable(rt, b2) = v1;

        v1 = value_from_s64(i, rt->arena, &rt->err); // TODO: this could be in-place
        *rt_variable(rt, b3) = v1;
        break;

        case OPCODE_EXIT:
        rt->state = RUNTIME_DONE;
        break;

        case OPCODE_POP:
        ASSERT(rt->stack > 0);
        rt->stack--;
        break;

        case OPCODE_SETV:
        ASSERT(rt->stack > 0);
        b1 = rt_read_u8(rt);
        *rt_variable(rt, b1) =  rt->values[rt->stack-1];
        break;

        case OPCODE_PUSHV:
        if (!rt_check_stack(rt, 1)) break;
        b1 = rt_read_u8(rt);
        rt->values[rt->stack++] = *rt_variable(rt, b1);
        break;

        case OPCODE_PUSHI:
        if (!rt_check_stack(rt, 1)) break;
        i = rt_read_s64(rt);
        v1 = value_from_s64(i, rt->arena, &rt->err);
        rt->values[rt->stack++] = v1;
        break;

        case OPCODE_PUSHF:
        if (!rt_check_stack(rt, 1)) break;
        f = rt_read_f64(rt);
        v1 = value_from_f64(f, rt->arena, &rt->err);
        rt->values[rt->stack++] = v1;
        break;

        case OPCODE_PUSHS:
        if (!rt_check_stack(rt, 1)) break;
        s = rt_read_str(rt);
        v1 = value_from_str(s, rt->arena, &rt->err);
        rt->values[rt->stack++] = v1;
        break;

        case OPCODE_PUSHA:
        if (!rt_check_stack(rt, 1)) break;
        o = rt_read_u32(rt);
        v1 = value_empty_array(o, rt->arena, &rt->err);
        rt->values[rt->stack++] = v1;
        break;

        case OPCODE_PUSHM:
        if (!rt_check_stack(rt, 1)) break;
        o = rt_read_u32(rt);
        v1 = value_empty_map(o, rt->arena, &rt->err);
        rt->values[rt->stack++] = v1;
        break;

        case OPCODE_PUSHN:
        if (!rt_check_stack(rt, 1)) break;
        rt->values[rt->stack++] = VALUE_NONE;
        break;

        case OPCODE_PUSHT:
        if (!rt_check_stack(rt, 1)) break;
        rt->values[rt->stack++] = VALUE_TRUE;
        break;

        case OPCODE_PUSHFL:
        if (!rt_check_stack(rt, 1)) break;
        rt->values[rt->stack++] = VALUE_FALSE;
        break;

        case OPCODE_LEN:
        ASSERT(rt->stack > 0);
        v1 = rt->values[rt->stack-1];
        t = value_type(v1);
        if (t != TYPE_ARRAY && t != TYPE_MAP) {
            REPORT(&rt->err, "Invalid operation 'len' on non-aggregate value");
            rt->state = RUNTIME_ERROR;
            break;
        }
        v2 = value_from_s64(value_length(v1), rt->arena, &rt->err);
        rt->values[rt->stack-1] = v2;
        break;

        case OPCODE_NEG:
        ASSERT(rt->stack > 0);
        v1 = rt->values[rt->stack-1];
        v2 = value_neg(v1, rt->arena, &rt->err);
        rt->values[rt->stack-1] = v2;
        break;

        case OPCODE_EQL:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_eql(v2, v1) ? VALUE_TRUE : VALUE_FALSE;
        rt->values[rt->stack++] = v3;
        break;

        case OPCODE_NQL:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_nql(v2, v1) ? VALUE_TRUE : VALUE_FALSE;
        rt->values[rt->stack++] = v3;
        break;

        case OPCODE_LSS:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_lower(v2, v1, &rt->err) ? VALUE_TRUE : VALUE_FALSE;
        rt->values[rt->stack++] = v3;
        break;

        case OPCODE_GRT:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_greater(v2, v1, &rt->err) ? VALUE_TRUE : VALUE_FALSE;
        rt->values[rt->stack++] = v3;
        break;

        case OPCODE_ADD:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_add(v2, v1, rt->arena, &rt->err);
        rt->values[rt->stack++] = v3;
        break;

        case OPCODE_SUB:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_sub(v2, v1, rt->arena, &rt->err);
        rt->values[rt->stack++] = v3;
        break;

        case OPCODE_MUL:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_mul(v2, v1, rt->arena, &rt->err);
        rt->values[rt->stack++] = v3;
        break;

        case OPCODE_DIV:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_div(v2, v1, rt->arena, &rt->err);
        rt->values[rt->stack++] = v3;
        break;

        case OPCODE_MOD:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_mod(v2, v1, rt->arena, &rt->err);
        rt->values[rt->stack++] = v3;
        break;

        case OPCODE_APPEND:
        ASSERT(rt->stack > 1);
        v2 = rt->values[--rt->stack];
        v1 = rt->values[rt->stack-1];
        value_append(v1, v2, rt->arena, &rt->err);
        break;

        case OPCODE_INSERT1:
        ASSERT(rt->stack > 2);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = rt->values[rt->stack-1];
        value_insert(v3, v1, v2, rt->arena, &rt->err);
        break;

        case OPCODE_INSERT2:
        ASSERT(rt->stack > 2);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = rt->values[rt->stack-1];
        value_insert(v2, v1, v3, rt->arena, &rt->err);
        break;

        case OPCODE_SELECT:
        ASSERT(rt->stack > 1);
        v1 = rt->values[--rt->stack];
        v2 = rt->values[--rt->stack];
        v3 = value_select(v2, v1, &rt->err);
        rt->values[rt->stack++] = v3;
        break;

        default:
        UNREACHABLE;
    }
}

WL_EvalResult wl_runtime_eval(WL_Runtime *rt)
{
    if (rt->state != RUNTIME_OUTPUT || rt->cur_output == rt->num_output) {

        switch (rt->state) {

            case RUNTIME_BEGIN:
            break;

            case RUNTIME_DONE:
            return (WL_EvalResult) { .type=WL_EVAL_DONE };

            case RUNTIME_ERROR:
            return (WL_EvalResult) { .type=WL_EVAL_ERROR };

            case RUNTIME_OUTPUT:
            rt->stack -= rt->num_output;
            break;

            case RUNTIME_SYSVAR:
            {
                ASSERT(rt->stack >= rt->stack_before_user);

                int pushed_by_user = rt->stack - rt->stack_before_user;
                if (pushed_by_user > 1) {
                    REPORT(&rt->err, "Invalid API usage");
                    rt->state = RUNTIME_ERROR;
                    return (WL_EvalResult) { .type=WL_EVAL_ERROR };
                }

                if (rt->stack == rt->stack_before_user) {
                    // User didn't push anything on the stack
                    if (!rt_check_stack(rt, 1))
                        return (WL_EvalResult) { .type=WL_EVAL_ERROR };
                    rt->values[rt->stack++] = VALUE_NONE;
                }

                rt_pop_frame(rt);
            }
            break;

            case RUNTIME_SYSCALL:
            ASSERT(rt->stack >= rt->stack_before_user);
            rt_pop_frame(rt);
            break;

            default:
            UNREACHABLE;
        }

        rt->state = RUNTIME_LOOP;

        do {

            step(rt);

            if (rt->err.yes)
                rt->state = RUNTIME_ERROR;

        } while (rt->state == RUNTIME_LOOP);

    }

    switch (rt->state) {

        case RUNTIME_LOOP:
        UNREACHABLE;

        case RUNTIME_DONE:
        break;

        case RUNTIME_ERROR:
        return (WL_EvalResult) { .type=WL_EVAL_ERROR };

        case RUNTIME_OUTPUT:
        {
            ASSERT(rt->cur_output < rt->num_output);

            Value v = rt->values[rt->stack - rt->num_output + rt->cur_output];
            Type type = value_type(v);

            String str;
            if (type == TYPE_STRING)
                str = value_to_str(v);
            else {
                int len = value_convert_to_str(v, rt->buf, SIZEOF(rt->buf));
                if (len > SIZEOF(rt->buf)) {
                    char *p = alloc(rt->arena, len, 1);
                    if (p == NULL) {
                        REPORT(&rt->err, "Out of memory");
                        rt->state = RUNTIME_ERROR;
                        return (WL_EvalResult) { .type=WL_EVAL_ERROR };
                    }
                    len = value_convert_to_str(v, p, len);
                    str = (String) { p, len };
                } else {
                    str = (String) { rt->buf, len };
                }
            }

            rt->cur_output++;
            return (WL_EvalResult) { .type=WL_EVAL_OUTPUT, .str={ str.ptr, str.len } };
        }

        case RUNTIME_SYSVAR:
        return (WL_EvalResult) { .type=WL_EVAL_SYSVAR, .str=(WL_String) { rt->str_for_user.ptr, rt->str_for_user.len } };

        case RUNTIME_SYSCALL:
        return (WL_EvalResult) { .type=WL_EVAL_SYSCALL, .str=(WL_String) { rt->str_for_user.ptr, rt->str_for_user.len } };
    }

    return (WL_EvalResult) { .type=WL_EVAL_DONE };
}

bool wl_streq(WL_String a, char *b, int blen)
{
    if (b == NULL) b = "";
    if (blen < 0) blen = strlen(b);
    return streq((String) { a.ptr, a.len }, (String) { b, blen });
}

int wl_arg_count(WL_Runtime *rt)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return -1;

    ASSERT(rt->num_frames > 0);
    return rt->frames[rt->num_frames-1].varbase - rt->vars; // TODO: is this right?
}

static Value user_arg(WL_Runtime *rt, int idx, Type type)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return -1;

    int tot = wl_arg_count(rt);
    if (idx < 0 || idx >= tot)
        return false;

    Value v = *rt_variable(rt, tot - idx - 1);
    if (value_type(v) != type)
        return VALUE_ERROR;

    return v;
}

bool wl_arg_none(WL_Runtime *rt, int idx)
{
    Value v = user_arg(rt, idx, TYPE_NONE);
    if (v == VALUE_ERROR)
        return false;
    return true;
}

bool wl_arg_bool(WL_Runtime *rt, int idx, bool *x)
{
    Value v = user_arg(rt, idx, TYPE_BOOL);
    if (v == VALUE_ERROR)
        return false;
    *x = (v == VALUE_TRUE);
    return true;
}

bool wl_arg_s64(WL_Runtime *rt, int idx, int64_t *x)
{
    Value v = user_arg(rt, idx, TYPE_INT);
    if (v == VALUE_ERROR)
        return false;
    *x = value_to_s64(v);
    return true;
}

bool wl_arg_f64(WL_Runtime *rt, int idx, double *x)
{
    Value v = user_arg(rt, idx, TYPE_FLOAT);
    if (v == VALUE_ERROR)
        return false;
    *x = value_to_f64(v);
    return true;
}

bool wl_arg_str(WL_Runtime *rt, int idx, WL_String *x)
{
    Value v = user_arg(rt, idx, TYPE_STRING);
    if (v == VALUE_ERROR)
        return false;
    String s = value_to_str(v);
    *x = (WL_String) { s.ptr, s.len };
    return true;
}

bool wl_arg_array(WL_Runtime *rt, int idx)
{
    Value v = user_arg(rt, idx, TYPE_ARRAY);
    if (v == VALUE_ERROR)
        return false;
    return true;
}

bool wl_arg_map(WL_Runtime *rt, int idx)
{
    Value v = user_arg(rt, idx, TYPE_MAP);
    if (v == VALUE_ERROR)
        return false;
    return true;
}

static Value user_peek(WL_Runtime *rt, int off, Type type)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return VALUE_ERROR;

    if (rt->stack + off < rt->stack_before_user || off >= 0)
        return VALUE_ERROR;

    Value v = rt->values[rt->stack + off];
    if (value_type(v) != type)
        return VALUE_ERROR;

    return v;
}

bool wl_peek_none(WL_Runtime *rt, int off)
{
    Value v = user_peek(rt, off, TYPE_NONE);
    if (v == VALUE_ERROR)
        return false;
    return true;
}

bool wl_peek_bool(WL_Runtime *rt, int off, bool *x)
{
    Value v = user_peek(rt, off, TYPE_BOOL);
    if (v == VALUE_ERROR)
        return false;
    *x = (v == VALUE_TRUE);
    return true;
}

bool wl_peek_s64(WL_Runtime *rt, int off, int64_t *x)
{
    Value v = user_peek(rt, off, TYPE_INT);
    if (v == VALUE_ERROR)
        return false;
    *x = value_to_s64(v);
    return true;
}

bool wl_peek_f64(WL_Runtime *rt, int off, double *x)
{
    Value v = user_peek(rt, off, TYPE_FLOAT);
    if (v == VALUE_ERROR)
        return false;
    *x = value_to_f64(v);
    return true;
}

bool wl_peek_str(WL_Runtime *rt, int off, WL_String *x)
{
    Value v = user_peek(rt, off, TYPE_STRING);
    if (v == VALUE_ERROR)
        return false;
    String s = value_to_str(v);
    *x = (WL_String) { s.ptr, s.len };
    return true;
}

bool wl_pop_any(WL_Runtime *rt)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return VALUE_ERROR;

    if (rt->stack == rt->stack_before_user)
        return false;

    ASSERT(rt->stack > 0);
    rt->stack--;
    return true;
}

static Value user_pop(WL_Runtime *rt, Type type)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return VALUE_ERROR;

    if (rt->stack == rt->stack_before_user)
        return VALUE_ERROR;

    ASSERT(rt->stack > 0);
    Value v = rt->values[rt->stack-1];
    if (value_type(v) != type)
        return VALUE_ERROR;

    rt->stack--;
    return v;
}

bool wl_pop_none(WL_Runtime *rt)
{
    Value v = user_pop(rt, TYPE_NONE);
    if (v == VALUE_ERROR)
        return false;
    return true;
}

bool wl_pop_bool(WL_Runtime *rt, bool *x)
{
    Value v = user_pop(rt, TYPE_BOOL);
    if (v == VALUE_ERROR)
        return false;
    *x = (v == VALUE_TRUE);
    return true;
}

bool wl_pop_s64(WL_Runtime *rt, int64_t *x)
{
    Value v = user_pop(rt, TYPE_INT);
    if (v == VALUE_ERROR)
        return false;
    *x = value_to_s64(v);
    return true;
}

bool wl_pop_f64(WL_Runtime *rt, double *x)
{
    Value v = user_pop(rt, TYPE_FLOAT);
    if (v == VALUE_ERROR)
        return false;
    *x = value_to_f64(v);
    return true;
}

bool wl_pop_str(WL_Runtime *rt, WL_String *x)
{
    Value v = user_pop(rt, TYPE_STRING);
    if (v == VALUE_ERROR)
        return false;
    String s = value_to_str(v);
    *x = (WL_String) { s.ptr, s.len };
    return true;
}

void wl_push_none(WL_Runtime *rt)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (!rt_check_stack(rt, 1))
        return;

    rt->values[rt->stack++] = VALUE_NONE;
}

void wl_push_true(WL_Runtime *rt)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (!rt_check_stack(rt, 1))
        return;

    rt->values[rt->stack++] = VALUE_TRUE;
}

void wl_push_false(WL_Runtime *rt)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (!rt_check_stack(rt, 1))
        return;

    rt->values[rt->stack++] = VALUE_FALSE;
}

void wl_push_s64(WL_Runtime *rt, int64_t x)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (!rt_check_stack(rt, 1))
        return;

    Value v = value_from_s64(x, rt->arena, &rt->err);
    if (v == VALUE_ERROR) {
        rt->state = RUNTIME_ERROR;
        return;
    }

    rt->values[rt->stack++] = v;
}

void wl_push_f64(WL_Runtime *rt, double x)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (!rt_check_stack(rt, 1))
        return;

    Value v = value_from_f64(x, rt->arena, &rt->err);
    if (v == VALUE_ERROR) {
        rt->state = RUNTIME_ERROR;
        return;
    }

    rt->values[rt->stack++] = v;
}

void wl_push_str(WL_Runtime *rt, WL_String x)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (!rt_check_stack(rt, 1))
        return;

    Value v = value_from_str((String) { x.ptr, x.len }, rt->arena, &rt->err);
    if (v == VALUE_ERROR) {
        rt->state = RUNTIME_ERROR;
        return;
    }

    rt->values[rt->stack++] = v;
}

void wl_push_array(WL_Runtime *rt, int cap)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (!rt_check_stack(rt, 1))
        return;

    Value v = value_empty_array(cap, rt->arena, &rt->err);
    if (v == VALUE_ERROR) {
        rt->state = RUNTIME_ERROR;
        return;
    }

    rt->values[rt->stack++] = v;
}

void wl_push_map(WL_Runtime *rt, int cap)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (!rt_check_stack(rt, 1))
        return;

    Value v = value_empty_map(cap, rt->arena, &rt->err);
    if (v == VALUE_ERROR) {
        rt->state = RUNTIME_ERROR;
        return;
    }

    rt->values[rt->stack++] = v;
}

void wl_push_arg(WL_Runtime *rt, int idx)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (!rt_check_stack(rt, 1))
        return;

    int tot = wl_arg_count(rt);
    if (idx < 0 || idx >= tot) {
        REPORT(&rt->err, "Invalid API usagge");
        rt->state = RUNTIME_ERROR;
        return;
    }

    rt->values[rt->stack++] = *rt_variable(rt, tot - idx - 1);
}

void wl_insert(WL_Runtime *rt)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

    if (rt->stack - rt->stack_before_user < 3) {
        REPORT(&rt->err, "Invalid API usagge");
        rt->state = RUNTIME_ERROR;
        return;
    }

    Value key = rt->values[--rt->stack];
    Value val = rt->values[--rt->stack];
    Value set = rt->values[rt->stack-1];

    if (!value_insert(set, key, val, rt->arena, &rt->err)) {
        rt->state = RUNTIME_ERROR;
        return;
    }
}

void wl_append(WL_Runtime *rt)
{
    if (rt->state != RUNTIME_SYSVAR &&
        rt->state != RUNTIME_SYSCALL)
        return;

     if (rt->stack - rt->stack_before_user < 2) {
        REPORT(&rt->err, "Invalid API usagge");
        rt->state = RUNTIME_ERROR;
        return;
    }

    Value val = rt->values[--rt->stack];
    Value set = rt->values[rt->stack-1];

    if (!value_append(set, val, rt->arena, &rt->err)) {
        rt->state = RUNTIME_ERROR;
        return;
    }
}

void wl_runtime_dump(WL_Runtime *rt)
{
    for (int i = 0; i < rt->num_frames; i++) {
        printf("=== frame %d ===\n", i);
        
        Frame *frame = &rt->frames[i];

        int num_vars;
        if (i+1 < rt->num_frames)
            num_vars = frame->varbase - rt->frames[i+1].varbase;
        else
            num_vars = frame->varbase - rt->vars;

        for (int j = 0; j < num_vars; j++) {
            printf("  %d = ", j);
            value_print(rt->values[frame->varbase - j]);
        }
    }
    printf("===============\n");
}
