/*
This is a compiler frontend in a single-header-file library form.
This is a **beta** so things may change between versions!

# How to use

In *one* of your C or C++ files to create the implementation:

```
#define LIB_COMPILER_IMPLEMENTATION
#include "lib_compiler.h"
```

In the rest of your files you can just include it like a regular
header.

# Examples

See online repository for code examples

# Overrides

You can override libc calls, the arena implementation using
preprocessor at compile time, here is an example of how you
would go about it:

```
#define LC_vsnprintf stbsp_vsnprintf
#define LC_MemoryZero(p, size) __builtin_memset(p, 0, size);
#define LC_MemoryCopy(dst, src, size) __builtin_memcpy(dst, src, size)

#define LIB_COMPILER_IMPLEMENTATION
#include "lib_compiler.h"
```

Look for '@override' to find things that can be overridden using macro preprocessor
Look for '@api' to find the main functions that you are supposed to use
Look for '@configurable' to find runtime callbacks you can register and other settings

# License (MIT)

See end of file

*/
#ifndef LIB_COMPILER_HEADER
#define LIB_COMPILER_HEADER

#define LIB_COMPILER_MAJOR 0
#define LIB_COMPILER_MINOR 6

#include <stdint.h>
#include <stdbool.h>
#include <stdarg.h>
#include <stddef.h>

#ifndef LC_THREAD_LOCAL // @override
    #if defined(__cplusplus) && __cplusplus >= 201103L
        #define LC_THREAD_LOCAL thread_local
    #elif defined(__GNUC__)
        #define LC_THREAD_LOCAL __thread
    #elif defined(_MSC_VER)
        #define LC_THREAD_LOCAL __declspec(thread)
    #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__)
        #define LC_THREAD_LOCAL _Thread_local
    #elif defined(__TINYC__)
        #define LC_THREAD_LOCAL _Thread_local
    #else
        #error Couldnt figure out thread local, needs to be provided manually
    #endif
#endif

#ifndef LC_FUNCTION // @override
    #define LC_FUNCTION
#endif

#ifndef LC_String // @override
typedef struct {
    char   *str;
    int64_t len;
} LC_String;
#endif

#ifndef LC_USE_CUSTOM_ARENA // @override
typedef struct LC_VMemory   LC_VMemory;
typedef struct LC_TempArena LC_TempArena;
typedef struct LC_Arena     LC_Arena;
typedef struct LC_SourceLoc LC_SourceLoc;

struct LC_VMemory {
    size_t   commit;
    size_t   reserve;
    uint8_t *data;
};

struct LC_Arena {
    LC_VMemory memory;
    int        alignment;
    size_t     len;
    size_t     base_len; // When popping to 0 this is the minimum "len" value
                         // It's so that Bootstrapped arena won't delete itself when Reseting.
};

struct LC_TempArena {
    LC_Arena *arena;
    size_t    pos;
};
#endif

typedef struct LC_MapEntry               LC_MapEntry;
typedef struct LC_Map                    LC_Map;
typedef struct LC_AST                    LC_AST;
typedef struct LC_ASTPackage             LC_ASTPackage;
typedef struct LC_ASTNoteList            LC_ASTNoteList;
typedef struct LC_ExprCompo              LC_ExprCompo;
typedef union LC_Val                     LC_Val;
typedef struct LC_ExprIdent              LC_ExprIdent;
typedef struct LC_ExprUnary              LC_ExprUnary;
typedef struct LC_ExprBinary             LC_ExprBinary;
typedef struct LC_ExprField              LC_ExprField;
typedef struct LC_ExprIndex              LC_ExprIndex;
typedef struct LC_ExprCompoItem          LC_ExprCompoItem;
typedef struct LC_ExprType               LC_ExprType;
typedef struct LC_ExprCast               LC_ExprCast;
typedef struct LC_ExprNote               LC_ExprNote;
typedef struct LC_StmtBlock              LC_StmtBlock;
typedef struct LC_StmtFor                LC_StmtFor;
typedef struct LC_StmtDefer              LC_StmtDefer;
typedef struct LC_StmtSwitch             LC_StmtSwitch;
typedef struct LC_StmtCase               LC_StmtCase;
typedef struct LC_StmtIf                 LC_StmtIf;
typedef struct LC_StmtBreak              LC_StmtBreak;
typedef struct LC_StmtAssign             LC_StmtAssign;
typedef struct LC_StmtExpr               LC_StmtExpr;
typedef struct LC_StmtVar                LC_StmtVar;
typedef struct LC_StmtConst              LC_StmtConst;
typedef struct LC_StmtReturn             LC_StmtReturn;
typedef struct LC_StmtNote               LC_StmtNote;
typedef struct LC_TypespecArray          LC_TypespecArray;
typedef struct LC_TypespecProc           LC_TypespecProc;
typedef struct LC_TypespecAggMem         LC_TypespecAggMem;
typedef struct LC_TypespecProcArg        LC_TypespecProcArg;
typedef struct LC_DeclBase               LC_DeclBase;
typedef struct LC_DeclProc               LC_DeclProc;
typedef struct LC_DeclTypedef            LC_DeclTypedef;
typedef struct LC_DeclAgg                LC_DeclAgg;
typedef struct LC_DeclVar                LC_DeclVar;
typedef struct LC_DeclConst              LC_DeclConst;
typedef struct LC_DeclNote               LC_DeclNote;
typedef union LC_ASTValue                LC_ASTValue;
typedef struct LC_TypeAndVal             LC_TypeAndVal;
typedef struct LC_TypeArray              LC_TypeArray;
typedef struct LC_TypePtr                LC_TypePtr;
typedef struct LC_TypeMemberList         LC_TypeMemberList;
typedef struct LC_TypeProc               LC_TypeProc;
typedef struct LC_TypeAgg                LC_TypeAgg;
typedef union LC_TypeValue               LC_TypeValue;
typedef struct LC_Type                   LC_Type;
typedef struct LC_TypeMember             LC_TypeMember;
typedef struct LC_Decl                   LC_Decl;
typedef struct LC_DeclStack              LC_DeclStack;
typedef struct LC_Map                    DeclScope;
typedef struct LC_ResolvedCompo          LC_ResolvedCompo;
typedef struct LC_ResolvedCompoItem      LC_ResolvedCompoItem;
typedef struct LC_ResolvedCompoArrayItem LC_ResolvedCompoArrayItem;
typedef struct LC_ResolvedArrayCompo     LC_ResolvedArrayCompo;
typedef union LC_TokenVal                LC_TokenVal;
typedef struct LC_Token                  LC_Token;
typedef struct LC_Lex                    LC_Lex;
typedef struct LC_Parser                 LC_Parser;
typedef struct LC_Resolver               LC_Resolver;
typedef struct LC_Lang                   LC_Lang;
typedef struct LC_ASTRef                 LC_ASTRef;
typedef struct LC_ASTRefList             LC_ASTRefList;
typedef struct LC_ASTFile                LC_ASTFile;
typedef LC_Val                           Expr_Atom;
typedef LC_TypespecArray                 Typespec_Pointer;
typedef uintptr_t                        LC_Intern;
typedef struct LC_GlobImport             LC_GlobImport;
typedef struct LC_UTF32Result            LC_UTF32Result;
typedef struct LC_UTF8Result             LC_UTF8Result;
typedef struct LC_UTF16Result            LC_UTF16Result;

typedef struct LC_StringNode LC_StringNode;
struct LC_StringNode {
    LC_StringNode *next;
    LC_String      string;
};

typedef struct LC_StringList LC_StringList;
struct LC_StringList {
    int64_t        node_count;
    int64_t        char_count;
    LC_StringNode *first;
    LC_StringNode *last;
};

typedef struct LC_String16 {
    wchar_t *str;
    int64_t  len;
} LC_String16;

struct LC_MapEntry {
    uint64_t key;
    uint64_t value;
};

struct LC_Map {
    LC_Arena    *arena;
    LC_MapEntry *entries;
    int          cap;
    int          len;
};

typedef struct LC_BigInt LC_BigInt;
struct LC_BigInt {
    unsigned digit_count;
    bool     is_negative;
    union {
        uint64_t  digit;
        uint64_t *digits;
    };
};

typedef enum LC_ASTKind {
    LC_ASTKind_Null,
    LC_ASTKind_Error,
    LC_ASTKind_Note,
    LC_ASTKind_NoteList,
    LC_ASTKind_File,
    LC_ASTKind_Package,
    LC_ASTKind_Ignore,
    LC_ASTKind_TypespecProcArg,
    LC_ASTKind_TypespecAggMem,
    LC_ASTKind_ExprCallItem,
    LC_ASTKind_ExprCompoundItem,
    LC_ASTKind_ExprNote, // a: int = #`sizeof(int)`;
    LC_ASTKind_StmtSwitchCase,
    LC_ASTKind_StmtSwitchDefault,
    LC_ASTKind_StmtElseIf,
    LC_ASTKind_StmtElse,
    LC_ASTKind_GlobImport,
    LC_ASTKind_DeclNote,
    LC_ASTKind_DeclProc,
    LC_ASTKind_DeclStruct,
    LC_ASTKind_DeclUnion,
    LC_ASTKind_DeclVar,
    LC_ASTKind_DeclConst,
    LC_ASTKind_DeclTypedef,
    LC_ASTKind_TypespecIdent,
    LC_ASTKind_TypespecField,
    LC_ASTKind_TypespecPointer,
    LC_ASTKind_TypespecArray,
    LC_ASTKind_TypespecProc,
    LC_ASTKind_StmtBlock,
    LC_ASTKind_StmtNote,
    LC_ASTKind_StmtReturn,
    LC_ASTKind_StmtBreak,
    LC_ASTKind_StmtContinue,
    LC_ASTKind_StmtDefer,
    LC_ASTKind_StmtFor,
    LC_ASTKind_StmtIf,
    LC_ASTKind_StmtSwitch,
    LC_ASTKind_StmtAssign,
    LC_ASTKind_StmtExpr,
    LC_ASTKind_StmtVar,
    LC_ASTKind_StmtConst,
    LC_ASTKind_ExprIdent,
    LC_ASTKind_ExprString,
    LC_ASTKind_ExprInt,
    LC_ASTKind_ExprFloat,
    LC_ASTKind_ExprBool,
    LC_ASTKind_ExprType,
    LC_ASTKind_ExprBinary,
    LC_ASTKind_ExprUnary,
    LC_ASTKind_ExprBuiltin,
    LC_ASTKind_ExprCall,
    LC_ASTKind_ExprCompound,
    LC_ASTKind_ExprCast,
    LC_ASTKind_ExprField,
    LC_ASTKind_ExprIndex,
    LC_ASTKind_ExprAddPtr,
    LC_ASTKind_ExprGetValueOfPointer,
    LC_ASTKind_ExprGetPointerOfValue,
    LC_ASTKind_Count,
    LC_ASTKind_FirstExpr     = LC_ASTKind_ExprIdent,
    LC_ASTKind_LastExpr      = LC_ASTKind_ExprGetPointerOfValue,
    LC_ASTKind_FirstStmt     = LC_ASTKind_StmtBlock,
    LC_ASTKind_LastStmt      = LC_ASTKind_StmtConst,
    LC_ASTKind_FirstTypespec = LC_ASTKind_TypespecIdent,
    LC_ASTKind_LastTypespec  = LC_ASTKind_TypespecProc,
    LC_ASTKind_FirstDecl     = LC_ASTKind_DeclProc,
    LC_ASTKind_LastDecl      = LC_ASTKind_DeclTypedef,
} LC_ASTKind;

typedef enum LC_TypeKind {
    LC_TypeKind_char,
    LC_TypeKind_uchar,
    LC_TypeKind_short,
    LC_TypeKind_ushort,
    LC_TypeKind_bool,
    LC_TypeKind_int,
    LC_TypeKind_uint,
    LC_TypeKind_long,
    LC_TypeKind_ulong,
    LC_TypeKind_llong,
    LC_TypeKind_ullong,
    LC_TypeKind_float,
    LC_TypeKind_double,
    LC_TypeKind_void,
    LC_TypeKind_Struct,
    LC_TypeKind_Union,
    LC_TypeKind_Pointer,
    LC_TypeKind_Array,
    LC_TypeKind_Proc,
    LC_TypeKind_UntypedInt,
    LC_TypeKind_UntypedFloat,
    LC_TypeKind_UntypedString,
    LC_TypeKind_Incomplete,
    LC_TypeKind_Completing,
    LC_TypeKind_Error,
    T_TotalCount,
    T_NumericCount = LC_TypeKind_void,
    T_Count        = LC_TypeKind_void + 1,
} LC_TypeKind;

typedef enum LC_DeclState {
    LC_DeclState_Unresolved,
    LC_DeclState_Resolving,
    LC_DeclState_Resolved,
    LC_DeclState_ResolvedBody, // proc
    LC_DeclState_Error,
    LC_DeclState_Count,
} LC_DeclState;

typedef enum LC_DeclKind {
    LC_DeclKind_Error,
    LC_DeclKind_Type,
    LC_DeclKind_Const,
    LC_DeclKind_Var,
    LC_DeclKind_Proc,
    LC_DeclKind_Import,
    LC_DeclKind_Count,
} LC_DeclKind;

typedef enum LC_TokenKind {
    LC_TokenKind_EOF,
    LC_TokenKind_Error,
    LC_TokenKind_Comment,
    LC_TokenKind_DocComment,
    LC_TokenKind_FileDocComment,
    LC_TokenKind_PackageDocComment,
    LC_TokenKind_Note,
    LC_TokenKind_Hash,
    LC_TokenKind_Ident,
    LC_TokenKind_Keyword,
    LC_TokenKind_String,
    LC_TokenKind_RawString,
    LC_TokenKind_Int,
    LC_TokenKind_Float,
    LC_TokenKind_Unicode,
    LC_TokenKind_OpenParen,
    LC_TokenKind_CloseParen,
    LC_TokenKind_OpenBrace,
    LC_TokenKind_CloseBrace,
    LC_TokenKind_OpenBracket,
    LC_TokenKind_CloseBracket,
    LC_TokenKind_Comma,
    LC_TokenKind_Question,
    LC_TokenKind_Semicolon,
    LC_TokenKind_Dot,
    LC_TokenKind_ThreeDots,
    LC_TokenKind_Colon,
    LC_TokenKind_Mul,
    LC_TokenKind_Div,
    LC_TokenKind_Mod,
    LC_TokenKind_LeftShift,
    LC_TokenKind_RightShift,
    LC_TokenKind_Add,
    LC_TokenKind_Sub,
    LC_TokenKind_Equals,
    LC_TokenKind_LesserThen,
    LC_TokenKind_GreaterThen,
    LC_TokenKind_LesserThenEq,
    LC_TokenKind_GreaterThenEq,
    LC_TokenKind_NotEquals,
    LC_TokenKind_BitAnd,
    LC_TokenKind_BitOr,
    LC_TokenKind_BitXor,
    LC_TokenKind_And,
    LC_TokenKind_Or,
    LC_TokenKind_AddPtr,
    LC_TokenKind_Neg,
    LC_TokenKind_Not,
    LC_TokenKind_Assign,
    LC_TokenKind_DivAssign,
    LC_TokenKind_MulAssign,
    LC_TokenKind_ModAssign,
    LC_TokenKind_SubAssign,
    LC_TokenKind_AddAssign,
    LC_TokenKind_BitAndAssign,
    LC_TokenKind_BitOrAssign,
    LC_TokenKind_BitXorAssign,
    LC_TokenKind_LeftShiftAssign,
    LC_TokenKind_RightShiftAssign,
    LC_TokenKind_Count,
} LC_TokenKind;

struct LC_ASTFile {
    LC_AST *package;
    LC_Lex *x;

    LC_AST *fimport;
    LC_AST *limport;
    LC_AST *fdecl;
    LC_AST *ldecl;

    LC_Token *doc_comment;
};

// This extension thing is only to minimize package ast size!
// I want all nodes to be equal in size, this way a lot of things are
// easier. You can loop through all the ast nodes easily and stuff like that.
typedef struct LC_ASTPackageExt LC_ASTPackageExt;
struct LC_ASTPackageExt {
    LC_StringList injected_filepaths; // to sidestep regular file finding, implement single file packages etc.
    LC_Token     *doc_comment;
    LC_DeclState  state;
    LC_String     path;

    // These are resolved later:
    // @todo: add foreign name?
    LC_Decl   *first_ordered;
    LC_Decl   *last_ordered;
    DeclScope *scope;
};

struct LC_ASTPackage {
    LC_AST           *ffile;
    LC_AST           *lfile;
    LC_Intern         name;
    LC_ASTPackageExt *ext;
};

struct LC_ASTNoteList {
    LC_AST *first;
    LC_AST *last;
};

struct LC_ResolvedCompo {
    int                   count;
    LC_ResolvedCompoItem *first;
    LC_ResolvedCompoItem *last;
};

struct LC_ResolvedCompoItem {
    LC_ResolvedCompoItem *next;
    LC_AST               *comp; // for proc this may be null because we might match default value
    LC_AST               *expr; // for proc this is very important, the result, ordered expression
    LC_TypeMember        *t;
    bool                  varg;
    bool                  defaultarg;
};

struct LC_ResolvedCompoArrayItem {
    LC_ResolvedCompoArrayItem *next;
    LC_AST                    *comp;
    int                        index;
};

struct LC_ResolvedArrayCompo {
    int                        count;
    LC_ResolvedCompoArrayItem *first;
    LC_ResolvedCompoArrayItem *last;
};

struct LC_ExprCompo {
    LC_AST *name;  // :name{thing} or name(thing)
    LC_AST *first; // {LC_ExprCompoItem, LC_ExprCompoItem}
    LC_AST *last;
    int     size;
    union {
        LC_ResolvedCompo      *resolved_items;
        LC_ResolvedArrayCompo *resolved_array_items;
    };
};

struct LC_ExprCompoItem {
    LC_Intern name;
    LC_AST   *index;
    LC_AST   *expr;
};

// clang-format off
union  LC_Val             { LC_BigInt i; double d; LC_Intern name; };
struct LC_ExprIdent       { LC_Intern name; LC_Decl *resolved_decl; };
struct LC_ExprUnary       { LC_TokenKind op; LC_AST *expr; };
struct LC_ExprBinary      { LC_TokenKind op; LC_AST *left; LC_AST *right; };
struct LC_ExprField       { LC_AST *left; LC_Intern right; LC_Decl *resolved_decl; LC_Decl *parent_decl; };
struct LC_ExprIndex       { LC_AST *base; LC_AST *index; };
struct LC_ExprType        { LC_AST *type; };
struct LC_ExprCast        { LC_AST *type; LC_AST *expr; };
struct LC_ExprNote        { LC_AST *expr; }; // v := #c(``)

typedef enum { SBLK_Norm, SBLK_Loop, SBLK_Proc, SBLK_Defer } LC_StmtBlockKind;
struct LC_StmtBlock       { LC_AST *first; LC_AST *last; LC_AST *first_defer; LC_StmtBlockKind kind; LC_Intern name; };
struct LC_StmtFor         { LC_AST *init; LC_AST *cond; LC_AST *inc; LC_AST *body; };
struct LC_StmtDefer       { LC_AST *next; LC_AST *body; };
struct LC_StmtSwitch      { LC_AST *first; LC_AST *last; LC_AST *expr; int total_switch_case_count; };
struct LC_StmtCase        { LC_AST *first; LC_AST *last; LC_AST *body; };
// 'else if' and 'else' is avaialable from 'first'.
// The else_if and else are also LC_StmtIf but
// have different kinds and don't use 'first', 'last'
struct LC_StmtIf          { LC_AST *expr; LC_AST *body; LC_AST *first; LC_AST *last; };
struct LC_StmtBreak       { LC_Intern name; };
struct LC_StmtAssign      { LC_TokenKind op; LC_AST *left; LC_AST *right; };
struct LC_StmtExpr        { LC_AST *expr; };
struct LC_StmtVar         { LC_AST *expr; LC_AST *type; LC_Intern name; LC_Decl *resolved_decl; };
struct LC_StmtConst       { LC_AST *expr; LC_Intern name; };
struct LC_StmtReturn      { LC_AST *expr; };
struct LC_StmtNote        { LC_AST *expr; }; // #c(``) in block
struct LC_TypespecArray   { LC_AST *base; LC_AST *index; };
struct LC_TypespecProc    { LC_AST *first; LC_AST *last; LC_AST *ret; bool vargs; bool vargs_any_promotion; };
struct LC_TypespecAggMem  { LC_Intern name; LC_AST *type; };
struct LC_TypespecProcArg { LC_Intern name; LC_AST *type; LC_AST *expr; LC_Decl *resolved_decl; };
struct LC_DeclBase        { LC_Intern name; LC_Token *doc_comment; LC_Decl *resolved_decl; };
struct LC_DeclProc        { LC_DeclBase base; LC_AST *body; LC_AST *type; };
struct LC_DeclTypedef     { LC_DeclBase base; LC_AST *type; };
struct LC_DeclAgg         { LC_DeclBase base; LC_AST *first; LC_AST *last; };
struct LC_DeclVar         { LC_DeclBase base; LC_AST *expr; LC_AST *type; };
struct LC_DeclConst       { LC_DeclBase base; LC_AST *expr; };
struct LC_DeclNote        { LC_DeclBase base; LC_AST *expr; bool processed; }; // #c(``) in file note list
struct LC_GlobImport      { LC_Intern name; LC_Intern path; bool resolved; LC_Decl *resolved_decl; };

struct LC_ASTRef          { LC_ASTRef *next; LC_ASTRef *prev; LC_AST *ast; };
struct LC_ASTRefList      { LC_ASTRef *first; LC_ASTRef *last; };
// clang-format on

struct LC_TypeAndVal {
    LC_Type *type;
    union {
        LC_Val v;
        union {
            LC_BigInt i;
            double    d;
            LC_Intern name;
        };
    };
};

struct LC_AST {
    LC_ASTKind kind;
    uint32_t   id;
    LC_AST    *next;
    LC_AST    *prev;
    LC_AST    *notes;
    LC_Token  *pos;

    LC_TypeAndVal const_val;
    LC_Type      *type;
    union {
        LC_ASTFile         afile;
        LC_ASTPackage      apackage;
        LC_ASTNoteList     anote_list;
        LC_ExprCompo       anote;
        LC_Val             eatom;
        LC_ExprIdent       eident;
        LC_ExprUnary       eunary;
        LC_ExprBinary      ebinary;
        LC_ExprField       efield;
        LC_ExprIndex       eindex;
        LC_ExprCompo       ecompo;
        LC_ExprCompoItem   ecompo_item;
        LC_ExprType        etype;
        LC_ExprCast        ecast;
        LC_ExprNote        enote;
        LC_StmtBlock       sblock;
        LC_StmtFor         sfor;
        LC_StmtDefer       sdefer;
        LC_StmtSwitch      sswitch;
        LC_StmtCase        scase;
        LC_StmtIf          sif;
        LC_StmtBreak       sbreak;
        LC_StmtBreak       scontinue;
        LC_StmtAssign      sassign;
        LC_StmtExpr        sexpr;
        LC_StmtVar         svar;
        LC_StmtConst       sconst;
        LC_StmtReturn      sreturn;
        LC_StmtNote        snote;
        LC_ExprIdent       tident;
        LC_TypespecArray   tarray;
        LC_TypespecArray   tpointer;
        LC_TypespecProc    tproc;
        LC_TypespecAggMem  tagg_mem;
        LC_TypespecProcArg tproc_arg;
        LC_DeclBase        dbase;
        LC_DeclProc        dproc;
        LC_DeclTypedef     dtypedef;
        LC_DeclAgg         dagg;
        LC_DeclVar         dvar;
        LC_DeclConst       dconst;
        LC_DeclNote        dnote;
        LC_GlobImport      gimport;
    };
};

struct LC_TypeArray {
    LC_Type *base;
    int      size;
};

struct LC_TypePtr {
    LC_Type *base;
};

struct LC_TypeMemberList {
    LC_TypeMember *first;
    LC_TypeMember *last;
    int            count;
};

struct LC_TypeProc {
    LC_TypeMemberList args;
    LC_Type          *ret;
    bool              vargs;
    bool              vargs_any_promotion;
};

struct LC_TypeAgg {
    LC_TypeMemberList mems;
};

struct LC_Type {
    LC_TypeKind kind;
    int         size;
    int         align;
    int         is_unsigned;
    int         id;
    int         padding;
    LC_Decl    *decl;
    union {
        LC_TypeArray tarray;
        LC_TypePtr   tptr;
        LC_TypeProc  tproc;
        LC_TypeAgg   tagg;
        LC_Type     *tbase;
        LC_Type     *tutdefault;
    };
};

struct LC_TypeMember {
    LC_TypeMember *next;
    LC_TypeMember *prev;
    LC_Intern      name;
    LC_Type       *type;
    LC_AST        *default_value_expr;
    LC_AST        *ast;
    int            offset;
};

struct LC_Decl {
    LC_DeclKind  kind;
    LC_DeclState state;
    uint8_t      is_foreign;

    LC_Decl  *next;
    LC_Decl  *prev;
    LC_Intern name;
    LC_Intern foreign_name;
    LC_AST   *ast;
    LC_AST   *package;

    LC_TypeMember *type_member;
    DeclScope     *scope;
    LC_Decl       *typedef_renamed_type_decl;
    union {
        LC_TypeAndVal val;
        struct {
            LC_Type *type;
            LC_Val   v;
        };
    };
};

typedef struct {
    LC_Arena *arena;
    LC_AST  **data;
    int       cap;
    int       len;
} LC_ASTArray;

typedef struct LC_ASTWalker LC_ASTWalker;
typedef void                LC_ASTWalkProc(LC_ASTWalker *, LC_AST *);
struct LC_ASTWalker {
    LC_ASTArray stack;

    int inside_builtin;
    int inside_note;

    uint8_t         visit_notes;
    uint8_t         depth_first;
    uint8_t         dont_recurse; // breathfirst only
    void           *user_data;
    LC_ASTWalkProc *proc;
};

struct LC_DeclStack {
    LC_Decl **stack;
    int       len;
    int       cap;
};

union LC_TokenVal {
    LC_BigInt i;
    double    f64;
    LC_Intern ident;
};

struct LC_Token {
    LC_TokenKind kind;
    int          line;
    int          column;
    int          len;
    char        *str;
    LC_Lex      *lex;
    union {
        LC_BigInt i;
        double    f64;
        LC_Intern ident;
    };
};

struct LC_Lex {
    char     *at;
    char     *begin;
    LC_Intern file;
    int       line;
    int       column;
    LC_Token *tokens;
    int       token_count;
    bool      insert_semicolon;
};

struct LC_Parser {
    LC_Token *at;
    LC_Token *begin;
    LC_Token *end;
    LC_Lex   *x;
};

struct LC_Resolver {
    LC_Type     *compo_context_type;
    int          compo_context_array_size;
    LC_Type     *expected_ret_type;
    LC_AST      *package;
    DeclScope   *active_scope;
    LC_DeclStack locals;
    LC_Map       duplicate_map; // currently used for finding duplicates in compos
    LC_ASTArray  stmt_block_stack;
};

typedef struct LC_Printer LC_Printer;
struct LC_Printer {
    LC_Arena     *arena;
    LC_StringList list;
    int           indent;
    LC_Intern     last_filename;
    int           last_line_num;
    LC_ASTArray   out_block_stack;
};

const int LC_OPF_Error    = 1;
const int LC_OPF_UTConst  = 2;
const int LC_OPF_LValue   = 4;
const int LC_OPF_Const    = 8;
const int LC_OPF_Returned = 16;

// warning: I introduced a null compare using the values in the operand
// make sure to revisit that when modifying the struct
typedef struct {
    int flags;
    union {
        LC_Decl      *decl;
        LC_TypeAndVal val;
        struct {
            LC_Type *type;
            LC_Val   v;
        };
    };
} LC_Operand;

typedef enum {
    LC_ARCH_Invalid,
    LC_ARCH_X64,
    LC_ARCH_X86,
    LC_ARCH_Count,
} LC_ARCH;

typedef enum {
    LC_GEN_Invalid,
    LC_GEN_C,
    LC_GEN_Count,
} LC_GEN;

typedef enum {
    LC_OS_Invalid,
    LC_OS_WINDOWS,
    LC_OS_LINUX,
    LC_OS_MAC,
    LC_OS_Count,
} LC_OS;

typedef struct {
    LC_String path;
    LC_String content;
    int       line;
} LoadedFile;

typedef enum {
    LC_OPResult_Error,
    LC_OPResult_Ok,
    LC_OPResult_Bool,
} LC_OPResult;

typedef struct {
    int left;
    int right;
} LC_Precedence;

typedef enum {
    LC_PrecedenceKind_Prefix,
    LC_PrecedenceKind_Infix,
    LC_PrecedenceKind_Postfix,
} LC_PrecedenceKind;

typedef enum {
    LC_CmpRes_LT,
    LC_CmpRes_GT,
    LC_CmpRes_EQ,
} LC_CmpRes;

typedef struct LC_FileIter LC_FileIter;
struct LC_FileIter {
    bool      is_valid;
    bool      is_directory;
    LC_String absolute_path;
    LC_String relative_path;
    LC_String filename;

    LC_String path;
    LC_Arena *arena;
    union {
        struct LC_Win32_FileIter *w32;
        void                     *dir;
    };
};

#define LC_LIST_KEYWORDS \
    X(for)               \
    X(import)            \
    X(if)                \
    X(else)              \
    X(return)            \
    X(defer)             \
    X(continue)          \
    X(break)             \
    X(default)           \
    X(case)              \
    X(typedef)           \
    X(switch)            \
    X(proc)              \
    X(struct)            \
    X(union)             \
    X(addptr)            \
    X(true)              \
    X(false)

#define LC_LIST_INTERNS    \
    X(foreign, true)       \
    X(api, true)           \
    X(weak, true)          \
    X(c, true)             \
    X(fallthrough, true)   \
    X(packed, true)        \
    X(not_init, true)      \
    X(unused, true)        \
    X(static_assert, true) \
    X(str, true)           \
    X(thread_local, true)  \
    X(dont_mangle, true)   \
    X(build_if, true)      \
    X(Any, false)          \
    X(main, false)         \
    X(debug_break, false)  \
    X(sizeof, false)       \
    X(alignof, false)      \
    X(typeof, false)       \
    X(lengthof, false)     \
    X(offsetof, false)

#define LC_LIST_TYPES \
    X(char, false)    \
    X(uchar, true)    \
    X(short, false)   \
    X(ushort, true)   \
    X(bool, false)    \
    X(int, false)     \
    X(uint, true)     \
    X(long, false)    \
    X(ulong, true)    \
    X(llong, false)   \
    X(ullong, true)   \
    X(float, false)   \
    X(double, false)

struct LC_Lang {
    LC_Arena *arena;

    LC_Arena *lex_arena;

    LC_Arena *ast_arena;
    int       ast_count;

    LC_Arena *decl_arena;
    int       decl_count;

    LC_Arena *type_arena;
    int       type_count;

    int errors;

    int    typeids;
    LC_Map type_map;

    LC_AST     *builtin_package;
    LC_Resolver resolver;
    LC_Parser  *parser;

    // Package registry
    LC_AST       *fpackage;
    LC_AST       *lpackage;
    LC_Intern     first_package;
    LC_ASTRefList ordered_packages;
    LC_StringList package_dirs;
    LC_ASTRefList discarded;

    LC_Map   interns;
    LC_Map   declared_notes;
    LC_Map   foreign_names;
    LC_Map   implicit_any;
    unsigned unique_counter;

    LC_Intern first_keyword;
    LC_Intern last_keyword;

#define X(x) LC_Intern k##x;
    LC_LIST_KEYWORDS
#undef X

#define X(x, declare) LC_Intern i##x;
    LC_LIST_INTERNS
#undef X

    LC_Type types[14]; // be careful when changing
#define X(TNAME, IS_UNSIGNED) LC_Type *t##TNAME;
    LC_LIST_TYPES
#undef X

    // When adding new special pointer types make sure to
    // also update LC_SetPointerSizeAndAlign so that it properly
    // updates the types after LC_LangBegin
    int      pointer_size;
    int      pointer_align;
    LC_Type *tvoid;
    LC_Type *tpvoid;
    LC_Type *tpchar;

    LC_Type  ttstring;
    LC_Type *tstring;
    LC_Type *tuntypednil;
    LC_Type *tuntypedbool;
    LC_Type *tuntypedint;
    LC_Type  ttuntypedfloat;
    LC_Type *tuntypedfloat;
    LC_Type  ttuntypedstring;
    LC_Type *tuntypedstring;
    LC_Type  ttany;
    LC_Type *tany;

    LC_Token NullToken;
    LC_AST   NullAST;
    LC_Token BuiltinToken;
    LC_Lex   NullLEX;

    LC_Printer printer;
    LC_Parser  quick_parser;

    // @configurable
    LC_ARCH arch;
    LC_GEN  gen;
    LC_OS   os;

    bool emit_line_directives;
    bool breakpoint_on_error;
    bool use_colored_terminal_output;

    void (*on_tokens_lexed)(LC_Lex *x);
    void (*on_tokens_interned)(LC_Lex *x);
    void (*on_decl_parsed)(LC_AST *n);
    void (*on_expr_parsed)(LC_AST *n);
    void (*on_stmt_parsed)(LC_AST *n);
    void (*on_typespec_parsed)(LC_AST *n);
    void (*on_decl_type_resolved)(LC_Decl *decl);
    void (*on_proc_body_resolved)(LC_Decl *decl);
    void (*on_expr_resolved)(LC_AST *expr, LC_Operand *op);
    void (*on_stmt_resolved)(LC_AST *n);
    void (*before_call_args_resolved)(LC_AST *n, LC_Type *type);
    void (*on_file_load)(LC_AST *package, LoadedFile *file);
    void (*on_message)(LC_Token *pos, char *str, int len); // pos and x can be null
    void (*on_fatal_error)(void);
    void *user_data;
};

extern LC_THREAD_LOCAL LC_Lang *L;

//
// Main @api
//

LC_FUNCTION LC_Lang *LC_LangAlloc(void);        // This allocates memory for LC_Lang which can be used to register callbacks and set configurables
LC_FUNCTION void     LC_LangBegin(LC_Lang *l);  // Prepare for compilation: init types, init builtins, set architecture variables stuff like that
LC_FUNCTION void     LC_LangEnd(LC_Lang *lang); // Deallocate language memory

LC_FUNCTION void      LC_RegisterPackageDir(char *dir);   // Add a package search directory
LC_FUNCTION void      LC_ParseAndResolve(LC_Intern name); // Fully resolve a package and all it's dependences
LC_FUNCTION LC_String LC_GenerateUnityBuild(void);        // Generate the C program and return as a string

// Smaller passes for AST modification
LC_FUNCTION void LC_ParsePackagesPass(LC_Intern name);           // These functions are equivalent to LC_ParseAndResolve,
LC_FUNCTION void LC_BuildIfPass(void);                           // you can use them to hook into the compilation process - you can modify the AST
LC_FUNCTION void LC_OrderAndResolveTopLevelPass(LC_Intern name); // before resolving or use resolved top declarations to generate some code.
LC_FUNCTION void LC_ResolveProcBodiesPass(void);                 // The Parse and Order functions can be called multiple times to accommodate this.

// Extended pass / optimization
LC_FUNCTION void LC_FindUnusedLocalsAndRemoveUnusedGlobalDeclsPass(void); // Extended pass that you can execute once you have resolved all packages

// These three functions are used to implement LC_FindUnusedLocalsAndRemoveUnusedGlobalDeclsPass
LC_FUNCTION void     LC_CountDeclRefs(LC_Arena *arena, LC_Map *map, LC_AST *ast);
LC_FUNCTION LC_Decl *LC_RemoveUnreferencedGlobalDeclsPass(LC_Map *map_of_visits);
LC_FUNCTION void     LC_ErrorOnUnreferencedLocalsPass(LC_Map *map_of_visits);

// Notes
LC_FUNCTION void    LC_DeclareNote(LC_Intern intern);
LC_FUNCTION bool    LC_IsNoteDeclared(LC_Intern intern);
LC_FUNCTION LC_AST *LC_HasNote(LC_AST *ast, LC_Intern i);

// Quick parse functions
LC_FUNCTION LC_AST *LC_ParseStmtf(const char *str, ...);
LC_FUNCTION LC_AST *LC_ParseExprf(const char *str, ...);
LC_FUNCTION LC_AST *LC_ParseDeclf(const char *str, ...);

// AST Walking and copy
LC_FUNCTION LC_AST      *LC_CopyAST(LC_Arena *arena, LC_AST *n);    // Deep copy the AST
LC_FUNCTION LC_ASTArray  LC_FlattenAST(LC_Arena *arena, LC_AST *n); // This walks the passed down tree and generates a flat array of pointers, very nice to use for traversing AST
LC_FUNCTION void         LC_WalkAST(LC_ASTWalker *ctx, LC_AST *n);
LC_FUNCTION LC_ASTWalker LC_GetDefaultWalker(LC_Arena *arena, LC_ASTWalkProc *proc);

LC_FUNCTION void    LC_ReserveAST(LC_ASTArray *arr, int size);
LC_FUNCTION void    LC_PushAST(LC_ASTArray *arr, LC_AST *ast);
LC_FUNCTION void    LC_PopAST(LC_ASTArray *arr);
LC_FUNCTION LC_AST *LC_GetLastAST(LC_ASTArray *arr);

// Interning API
LC_FUNCTION LC_Intern LC_ILit(char *str);
LC_FUNCTION void      LC_InternTokens(LC_Lex *x);

LC_FUNCTION LC_Intern LC_InternStrLen(char *str, int len);
LC_FUNCTION LC_Intern LC_GetUniqueIntern(const char *name_for_debug);
LC_FUNCTION char     *LC_GetUniqueName(const char *name_for_debug);

//
// Package functions
//
LC_FUNCTION LC_Operand    LC_ImportPackage(LC_AST *import, LC_AST *dst, LC_AST *src);
LC_FUNCTION LC_Intern     LC_MakePackageNameFromPath(LC_String path);
LC_FUNCTION bool          LC_PackageNameValid(LC_Intern name);
LC_FUNCTION bool          LC_PackageNameDuplicate(LC_Intern name);
LC_FUNCTION void          LC_AddPackageToList(LC_AST *n);
LC_FUNCTION LC_AST       *LC_RegisterPackage(LC_String path);
LC_FUNCTION void          LC_AddFileToPackage(LC_AST *pkg, LC_AST *f);
LC_FUNCTION LC_AST       *LC_FindImportInRefList(LC_ASTRefList *arr, LC_Intern path);
LC_FUNCTION void          LC_AddASTToRefList(LC_ASTRefList *refs, LC_AST *ast);
LC_FUNCTION LC_ASTRefList LC_GetPackageImports(LC_AST *package);
LC_FUNCTION LC_AST       *LC_GetPackageByName(LC_Intern name);
LC_FUNCTION LC_StringList LC_ListFilesInPackage(LC_Arena *arena, LC_String path);
LC_FUNCTION LoadedFile    LC_ReadFileHook(LC_AST *package, LC_String path);
LC_FUNCTION void          LC_ParsePackage(LC_AST *n);
LC_FUNCTION void          LC_AddOrderedPackageToRefList(LC_AST *n);
LC_FUNCTION LC_AST       *LC_OrderPackagesAndBasicResolve(LC_AST *pos, LC_Intern name);
LC_FUNCTION void          LC_AddSingleFilePackage(LC_Intern name, LC_String path);

//
// Lexing functions
//
LC_FUNCTION LC_Lex   *LC_LexStream(char *file, char *str, int line);
LC_FUNCTION LC_String LC_GetTokenLine(LC_Token *token);

LC_FUNCTION void      LC_LexingError(LC_Token *pos, const char *str, ...);
LC_FUNCTION bool      LC_IsAssign(LC_TokenKind kind);
LC_FUNCTION bool      LC_IsHexDigit(char c);
LC_FUNCTION bool      LC_IsBinDigit(char c);
LC_FUNCTION uint64_t  LC_MapCharToNumber(char c);
LC_FUNCTION uint64_t  LC_GetEscapeCode(char c);
LC_FUNCTION LC_String LC_GetEscapeString(char c);
LC_FUNCTION void      LC_LexAdvance(LC_Lex *x);
LC_FUNCTION void      LC_EatWhitespace(LC_Lex *x);
LC_FUNCTION void      LC_EatIdent(LC_Lex *x);
LC_FUNCTION void      LC_SetTokenLen(LC_Lex *x, LC_Token *t);
LC_FUNCTION void      LC_EatUntilIncluding(LC_Lex *x, char c);
LC_FUNCTION LC_BigInt LC_LexBigInt(char *string, int len, uint64_t base);
LC_FUNCTION void      LC_LexNestedComments(LC_Lex *x, LC_Token *t);
LC_FUNCTION void      LC_LexStringLiteral(LC_Lex *x, LC_Token *t, LC_TokenKind kind);
LC_FUNCTION void      LC_LexUnicodeLiteral(LC_Lex *x, LC_Token *t);
LC_FUNCTION void      LC_LexIntOrFloat(LC_Lex *x, LC_Token *t);
LC_FUNCTION void      LC_LexCase2(LC_Lex *x, LC_Token *t, LC_TokenKind tk0, char c, LC_TokenKind tk1);
LC_FUNCTION void      LC_LexCase3(LC_Lex *x, LC_Token *t, LC_TokenKind tk, char c0, LC_TokenKind tk0, char c1, LC_TokenKind tk1);
LC_FUNCTION void      LC_LexCase4(LC_Lex *x, LC_Token *t, LC_TokenKind tk, char c0, LC_TokenKind tk0, char c1, LC_TokenKind tk1, char c2, LC_TokenKind tk2);
LC_FUNCTION void      LC_LexNext(LC_Lex *x, LC_Token *t);

//
// LC_Map API
//
LC_FUNCTION void         LC_MapReserve(LC_Map *map, int size);
LC_FUNCTION int          LC_NextPow2(int v);
LC_FUNCTION LC_MapEntry *LC_GetMapEntryEx(LC_Map *map, uint64_t key);
LC_FUNCTION bool         LC_InsertWithoutReplace(LC_Map *map, void *key, void *value);
LC_FUNCTION LC_MapEntry *LC_InsertMapEntry(LC_Map *map, uint64_t key, uint64_t value);
LC_FUNCTION LC_MapEntry *LC_GetMapEntry(LC_Map *map, uint64_t key);
LC_FUNCTION void         LC_MapInsert(LC_Map *map, LC_String keystr, void *value);
LC_FUNCTION void        *LC_MapGet(LC_Map *map, LC_String keystr);
LC_FUNCTION void         LC_MapInsertU64(LC_Map *map, uint64_t key, void *value);
LC_FUNCTION void        *LC_MapGetU64(LC_Map *map, uint64_t key);
LC_FUNCTION void        *LC_MapGetP(LC_Map *map, void *key);
LC_FUNCTION void         LC_MapInsertP(LC_Map *map, void *key, void *value);
LC_FUNCTION void         LC_MapClear(LC_Map *map);

//
// LC_AST Creation
//
LC_FUNCTION LC_AST *LC_CreateAST(LC_Token *pos, LC_ASTKind kind);
LC_FUNCTION LC_AST *LC_CreateUnary(LC_Token *pos, LC_TokenKind op, LC_AST *expr);
LC_FUNCTION LC_AST *LC_CreateBinary(LC_Token *pos, LC_AST *left, LC_AST *right, LC_TokenKind op);
LC_FUNCTION LC_AST *LC_CreateIndex(LC_Token *pos, LC_AST *left, LC_AST *index);

LC_FUNCTION bool LC_ContainsCallExpr(LC_AST *ast);
LC_FUNCTION void LC_SetASTPosOnAll(LC_AST *n, LC_Token *pos);
LC_FUNCTION bool LC_ContainsCBuiltin(LC_AST *n);

//
// LC_Type functions
//
LC_FUNCTION void           LC_SetPointerSizeAndAlign(int size, int align);
LC_FUNCTION LC_Type       *LC_CreateType(LC_TypeKind kind);
LC_FUNCTION LC_Type       *LC_CreateTypedef(LC_Decl *decl, LC_Type *base);
LC_FUNCTION LC_Type       *LC_CreatePointerType(LC_Type *type);
LC_FUNCTION LC_Type       *LC_CreateArrayType(LC_Type *type, int size);
LC_FUNCTION LC_Type       *LC_CreateProcType(LC_TypeMemberList args, LC_Type *ret, bool has_vargs, bool has_vargs_any_promotion);
LC_FUNCTION LC_Type       *LC_CreateIncompleteType(LC_Decl *decl);
LC_FUNCTION LC_Type       *LC_CreateUntypedIntEx(LC_Type *base, LC_Decl *decl);
LC_FUNCTION LC_Type       *LC_CreateUntypedInt(LC_Type *base);
LC_FUNCTION LC_TypeMember *LC_AddTypeToList(LC_TypeMemberList *list, LC_Intern name, LC_Type *type, LC_AST *ast);
LC_FUNCTION int            LC_GetLevelsOfIndirection(LC_Type *type);
LC_FUNCTION bool           LC_BigIntFits(LC_BigInt i, LC_Type *type);
LC_FUNCTION LC_Type       *LC_StripPointer(LC_Type *type);

//
// Parsing functions
//
LC_FUNCTION LC_AST *LC_ParseFile(LC_AST *package, char *filename, char *content, int line);
LC_FUNCTION LC_AST *LC_ParseTokens(LC_AST *package, LC_Lex *x);

LC_FUNCTION LC_Parser     LC_MakeParser(LC_Lex *x);
LC_FUNCTION LC_Parser    *LC_MakeParserQuick(char *str);
LC_FUNCTION LC_Token     *LC_Next(void);
LC_FUNCTION LC_Token     *LC_Get(void);
LC_FUNCTION LC_Token     *LC_GetI(int i);
LC_FUNCTION LC_Token     *LC_Is(LC_TokenKind kind);
LC_FUNCTION LC_Token     *LC_IsKeyword(LC_Intern intern);
LC_FUNCTION LC_Token     *LC_Match(LC_TokenKind kind);
LC_FUNCTION LC_Token     *LC_MatchKeyword(LC_Intern intern);
LC_FUNCTION LC_Precedence LC_GetPrecedence(LC_PrecedenceKind binding, LC_TokenKind kind);
LC_FUNCTION LC_AST       *LC_ParseExprEx(int min_bp);
LC_FUNCTION LC_AST       *LC_ParseCompo(LC_Token *pos, LC_AST *left);
LC_FUNCTION LC_AST       *LC_ParseExpr(void);
LC_FUNCTION LC_AST       *LC_ParseProcType(LC_Token *pos);
LC_FUNCTION LC_AST       *LC_ParseType(void);
LC_FUNCTION LC_AST       *LC_ParseForStmt(LC_Token *pos);
LC_FUNCTION LC_AST       *LC_ParseSwitchStmt(LC_Token *pos);
LC_FUNCTION LC_AST       *LC_ParseStmt(bool check_semicolon);
LC_FUNCTION LC_AST       *LC_ParseStmtBlock(int flags);
LC_FUNCTION LC_AST       *LC_ParseProcDecl(LC_Token *name);
LC_FUNCTION LC_AST       *LC_ParseStruct(LC_ASTKind kind, LC_Token *ident);
LC_FUNCTION LC_AST       *LC_ParseTypedef(LC_Token *ident);
LC_FUNCTION LC_AST       *LC_CreateNote(LC_Token *pos, LC_Intern ident);
LC_FUNCTION LC_AST       *LC_ParseNote(void);
LC_FUNCTION LC_AST       *LC_ParseNotes(void);
LC_FUNCTION bool          LC_ResolveBuildIf(LC_AST *build_if);
LC_FUNCTION LC_AST       *LC_ParseImport(void);
LC_FUNCTION LC_AST       *LC_ParseDecl(LC_AST *file);
LC_FUNCTION bool          LC_EatUntilNextValidDecl(void);
LC_FUNCTION bool          LC_ParseHashBuildOn(LC_AST *n);
LC_FUNCTION LC_AST       *LC_ParseFileEx(LC_AST *package);

//
// Resolution functions
//
LC_FUNCTION void          LC_AddDecl(LC_DeclStack *scope, LC_Decl *decl);
LC_FUNCTION void          LC_InitDeclStack(LC_DeclStack *stack, int size);
LC_FUNCTION LC_DeclStack *LC_CreateDeclStack(int size);
LC_FUNCTION LC_Decl      *LC_FindDeclOnStack(LC_DeclStack *scp, LC_Intern name);

LC_FUNCTION DeclScope *LC_CreateScope(int size);
LC_FUNCTION LC_Decl   *LC_CreateDecl(LC_DeclKind kind, LC_Intern name, LC_AST *n);
LC_FUNCTION LC_Operand LC_AddDeclToScope(DeclScope *scp, LC_Decl *decl);
LC_FUNCTION LC_Decl   *LC_FindDeclInScope(DeclScope *scope, LC_Intern name);
LC_FUNCTION LC_Operand LC_ThereIsNoDecl(DeclScope *scp, LC_Decl *decl, bool check_locals);
LC_FUNCTION void       LC_MarkDeclError(LC_Decl *decl);
LC_FUNCTION LC_Decl   *LC_GetLocalOrGlobalDecl(LC_Intern name);
LC_FUNCTION LC_Operand LC_PutGlobalDecl(LC_Decl *decl);
LC_FUNCTION LC_Operand LC_CreateLocalDecl(LC_DeclKind kind, LC_Intern name, LC_AST *ast);
LC_FUNCTION LC_Decl   *LC_AddConstIntDecl(char *key, int64_t value);
LC_FUNCTION LC_Decl   *LC_GetBuiltin(LC_Intern name);
LC_FUNCTION void       LC_AddBuiltinConstInt(char *key, int64_t value);

LC_FUNCTION void                       LC_RegisterDeclsFromFile(LC_AST *file);
LC_FUNCTION void                       LC_ResolveDeclsFromFile(LC_AST *file);
LC_FUNCTION void                       LC_PackageDecls(LC_AST *package);
LC_FUNCTION void                       LC_ResolveProcBodies(LC_AST *package);
LC_FUNCTION void                       LC_ResolveIncompleteTypes(LC_AST *package);
LC_FUNCTION LC_Operand                 LC_ResolveNote(LC_AST *n, bool is_decl);
LC_FUNCTION LC_Operand                 LC_ResolveProcBody(LC_Decl *decl);
LC_FUNCTION LC_ResolvedCompoItem      *LC_AddResolvedCallItem(LC_ResolvedCompo *list, LC_TypeMember *t, LC_AST *comp, LC_AST *expr);
LC_FUNCTION LC_Operand                 LC_ResolveCompoCall(LC_AST *n, LC_Type *type);
LC_FUNCTION LC_Operand                 LC_ResolveCompoAggregate(LC_AST *n, LC_Type *type);
LC_FUNCTION LC_ResolvedCompoArrayItem *LC_AddResolvedCompoArrayItem(LC_ResolvedArrayCompo *arr, int index, LC_AST *comp);
LC_FUNCTION LC_Operand                 LC_ResolveCompoArray(LC_AST *n, LC_Type *type);
LC_FUNCTION LC_Operand                 LC_ResolveTypeOrExpr(LC_AST *n);
LC_FUNCTION LC_Operand                 LC_ExpectBuiltinWithOneArg(LC_AST *n);
LC_FUNCTION LC_Operand                 LC_ResolveBuiltin(LC_AST *n);
LC_FUNCTION bool                       LC_TryTyping(LC_AST *n, LC_Operand op);
LC_FUNCTION bool                       LC_TryDefaultTyping(LC_AST *n, LC_Operand *o);
LC_FUNCTION LC_Operand                 LC_ResolveNameInScope(LC_AST *n, LC_Decl *parent_decl);
LC_FUNCTION LC_Operand                 LC_ResolveExpr(LC_AST *expr);
LC_FUNCTION LC_Operand                 LC_ResolveExprAndPushCompoContext(LC_AST *expr, LC_Type *type);
LC_FUNCTION LC_Operand                 LC_ResolveExprEx(LC_AST *n);
LC_FUNCTION LC_Operand                 LC_ResolveStmtBlock(LC_AST *n);
LC_FUNCTION LC_Operand                 LC_ResolveVarDecl(LC_Decl *decl);
LC_FUNCTION LC_Operand                 LC_MakeSureNoDeferBlock(LC_AST *n, char *str);
LC_FUNCTION LC_Operand                 LC_MakeSureInsideLoopBlock(LC_AST *n, char *str);
LC_FUNCTION LC_Operand                 LC_MatchLabeledBlock(LC_AST *n);
LC_FUNCTION LC_Operand                 LC_ResolveStmt(LC_AST *n);
LC_FUNCTION LC_Operand                 LC_ResolveConstDecl(LC_Decl *decl);
LC_FUNCTION LC_Operand                 LC_ResolveName(LC_AST *pos, LC_Intern intern);
LC_FUNCTION LC_Operand                 LC_ResolveConstInt(LC_AST *n, LC_Type *int_type, uint64_t *out_size);
LC_FUNCTION LC_Operand                 LC_ResolveType(LC_AST *n);
LC_FUNCTION LC_Operand                 LC_ResolveBinaryExpr(LC_AST *n, LC_Operand l, LC_Operand r);
LC_FUNCTION LC_Operand                 LC_ResolveTypeVargs(LC_AST *pos, LC_Operand v);
LC_FUNCTION LC_Operand                 LC_ResolveTypeCast(LC_AST *pos, LC_Operand t, LC_Operand v);
LC_FUNCTION LC_Operand                 LC_ResolveTypeVarDecl(LC_AST *pos, LC_Operand t, LC_Operand v);
LC_FUNCTION LC_Operand                 LC_ResolveTypeAggregate(LC_AST *pos, LC_Type *type);

extern LC_Operand       LC_OPNull;
LC_FUNCTION LC_Operand  LC_OPError(void);
LC_FUNCTION LC_Operand  LC_OPConstType(LC_Type *type);
LC_FUNCTION LC_Operand  LC_OPDecl(LC_Decl *decl);
LC_FUNCTION LC_Operand  LC_OPType(LC_Type *type);
LC_FUNCTION LC_Operand  LC_OPLValueAndType(LC_Type *type);
LC_FUNCTION LC_Operand  LC_ConstCastFloat(LC_AST *pos, LC_Operand op);
LC_FUNCTION LC_Operand  LC_ConstCastInt(LC_AST *pos, LC_Operand op);
LC_FUNCTION LC_Operand  LC_OPInt(int64_t v);
LC_FUNCTION LC_Operand  LC_OPIntT(LC_Type *type, int64_t v);
LC_FUNCTION LC_Operand  LC_OPModDefaultUT(LC_Operand val);
LC_FUNCTION LC_Operand  LC_OPModType(LC_Operand op, LC_Type *type);
LC_FUNCTION LC_Operand  LC_OPModBool(LC_Operand op);
LC_FUNCTION LC_Operand  LC_OPModBoolV(LC_Operand op, int v);
LC_FUNCTION LC_Operand  LC_EvalBinary(LC_AST *pos, LC_Operand a, LC_TokenKind op, LC_Operand b);
LC_FUNCTION LC_Operand  LC_EvalUnary(LC_AST *pos, LC_TokenKind op, LC_Operand a);
LC_FUNCTION LC_OPResult LC_IsBinaryExprValidForType(LC_TokenKind op, LC_Type *type);
LC_FUNCTION LC_OPResult LC_IsUnaryOpValidForType(LC_TokenKind op, LC_Type *type);
LC_FUNCTION LC_OPResult LC_IsAssignValidForType(LC_TokenKind op, LC_Type *type);

//
// Error
//
LC_FUNCTION void       LC_IgnoreMessage(LC_Token *pos, char *str, int len);
LC_FUNCTION void       LC_SendErrorMessage(LC_Token *pos, LC_String s8);
LC_FUNCTION void       LC_SendErrorMessagef(LC_Lex *x, LC_Token *pos, const char *str, ...);
LC_FUNCTION LC_AST    *LC_ReportParseError(LC_Token *pos, const char *str, ...);
LC_FUNCTION LC_Operand LC_ReportASTError(LC_AST *n, const char *str, ...);
LC_FUNCTION LC_Operand LC_ReportASTErrorEx(LC_AST *n1, LC_AST *n2, const char *str, ...);
LC_FUNCTION void       LC_HandleFatalError(void);

#define LC_ASSERT(n, cond)                                                                                               \
    if (!(cond)) {                                                                                                       \
        LC_ReportASTError(n, "internal compiler error: assert condition failed: %s, inside of %s", #cond, __FUNCTION__); \
        LC_HandleFatalError();                                                                                           \
    }

//
// Code generation and printing helpers
//
LC_FUNCTION LC_StringList *LC_BeginStringGen(LC_Arena *arena);
LC_FUNCTION LC_String      LC_EndStringGen(LC_Arena *arena);

// clang-format off
#define LC_Genf(...) LC_Addf(L->printer.arena, &L->printer.list, __VA_ARGS__)
#define LC_GenLinef(...) do { LC_Genf("\n"); LC_GenIndent(); LC_Genf(__VA_ARGS__); } while (0)
// clang-format on

LC_FUNCTION void  LC_GenIndent(void);
LC_FUNCTION void  LC_GenLine(void);
LC_FUNCTION char *LC_Strf(const char *str, ...);
LC_FUNCTION char *LC_GenLCType(LC_Type *type);
LC_FUNCTION char *LC_GenLCTypeVal(LC_TypeAndVal v);
LC_FUNCTION char *LC_GenLCAggName(LC_Type *t);
LC_FUNCTION void  LC_GenLCNode(LC_AST *n);

// C code generation
LC_FUNCTION void LC_GenCHeader(LC_AST *package);
LC_FUNCTION void LC_GenCImpl(LC_AST *package);

LC_FUNCTION void LC_GenCLineDirective(LC_AST *node);
LC_FUNCTION void LC_GenLastCLineDirective(void);
LC_FUNCTION void LC_GenCLineDirectiveNum(int num);

LC_FUNCTION char     *LC_GenCTypeParen(char *str, char c);
LC_FUNCTION char     *LC_GenCType(LC_Type *type, char *str);
LC_FUNCTION LC_Intern LC_GetStringFromSingleArgNote(LC_AST *note);
LC_FUNCTION void      LC_GenCCompound(LC_AST *n);
LC_FUNCTION void      LC_GenCString(char *s, LC_Type *type);
LC_FUNCTION char     *LC_GenCVal(LC_TypeAndVal v, LC_Type *type);
LC_FUNCTION void      LC_GenCExpr(LC_AST *n);
LC_FUNCTION void      LC_GenCNote(LC_AST *note);
LC_FUNCTION void      LC_GenCVarExpr(LC_AST *n, bool is_declaration);
LC_FUNCTION void      LC_GenCDefers(LC_AST *block);
LC_FUNCTION void      LC_GenCDefersLoopBreak(LC_AST *n);
LC_FUNCTION void      LC_GenCDefersReturn(LC_AST *n);
LC_FUNCTION void      LC_GenCStmt2(LC_AST *n, int flags);
LC_FUNCTION void      LC_GenCStmt(LC_AST *n);
LC_FUNCTION void      LC_GenCExprParen(LC_AST *expr);
LC_FUNCTION void      LC_GenCStmtBlock(LC_AST *n);
LC_FUNCTION void      LC_GenCProcDecl(LC_Decl *decl);
LC_FUNCTION void      LC_GenCAggForwardDecl(LC_Decl *decl);
LC_FUNCTION void      LC_GenCTypeDecl(LC_Decl *decl);
LC_FUNCTION void      LC_GenCVarFDecl(LC_Decl *decl);

//
// Inline helpers
//
static inline bool LC_IsUTInt(LC_Type *x) { return x->kind == LC_TypeKind_UntypedInt; }
static inline bool LC_IsUTFloat(LC_Type *x) { return x->kind == LC_TypeKind_UntypedFloat; }
static inline bool LC_IsUTStr(LC_Type *x) { return x->kind == LC_TypeKind_UntypedString; }
static inline bool LC_IsUntyped(LC_Type *x) { return (x)->kind >= LC_TypeKind_UntypedInt && x->kind <= LC_TypeKind_UntypedString; }

static inline bool LC_IsNum(LC_Type *x) { return x->kind >= LC_TypeKind_char && x->kind <= LC_TypeKind_double; }
static inline bool LC_IsInt(LC_Type *x) { return (x->kind >= LC_TypeKind_char && x->kind <= LC_TypeKind_ullong) || LC_IsUTInt(x); }
static inline bool LC_IsIntLike(LC_Type *x) { return LC_IsInt(x) || x->kind == LC_TypeKind_Pointer || x->kind == LC_TypeKind_Proc; }
static inline bool LC_IsFloat(LC_Type *x) { return ((x)->kind == LC_TypeKind_float || (x->kind == LC_TypeKind_double)) || LC_IsUTFloat(x); }
static inline bool LC_IsSmallerThenInt(LC_Type *x) { return x->kind < LC_TypeKind_int; }

static inline bool LC_IsAggType(LC_Type *x) { return ((x)->kind == LC_TypeKind_Struct || (x)->kind == LC_TypeKind_Union); }
static inline bool LC_IsArray(LC_Type *x) { return (x)->kind == LC_TypeKind_Array; }
static inline bool LC_IsProc(LC_Type *x) { return (x)->kind == LC_TypeKind_Proc; }
static inline bool LC_IsVoidPtr(LC_Type *x) { return (x) == L->tpvoid; }
static inline bool LC_IsPtr(LC_Type *x) { return (x)->kind == LC_TypeKind_Pointer; }
static inline bool LC_IsPtrLike(LC_Type *x) { return (x)->kind == LC_TypeKind_Pointer || x->kind == LC_TypeKind_Proc; }
static inline bool LC_IsStr(LC_Type *x) { return x == L->tpchar || x == L->tstring; }

static inline bool LC_IsDecl(LC_AST *x) { return (x->kind >= LC_ASTKind_FirstDecl && x->kind <= LC_ASTKind_LastDecl); }
static inline bool LC_IsStmt(LC_AST *x) { return (x->kind >= LC_ASTKind_FirstStmt && x->kind <= LC_ASTKind_LastStmt); }
static inline bool LC_IsExpr(LC_AST *x) { return (x->kind >= LC_ASTKind_FirstExpr && x->kind <= LC_ASTKind_LastExpr); }
static inline bool LC_IsType(LC_AST *x) { return (x->kind >= LC_ASTKind_FirstTypespec && x->kind <= LC_ASTKind_LastTypespec); }
static inline bool LC_IsAgg(LC_AST *x) { return (x->kind == LC_ASTKind_DeclStruct || x->kind == LC_ASTKind_DeclUnion); }

// I tried removing this because I thought it's redundant
// but this reminded me that "Untyped bool" can appear from normal expressions: (a == b)
// This is required, maybe there is a way around it, not sure
static inline bool LC_IsUTConst(LC_Operand op) { return (op.flags & LC_OPF_UTConst) != 0; }
static inline bool LC_IsConst(LC_Operand op) { return (op.flags & LC_OPF_Const) != 0; }
static inline bool LC_IsLValue(LC_Operand op) { return (op.flags & LC_OPF_LValue) != 0; }
static inline bool LC_IsError(LC_Operand op) { return (op.flags & LC_OPF_Error) != 0; }

static inline LC_Type *LC_GetBase(LC_Type *x) { return (x)->tbase; }

//
// Stringifying functions
//
LC_FUNCTION const char *LC_OSToString(LC_OS os);
LC_FUNCTION const char *LC_GENToString(LC_GEN os);
LC_FUNCTION const char *LC_ARCHToString(LC_ARCH arch);
LC_FUNCTION const char *LC_ASTKindToString(LC_ASTKind kind);
LC_FUNCTION const char *LC_TypeKindToString(LC_TypeKind kind);
LC_FUNCTION const char *LC_DeclKindToString(LC_DeclKind decl_kind);
LC_FUNCTION const char *LC_TokenKindToString(LC_TokenKind token_kind);
LC_FUNCTION const char *LC_TokenKindToOperator(LC_TokenKind token_kind);

/*
The bigint code was written by Christoffer Lerno, he is the programmer
behind C3. He allowed me to use this code without any restrictions. Great guy!
You can check out C3 compiler: https://github.com/c3lang/c3c
He also writes very helpful blogs about compilers: https://c3.handmade.network/blog
*/
LC_FUNCTION LC_BigInt LC_Bigint_u64(uint64_t val);
LC_FUNCTION uint64_t *LC_Bigint_ptr(LC_BigInt *big_int);
LC_FUNCTION size_t    LC_Bigint_bits_needed(LC_BigInt *big_int);
LC_FUNCTION void      LC_Bigint_init_unsigned(LC_BigInt *big_int, uint64_t value);
LC_FUNCTION void      LC_Bigint_init_signed(LC_BigInt *dest, int64_t value);
LC_FUNCTION void      LC_Bigint_init_bigint(LC_BigInt *dest, LC_BigInt *src);
LC_FUNCTION void      LC_Bigint_negate(LC_BigInt *dest, LC_BigInt *source);
LC_FUNCTION size_t    LC_Bigint_clz(LC_BigInt *big_int, size_t bit_count);
LC_FUNCTION bool      LC_Bigint_eql(LC_BigInt a, LC_BigInt b);
LC_FUNCTION bool      LC_Bigint_fits_in_bits(LC_BigInt *big_int, size_t bit_count, bool is_signed);
LC_FUNCTION uint64_t  LC_Bigint_as_unsigned(LC_BigInt *bigint);
LC_FUNCTION void      LC_Bigint_add(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_add_wrap(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2, size_t bit_count, bool is_signed);
LC_FUNCTION void      LC_Bigint_sub(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_sub_wrap(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2, size_t bit_count, bool is_signed);
LC_FUNCTION void      LC_Bigint_mul(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_mul_wrap(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2, size_t bit_count, bool is_signed);
LC_FUNCTION void      LC_Bigint_unsigned_division(LC_BigInt *op1, LC_BigInt *op2, LC_BigInt *Quotient, LC_BigInt *Remainder);
LC_FUNCTION void      LC_Bigint_div_trunc(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_div_floor(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_rem(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_mod(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_or(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_and(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_xor(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_shl(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_shl_int(LC_BigInt *dest, LC_BigInt *op1, uint64_t shift);
LC_FUNCTION void      LC_Bigint_shl_trunc(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2, size_t bit_count, bool is_signed);
LC_FUNCTION void      LC_Bigint_shr(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION void      LC_Bigint_not(LC_BigInt *dest, LC_BigInt *op, size_t bit_count, bool is_signed);
LC_FUNCTION void      LC_Bigint_truncate(LC_BigInt *dst, LC_BigInt *op, size_t bit_count, bool is_signed);
LC_FUNCTION LC_CmpRes LC_Bigint_cmp(LC_BigInt *op1, LC_BigInt *op2);
LC_FUNCTION char     *LC_Bigint_str(LC_BigInt *bigint, uint64_t base);
LC_FUNCTION int64_t   LC_Bigint_as_signed(LC_BigInt *bigint);
LC_FUNCTION LC_CmpRes LC_Bigint_cmp_zero(LC_BigInt *op);
LC_FUNCTION double    LC_Bigint_as_float(LC_BigInt *bigint);

//
// Unicode API
//
struct LC_UTF32Result {
    uint32_t out_str;
    int      advance;
    int      error;
};

struct LC_UTF8Result {
    uint8_t out_str[4];
    int     len;
    int     error;
};

struct LC_UTF16Result {
    uint16_t out_str[2];
    int      len;
    int      error;
};

LC_FUNCTION LC_UTF32Result LC_ConvertUTF16ToUTF32(uint16_t *c, int max_advance);
LC_FUNCTION LC_UTF8Result  LC_ConvertUTF32ToUTF8(uint32_t codepoint);
LC_FUNCTION LC_UTF32Result LC_ConvertUTF8ToUTF32(char *c, int max_advance);
LC_FUNCTION LC_UTF16Result LC_ConvertUTF32ToUTF16(uint32_t codepoint);
LC_FUNCTION int64_t        LC_CreateCharFromWidechar(char *buffer, int64_t buffer_size, wchar_t *in, int64_t inlen);
LC_FUNCTION int64_t        LC_CreateWidecharFromChar(wchar_t *buffer, int64_t buffer_size, char *in, int64_t inlen);

//
// Filesystem API
//
LC_FUNCTION bool      LC_IsDir(LC_Arena *temp, LC_String path);
LC_FUNCTION LC_String LC_GetAbsolutePath(LC_Arena *arena, LC_String relative);
LC_FUNCTION bool      LC_EnableTerminalColors(void);
LC_FUNCTION LC_String LC_ReadFile(LC_Arena *arena, LC_String path);

LC_FUNCTION bool        LC_IsValid(LC_FileIter it);
LC_FUNCTION void        LC_Advance(LC_FileIter *it);
LC_FUNCTION LC_FileIter LC_IterateFiles(LC_Arena *scratch_arena, LC_String path);

//
// Arena API
//
#define LC_PushSize(a, size) LC__PushSize(a, size)
#define LC_PushSizeNonZeroed(a, size) LC__PushSizeNonZeroed(a, size)

#define LC_PushArrayNonZeroed(a, T, c) (T *)LC__PushSizeNonZeroed(a, sizeof(T) * (c))
#define LC_PushStructNonZeroed(a, T) (T *)LC__PushSizeNonZeroed(a, sizeof(T))
#define LC_PushStruct(a, T) (T *)LC__PushSize(a, sizeof(T))
#define LC_PushArray(a, T, c) (T *)LC__PushSize(a, sizeof(T) * (c))

#define LC_Assertf(cond, ...) LC_ASSERT(NULL, cond)

#ifndef LC_USE_CUSTOM_ARENA
LC_FUNCTION void      LC_InitArenaEx(LC_Arena *a, size_t reserve);
LC_FUNCTION void      LC_InitArena(LC_Arena *a);
LC_FUNCTION void      LC_InitArenaFromBuffer(LC_Arena *arena, void *buffer, size_t size);
LC_FUNCTION LC_Arena *LC_BootstrapArena(void);
LC_FUNCTION LC_Arena  LC_PushArena(LC_Arena *arena, size_t size);
LC_FUNCTION LC_Arena *LC_PushArenaP(LC_Arena *arena, size_t size);

LC_FUNCTION void        *LC__PushSizeNonZeroed(LC_Arena *a, size_t size);
LC_FUNCTION void        *LC__PushSize(LC_Arena *arena, size_t size);
LC_FUNCTION LC_TempArena LC_BeginTemp(LC_Arena *arena);
LC_FUNCTION void         LC_EndTemp(LC_TempArena checkpoint);

LC_FUNCTION void LC_PopToPos(LC_Arena *arena, size_t pos);
LC_FUNCTION void LC_PopSize(LC_Arena *arena, size_t size);
LC_FUNCTION void LC_DeallocateArena(LC_Arena *arena);
LC_FUNCTION void LC_ResetArena(LC_Arena *arena);

LC_FUNCTION LC_VMemory LC_VReserve(size_t size);
LC_FUNCTION bool       LC_VCommit(LC_VMemory *m, size_t commit);
LC_FUNCTION void       LC_VDeallocate(LC_VMemory *m);
LC_FUNCTION bool       LC_VDecommitPos(LC_VMemory *m, size_t pos);
#endif // LC_USE_CUSTOM_ARENA

LC_FUNCTION size_t LC_GetAlignOffset(size_t size, size_t align);
LC_FUNCTION size_t LC_AlignUp(size_t size, size_t align);
LC_FUNCTION size_t LC_AlignDown(size_t size, size_t align);

#define LC_IS_POW2(x) (((x) & ((x)-1)) == 0)
#define LC_MIN(x, y) ((x) <= (y) ? (x) : (y))
#define LC_MAX(x, y) ((x) >= (y) ? (x) : (y))
#define LC_StrLenof(x) ((int64_t)((sizeof(x) / sizeof((x)[0]))))

#define LC_CLAMP_TOP(x, max) ((x) >= (max) ? (max) : (x))
#define LC_CLAMP_BOT(x, min) ((x) <= (min) ? (min) : (x))
#define LC_CLAMP(x, min, max) ((x) >= (max) ? (max) : (x) <= (min) ? (min) \
                                                                   : (x))
#define LC_KIB(x) ((x##ull) * 1024ull)
#define LC_MIB(x) (LC_KIB(x) * 1024ull)
#define LC_GIB(x) (LC_MIB(x) * 1024ull)
#define LC_TIB(x) (LC_GIB(x) * 1024ull)

//
// String API
//

typedef int LC_FindFlag;
enum {
    LC_FindFlag_None          = 0,
    LC_FindFlag_IgnoreCase    = 1,
    LC_FindFlag_MatchFindLast = 2,
};

typedef int LC_SplitFlag;
enum {
    LC_SplitFlag_None           = 0,
    LC_SplitFlag_IgnoreCase     = 1,
    LC_SplitFlag_SplitInclusive = 2,
};

static const bool LC_IgnoreCase = true;

#if defined(__has_attribute)
    #if __has_attribute(format)
        #define LC__PrintfFormat(fmt, va) __attribute__((format(printf, fmt, va)))
    #endif
#endif

#ifndef LC__PrintfFormat
    #define LC__PrintfFormat(fmt, va)
#endif

#define LC_Lit(string) LC_MakeString((char *)string, sizeof(string) - 1)
#define LC_Expand(string) (int)(string).len, (string).str

#define LC_FORMAT(allocator, str, result)                 \
    va_list args1;                                        \
    va_start(args1, str);                                 \
    LC_String result = LC_FormatV(allocator, str, args1); \
    va_end(args1)

#ifdef __cplusplus
    #define LC_IF_CPP(x) x
#else
    #define LC_IF_CPP(x)
#endif

LC_FUNCTION char LC_ToLowerCase(char a);
LC_FUNCTION char LC_ToUpperCase(char a);
LC_FUNCTION bool LC_IsWhitespace(char w);
LC_FUNCTION bool LC_IsAlphabetic(char a);
LC_FUNCTION bool LC_IsIdent(char a);
LC_FUNCTION bool LC_IsDigit(char a);
LC_FUNCTION bool LC_IsAlphanumeric(char a);

LC_FUNCTION int64_t       LC_StrLen(char *string);
LC_FUNCTION bool          LC_AreEqual(LC_String a, LC_String b, unsigned ignore_case LC_IF_CPP(= false));
LC_FUNCTION bool          LC_EndsWith(LC_String a, LC_String end, unsigned ignore_case LC_IF_CPP(= false));
LC_FUNCTION bool          LC_StartsWith(LC_String a, LC_String start, unsigned ignore_case LC_IF_CPP(= false));
LC_FUNCTION LC_String     LC_MakeString(char *str, int64_t len);
LC_FUNCTION LC_String     LC_CopyString(LC_Arena *allocator, LC_String string);
LC_FUNCTION LC_String     LC_CopyChar(LC_Arena *allocator, char *s);
LC_FUNCTION LC_String     LC_NormalizePath(LC_Arena *allocator, LC_String s);
LC_FUNCTION void          LC_NormalizePathUnsafe(LC_String s); // make sure there is no way string is const etc.
LC_FUNCTION LC_String     LC_Chop(LC_String string, int64_t len);
LC_FUNCTION LC_String     LC_Skip(LC_String string, int64_t len);
LC_FUNCTION LC_String     LC_GetPostfix(LC_String string, int64_t len);
LC_FUNCTION LC_String     LC_GetPrefix(LC_String string, int64_t len);
LC_FUNCTION bool          LC_Seek(LC_String string, LC_String find, LC_FindFlag flags LC_IF_CPP(= LC_FindFlag_None), int64_t *index_out LC_IF_CPP(= 0));
LC_FUNCTION int64_t       LC_Find(LC_String string, LC_String find, LC_FindFlag flags LC_IF_CPP(= LC_FindFlag_None));
LC_FUNCTION LC_String     LC_ChopLastSlash(LC_String s);
LC_FUNCTION LC_String     LC_ChopLastPeriod(LC_String s);
LC_FUNCTION LC_String     LC_SkipToLastSlash(LC_String s);
LC_FUNCTION LC_String     LC_SkipToLastPeriod(LC_String s);
LC_FUNCTION LC_String     LC_GetNameNoExt(LC_String s);
LC_FUNCTION LC_String     LC_MakeFromChar(char *string);
LC_FUNCTION LC_String     LC_MakeEmptyString(void);
LC_FUNCTION LC_StringList LC_MakeEmptyList(void);
LC_FUNCTION LC_String     LC_FormatV(LC_Arena *allocator, const char *str, va_list args1);
LC_FUNCTION LC_String     LC_Format(LC_Arena *allocator, const char *str, ...) LC__PrintfFormat(2, 3);

LC_FUNCTION LC_StringList LC_Split(LC_Arena *allocator, LC_String string, LC_String find, LC_SplitFlag flags LC_IF_CPP(= LC_SplitFlag_None));
LC_FUNCTION LC_String     LC_MergeWithSeparator(LC_Arena *allocator, LC_StringList list, LC_String separator LC_IF_CPP(= LC_Lit(" ")));
LC_FUNCTION LC_String     LC_MergeString(LC_Arena *allocator, LC_StringList list);

LC_FUNCTION LC_StringNode *LC_CreateNode(LC_Arena *allocator, LC_String string);
LC_FUNCTION void           LC_ReplaceNodeString(LC_StringList *list, LC_StringNode *node, LC_String new_string);
LC_FUNCTION void           LC_AddExistingNode(LC_StringList *list, LC_StringNode *node);
LC_FUNCTION void           LC_AddArray(LC_Arena *allocator, LC_StringList *list, char **array, int count);
LC_FUNCTION void           LC_AddArrayWithPrefix(LC_Arena *allocator, LC_StringList *list, char *prefix, char **array, int count);
LC_FUNCTION LC_StringList  LC_MakeList(LC_Arena *allocator, LC_String a);
LC_FUNCTION LC_StringList  LC_CopyList(LC_Arena *allocator, LC_StringList a);
LC_FUNCTION LC_StringList  LC_ConcatLists(LC_Arena *allocator, LC_StringList a, LC_StringList b);
LC_FUNCTION LC_StringNode *LC_AddNode(LC_Arena *allocator, LC_StringList *list, LC_String string);
LC_FUNCTION LC_StringNode *LC_Add(LC_Arena *allocator, LC_StringList *list, LC_String string);
LC_FUNCTION LC_String      LC_Addf(LC_Arena *allocator, LC_StringList *list, const char *str, ...) LC__PrintfFormat(3, 4);

LC_FUNCTION wchar_t *LC_ToWidechar(LC_Arena *allocator, LC_String string);

//
// Linked list API
//
#define LC_SLLAddMod(f, l, n, next) \
    do {                            \
        (n)->next = 0;              \
        if ((f) == 0) {             \
            (f) = (l) = (n);        \
        } else {                    \
            (l) = (l)->next = (n);  \
        }                           \
    } while (0)
#define LC_AddSLL(f, l, n) LC_SLLAddMod(f, l, n, next)

#define LC_SLLPopFirstMod(f, l, next) \
    do {                              \
        if ((f) == (l)) {             \
            (f) = (l) = 0;            \
        } else {                      \
            (f) = (f)->next;          \
        }                             \
    } while (0)
#define LC_SLLPopFirst(f, l) LC_SLLPopFirstMod(f, l, next)

#define LC_SLLStackAddMod(stack_base, new_stack_base, next) \
    do {                                                    \
        (new_stack_base)->next = (stack_base);              \
        (stack_base)           = (new_stack_base);          \
    } while (0)

#define LC_DLLAddMod(f, l, node, next, prev) \
    do {                                     \
        if ((f) == 0) {                      \
            (f) = (l)    = (node);           \
            (node)->prev = 0;                \
            (node)->next = 0;                \
        } else {                             \
            (l)->next    = (node);           \
            (node)->prev = (l);              \
            (node)->next = 0;                \
            (l)          = (node);           \
        }                                    \
    } while (0)
#define LC_DLLAdd(f, l, node) LC_DLLAddMod(f, l, node, next, prev)
#define LC_DLLAddFront(f, l, node) LC_DLLAddMod(l, f, node, prev, next)
#define LC_DLLRemoveMod(first, last, node, next, prev) \
    do {                                               \
        if ((first) == (last)) {                       \
            (first) = (last) = 0;                      \
        } else if ((last) == (node)) {                 \
            (last)       = (last)->prev;               \
            (last)->next = 0;                          \
        } else if ((first) == (node)) {                \
            (first)       = (first)->next;             \
            (first)->prev = 0;                         \
        } else {                                       \
            (node)->prev->next = (node)->next;         \
            (node)->next->prev = (node)->prev;         \
        }                                              \
    } while (0)
#define LC_DLLRemove(first, last, node) LC_DLLRemoveMod(first, last, node, next, prev)

#define LC_ASTFor(it, x) for (LC_AST *it = x; it; it = it->next)
#define LC_DeclFor(it, x) for (LC_Decl *it = x; it; it = it->next)
#define LC_TypeFor(it, x) for (LC_TypeMember *it = x; it; it = it->next)

#if defined(__APPLE__) && defined(__MACH__)
    #define LC_OPERATING_SYSTEM_MAC 1
    #define LC_OPERATING_SYSTEM_UNIX 1
#elif defined(_WIN32)
    #define LC_OPERATING_SYSTEM_WINDOWS 1
#elif defined(__linux__)
    #define LC_OPERATING_SYSTEM_UNIX 1
    #define LC_OPERATING_SYSTEM_LINUX 1
#endif

#ifndef LC_OPERATING_SYSTEM_MAC
    #define LC_OPERATING_SYSTEM_MAC 0
#endif

#ifndef LC_OPERATING_SYSTEM_UNIX
    #define LC_OPERATING_SYSTEM_UNIX 0
#endif

#ifndef LC_OPERATING_SYSTEM_WINDOWS
    #define LC_OPERATING_SYSTEM_WINDOWS 0
#endif

#ifndef LC_OPERATING_SYSTEM_LINUX
    #define LC_OPERATING_SYSTEM_LINUX 0
#endif

#endif
#ifdef LIB_COMPILER_IMPLEMENTATION


#if __clang__
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wswitch"
    #pragma clang diagnostic ignored "-Wwritable-strings"
#endif

#ifndef LC_ParseFloat // @override
    #include <stdlib.h>
    #define LC_ParseFloat(str, len) strtod(str, NULL)
#endif

#ifndef LC_Print // @override
    #include <stdio.h>
    #define LC_Print(str, len) printf("%.*s", (int)len, str)
#endif

#ifndef LC_Exit // @override
    #include <stdio.h>
    #define LC_Exit(x) exit(x)
#endif

#ifndef LC_MemoryZero // @override
    #include <string.h>
    #define LC_MemoryZero(p, size) memset(p, 0, size)
#endif

#ifndef LC_MemoryCopy // @override
    #include <string.h>
    #define LC_MemoryCopy(dst, src, size) memcpy(dst, src, size);
#endif

#ifndef LC_vsnprintf // @override
    #include <stdio.h>
    #define LC_vsnprintf vsnprintf
#endif

#ifdef _WIN32
    #ifndef NOMINMAX
        #define NOMINMAX
    #endif
    #ifndef WIN32_LEAN_AND_MEAN
        #define WIN32_LEAN_AND_MEAN
    #endif
    #include <windows.h>
#endif

LC_THREAD_LOCAL LC_Lang *L;

LC_FUNCTION LC_UTF32Result LC_ConvertUTF16ToUTF32(uint16_t *c, int max_advance) {
    LC_UTF32Result result;
    LC_MemoryZero(&result, sizeof(result));
    if (max_advance >= 1) {
        result.advance = 1;
        result.out_str = c[0];
        if (c[0] >= 0xD800 && c[0] <= 0xDBFF && c[1] >= 0xDC00 && c[1] <= 0xDFFF) {
            if (max_advance >= 2) {
                result.out_str = 0x10000;
                result.out_str += (uint32_t)(c[0] & 0x03FF) << 10u | (c[1] & 0x03FF);
                result.advance = 2;
            } else
                result.error = 2;
        }
    } else {
        result.error = 1;
    }
    return result;
}

LC_FUNCTION LC_UTF8Result LC_ConvertUTF32ToUTF8(uint32_t codepoint) {
    LC_UTF8Result result;
    LC_MemoryZero(&result, sizeof(result));

    if (codepoint <= 0x7F) {
        result.len        = 1;
        result.out_str[0] = (char)codepoint;
    } else if (codepoint <= 0x7FF) {
        result.len        = 2;
        result.out_str[0] = 0xc0 | (0x1f & (codepoint >> 6));
        result.out_str[1] = 0x80 | (0x3f & codepoint);
    } else if (codepoint <= 0xFFFF) { // 16 bit word
        result.len        = 3;
        result.out_str[0] = 0xe0 | (0xf & (codepoint >> 12)); // 4 bits
        result.out_str[1] = 0x80 | (0x3f & (codepoint >> 6)); // 6 bits
        result.out_str[2] = 0x80 | (0x3f & codepoint);        // 6 bits
    } else if (codepoint <= 0x10FFFF) {                       // 21 bit word
        result.len        = 4;
        result.out_str[0] = 0xf0 | (0x7 & (codepoint >> 18));  // 3 bits
        result.out_str[1] = 0x80 | (0x3f & (codepoint >> 12)); // 6 bits
        result.out_str[2] = 0x80 | (0x3f & (codepoint >> 6));  // 6 bits
        result.out_str[3] = 0x80 | (0x3f & codepoint);         // 6 bits
    } else {
        result.error = 1;
    }

    return result;
}

LC_FUNCTION LC_UTF32Result LC_ConvertUTF8ToUTF32(char *c, int max_advance) {
    LC_UTF32Result result;
    LC_MemoryZero(&result, sizeof(result));

    if ((c[0] & 0x80) == 0) { // Check if leftmost zero of first byte is unset
        if (max_advance >= 1) {
            result.out_str = c[0];
            result.advance = 1;
        } else result.error = 1;
    }

    else if ((c[0] & 0xe0) == 0xc0) {
        if ((c[1] & 0xc0) == 0x80) { // Continuation byte required
            if (max_advance >= 2) {
                result.out_str = (uint32_t)(c[0] & 0x1f) << 6u | (c[1] & 0x3f);
                result.advance = 2;
            } else result.error = 2;
        } else result.error = 2;
    }

    else if ((c[0] & 0xf0) == 0xe0) {
        if ((c[1] & 0xc0) == 0x80 && (c[2] & 0xc0) == 0x80) { // Two continuation bytes required
            if (max_advance >= 3) {
                result.out_str = (uint32_t)(c[0] & 0xf) << 12u | (uint32_t)(c[1] & 0x3f) << 6u | (c[2] & 0x3f);
                result.advance = 3;
            } else result.error = 3;
        } else result.error = 3;
    }

    else if ((c[0] & 0xf8) == 0xf0) {
        if ((c[1] & 0xc0) == 0x80 && (c[2] & 0xc0) == 0x80 && (c[3] & 0xc0) == 0x80) { // Three continuation bytes required
            if (max_advance >= 4) {
                result.out_str = (uint32_t)(c[0] & 0xf) << 18u | (uint32_t)(c[1] & 0x3f) << 12u | (uint32_t)(c[2] & 0x3f) << 6u | (uint32_t)(c[3] & 0x3f);
                result.advance = 4;
            } else result.error = 4;
        } else result.error = 4;
    } else result.error = 4;

    return result;
}

LC_FUNCTION LC_UTF16Result LC_ConvertUTF32ToUTF16(uint32_t codepoint) {
    LC_UTF16Result result;
    LC_MemoryZero(&result, sizeof(result));
    if (codepoint < 0x10000) {
        result.out_str[0] = (uint16_t)codepoint;
        result.out_str[1] = 0;
        result.len        = 1;
    } else if (codepoint <= 0x10FFFF) {
        uint32_t code     = (codepoint - 0x10000);
        result.out_str[0] = (uint16_t)(0xD800 | (code >> 10));
        result.out_str[1] = (uint16_t)(0xDC00 | (code & 0x3FF));
        result.len        = 2;
    } else {
        result.error = 1;
    }

    return result;
}

#define LC__HANDLE_DECODE_ERROR(question_mark)                            \
    {                                                                     \
        if (outlen < buffer_size - 1) buffer[outlen++] = (question_mark); \
        break;                                                            \
    }

LC_FUNCTION int64_t LC_CreateCharFromWidechar(char *buffer, int64_t buffer_size, wchar_t *in, int64_t inlen) {
    int64_t outlen = 0;
    for (int64_t i = 0; i < inlen && in[i];) {
        LC_UTF32Result decode = LC_ConvertUTF16ToUTF32((uint16_t *)(in + i), (int)(inlen - i));
        if (!decode.error) {
            i += decode.advance;
            LC_UTF8Result encode = LC_ConvertUTF32ToUTF8(decode.out_str);
            if (!encode.error) {
                for (int64_t j = 0; j < encode.len; j++) {
                    if (outlen < buffer_size - 1) {
                        buffer[outlen++] = encode.out_str[j];
                    }
                }
            } else LC__HANDLE_DECODE_ERROR('?');
        } else LC__HANDLE_DECODE_ERROR('?');
    }

    buffer[outlen] = 0;
    return outlen;
}

LC_FUNCTION int64_t LC_CreateWidecharFromChar(wchar_t *buffer, int64_t buffer_size, char *in, int64_t inlen) {
    int64_t outlen = 0;
    for (int64_t i = 0; i < inlen;) {
        LC_UTF32Result decode = LC_ConvertUTF8ToUTF32(in + i, (int)(inlen - i));
        if (!decode.error) {
            i += decode.advance;
            LC_UTF16Result encode = LC_ConvertUTF32ToUTF16(decode.out_str);
            if (!encode.error) {
                for (int64_t j = 0; j < encode.len; j++) {
                    if (outlen < buffer_size - 1) {
                        buffer[outlen++] = encode.out_str[j];
                    }
                }
            } else LC__HANDLE_DECODE_ERROR(0x003f);
        } else LC__HANDLE_DECODE_ERROR(0x003f);
    }

    buffer[outlen] = 0;
    return outlen;
}

#undef LC__HANDLE_DECODE_ERROR
LC_FUNCTION int64_t LC__ClampTop(int64_t val, int64_t max) {
    if (val > max) val = max;
    return val;
}

LC_FUNCTION char LC_ToLowerCase(char a) {
    if (a >= 'A' && a <= 'Z') a += 32;
    return a;
}

LC_FUNCTION char LC_ToUpperCase(char a) {
    if (a >= 'a' && a <= 'z') a -= 32;
    return a;
}

LC_FUNCTION bool LC_IsWhitespace(char w) {
    bool result = w == '\n' || w == ' ' || w == '\t' || w == '\v' || w == '\r';
    return result;
}

LC_FUNCTION bool LC_IsAlphabetic(char a) {
    bool result = (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z');
    return result;
}

LC_FUNCTION bool LC_IsIdent(char a) {
    bool result = (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z') || a == '_';
    return result;
}

LC_FUNCTION bool LC_IsDigit(char a) {
    bool result = a >= '0' && a <= '9';
    return result;
}

LC_FUNCTION bool LC_IsAlphanumeric(char a) {
    bool result = LC_IsDigit(a) || LC_IsAlphabetic(a);
    return result;
}

LC_FUNCTION bool LC_AreEqual(LC_String a, LC_String b, unsigned ignore_case) {
    if (a.len != b.len) return false;
    for (int64_t i = 0; i < a.len; i++) {
        char A = a.str[i];
        char B = b.str[i];
        if (ignore_case) {
            A = LC_ToLowerCase(A);
            B = LC_ToLowerCase(B);
        }
        if (A != B)
            return false;
    }
    return true;
}

LC_FUNCTION bool LC_EndsWith(LC_String a, LC_String end, unsigned ignore_case) {
    LC_String a_end  = LC_GetPostfix(a, end.len);
    bool      result = LC_AreEqual(end, a_end, ignore_case);
    return result;
}

LC_FUNCTION bool LC_StartsWith(LC_String a, LC_String start, unsigned ignore_case) {
    LC_String a_start = LC_GetPrefix(a, start.len);
    bool      result  = LC_AreEqual(start, a_start, ignore_case);
    return result;
}

LC_FUNCTION LC_String LC_MakeString(char *str, int64_t len) {
    LC_String result;
    result.str = (char *)str;
    result.len = len;
    return result;
}

LC_FUNCTION LC_String LC_CopyString(LC_Arena *allocator, LC_String string) {
    char *copy = (char *)LC_PushSize(allocator, sizeof(char) * (string.len + 1));
    LC_MemoryCopy(copy, string.str, string.len);
    copy[string.len] = 0;
    LC_String result = LC_MakeString(copy, string.len);
    return result;
}

LC_FUNCTION LC_String LC_CopyChar(LC_Arena *allocator, char *s) {
    int64_t len  = LC_StrLen(s);
    char   *copy = (char *)LC_PushSize(allocator, sizeof(char) * (len + 1));
    LC_MemoryCopy(copy, s, len);
    copy[len]        = 0;
    LC_String result = LC_MakeString(copy, len);
    return result;
}

LC_FUNCTION LC_String LC_NormalizePath(LC_Arena *allocator, LC_String s) {
    LC_String copy = LC_CopyString(allocator, s);
    for (int64_t i = 0; i < copy.len; i++) {
        if (copy.str[i] == '\\')
            copy.str[i] = '/';
    }
    return copy;
}

LC_FUNCTION void LC_NormalizePathUnsafe(LC_String s) {
    for (int64_t i = 0; i < s.len; i++) {
        if (s.str[i] == '\\')
            s.str[i] = '/';
    }
}

LC_FUNCTION LC_String LC_Chop(LC_String string, int64_t len) {
    len              = LC__ClampTop(len, string.len);
    LC_String result = LC_MakeString(string.str, string.len - len);
    return result;
}

LC_FUNCTION LC_String LC_Skip(LC_String string, int64_t len) {
    len              = LC__ClampTop(len, string.len);
    int64_t   remain = string.len - len;
    LC_String result = LC_MakeString(string.str + len, remain);
    return result;
}

LC_FUNCTION LC_String LC_GetPostfix(LC_String string, int64_t len) {
    len                  = LC__ClampTop(len, string.len);
    int64_t   remain_len = string.len - len;
    LC_String result     = LC_MakeString(string.str + remain_len, len);
    return result;
}

LC_FUNCTION LC_String LC_GetPrefix(LC_String string, int64_t len) {
    len              = LC__ClampTop(len, string.len);
    LC_String result = LC_MakeString(string.str, len);
    return result;
}

LC_FUNCTION LC_String LC_GetNameNoExt(LC_String s) {
    return LC_SkipToLastSlash(LC_ChopLastPeriod(s));
}

LC_FUNCTION LC_String LC_Slice(LC_String string, int64_t first_index, int64_t one_past_last_index) {
    if (one_past_last_index < 0) one_past_last_index = string.len + one_past_last_index + 1;
    if (first_index < 0) first_index = string.len + first_index;
    LC_ASSERT(NULL, first_index < one_past_last_index && "LC_Slice, first_index is bigger then one_past_last_index");
    LC_ASSERT(NULL, string.len > 0 && "Slicing string of length 0! Might be an error!");
    LC_String result = string;
    if (string.len > 0) {
        if (one_past_last_index > first_index) {
            first_index         = LC__ClampTop(first_index, string.len - 1);
            one_past_last_index = LC__ClampTop(one_past_last_index, string.len);
            result.str += first_index;
            result.len = one_past_last_index - first_index;
        } else {
            result.len = 0;
        }
    }
    return result;
}

LC_FUNCTION bool LC_Seek(LC_String string, LC_String find, LC_FindFlag flags, int64_t *index_out) {
    bool ignore_case = flags & LC_FindFlag_IgnoreCase ? true : false;
    bool result      = false;
    if (flags & LC_FindFlag_MatchFindLast) {
        for (int64_t i = string.len; i != 0; i--) {
            int64_t   index     = i - 1;
            LC_String substring = LC_Slice(string, index, index + find.len);
            if (LC_AreEqual(substring, find, ignore_case)) {
                if (index_out)
                    *index_out = index;
                result = true;
                break;
            }
        }
    } else {
        for (int64_t i = 0; i < string.len; i++) {
            LC_String substring = LC_Slice(string, i, i + find.len);
            if (LC_AreEqual(substring, find, ignore_case)) {
                if (index_out)
                    *index_out = i;
                result = true;
                break;
            }
        }
    }

    return result;
}

LC_FUNCTION int64_t LC_Find(LC_String string, LC_String find, LC_FindFlag flag) {
    int64_t result = -1;
    LC_Seek(string, find, flag, &result);
    return result;
}

LC_FUNCTION LC_StringList LC_Split(LC_Arena *allocator, LC_String string, LC_String find, LC_SplitFlag flags) {
    LC_StringList result = LC_MakeEmptyList();
    int64_t       index  = 0;

    LC_FindFlag find_flag = flags & LC_SplitFlag_IgnoreCase ? LC_FindFlag_IgnoreCase : LC_FindFlag_None;
    while (LC_Seek(string, find, find_flag, &index)) {
        LC_String before_match = LC_MakeString(string.str, index);
        LC_AddNode(allocator, &result, before_match);
        if (flags & LC_SplitFlag_SplitInclusive) {
            LC_String match = LC_MakeString(string.str + index, find.len);
            LC_AddNode(allocator, &result, match);
        }
        string = LC_Skip(string, index + find.len);
    }
    if (string.len) LC_AddNode(allocator, &result, string);
    return result;
}

LC_FUNCTION LC_String LC_MergeWithSeparator(LC_Arena *allocator, LC_StringList list, LC_String separator) {
    if (list.node_count == 0) return LC_MakeEmptyString();
    if (list.char_count == 0) return LC_MakeEmptyString();

    int64_t   base_size = (list.char_count + 1);
    int64_t   sep_size  = (list.node_count - 1) * separator.len;
    int64_t   size      = base_size + sep_size;
    char     *buff      = (char *)LC_PushSize(allocator, sizeof(char) * (size + 1));
    LC_String string    = LC_MakeString(buff, 0);
    for (LC_StringNode *it = list.first; it; it = it->next) {
        LC_ASSERT(NULL, string.len + it->string.len <= size);
        LC_MemoryCopy(string.str + string.len, it->string.str, it->string.len);
        string.len += it->string.len;
        if (it != list.last) {
            LC_MemoryCopy(string.str + string.len, separator.str, separator.len);
            string.len += separator.len;
        }
    }
    LC_ASSERT(NULL, string.len == size - 1);
    string.str[size] = 0;
    return string;
}

LC_FUNCTION LC_String LC_MergeString(LC_Arena *allocator, LC_StringList list) {
    return LC_MergeWithSeparator(allocator, list, LC_Lit(""));
}

LC_FUNCTION LC_String LC_ChopLastSlash(LC_String s) {
    LC_String result = s;
    LC_Seek(s, LC_Lit("/"), LC_FindFlag_MatchFindLast, &result.len);
    return result;
}

LC_FUNCTION LC_String LC_ChopLastPeriod(LC_String s) {
    LC_String result = s;
    LC_Seek(s, LC_Lit("."), LC_FindFlag_MatchFindLast, &result.len);
    return result;
}

LC_FUNCTION LC_String LC_SkipToLastSlash(LC_String s) {
    int64_t   pos;
    LC_String result = s;
    if (LC_Seek(s, LC_Lit("/"), LC_FindFlag_MatchFindLast, &pos)) {
        result = LC_Skip(result, pos + 1);
    }
    return result;
}

LC_FUNCTION LC_String LC_SkipToLastPeriod(LC_String s) {
    int64_t   pos;
    LC_String result = s;
    if (LC_Seek(s, LC_Lit("."), LC_FindFlag_MatchFindLast, &pos)) {
        result = LC_Skip(result, pos + 1);
    }
    return result;
}

LC_FUNCTION int64_t LC_StrLen(char *string) {
    int64_t len = 0;
    while (*string++ != 0)
        len++;
    return len;
}

LC_FUNCTION LC_String LC_MakeFromChar(char *string) {
    LC_String result;
    result.str = (char *)string;
    result.len = LC_StrLen(string);
    return result;
}

LC_FUNCTION LC_String LC_MakeEmptyString(void) {
    return LC_MakeString(0, 0);
}

LC_FUNCTION LC_StringList LC_MakeEmptyList(void) {
    LC_StringList result;
    result.first      = 0;
    result.last       = 0;
    result.char_count = 0;
    result.node_count = 0;
    return result;
}

LC_FUNCTION LC_String LC_FormatV(LC_Arena *allocator, const char *str, va_list args1) {
    va_list args2;
    va_copy(args2, args1);
    int64_t len = LC_vsnprintf(0, 0, str, args2);
    va_end(args2);

    char *result = (char *)LC_PushSize(allocator, sizeof(char) * (len + 1));
    LC_vsnprintf(result, (int)(len + 1), str, args1);
    LC_String res = LC_MakeString(result, len);
    return res;
}

LC_FUNCTION LC_String LC_Format(LC_Arena *allocator, const char *str, ...) {
    LC_FORMAT(allocator, str, result);
    return result;
}

LC_FUNCTION LC_StringNode *LC_CreateNode(LC_Arena *allocator, LC_String string) {
    LC_StringNode *result = (LC_StringNode *)LC_PushSize(allocator, sizeof(LC_StringNode));
    result->string        = string;
    result->next          = 0;
    return result;
}

LC_FUNCTION void LC_ReplaceNodeString(LC_StringList *list, LC_StringNode *node, LC_String new_string) {
    list->char_count -= node->string.len;
    list->char_count += new_string.len;
    node->string = new_string;
}

LC_FUNCTION void LC_AddExistingNode(LC_StringList *list, LC_StringNode *node) {
    if (list->first) {
        list->last->next = node;
        list->last       = list->last->next;
    } else {
        list->first = list->last = node;
    }
    list->node_count += 1;
    list->char_count += node->string.len;
}

LC_FUNCTION void LC_AddArray(LC_Arena *allocator, LC_StringList *list, char **array, int count) {
    for (int i = 0; i < count; i += 1) {
        LC_String s = LC_MakeFromChar(array[i]);
        LC_AddNode(allocator, list, s);
    }
}

LC_FUNCTION void LC_AddArrayWithPrefix(LC_Arena *allocator, LC_StringList *list, char *prefix, char **array, int count) {
    for (int i = 0; i < count; i += 1) {
        LC_Addf(allocator, list, "%s%s", prefix, array[i]);
    }
}

LC_FUNCTION LC_StringList LC_MakeList(LC_Arena *allocator, LC_String a) {
    LC_StringList result = LC_MakeEmptyList();
    LC_AddNode(allocator, &result, a);
    return result;
}

LC_FUNCTION LC_StringList LC_CopyList(LC_Arena *allocator, LC_StringList a) {
    LC_StringList result = LC_MakeEmptyList();
    for (LC_StringNode *it = a.first; it; it = it->next) LC_AddNode(allocator, &result, it->string);
    return result;
}

LC_FUNCTION LC_StringList LC_ConcatLists(LC_Arena *allocator, LC_StringList a, LC_StringList b) {
    LC_StringList result = LC_MakeEmptyList();
    for (LC_StringNode *it = a.first; it; it = it->next) LC_AddNode(allocator, &result, it->string);
    for (LC_StringNode *it = b.first; it; it = it->next) LC_AddNode(allocator, &result, it->string);
    return result;
}

LC_FUNCTION LC_StringNode *LC_AddNode(LC_Arena *allocator, LC_StringList *list, LC_String string) {
    LC_StringNode *node = LC_CreateNode(allocator, string);
    LC_AddExistingNode(list, node);
    return node;
}

LC_FUNCTION LC_StringNode *LC_Add(LC_Arena *allocator, LC_StringList *list, LC_String string) {
    LC_String      copy = LC_CopyString(allocator, string);
    LC_StringNode *node = LC_CreateNode(allocator, copy);
    LC_AddExistingNode(list, node);
    return node;
}

LC_FUNCTION LC_String LC_Addf(LC_Arena *allocator, LC_StringList *list, const char *str, ...) {
    LC_FORMAT(allocator, str, result);
    LC_AddNode(allocator, list, result);
    return result;
}

LC_FUNCTION LC_String16 LC_ToWidecharEx(LC_Arena *allocator, LC_String string) {
    LC_ASSERT(NULL, sizeof(wchar_t) == 2);
    wchar_t    *buffer = (wchar_t *)LC_PushSize(allocator, sizeof(wchar_t) * (string.len + 1));
    int64_t     size   = LC_CreateWidecharFromChar(buffer, string.len + 1, string.str, string.len);
    LC_String16 result = {buffer, size};
    return result;
}

LC_FUNCTION wchar_t *LC_ToWidechar(LC_Arena *allocator, LC_String string) {
    LC_String16 result = LC_ToWidecharEx(allocator, string);
    return result.str;
}

LC_FUNCTION LC_String LC_FromWidecharEx(LC_Arena *allocator, wchar_t *wstring, int64_t wsize) {
    LC_ASSERT(NULL, sizeof(wchar_t) == 2);

    int64_t   buffer_size = (wsize + 1) * 2;
    char     *buffer      = (char *)LC_PushSize(allocator, buffer_size);
    int64_t   size        = LC_CreateCharFromWidechar(buffer, buffer_size, wstring, wsize);
    LC_String result      = LC_MakeString(buffer, size);

    LC_ASSERT(NULL, size < buffer_size);
    return result;
}

LC_FUNCTION int64_t LC_WideLength(wchar_t *string) {
    int64_t len = 0;
    while (*string++ != 0)
        len++;
    return len;
}

LC_FUNCTION LC_String LC_FromWidechar(LC_Arena *allocator, wchar_t *wstring) {
    int64_t   size   = LC_WideLength(wstring);
    LC_String result = LC_FromWidecharEx(allocator, wstring, size);
    return result;
}

LC_FUNCTION const char *LC_OSToString(LC_OS os) {
    switch (os) {
    case LC_OS_WINDOWS: return "OS_WINDOWS";
    case LC_OS_LINUX: return "OS_LINUX";
    case LC_OS_MAC: return "OS_MAC";
    default: return "UNKNOWN_OPERATING_SYSTEM";
    }
}

LC_FUNCTION const char *LC_GENToString(LC_GEN os) {
    switch (os) {
    case LC_GEN_C: return "GEN_C";
    default: return "UNKNOWN_GENERATOR";
    }
}

LC_FUNCTION const char *LC_ARCHToString(LC_ARCH arch) {
    switch (arch) {
    case LC_ARCH_X86: return "ARCH_X86";
    case LC_ARCH_X64: return "ARCH_X64";
    default: return "UNKNOWN_ARCHITECTURE";
    }
}

LC_FUNCTION const char *LC_ASTKindToString(LC_ASTKind kind) {
    static const char *strs[] = {
        "ast null",
        "ast error",
        "ast note",
        "ast note list",
        "ast file",
        "ast package",
        "ast ignore",
        "typespec procdure argument",
        "typespec aggregate member",
        "expr call item",
        "expr compound item",
        "expr note",
        "stmt switch case",
        "stmt switch default",
        "stmt else if",
        "stmt else",
        "import",
        "global note",
        "decl proc",
        "decl struct",
        "decl union",
        "decl var",
        "decl const",
        "decl typedef",
        "typespec ident",
        "typespec field",
        "typespec pointer",
        "typespec array",
        "typespec proc",
        "stmt block",
        "stmt note",
        "stmt return",
        "stmt break",
        "stmt continue",
        "stmt defer",
        "stmt for",
        "stmt if",
        "stmt switch",
        "stmt assign",
        "stmt expr",
        "stmt var",
        "stmt const",
        "expr ident",
        "expr string",
        "expr int",
        "expr float",
        "expr bool",
        "expr type",
        "expr binary",
        "expr unary",
        "expr builtin",
        "expr call",
        "expr compound",
        "expr cast",
        "expr field",
        "expr index",
        "expr pointerindex",
        "expr getvalueofpointer",
        "expr getpointerofvalue",
    };
    if (kind < 0 || kind >= LC_ASTKind_Count) {
        return "<invalid_ast_kind>";
    }
    return strs[kind];
}

LC_FUNCTION const char *LC_TypeKindToString(LC_TypeKind kind) {
    static const char *strs[] = {
        "LC_TypeKind_char",
        "LC_TypeKind_uchar",
        "LC_TypeKind_short",
        "LC_TypeKind_ushort",
        "LC_TypeKind_bool",
        "LC_TypeKind_int",
        "LC_TypeKind_uint",
        "LC_TypeKind_long",
        "LC_TypeKind_ulong",
        "LC_TypeKind_llong",
        "LC_TypeKind_ullong",
        "LC_TypeKind_float",
        "LC_TypeKind_double",
        "LC_TypeKind_void",
        "LC_TypeKind_Struct",
        "LC_TypeKind_Union",
        "LC_TypeKind_Pointer",
        "LC_TypeKind_Array",
        "LC_TypeKind_Proc",
        "LC_TypeKind_UntypedInt",
        "LC_TypeKind_UntypedFloat",
        "LC_TypeKind_UntypedString",
        "LC_TypeKind_Incomplete",
        "LC_TypeKind_Completing",
        "LC_TypeKind_Error",
    };
    if (kind < 0 || kind >= T_TotalCount) {
        return "<invalid_type_kind>";
    }
    return strs[kind];
}

LC_FUNCTION const char *LC_DeclKindToString(LC_DeclKind decl_kind) {
    static const char *strs[] = {
        "declaration of error kind",
        "type declaration",
        "const declaration",
        "variable declaration",
        "procedure declaration",
        "import declaration",
    };
    if (decl_kind < 0 || decl_kind >= LC_DeclKind_Count) {
        return "<invalid_decl_kind>";
    }
    return strs[decl_kind];
}

LC_FUNCTION const char *LC_TokenKindToString(LC_TokenKind token_kind) {
    static const char *strs[] = {
        "end of file",
        "token error",
        "comment",
        "doc comment",
        "file doc comment",
        "package doc comment",
        "note '@'",
        "hash '#'",
        "identifier",
        "keyword",
        "string literal",
        "raw string literal",
        "integer literal",
        "float literal",
        "unicode literal",
        "open paren '('",
        "close paren ')'",
        "open brace '{'",
        "close brace '}'",
        "open bracket '['",
        "close bracket ']'",
        "comma ','",
        "question mark '?'",
        "semicolon ';'",
        "period '.'",
        "three dots '...'",
        "colon ':'",
        "multiply '*'",
        "divide '/'",
        "modulo '%'",
        "left shift '<<'",
        "right shift '>>'",
        "add '+'",
        "subtract '-'",
        "equals '=='",
        "lesser then '<'",
        "greater then '>'",
        "lesser then or equal '<='",
        "greater then or equal '>='",
        "not equal '!='",
        "bit and '&'",
        "bit or '|'",
        "bit xor '^'",
        "and '&&'",
        "or '||'",
        "addptr keyword",
        "negation '~'",
        "exclamation '!'",
        "assignment '='",
        "assignment '/='",
        "assignment '*='",
        "assignment '%='",
        "assignment '-='",
        "assignment '+='",
        "assignment '&='",
        "assignment '|='",
        "assignment '^='",
        "assignment '<<='",
        "assignment '>>='",
    };

    if (token_kind < 0 || token_kind >= LC_TokenKind_Count) {
        return "<invalid_token_kind>";
    }
    return strs[token_kind];
}

LC_FUNCTION const char *LC_TokenKindToOperator(LC_TokenKind token_kind) {
    static const char *strs[] = {
        "end of file",
        "token error",
        "comment",
        "doc comment",
        "file doc comment",
        "package doc comment",
        "@",
        "#",
        "identifier",
        "keyword",
        "string literal",
        "raw string literal",
        "integer literal",
        "float literal",
        "unicode literal",
        "(",
        ")",
        "{",
        "}",
        "[",
        "]",
        ",",
        "?",
        ";",
        ".",
        "...",
        ":",
        "*",
        "/",
        "%",
        "<<",
        ">>",
        "+",
        "-",
        "==",
        "<",
        ">",
        "<=",
        ">=",
        "!=",
        "&",
        "|",
        "^",
        "&&",
        "||",
        "+",
        "~",
        "!",
        "=",
        "/=",
        "*=",
        "%=",
        "-=",
        "+=",
        "&=",
        "|=",
        "^=",
        "<<=",
        ">>=",
    };
    if (token_kind < 0 || token_kind >= LC_TokenKind_Count) {
        return "<invalid_token_operator>";
    }
    return strs[token_kind];
}

#if __cplusplus
    #define LC_Alignof(...) alignof(__VA_ARGS__)
#else
    #define LC_Alignof(...) _Alignof(__VA_ARGS__)
#endif

#define LC_WRAP_AROUND_POWER_OF_2(x, pow2) (((x) & ((pow2)-1llu)))

#if defined(_MSC_VER)
    #define LC_DebugBreak() (L->breakpoint_on_error && IsDebuggerPresent() && (__debugbreak(), 0))
#else
    #define LC_DebugBreak() (L->breakpoint_on_error && (__builtin_trap(), 0))
#endif
#define LC_FatalError() (L->breakpoint_on_error ? LC_DebugBreak() : (LC_Exit(1), 0))

LC_FUNCTION void LC_IgnoreMessage(LC_Token *pos, char *str, int len) {
}

LC_FUNCTION void LC_SendErrorMessage(LC_Token *pos, LC_String s8) {
    if (L->on_message) {
        L->on_message(pos, s8.str, (int)s8.len);
    } else {
        if (pos) {
            LC_String line = LC_GetTokenLine(pos);
            LC_String fmt  = LC_Format(L->arena, "%s(%d,%d): error: %.*s\n%.*s", (char *)pos->lex->file, pos->line, pos->column, LC_Expand(s8), LC_Expand(line));
            LC_Print(fmt.str, fmt.len);
        } else {
            LC_Print(s8.str, s8.len);
        }
    }
    LC_DebugBreak();
}

LC_FUNCTION void LC_SendErrorMessagef(LC_Lex *x, LC_Token *pos, const char *str, ...) {
    LC_FORMAT(L->arena, str, s8);
    LC_SendErrorMessage(pos, s8);
}

LC_FUNCTION void LC_HandleFatalError(void) {
    if (L->on_fatal_error) {
        L->on_fatal_error();
        return;
    }
    LC_FatalError();
}

LC_FUNCTION void LC_MapReserve(LC_Map *map, int size) {
    LC_Map old_map = *map;

    LC_ASSERT(NULL, LC_IS_POW2(size));
    map->len = 0;
    map->cap = size;
    LC_ASSERT(NULL, map->arena);

    map->entries = LC_PushArray(map->arena, LC_MapEntry, map->cap);

    if (old_map.entries) {
        for (int i = 0; i < old_map.cap; i += 1) {
            LC_MapEntry *it = old_map.entries + i;
            if (it->key) LC_InsertMapEntry(map, it->key, it->value);
        }
    }
}

// FNV HASH (1a?)
LC_FUNCTION uint64_t LC_HashBytes(void *data, uint64_t size) {
    uint8_t *data8 = (uint8_t *)data;
    uint64_t hash  = (uint64_t)14695981039346656037ULL;
    for (uint64_t i = 0; i < size; i++) {
        hash = hash ^ (uint64_t)(data8[i]);
        hash = hash * (uint64_t)1099511628211ULL;
    }
    return hash;
}

LC_FUNCTION uint64_t LC_HashMix(uint64_t x, uint64_t y) {
    x ^= y;
    x *= 0xff51afd7ed558ccd;
    x ^= x >> 32;
    return x;
}

LC_FUNCTION int LC_NextPow2(int v) {
    v--;
    v |= v >> 1;
    v |= v >> 2;
    v |= v >> 4;
    v |= v >> 8;
    v |= v >> 16;
    v++;
    return v;
}

LC_FUNCTION LC_MapEntry *LC_GetMapEntryEx(LC_Map *map, uint64_t key) {
    LC_ASSERT(NULL, key);
    if (map->len * 2 >= map->cap) {
        LC_MapReserve(map, map->cap * 2);
    }

    uint64_t hash = LC_HashBytes(&key, sizeof(key));
    if (hash == 0) hash += 1;
    uint64_t index = LC_WRAP_AROUND_POWER_OF_2(hash, map->cap);
    uint64_t i     = index;
    for (;;) {
        LC_MapEntry *it = map->entries + i;
        if (it->key == key || it->key == 0) {
            return it;
        }

        i = LC_WRAP_AROUND_POWER_OF_2(i + 1, map->cap);
        if (i == index) return NULL;
    }
    LC_ASSERT(NULL, !"invalid codepath");
}

LC_FUNCTION bool LC_InsertWithoutReplace(LC_Map *map, void *key, void *value) {
    LC_MapEntry *entry = LC_GetMapEntryEx(map, (uint64_t)key);
    if (entry->key != 0) return false;

    map->len += 1;
    entry->key   = (uint64_t)key;
    entry->value = (uint64_t)value;
    return true;
}

LC_FUNCTION LC_MapEntry *LC_InsertMapEntry(LC_Map *map, uint64_t key, uint64_t value) {
    LC_MapEntry *entry = LC_GetMapEntryEx(map, key);
    if (entry->key == key) {
        entry->value = value;
    }
    if (entry->key == 0) {
        entry->key   = key;
        entry->value = value;
        map->len += 1;
    }
    return entry;
}

LC_FUNCTION LC_MapEntry *LC_GetMapEntry(LC_Map *map, uint64_t key) {
    LC_MapEntry *entry = LC_GetMapEntryEx(map, key);
    if (entry && entry->key == key) {
        return entry;
    }
    return NULL;
}

LC_FUNCTION void LC_MapInsert(LC_Map *map, LC_String keystr, void *value) {
    uint64_t key = LC_HashBytes(keystr.str, keystr.len);
    LC_InsertMapEntry(map, key, (uint64_t)value);
}

LC_FUNCTION void *LC_MapGet(LC_Map *map, LC_String keystr) {
    uint64_t     key = LC_HashBytes(keystr.str, keystr.len);
    LC_MapEntry *r   = LC_GetMapEntry(map, key);
    return r ? (void *)r->value : 0;
}

LC_FUNCTION void LC_MapInsertU64(LC_Map *map, uint64_t key, void *value) {
    LC_InsertMapEntry(map, key, (uint64_t)value);
}

LC_FUNCTION void *LC_MapGetU64(LC_Map *map, uint64_t key) {
    LC_MapEntry *r = LC_GetMapEntry(map, key);
    return r ? (void *)r->value : 0;
}

LC_FUNCTION void *LC_MapGetP(LC_Map *map, void *key) {
    return LC_MapGetU64(map, (uint64_t)key);
}

LC_FUNCTION void LC_MapInsertP(LC_Map *map, void *key, void *value) {
    LC_InsertMapEntry(map, (uint64_t)key, (uint64_t)value);
}

LC_FUNCTION void LC_MapClear(LC_Map *map) {
    if (map->len != 0) LC_MemoryZero(map->entries, map->cap * sizeof(LC_MapEntry));
    map->len = 0;
}

LC_FUNCTION size_t LC_GetAlignOffset(size_t size, size_t align) {
    size_t mask = align - 1;
    size_t val  = size & mask;
    if (val) {
        val = align - val;
    }
    return val;
}

LC_FUNCTION size_t LC_AlignUp(size_t size, size_t align) {
    size_t result = size + LC_GetAlignOffset(size, align);
    return result;
}

LC_FUNCTION size_t LC_AlignDown(size_t size, size_t align) {
    size += 1; // Make sure when align is 8 doesn't get rounded down to 0
    size_t result = size - (align - LC_GetAlignOffset(size, align));
    return result;
}

typedef struct {
    int  len;
    char str[];
} INTERN_Entry;

LC_FUNCTION LC_Intern LC_InternStrLen(char *str, int len) {
    LC_String     key   = LC_MakeString(str, len);
    INTERN_Entry *entry = (INTERN_Entry *)LC_MapGet(&L->interns, key);
    if (entry == NULL) {
        LC_ASSERT(NULL, sizeof(INTERN_Entry) == sizeof(int));
        entry      = (INTERN_Entry *)LC_PushSize(L->arena, sizeof(int) + sizeof(char) * (len + 1));
        entry->len = len;
        LC_MemoryCopy(entry->str, str, len);
        entry->str[len] = 0;
        LC_MapInsert(&L->interns, key, entry);
    }

    return (uintptr_t)entry->str;
}

LC_FUNCTION LC_Intern LC_ILit(char *str) {
    return LC_InternStrLen(str, (int)LC_StrLen(str));
}

LC_FUNCTION LC_Intern LC_GetUniqueIntern(const char *name_for_debug) {
    LC_String name   = LC_Format(L->arena, "U%u_%s", ++L->unique_counter, name_for_debug);
    LC_Intern result = LC_InternStrLen(name.str, (int)name.len);
    return result;
}

LC_FUNCTION char *LC_GetUniqueName(const char *name_for_debug) {
    LC_String name = LC_Format(L->arena, "U%u_%s", ++L->unique_counter, name_for_debug);
    return name.str;
}

LC_FUNCTION void LC_DeclareNote(LC_Intern intern) {
    LC_MapInsertU64(&L->declared_notes, intern, (void *)intern);
}

LC_FUNCTION bool LC_IsNoteDeclared(LC_Intern intern) {
    void *p      = LC_MapGetU64(&L->declared_notes, intern);
    bool  result = p != NULL;
    return result;
}
LC_FUNCTION void LC_LexingError(LC_Token *pos, const char *str, ...) {
    LC_FORMAT(L->arena, str, s8);
    LC_SendErrorMessage(pos, s8);
    L->errors += 1;
    pos->kind = LC_TokenKind_Error;
}

#define LC_IF(cond, ...)                    \
    do {                                    \
        if (cond) {                         \
            LC_LexingError(t, __VA_ARGS__); \
            return;                         \
        }                                   \
    } while (0)

LC_FUNCTION bool LC_IsAssign(LC_TokenKind kind) {
    bool result = kind >= LC_TokenKind_Assign && kind <= LC_TokenKind_RightShiftAssign;
    return result;
}

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

LC_FUNCTION bool LC_IsBinDigit(char c) {
    bool result = (c >= '0' && c <= '1');
    return result;
}

LC_FUNCTION uint64_t LC_MapCharToNumber(char c) {
    // clang-format off
    switch (c) {
        case '0': return 0;
        case '1': return 1;
        case '2': return 2;
        case '3': return 3;
        case '4': return 4;
        case '5': return 5;
        case '6': return 6;
        case '7': return 7;
        case '8': return 8;
        case '9': return 9;
        case 'a': case 'A': return 10;
        case 'b': case 'B': return 11;
        case 'c': case 'C': return 12;
        case 'd': case 'D': return 13;
        case 'e': case 'E': return 14;
        case 'f': case 'F': return 15;
        default: return 255;
    }
    // clang-format on
}

LC_FUNCTION uint64_t LC_GetEscapeCode(char c) {
    switch (c) {
    case 'a': return '\a';
    case 'b': return '\b';
    case 'e': return 0x1B;
    case 'f': return '\f';
    case 'n': return '\n';
    case 'r': return '\r';
    case 't': return '\t';
    case 'v': return '\v';
    case '\\': return '\\';
    case '\'': return '\'';
    case '\"': return '\"';
    case '0': return '\0';
    default: return UINT64_MAX;
    }
}

LC_FUNCTION LC_String LC_GetEscapeString(char c) {
    switch (c) {
    case '\a': return LC_Lit("\\a");
    case '\b': return LC_Lit("\\b");
    case 0x1B: return LC_Lit("\\x1B");
    case '\f': return LC_Lit("\\f");
    case '\n': return LC_Lit("\\n");
    case '\r': return LC_Lit("\\r");
    case '\t': return LC_Lit("\\t");
    case '\v': return LC_Lit("\\v");
    case '\\': return LC_Lit("\\\\");
    case '\'': return LC_Lit("\\'");
    case '\"': return LC_Lit("\\\"");
    case '\0': return LC_Lit("\\0");
    default: return LC_Lit("");
    }
}

LC_FUNCTION void LC_LexAdvance(LC_Lex *x) {
    if (x->at[0] == 0) {
        return;
    } else if (x->at[0] == '\n') {
        x->line += 1;
        x->column = 0;
    }
    x->column += 1;
    x->at += 1;
}

LC_FUNCTION void LC_EatWhitespace(LC_Lex *x) {
    while (LC_IsWhitespace(x->at[0])) LC_LexAdvance(x);
}

LC_FUNCTION void LC_EatIdent(LC_Lex *x) {
    while (x->at[0] == '_' || LC_IsAlphanumeric(x->at[0])) LC_LexAdvance(x);
}

LC_FUNCTION void LC_SetTokenLen(LC_Lex *x, LC_Token *t) {
    t->len = (int)(x->at - t->str);
    LC_ASSERT(NULL, t->len < 2000000000);
}

LC_FUNCTION void LC_EatUntilIncluding(LC_Lex *x, char c) {
    while (x->at[0] != 0 && x->at[0] != c) LC_LexAdvance(x);
    LC_LexAdvance(x);
}

// @todo: add temporary allocation + copy at end to perm
LC_FUNCTION LC_BigInt LC_LexBigInt(char *string, int len, uint64_t base) {
    LC_ASSERT(NULL, base >= 2 && base <= 16);
    LC_BigInt m        = LC_Bigint_u64(1);
    LC_BigInt base_mul = LC_Bigint_u64(base);
    LC_BigInt result   = LC_Bigint_u64(0);

    LC_BigInt tmp = {0};
    for (int i = len - 1; i >= 0; --i) {
        uint64_t u = LC_MapCharToNumber(string[i]);
        LC_ASSERT(NULL, u < base);
        LC_BigInt val = LC_Bigint_u64(u);
        LC_Bigint_mul(&tmp, &val, &m);
        LC_BigInt new_val = tmp;
        LC_Bigint_add(&tmp, &result, &new_val);
        result = tmp;
        LC_Bigint_mul(&tmp, &m, &base_mul);
        m = tmp;
    }

    return result;
}

LC_FUNCTION void LC_LexNestedComments(LC_Lex *x, LC_Token *t) {
    t->kind = LC_TokenKind_Comment;
    LC_LexAdvance(x);

    if (x->at[0] == '*') {
        LC_LexAdvance(x);
        t->kind = LC_TokenKind_DocComment;

        if (x->at[0] == ' ' && x->at[1] == 'f' && x->at[2] == 'i' && x->at[3] == 'l' && x->at[4] == 'e') {
            t->kind = LC_TokenKind_FileDocComment;
        }

        if (x->at[0] == ' ' && x->at[1] == 'p' && x->at[2] == 'a' && x->at[3] == 'c' && x->at[4] == 'k' && x->at[5] == 'a' && x->at[6] == 'g' && x->at[7] == 'e') {
            t->kind = LC_TokenKind_PackageDocComment;
        }
    }

    int counter = 0;
    for (;;) {
        if (x->at[0] == '*' && x->at[1] == '/') {
            if (counter <= 0) break;
            counter -= 1;
        } else if (x->at[0] == '/' && x->at[1] == '*') {
            counter += 1;
            LC_LexAdvance(x);
        }
        LC_IF(x->at[0] == 0, "Unclosed block comment");
        LC_LexAdvance(x);
    }
    t->str += 2;
    LC_SetTokenLen(x, t);
    LC_LexAdvance(x);
    LC_LexAdvance(x);
}

LC_FUNCTION void LC_LexStringLiteral(LC_Lex *x, LC_Token *t, LC_TokenKind kind) {
    t->kind = kind;
    if (kind == LC_TokenKind_RawString) {
        LC_EatUntilIncluding(x, '`');
    } else if (kind == LC_TokenKind_String) {
        for (;;) {
            LC_IF(x->at[0] == '\n', "got a new line while parsing a '\"' string literal");
            LC_IF(x->at[0] == 0, "reached end of file during string lexing");
            if (x->at[0] == '"') break;
            if (x->at[0] == '\\' && x->at[1] == '"') LC_LexAdvance(x);
            LC_LexAdvance(x);
        }
        LC_LexAdvance(x);
    } else {
        LC_IF(1, "internal compiler error: unhandled case in %s", __FUNCTION__);
    }

    LC_SetTokenLen(x, t);
    t->len -= 2;
    t->str += 1;
}

LC_FUNCTION void LC_LexUnicodeLiteral(LC_Lex *x, LC_Token *t) {
    t->kind               = LC_TokenKind_Unicode;
    LC_UTF32Result decode = LC_ConvertUTF8ToUTF32(x->at, 4);
    LC_IF(decode.error, "invalid utf8 sequence");

    uint8_t c[8] = {0};
    for (int i = 0; i < decode.advance; i += 1) {
        c[i] = x->at[0];
        LC_LexAdvance(x);
    }
    uint64_t result = *(uint64_t *)&c[0];

    if (result == '\\') {
        LC_ASSERT(NULL, decode.advance == 1);
        result = LC_GetEscapeCode(x->at[0]);
        LC_IF(result == UINT64_MAX, "invalid escape code");
        LC_LexAdvance(x);
    }
    LC_IF(x->at[0] != '\'', "unclosed unicode literal");

    LC_Bigint_init_signed(&t->i, result);
    LC_LexAdvance(x);
    LC_SetTokenLen(x, t);
    t->str += 1;
    t->len -= 2;

    LC_IF(t->len == 0, "empty unicode literal");
}

LC_FUNCTION void LC_LexIntOrFloat(LC_Lex *x, LC_Token *t) {
    t->kind = LC_TokenKind_Int;
    for (;;) {
        if (x->at[0] == '.') {
            LC_IF(t->kind == LC_TokenKind_Float, "failed to parse a floating point number, invalid format, found multiple '.'");
            if (t->kind == LC_TokenKind_Int) t->kind = LC_TokenKind_Float;
        } else if (!LC_IsDigit(x->at[0])) break;
        LC_LexAdvance(x);
    }

    LC_SetTokenLen(x, t);
    if (t->kind == LC_TokenKind_Int) {
        t->i = LC_LexBigInt(t->str, t->len, 10);
    } else if (t->kind == LC_TokenKind_Float) {
        t->f64 = LC_ParseFloat(t->str, t->len);
    } else {
        LC_IF(1, "internal compiler error: unhandled case in %s", __FUNCTION__);
    }
}

LC_FUNCTION void LC_LexCase2(LC_Lex *x, LC_Token *t, LC_TokenKind tk0, char c, LC_TokenKind tk1) {
    t->kind = tk0;
    if (x->at[0] == c) {
        LC_LexAdvance(x);
        t->kind = tk1;
    }
}

LC_FUNCTION void LC_LexCase3(LC_Lex *x, LC_Token *t, LC_TokenKind tk, char c0, LC_TokenKind tk0, char c1, LC_TokenKind tk1) {
    t->kind = tk;
    if (x->at[0] == c0) {
        t->kind = tk0;
        LC_LexAdvance(x);
    } else if (x->at[0] == c1) {
        t->kind = tk1;
        LC_LexAdvance(x);
    }
}

LC_FUNCTION void LC_LexCase4(LC_Lex *x, LC_Token *t, LC_TokenKind tk, char c0, LC_TokenKind tk0, char c1, LC_TokenKind tk1, char c2, LC_TokenKind tk2) {
    t->kind = tk;
    if (x->at[0] == c0) {
        t->kind = tk0;
        LC_LexAdvance(x);
    } else if (x->at[0] == c1) {
        LC_LexAdvance(x);
        LC_LexCase2(x, t, tk1, c2, tk2);
    }
}

LC_FUNCTION void LC_LexNext(LC_Lex *x, LC_Token *t) {
    LC_EatWhitespace(x);
    LC_MemoryZero(t, sizeof(LC_Token));
    t->str    = x->at;
    t->line   = x->line + 1;
    t->column = x->column;
    t->lex    = x;
    char *c   = x->at;
    LC_LexAdvance(x);

    switch (c[0]) {
    case 0: t->kind = LC_TokenKind_EOF; break;
    case '(': t->kind = LC_TokenKind_OpenParen; break;
    case ')': t->kind = LC_TokenKind_CloseParen; break;
    case '{': t->kind = LC_TokenKind_OpenBrace; break;
    case '}': t->kind = LC_TokenKind_CloseBrace; break;
    case '[': t->kind = LC_TokenKind_OpenBracket; break;
    case ']': t->kind = LC_TokenKind_CloseBracket; break;
    case ',': t->kind = LC_TokenKind_Comma; break;
    case ':': t->kind = LC_TokenKind_Colon; break;
    case ';': t->kind = LC_TokenKind_Semicolon; break;
    case '~': t->kind = LC_TokenKind_Neg; break;
    case '#': t->kind = LC_TokenKind_Hash; break;
    case '@': t->kind = LC_TokenKind_Note; break;
    case '\'': LC_LexUnicodeLiteral(x, t); break;
    case '"': LC_LexStringLiteral(x, t, LC_TokenKind_String); break;
    case '`': LC_LexStringLiteral(x, t, LC_TokenKind_RawString); break;
    case '=': LC_LexCase2(x, t, LC_TokenKind_Assign, '=', LC_TokenKind_Equals); break;
    case '!': LC_LexCase2(x, t, LC_TokenKind_Not, '=', LC_TokenKind_NotEquals); break;
    case '*': LC_LexCase2(x, t, LC_TokenKind_Mul, '=', LC_TokenKind_MulAssign); break;
    case '%': LC_LexCase2(x, t, LC_TokenKind_Mod, '=', LC_TokenKind_ModAssign); break;
    case '+': LC_LexCase2(x, t, LC_TokenKind_Add, '=', LC_TokenKind_AddAssign); break;
    case '-': LC_LexCase2(x, t, LC_TokenKind_Sub, '=', LC_TokenKind_SubAssign); break;
    case '^': LC_LexCase2(x, t, LC_TokenKind_BitXor, '=', LC_TokenKind_BitXorAssign); break;
    case '&': LC_LexCase3(x, t, LC_TokenKind_BitAnd, '=', LC_TokenKind_BitAndAssign, '&', LC_TokenKind_And); break;
    case '|': LC_LexCase3(x, t, LC_TokenKind_BitOr, '=', LC_TokenKind_BitOrAssign, '|', LC_TokenKind_Or); break;
    case '>': LC_LexCase4(x, t, LC_TokenKind_GreaterThen, '=', LC_TokenKind_GreaterThenEq, '>', LC_TokenKind_RightShift, '=', LC_TokenKind_RightShiftAssign); break;
    case '<': LC_LexCase4(x, t, LC_TokenKind_LesserThen, '=', LC_TokenKind_LesserThenEq, '<', LC_TokenKind_LeftShift, '=', LC_TokenKind_LeftShiftAssign); break;
    case '.': {
        t->kind = LC_TokenKind_Dot;
        if (x->at[0] == '.' && x->at[1] == '.') {
            t->kind = LC_TokenKind_ThreeDots;
            LC_LexAdvance(x);
            LC_LexAdvance(x);
        }
    } break;

    case '0': {
        if (x->at[0] == 'x') {
            t->kind = LC_TokenKind_Int;
            LC_LexAdvance(x);
            while (LC_IsHexDigit(x->at[0])) LC_LexAdvance(x);
            LC_SetTokenLen(x, t);
            LC_IF(t->len < 3, "invalid hex number");
            t->i = LC_LexBigInt(t->str + 2, t->len - 2, 16);
            break;
        }
        if (x->at[0] == 'b') {
            t->kind = LC_TokenKind_Int;
            LC_LexAdvance(x);
            while (LC_IsBinDigit(x->at[0])) LC_LexAdvance(x);
            LC_SetTokenLen(x, t);
            LC_IF(t->len < 3, "invalid binary number");
            t->i = LC_LexBigInt(t->str + 2, t->len - 2, 2);
            break;
        }
    } // @fallthrough

    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9': {
        LC_LexIntOrFloat(x, t);
    } break;

    case 'A':
    case 'a':
    case 'B':
    case 'b':
    case 'C':
    case 'c':
    case 'D':
    case 'd':
    case 'E':
    case 'e':
    case 'F':
    case 'f':
    case 'G':
    case 'g':
    case 'H':
    case 'h':
    case 'I':
    case 'i':
    case 'J':
    case 'j':
    case 'K':
    case 'k':
    case 'L':
    case 'l':
    case 'M':
    case 'm':
    case 'N':
    case 'n':
    case 'O':
    case 'o':
    case 'P':
    case 'p':
    case 'Q':
    case 'q':
    case 'R':
    case 'r':
    case 'S':
    case 's':
    case 'T':
    case 't':
    case 'U':
    case 'u':
    case 'V':
    case 'v':
    case 'W':
    case 'w':
    case 'X':
    case 'x':
    case 'Y':
    case 'y':
    case 'Z':
    case 'z':
    case '_': {
        t->kind = LC_TokenKind_Ident;
        LC_EatIdent(x);
    } break;

    case '/': {
        t->kind = LC_TokenKind_Div;
        if (x->at[0] == '=') {
            t->kind = LC_TokenKind_DivAssign;
            LC_LexAdvance(x);
        } else if (x->at[0] == '/') {
            t->kind = LC_TokenKind_Comment;
            LC_LexAdvance(x);
            while (x->at[0] != '\n' && x->at[0] != 0) LC_LexAdvance(x);
            LC_SetTokenLen(x, t);
        } else if (x->at[0] == '*') {
            LC_LexNestedComments(x, t);
        }
    } break;

    default: LC_IF(1, "invalid character");
    }
    if (t->len == 0 && t->kind != LC_TokenKind_String && t->kind != LC_TokenKind_RawString) LC_SetTokenLen(x, t);
    if (t->kind == LC_TokenKind_Comment) LC_LexNext(x, t);
}

LC_FUNCTION LC_Lex *LC_LexStream(char *file, char *str, int line) {
    LC_Lex *x = LC_PushStruct(L->lex_arena, LC_Lex);
    x->begin  = str;
    x->at     = str;
    x->file   = LC_ILit(file);
    x->line   = line;

    for (;;) {
        LC_Token *t = LC_PushStruct(L->lex_arena, LC_Token);
        if (!x->tokens) x->tokens = t;
        x->token_count += 1;

        LC_LexNext(x, t);
        if (t->kind == LC_TokenKind_EOF) break;
    }
    if (L->on_tokens_lexed) L->on_tokens_lexed(x);

    return x;
}

LC_FUNCTION LC_String LC_GetTokenLine(LC_Token *token) {
    LC_Lex       *x       = token->lex;
    LC_String     content = LC_MakeFromChar(x->begin);
    LC_StringList lines   = LC_Split(L->arena, content, LC_Lit("\n"), 0);

    LC_String l[3] = {LC_MakeEmptyString()};

    int line = 1;
    for (LC_StringNode *it = lines.first; it; it = it->next) {
        LC_String sline = it->string;
        if (token->line - 1 == line) {
            l[0] = LC_Format(L->arena, "> %.*s\n", LC_Expand(sline));
        }
        if (token->line + 1 == line) {
            l[2] = LC_Format(L->arena, "> %.*s\n", LC_Expand(sline));
            break;
        }
        if (token->line == line) {
            int       begin     = (int)(token->str - sline.str);
            LC_String left      = LC_GetPrefix(sline, begin);
            LC_String past_left = LC_Skip(sline, begin);
            LC_String mid       = LC_GetPrefix(past_left, token->len);
            LC_String right     = LC_Skip(past_left, token->len);

            char *green = "\033[32m";
            char *reset = "\033[0m";
            if (!L->use_colored_terminal_output) {
                green = ">>>>";
                reset = "<<<<";
            }
            l[1] = LC_Format(L->arena, "> %.*s%s%.*s%s%.*s\n", LC_Expand(left), green, LC_Expand(mid), reset, LC_Expand(right));
        }
        line += 1;
    }

    LC_String result = LC_Format(L->arena, "%.*s%.*s%.*s", LC_Expand(l[0]), LC_Expand(l[1]), LC_Expand(l[2]));
    return result;
}

LC_FUNCTION void LC_InternTokens(LC_Lex *x) {
    // @todo: add scratch, we can dump the LC_PushArray strings
    for (int i = 0; i < x->token_count; i += 1) {
        LC_Token *t = x->tokens + i;
        if (t->kind == LC_TokenKind_String) {
            int   string_len = 0;
            char *string     = LC_PushArray(L->arena, char, t->len);
            for (int i = 0; i < t->len; i += 1) {
                char c0 = t->str[i];
                char c1 = t->str[i + 1];
                if (i + 1 >= t->len) c1 = 0;

                if (c0 == '\\') {
                    uint64_t code = LC_GetEscapeCode(c1);
                    if (code == UINT64_MAX) {
                        LC_LexingError(t, "invalid escape code in string '%c%c'", c0, c1);
                        break;
                    }

                    c0 = (char)code;
                    i += 1;
                }

                string[string_len++] = c0;
            }
            t->ident = LC_InternStrLen(string, string_len);
        }
        if (t->kind == LC_TokenKind_Note || t->kind == LC_TokenKind_Ident || t->kind == LC_TokenKind_RawString) {
            t->ident = LC_InternStrLen(t->str, t->len);
        }
        if (t->kind == LC_TokenKind_Ident) {
            bool is_keyword = t->ident >= L->first_keyword && t->ident <= L->last_keyword;
            if (is_keyword) {
                t->kind = LC_TokenKind_Keyword;
                if (L->kaddptr == t->ident) t->kind = LC_TokenKind_AddPtr;
            }
        }
    }
    if (L->on_tokens_interned) L->on_tokens_interned(x);
}

#undef LC_IF

#ifndef malloc_arena
    #define malloc_arena(size) LC_PushSize(L->arena, size)
#endif
#define ALLOC_DIGITS(_digits) ((_digits) ? (uint64_t *)malloc_arena(sizeof(uint64_t) * (_digits)) : NULL)

LC_FUNCTION uint32_t LC_u32_min(uint32_t a, uint32_t b) {
    return a < b ? a : b;
}

LC_FUNCTION size_t LC_size_max(size_t a, size_t b) {
    return a > b ? a : b;
}

LC_FUNCTION unsigned LC_unsigned_max(unsigned a, unsigned b) {
    return a > b ? a : b;
}

LC_FUNCTION uint64_t *LC_Bigint_ptr(LC_BigInt *big_int) {
    return big_int->digit_count == 1 ? &big_int->digit : big_int->digits;
}

LC_FUNCTION LC_BigInt LC_Bigint_u64(uint64_t val) {
    LC_BigInt result = {0};
    LC_Bigint_init_unsigned(&result, val);
    return result;
}

LC_FUNCTION void LC_normalize(LC_BigInt *big_int) {
    uint64_t *digits        = LC_Bigint_ptr(big_int);
    unsigned  last_non_zero = UINT32_MAX;
    for (unsigned i = 0; i < big_int->digit_count; i++) {
        if (digits[i] != 0) {
            last_non_zero = i;
        }
    }
    if (last_non_zero == UINT32_MAX) {
        big_int->is_negative = false;
        big_int->digit_count = 0;
        return;
    }
    big_int->digit_count = last_non_zero + 1;
    if (!last_non_zero) {
        big_int->digit = digits[0];
    }
}

LC_FUNCTION char LC_digit_to_char(uint8_t digit, bool upper) {
    if (digit <= 9) {
        return (char)(digit + '0');
    }
    if (digit <= 35) {
        return (char)(digit + (upper ? 'A' : 'a') - 10);
    }
    LC_ASSERT(NULL, !"Can't reach");
    return 0;
}

LC_FUNCTION bool LC_bit_at_index(LC_BigInt *big_int, size_t index) {
    size_t digit_index = index / 64;
    if (digit_index >= big_int->digit_count) {
        return false;
    }
    size_t    digit_bit_index = index % 64;
    uint64_t *digits          = LC_Bigint_ptr(big_int);
    uint64_t  digit           = digits[digit_index];
    return ((digit >> digit_bit_index) & 0x1U) == 0x1U;
}

LC_FUNCTION size_t LC_Bigint_bits_needed(LC_BigInt *big_int) {
    size_t full_bits          = big_int->digit_count * 64;
    size_t leading_zero_count = LC_Bigint_clz(big_int, full_bits);
    size_t bits_needed        = full_bits - leading_zero_count;
    return bits_needed + big_int->is_negative;
}

LC_FUNCTION void LC_Bigint_init_unsigned(LC_BigInt *big_int, uint64_t value) {
    if (value == 0) {
        big_int->digit_count = 0;
        big_int->is_negative = false;
        return;
    }
    big_int->digit_count = 1;
    big_int->digit       = value;
    big_int->is_negative = false;
}

LC_FUNCTION void LC_Bigint_init_signed(LC_BigInt *dest, int64_t value) {
    if (value >= 0) {
        LC_Bigint_init_unsigned(dest, (uint64_t)value);
        return;
    }
    dest->is_negative = true;
    dest->digit_count = 1;
    dest->digit       = ((uint64_t)(-(value + 1))) + 1;
}

LC_FUNCTION void LC_Bigint_init_bigint(LC_BigInt *dest, LC_BigInt *src) {
    if (src->digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }
    if (src->digit_count == 1) {
        dest->digit_count = 1;
        dest->digit       = src->digit;
        dest->is_negative = src->is_negative;
        return;
    }
    dest->is_negative = src->is_negative;
    dest->digit_count = src->digit_count;
    dest->digits      = ALLOC_DIGITS(dest->digit_count);
    LC_MemoryCopy(dest->digits, src->digits, sizeof(uint64_t) * dest->digit_count);
}

LC_FUNCTION void LC_Bigint_negate(LC_BigInt *dest, LC_BigInt *source) {
    LC_Bigint_init_bigint(dest, source);
    dest->is_negative = !dest->is_negative;
    LC_normalize(dest);
}

LC_FUNCTION void LC_to_twos_complement(LC_BigInt *dest, LC_BigInt *source, size_t bit_count) {
    if (bit_count == 0 || source->digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }
    if (source->is_negative) {
        LC_BigInt negated = {0};
        LC_Bigint_negate(&negated, source);

        LC_BigInt inverted = {0};
        LC_Bigint_not(&inverted, &negated, bit_count, false);

        LC_BigInt one = {0};
        LC_Bigint_init_unsigned(&one, 1);

        LC_Bigint_add(dest, &inverted, &one);
        return;
    }

    dest->is_negative       = false;
    uint64_t *source_digits = LC_Bigint_ptr(source);
    if (source->digit_count == 1) {
        dest->digit = source_digits[0];
        if (bit_count < 64) {
            dest->digit &= (1ULL << bit_count) - 1;
        }
        dest->digit_count = 1;
        LC_normalize(dest);
        return;
    }
    unsigned digits_to_copy = (unsigned int)(bit_count / 64);
    unsigned leftover_bits  = (unsigned int)(bit_count % 64);
    dest->digit_count       = digits_to_copy + ((leftover_bits == 0) ? 0 : 1);
    if (dest->digit_count == 1 && leftover_bits == 0) {
        dest->digit = source_digits[0];
        if (dest->digit == 0) dest->digit_count = 0;
        return;
    }
    dest->digits = (uint64_t *)malloc_arena(dest->digit_count * sizeof(uint64_t));
    for (size_t i = 0; i < digits_to_copy; i += 1) {
        uint64_t digit  = (i < source->digit_count) ? source_digits[i] : 0;
        dest->digits[i] = digit;
    }
    if (leftover_bits != 0) {
        uint64_t digit               = (digits_to_copy < source->digit_count) ? source_digits[digits_to_copy] : 0;
        dest->digits[digits_to_copy] = digit & ((1ULL << leftover_bits) - 1);
    }
    LC_normalize(dest);
}

LC_FUNCTION size_t LC_Bigint_clz(LC_BigInt *big_int, size_t bit_count) {
    if (big_int->is_negative || bit_count == 0) {
        return 0;
    }
    if (big_int->digit_count == 0) {
        return bit_count;
    }
    size_t count = 0;
    for (size_t i = bit_count - 1;;) {
        if (LC_bit_at_index(big_int, i)) {
            return count;
        }
        count++;
        if (i == 0) break;
        i--;
    }
    return count;
}

LC_FUNCTION bool LC_Bigint_eql(LC_BigInt a, LC_BigInt b) {
    return LC_Bigint_cmp(&a, &b) == LC_CmpRes_EQ;
}

LC_FUNCTION void LC_from_twos_complement(LC_BigInt *dest, LC_BigInt *src, size_t bit_count, bool is_signed) {
    LC_ASSERT(NULL, !src->is_negative);

    if (bit_count == 0 || src->digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }

    if (is_signed && LC_bit_at_index(src, bit_count - 1)) {
        LC_BigInt negative_one = {0};
        LC_Bigint_init_signed(&negative_one, -1);

        LC_BigInt minus_one = {0};
        LC_Bigint_add(&minus_one, src, &negative_one);

        LC_BigInt inverted = {0};
        LC_Bigint_not(&inverted, &minus_one, bit_count, false);

        LC_Bigint_negate(dest, &inverted);
        return;
    }

    LC_Bigint_init_bigint(dest, src);
}

void LC_Bigint_init_data(LC_BigInt *dest, uint64_t *digits, unsigned int digit_count, bool is_negative) {
    if (digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }

    if (digit_count == 1) {
        dest->digit_count = 1;
        dest->digit       = digits[0];
        dest->is_negative = is_negative;
        LC_normalize(dest);
        return;
    }

    dest->digit_count = digit_count;
    dest->is_negative = is_negative;
    dest->digits      = ALLOC_DIGITS(digit_count);
    LC_MemoryCopy(dest->digits, digits, sizeof(uint64_t) * digit_count);

    LC_normalize(dest);
}

LC_FUNCTION bool LC_Bigint_fits_in_bits(LC_BigInt *big_int, size_t bit_count, bool is_signed) {
    LC_ASSERT(NULL, big_int->digit_count != 1 || big_int->digit != 0);
    if (bit_count == 0) {
        return LC_Bigint_cmp_zero(big_int) == LC_CmpRes_EQ;
    }
    if (big_int->digit_count == 0) {
        return true;
    }

    if (!is_signed) {
        size_t full_bits          = big_int->digit_count * 64;
        size_t leading_zero_count = LC_Bigint_clz(big_int, full_bits);
        return bit_count >= full_bits - leading_zero_count;
    }

    LC_BigInt one = {0};
    LC_Bigint_init_unsigned(&one, 1);

    LC_BigInt shl_amt = {0};
    LC_Bigint_init_unsigned(&shl_amt, bit_count - 1);

    LC_BigInt max_value_plus_one = {0};
    LC_Bigint_shl(&max_value_plus_one, &one, &shl_amt);

    LC_BigInt max_value = {0};
    LC_Bigint_sub(&max_value, &max_value_plus_one, &one);

    LC_BigInt min_value = {0};
    LC_Bigint_negate(&min_value, &max_value_plus_one);

    LC_CmpRes min_cmp = LC_Bigint_cmp(big_int, &min_value);
    LC_CmpRes max_cmp = LC_Bigint_cmp(big_int, &max_value);

    return (min_cmp == LC_CmpRes_GT || min_cmp == LC_CmpRes_EQ) && (max_cmp == LC_CmpRes_LT || max_cmp == LC_CmpRes_EQ);
}

LC_FUNCTION uint64_t LC_Bigint_as_unsigned(LC_BigInt *bigint) {
    LC_ASSERT(NULL, !bigint->is_negative);
    if (bigint->digit_count == 0) {
        return 0;
    }
    if (bigint->digit_count != 1) {
        LC_ASSERT(NULL, !"Bigint exceeds u64");
    }
    return bigint->digit;
}

#if defined(_MSC_VER)

LC_FUNCTION bool LC_add_u64_overflow(uint64_t op1, uint64_t op2, uint64_t *result) {
    *result = op1 + op2;
    return *result < op1 || *result < op2;
}

LC_FUNCTION bool LC_sub_u64_overflow(uint64_t op1, uint64_t op2, uint64_t *result) {
    *result = op1 - op2;
    return *result > op1;
}

LC_FUNCTION bool LC_mul_u64_overflow(uint64_t op1, uint64_t op2, uint64_t *result) {
    *result = op1 * op2;

    if (op1 == 0 || op2 == 0) return false;
    if (op1 > UINT64_MAX / op2) return true;
    if (op2 > UINT64_MAX / op1) return true;
    return false;
}

#else

LC_FUNCTION bool LC_add_u64_overflow(uint64_t op1, uint64_t op2, uint64_t *result) {
    return __builtin_uaddll_overflow((unsigned long long)op1, (unsigned long long)op2,
                                     (unsigned long long *)result);
}

LC_FUNCTION bool LC_sub_u64_overflow(uint64_t op1, uint64_t op2, uint64_t *result) {
    return __builtin_usubll_overflow((unsigned long long)op1, (unsigned long long)op2,
                                     (unsigned long long *)result);
}

LC_FUNCTION bool LC_mul_u64_overflow(uint64_t op1, uint64_t op2, uint64_t *result) {
    return __builtin_umulll_overflow((unsigned long long)op1, (unsigned long long)op2,
                                     (unsigned long long *)result);
}

#endif

LC_FUNCTION void LC_Bigint_add(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    if (op1->digit_count == 0) {
        LC_Bigint_init_bigint(dest, op2);
        return;
    }
    if (op2->digit_count == 0) {
        LC_Bigint_init_bigint(dest, op1);
        return;
    }
    if (op1->is_negative == op2->is_negative) {
        dest->is_negative = op1->is_negative;

        uint64_t *op1_digits = LC_Bigint_ptr(op1);
        uint64_t *op2_digits = LC_Bigint_ptr(op2);
        uint64_t  overflow   = LC_add_u64_overflow(op1_digits[0], op2_digits[0], &dest->digit);
        if (overflow == 0 && op1->digit_count == 1 && op2->digit_count == 1) {
            dest->digit_count = 1;
            LC_normalize(dest);
            return;
        }
        unsigned i           = 1;
        uint64_t first_digit = dest->digit;
        dest->digits         = ALLOC_DIGITS(LC_unsigned_max(op1->digit_count, op2->digit_count) + 1);
        dest->digits[0]      = first_digit;

        for (;;) {
            bool     found_digit = false;
            uint64_t x           = (uint64_t)overflow;
            overflow             = 0;

            if (i < op1->digit_count) {
                found_digit    = true;
                uint64_t digit = op1_digits[i];
                overflow += LC_add_u64_overflow(x, digit, &x);
            }

            if (i < op2->digit_count) {
                found_digit    = true;
                uint64_t digit = op2_digits[i];
                overflow += LC_add_u64_overflow(x, digit, &x);
            }

            dest->digits[i] = x;
            i += 1;

            if (!found_digit) {
                dest->digit_count = i;
                LC_normalize(dest);
                return;
            }
        }
    }
    LC_BigInt *op_pos;
    LC_BigInt *op_neg;
    if (op1->is_negative) {
        op_neg = op1;
        op_pos = op2;
    } else {
        op_pos = op1;
        op_neg = op2;
    }

    LC_BigInt op_neg_abs = {0};
    LC_Bigint_negate(&op_neg_abs, op_neg);
    LC_BigInt *bigger_op;
    LC_BigInt *smaller_op;
    switch (LC_Bigint_cmp(op_pos, &op_neg_abs)) {
    case LC_CmpRes_EQ:
        LC_Bigint_init_unsigned(dest, 0);
        return;
    case LC_CmpRes_LT:
        bigger_op         = &op_neg_abs;
        smaller_op        = op_pos;
        dest->is_negative = true;
        break;
    case LC_CmpRes_GT:
        bigger_op         = op_pos;
        smaller_op        = &op_neg_abs;
        dest->is_negative = false;
        break;
    default:
        LC_ASSERT(NULL, !"UNREACHABLE");
    }
    uint64_t *bigger_op_digits  = LC_Bigint_ptr(bigger_op);
    uint64_t *smaller_op_digits = LC_Bigint_ptr(smaller_op);
    uint64_t  overflow          = (uint64_t)LC_sub_u64_overflow(bigger_op_digits[0], smaller_op_digits[0], &dest->digit);
    if (overflow == 0 && bigger_op->digit_count == 1 && smaller_op->digit_count == 1) {
        dest->digit_count = 1;
        LC_normalize(dest);
        return;
    }
    uint64_t first_digit = dest->digit;
    dest->digits         = ALLOC_DIGITS(bigger_op->digit_count);
    dest->digits[0]      = first_digit;
    unsigned i           = 1;

    for (;;) {
        bool     found_digit   = false;
        uint64_t x             = bigger_op_digits[i];
        uint64_t prev_overflow = overflow;
        overflow               = 0;

        if (i < smaller_op->digit_count) {
            found_digit    = true;
            uint64_t digit = smaller_op_digits[i];
            overflow += LC_sub_u64_overflow(x, digit, &x);
        }
        if (LC_sub_u64_overflow(x, prev_overflow, &x)) {
            found_digit = true;
            overflow += 1;
        }
        dest->digits[i] = x;
        i += 1;

        if (!found_digit || i >= bigger_op->digit_count) {
            break;
        }
    }
    LC_ASSERT(NULL, overflow == 0);
    dest->digit_count = i;
    LC_normalize(dest);
}

LC_FUNCTION void LC_Bigint_add_wrap(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2, size_t bit_count, bool is_signed) {
    LC_BigInt unwrapped = {0};
    LC_Bigint_add(&unwrapped, op1, op2);
    LC_Bigint_truncate(dest, &unwrapped, bit_count, is_signed);
}

LC_FUNCTION void LC_Bigint_sub(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    LC_BigInt op2_negated = {0};
    LC_Bigint_negate(&op2_negated, op2);
    LC_Bigint_add(dest, op1, &op2_negated);
    return;
}

LC_FUNCTION void LC_Bigint_sub_wrap(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2, size_t bit_count, bool is_signed) {
    LC_BigInt op2_negated = {0};
    LC_Bigint_negate(&op2_negated, op2);
    LC_Bigint_add_wrap(dest, op1, &op2_negated, bit_count, is_signed);
    return;
}

LC_FUNCTION void mul_overflow(uint64_t op1, uint64_t op2, uint64_t *lo, uint64_t *hi) {
    uint64_t u1 = (op1 & 0xffffffff);
    uint64_t v1 = (op2 & 0xffffffff);
    uint64_t t  = (u1 * v1);
    uint64_t w3 = (t & 0xffffffff);
    uint64_t k  = (t >> 32);

    op1 >>= 32;
    t           = (op1 * v1) + k;
    k           = (t & 0xffffffff);
    uint64_t w1 = (t >> 32);

    op2 >>= 32;
    t = (u1 * op2) + k;
    k = (t >> 32);

    *hi = (op1 * op2) + w1 + k;
    *lo = (t << 32) + w3;
}

LC_FUNCTION void LC_mul_scalar(LC_BigInt *dest, LC_BigInt *op, uint64_t scalar) {
    LC_Bigint_init_unsigned(dest, 0);

    LC_BigInt bi_64;
    LC_Bigint_init_unsigned(&bi_64, 64);

    uint64_t *op_digits = LC_Bigint_ptr(op);
    size_t    i         = op->digit_count - 1;

    while (1) {
        LC_BigInt shifted;
        LC_Bigint_shl(&shifted, dest, &bi_64);

        uint64_t result_scalar;
        uint64_t carry_scalar;
        mul_overflow(scalar, op_digits[i], &result_scalar, &carry_scalar);

        LC_BigInt result;
        LC_Bigint_init_unsigned(&result, result_scalar);

        LC_BigInt carry;
        LC_Bigint_init_unsigned(&carry, carry_scalar);

        LC_BigInt carry_shifted;
        LC_Bigint_shl(&carry_shifted, &carry, &bi_64);

        LC_BigInt tmp;
        LC_Bigint_add(&tmp, &shifted, &carry_shifted);

        LC_Bigint_add(dest, &tmp, &result);

        if (i == 0) {
            break;
        }
        i -= 1;
    }
}

LC_FUNCTION void LC_Bigint_mul(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    if (op1->digit_count == 0 || op2->digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }
    uint64_t *op1_digits = LC_Bigint_ptr(op1);
    uint64_t *op2_digits = LC_Bigint_ptr(op2);

    uint64_t carry;
    mul_overflow(op1_digits[0], op2_digits[0], &dest->digit, &carry);
    if (carry == 0 && op1->digit_count == 1 && op2->digit_count == 1) {
        dest->is_negative = (op1->is_negative != op2->is_negative);
        dest->digit_count = 1;
        LC_normalize(dest);
        return;
    }

    LC_Bigint_init_unsigned(dest, 0);

    LC_BigInt bi_64;
    LC_Bigint_init_unsigned(&bi_64, 64);

    size_t i = op2->digit_count - 1;
    for (;;) {
        LC_BigInt shifted;
        LC_Bigint_shl(&shifted, dest, &bi_64);

        LC_BigInt scalar_result;
        LC_mul_scalar(&scalar_result, op1, op2_digits[i]);

        LC_Bigint_add(dest, &scalar_result, &shifted);

        if (i == 0) {
            break;
        }
        i -= 1;
    }

    dest->is_negative = (op1->is_negative != op2->is_negative);
    LC_normalize(dest);
}

LC_FUNCTION void LC_Bigint_mul_wrap(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2, size_t bit_count, bool is_signed) {
    LC_BigInt unwrapped = {0};
    LC_Bigint_mul(&unwrapped, op1, op2);
    LC_Bigint_truncate(dest, &unwrapped, bit_count, is_signed);
}

LC_FUNCTION unsigned LC_count_leading_zeros(uint32_t val) {
    if (val == 0) return 32;

#if _MSC_VER
    unsigned long Index;
    _BitScanReverse(&Index, val);
    return Index ^ 31;
#else
    return __builtin_clz(val);
#endif
}

// Make a 64-bit integer from a high / low pair of 32-bit integers.
LC_FUNCTION uint64_t LC_make_64(uint32_t hi, uint32_t lo) {
    return (((uint64_t)hi) << 32) | ((uint64_t)lo);
}

// Return the high 32 bits of a 64 bit value.
LC_FUNCTION uint32_t LC_hi_32(uint64_t value) {
    return (uint32_t)(value >> 32);
}

// Return the low 32 bits of a 64 bit value.
LC_FUNCTION uint32_t LC_lo_32(uint64_t val) {
    return (uint32_t)val;
}

// Implementation of Knuth's Algorithm D (Division of nonnegative integers)
// from "Art of Computer Programming, Volume 2", section 4.3.1, p. 272. The
// variables here have the same names as in the algorithm. Comments explain
// the algorithm and any deviation from it.
LC_FUNCTION void LC_knuth_div(uint32_t *u, uint32_t *v, uint32_t *q, uint32_t *r, unsigned m, unsigned n) {
    LC_ASSERT(NULL, u && "Must provide dividend");
    LC_ASSERT(NULL, v && "Must provide divisor");
    LC_ASSERT(NULL, q && "Must provide quotient");
    LC_ASSERT(NULL, u != v && u != q && v != q && "Must use different memory");
    LC_ASSERT(NULL, n > 1 && "n must be > 1");

    // b denotes the base of the number system. In our case b is 2^32.
    uint64_t b = ((uint64_t)1) << 32;

    // D1. [Normalize.] Set d = b / (v[n-1] + 1) and multiply all the digits of
    // u and v by d. Note that we have taken Knuth's advice here to use a power
    // of 2 value for d such that d * v[n-1] >= b/2 (b is the base). A power of
    // 2 allows us to shift instead of multiply and it is easy to determine the
    // shift amount from the leading zeros.  We are basically normalizing the u
    // and v so that its high bits are shifted to the top of v's range without
    // overflow. Note that this can require an extra word in u so that u must
    // be of length m+n+1.
    unsigned shift   = LC_count_leading_zeros(v[n - 1]);
    uint32_t v_carry = 0;
    uint32_t u_carry = 0;
    if (shift) {
        for (unsigned i = 0; i < m + n; ++i) {
            uint32_t u_tmp = u[i] >> (32 - shift);
            u[i]           = (u[i] << shift) | u_carry;
            u_carry        = u_tmp;
        }
        for (unsigned i = 0; i < n; ++i) {
            uint32_t v_tmp = v[i] >> (32 - shift);
            v[i]           = (v[i] << shift) | v_carry;
            v_carry        = v_tmp;
        }
    }
    u[m + n] = u_carry;

    // D2. [Initialize j.]  Set j to m. This is the loop counter over the places.
    int j = (int)m;
    do {
        // D3. [Calculate q'.].
        //     Set qp = (u[j+n]*b + u[j+n-1]) / v[n-1]. (qp=qprime=q')
        //     Set rp = (u[j+n]*b + u[j+n-1]) % v[n-1]. (rp=rprime=r')
        // Now test if qp == b or qp*v[n-2] > b*rp + u[j+n-2]; if so, decrease
        // qp by 1, increase rp by v[n-1], and repeat this test if rp < b. The test
        // on v[n-2] determines at high speed most of the cases in which the trial
        // value qp is one too large, and it eliminates all cases where qp is two
        // too large.
        uint64_t dividend = LC_make_64(u[j + n], u[j + n - 1]);
        uint64_t qp       = dividend / v[n - 1];
        uint64_t rp       = dividend % v[n - 1];
        if (qp == b || qp * v[n - 2] > b * rp + u[j + n - 2]) {
            qp--;
            rp += v[n - 1];
            if (rp < b && (qp == b || qp * v[n - 2] > b * rp + u[j + n - 2])) {
                qp--;
            }
        }

        // D4. [Multiply and subtract.] Replace (u[j+n]u[j+n-1]...u[j]) with
        // (u[j+n]u[j+n-1]..u[j]) - qp * (v[n-1]...v[1]v[0]). This computation
        // consists of a simple multiplication by a one-place number, combined with
        // a subtraction.
        // The digits (u[j+n]...u[j]) should be kept positive; if the result of
        // this step is actually negative, (u[j+n]...u[j]) should be left as the
        // true value plus b**(n+1), namely as the b's complement of
        // the true value, and a "borrow" to the left should be remembered.
        int64_t borrow = 0;
        for (unsigned i = 0; i < n; ++i) {
            uint64_t p      = ((uint64_t)qp) * ((uint64_t)(v[i]));
            int64_t  subres = ((int64_t)(u[j + i])) - borrow - LC_lo_32(p);
            u[j + i]        = LC_lo_32((uint64_t)subres);
            borrow          = LC_hi_32(p) - LC_hi_32((uint64_t)subres);
        }
        bool is_neg = u[j + n] < borrow;
        u[j + n] -= LC_lo_32((uint64_t)borrow);

        // D5. [Test remainder.] Set q[j] = qp. If the result of step D4 was
        // negative, go to step D6; otherwise go on to step D7.
        q[j] = LC_lo_32(qp);
        if (is_neg) {
            // D6. [Add back]. The probability that this step is necessary is very
            // small, on the order of only 2/b. Make sure that test data accounts for
            // this possibility. Decrease q[j] by 1
            q[j]--;
            // and add (0v[n-1]...v[1]v[0]) to (u[j+n]u[j+n-1]...u[j+1]u[j]).
            // A carry will occur to the left of u[j+n], and it should be ignored
            // since it cancels with the borrow that occurred in D4.
            bool carry = false;
            for (unsigned i = 0; i < n; i++) {
                uint32_t limit = LC_u32_min(u[j + i], v[i]);
                u[j + i] += v[i] + carry;
                carry = u[j + i] < limit || (carry && u[j + i] == limit);
            }
            u[j + n] += carry;
        }

        // D7. [Loop on j.]  Decrease j by one. Now if j >= 0, go back to D3.
    } while (--j >= 0);

    // D8. [Unnormalize]. Now q[...] is the desired quotient, and the desired
    // remainder may be obtained by dividing u[...] by d. If r is non-null we
    // compute the remainder (urem uses this).
    if (r) {
        // The value d is expressed by the "shift" value above since we avoided
        // multiplication by d by using a shift left. So, all we have to do is
        // shift right here.
        if (shift) {
            uint32_t carry = 0;
            for (int i = (int)n - 1; i >= 0; i--) {
                r[i]  = (u[i] >> shift) | carry;
                carry = u[i] << (32 - shift);
            }
        } else {
            for (int i = (int)n - 1; i >= 0; i--) {
                r[i] = u[i];
            }
        }
    }
}

LC_FUNCTION void LC_Bigint_unsigned_division(LC_BigInt *op1, LC_BigInt *op2, LC_BigInt *Quotient, LC_BigInt *Remainder) {
    LC_CmpRes cmp = LC_Bigint_cmp(op1, op2);
    if (cmp == LC_CmpRes_LT) {
        if (!Quotient) {
            LC_Bigint_init_unsigned(Quotient, 0);
        }
        if (!Remainder) {
            LC_Bigint_init_bigint(Remainder, op1);
        }
        return;
    }
    if (cmp == LC_CmpRes_EQ) {
        if (!Quotient) {
            LC_Bigint_init_unsigned(Quotient, 1);
        }
        if (!Remainder) {
            LC_Bigint_init_unsigned(Remainder, 0);
        }
        return;
    }

    uint64_t *lhs      = LC_Bigint_ptr(op1);
    uint64_t *rhs      = LC_Bigint_ptr(op2);
    unsigned  lhsWords = op1->digit_count;
    unsigned  rhsWords = op2->digit_count;

    // First, compose the values into an array of 32-bit words instead of
    // 64-bit words. This is a necessity of both the "short division" algorithm
    // and the Knuth "classical algorithm" which requires there to be native
    // operations for +, -, and * on an m bit value with an m*2 bit result. We
    // can't use 64-bit operands here because we don't have native results of
    // 128-bits. Furthermore, casting the 64-bit values to 32-bit values won't
    // work on large-endian machines.
    unsigned n = rhsWords * 2;
    unsigned m = (lhsWords * 2) - n;

    // Allocate space for the temporary values we need either on the stack, if
    // it will fit, or on the heap if it won't.
    uint32_t  space[128];
    uint32_t *U = NULL;
    uint32_t *V = NULL;
    uint32_t *Q = NULL;
    uint32_t *R = NULL;
    if ((Remainder ? 4 : 3) * n + 2 * m + 1 <= 128) {
        U = &space[0];
        V = &space[m + n + 1];
        Q = &space[(m + n + 1) + n];
        if (Remainder) {
            R = &space[(m + n + 1) + n + (m + n)];
        }
    } else {
        U = (uint32_t *)malloc_arena(sizeof(uint32_t) * (m + n + 1));
        V = (uint32_t *)malloc_arena(sizeof(uint32_t) * n);
        Q = (uint32_t *)malloc_arena(sizeof(uint32_t) * (m + n));
        if (Remainder) {
            R = (uint32_t *)malloc_arena(sizeof(uint32_t) * n);
        }
    }

    // Initialize the dividend
    LC_MemoryZero(U, (m + n + 1) * sizeof(uint32_t));
    for (unsigned i = 0; i < lhsWords; ++i) {
        uint64_t tmp = lhs[i];
        U[i * 2]     = LC_lo_32(tmp);
        U[i * 2 + 1] = LC_hi_32(tmp);
    }
    U[m + n] = 0; // this extra word is for "spill" in the Knuth algorithm.

    // Initialize the divisor
    LC_MemoryZero(V, (n) * sizeof(uint32_t));
    for (unsigned i = 0; i < rhsWords; ++i) {
        uint64_t tmp = rhs[i];
        V[i * 2]     = LC_lo_32(tmp);
        V[i * 2 + 1] = LC_hi_32(tmp);
    }

    // initialize the quotient and remainder
    LC_MemoryZero(Q, (m + n) * sizeof(uint32_t));
    if (Remainder) LC_MemoryZero(R, n * sizeof(uint32_t));

    // Now, adjust m and n for the Knuth division. n is the number of words in
    // the divisor. m is the number of words by which the dividend exceeds the
    // divisor (i.e. m+n is the length of the dividend). These sizes must not
    // contain any zero words or the Knuth algorithm fails.
    for (unsigned i = n; i > 0 && V[i - 1] == 0; i--) {
        n--;
        m++;
    }
    for (unsigned i = m + n; i > 0 && U[i - 1] == 0; i--) {
        m--;
    }

    // If we're left with only a single word for the divisor, Knuth doesn't work
    // so we implement the short division algorithm here. This is much simpler
    // and faster because we are certain that we can divide a 64-bit quantity
    // by a 32-bit quantity at hardware speed and short division is simply a
    // series of such operations. This is just like doing short division but we
    // are using base 2^32 instead of base 10.
    LC_ASSERT(NULL, n != 0 && "Divide by zero?");
    if (n == 1) {
        uint32_t divisor = V[0];
        uint32_t rem     = 0;
        for (int i = (int)m; i >= 0; i--) {
            uint64_t partial_dividend = LC_make_64(rem, U[i]);
            if (partial_dividend == 0) {
                Q[i] = 0;
                rem  = 0;
            } else if (partial_dividend < divisor) {
                Q[i] = 0;
                rem  = LC_lo_32(partial_dividend);
            } else if (partial_dividend == divisor) {
                Q[i] = 1;
                rem  = 0;
            } else {
                Q[i] = LC_lo_32(partial_dividend / divisor);
                rem  = LC_lo_32(partial_dividend - (Q[i] * divisor));
            }
        }
        if (R) {
            R[0] = rem;
        }
    } else {
        // Now we're ready to invoke the Knuth classical divide algorithm. In this
        // case n > 1.
        LC_knuth_div(U, V, Q, R, m, n);
    }

    // If the caller wants the quotient
    if (Quotient) {
        Quotient->is_negative = false;
        Quotient->digit_count = lhsWords;
        if (lhsWords == 1) {
            Quotient->digit = LC_make_64(Q[1], Q[0]);
        } else {
            Quotient->digits = ALLOC_DIGITS(lhsWords);
            for (size_t i = 0; i < lhsWords; i += 1) {
                Quotient->digits[i] = LC_make_64(Q[i * 2 + 1], Q[i * 2]);
            }
        }
    }

    // If the caller wants the remainder
    if (Remainder) {
        Remainder->is_negative = false;
        Remainder->digit_count = rhsWords;
        if (rhsWords == 1) {
            Remainder->digit = LC_make_64(R[1], R[0]);
        } else {
            Remainder->digits = ALLOC_DIGITS(rhsWords);
            for (size_t i = 0; i < rhsWords; i += 1) {
                Remainder->digits[i] = LC_make_64(R[i * 2 + 1], R[i * 2]);
            }
        }
    }
}

LC_FUNCTION void LC_Bigint_div_trunc(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    LC_ASSERT(NULL, op2->digit_count != 0); // division by zero
    if (op1->digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }
    uint64_t *op1_digits = LC_Bigint_ptr(op1);
    uint64_t *op2_digits = LC_Bigint_ptr(op2);
    if (op1->digit_count == 1 && op2->digit_count == 1) {
        dest->digit       = op1_digits[0] / op2_digits[0];
        dest->digit_count = 1;
        dest->is_negative = op1->is_negative != op2->is_negative;
        LC_normalize(dest);
        return;
    }
    if (op2->digit_count == 1 && op2_digits[0] == 1) {
        // X / 1 == X
        LC_Bigint_init_bigint(dest, op1);
        dest->is_negative = op1->is_negative != op2->is_negative;
        LC_normalize(dest);
        return;
    }

    LC_BigInt *op1_positive;
    LC_BigInt  op1_positive_data;
    if (op1->is_negative) {
        LC_Bigint_negate(&op1_positive_data, op1);
        op1_positive = &op1_positive_data;
    } else {
        op1_positive = op1;
    }

    LC_BigInt *op2_positive;
    LC_BigInt  op2_positive_data;
    if (op2->is_negative) {
        LC_Bigint_negate(&op2_positive_data, op2);
        op2_positive = &op2_positive_data;
    } else {
        op2_positive = op2;
    }

    LC_Bigint_unsigned_division(op1_positive, op2_positive, dest, NULL);
    dest->is_negative = op1->is_negative != op2->is_negative;
    LC_normalize(dest);
}

LC_FUNCTION void LC_Bigint_div_floor(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    if (op1->is_negative != op2->is_negative) {
        LC_Bigint_div_trunc(dest, op1, op2);
        LC_BigInt mult_again = {0};
        LC_Bigint_mul(&mult_again, dest, op2);
        mult_again.is_negative = op1->is_negative;
        if (LC_Bigint_cmp(&mult_again, op1) != LC_CmpRes_EQ) {
            LC_BigInt tmp = {0};
            LC_Bigint_init_bigint(&tmp, dest);
            LC_BigInt neg_one = {0};
            LC_Bigint_init_signed(&neg_one, -1);
            LC_Bigint_add(dest, &tmp, &neg_one);
        }
        LC_normalize(dest);
    } else {
        LC_Bigint_div_trunc(dest, op1, op2);
    }
}

LC_FUNCTION void LC_Bigint_rem(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    LC_ASSERT(NULL, op2->digit_count != 0); // division by zero
    if (op1->digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }
    uint64_t *op1_digits = LC_Bigint_ptr(op1);
    uint64_t *op2_digits = LC_Bigint_ptr(op2);

    if (op1->digit_count == 1 && op2->digit_count == 1) {
        dest->digit       = op1_digits[0] % op2_digits[0];
        dest->digit_count = 1;
        dest->is_negative = op1->is_negative;
        LC_normalize(dest);
        return;
    }
    if (op2->digit_count == 2 && op2_digits[0] == 0 && op2_digits[1] == 1) {
        // special case this divisor
        LC_Bigint_init_unsigned(dest, op1_digits[0]);
        dest->is_negative = op1->is_negative;
        LC_normalize(dest);
        return;
    }

    if (op2->digit_count == 1 && op2_digits[0] == 1) {
        // X % 1 == 0
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }

    LC_BigInt *op1_positive;
    LC_BigInt  op1_positive_data;
    if (op1->is_negative) {
        LC_Bigint_negate(&op1_positive_data, op1);
        op1_positive = &op1_positive_data;
    } else {
        op1_positive = op1;
    }

    LC_BigInt *op2_positive;
    LC_BigInt  op2_positive_data;
    if (op2->is_negative) {
        LC_Bigint_negate(&op2_positive_data, op2);
        op2_positive = &op2_positive_data;
    } else {
        op2_positive = op2;
    }

    LC_Bigint_unsigned_division(op1_positive, op2_positive, NULL, dest);
    dest->is_negative = op1->is_negative;
    LC_normalize(dest);
}

LC_FUNCTION void LC_Bigint_mod(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    if (op1->is_negative) {
        LC_BigInt first_rem;
        LC_Bigint_rem(&first_rem, op1, op2);
        first_rem.is_negative = !op2->is_negative;
        LC_BigInt op2_minus_rem;
        LC_Bigint_add(&op2_minus_rem, op2, &first_rem);
        LC_Bigint_rem(dest, &op2_minus_rem, op2);
        dest->is_negative = false;
    } else {
        LC_Bigint_rem(dest, op1, op2);
        dest->is_negative = false;
    }
}

LC_FUNCTION void LC_Bigint_or(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    if (op1->digit_count == 0) {
        LC_Bigint_init_bigint(dest, op2);
        return;
    }
    if (op2->digit_count == 0) {
        LC_Bigint_init_bigint(dest, op1);
        return;
    }
    if (op1->is_negative || op2->is_negative) {
        size_t big_bit_count = LC_size_max(LC_Bigint_bits_needed(op1), LC_Bigint_bits_needed(op2));

        LC_BigInt twos_comp_op1 = {0};
        LC_to_twos_complement(&twos_comp_op1, op1, big_bit_count);

        LC_BigInt twos_comp_op2 = {0};
        LC_to_twos_complement(&twos_comp_op2, op2, big_bit_count);

        LC_BigInt twos_comp_dest = {0};
        LC_Bigint_or(&twos_comp_dest, &twos_comp_op1, &twos_comp_op2);

        LC_from_twos_complement(dest, &twos_comp_dest, big_bit_count, true);
    } else {
        dest->is_negative    = false;
        uint64_t *op1_digits = LC_Bigint_ptr(op1);
        uint64_t *op2_digits = LC_Bigint_ptr(op2);
        if (op1->digit_count == 1 && op2->digit_count == 1) {
            dest->digit_count = 1;
            dest->digit       = op1_digits[0] | op2_digits[0];
            LC_normalize(dest);
            return;
        }
        dest->digit_count = LC_unsigned_max(op1->digit_count, op2->digit_count);
        dest->digits      = ALLOC_DIGITS(dest->digit_count);
        for (size_t i = 0; i < dest->digit_count; i += 1) {
            uint64_t digit = 0;
            if (i < op1->digit_count) {
                digit |= op1_digits[i];
            }
            if (i < op2->digit_count) {
                digit |= op2_digits[i];
            }
            dest->digits[i] = digit;
        }
        LC_normalize(dest);
    }
}

LC_FUNCTION void LC_Bigint_and(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    if (op1->digit_count == 0 || op2->digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }
    if (op1->is_negative || op2->is_negative) {
        size_t big_bit_count = LC_size_max(LC_Bigint_bits_needed(op1), LC_Bigint_bits_needed(op2));

        LC_BigInt twos_comp_op1 = {0};
        LC_to_twos_complement(&twos_comp_op1, op1, big_bit_count);

        LC_BigInt twos_comp_op2 = {0};
        LC_to_twos_complement(&twos_comp_op2, op2, big_bit_count);

        LC_BigInt twos_comp_dest = {0};
        LC_Bigint_and(&twos_comp_dest, &twos_comp_op1, &twos_comp_op2);

        LC_from_twos_complement(dest, &twos_comp_dest, big_bit_count, true);
    } else {
        dest->is_negative    = false;
        uint64_t *op1_digits = LC_Bigint_ptr(op1);
        uint64_t *op2_digits = LC_Bigint_ptr(op2);
        if (op1->digit_count == 1 && op2->digit_count == 1) {
            dest->digit_count = 1;
            dest->digit       = op1_digits[0] & op2_digits[0];
            LC_normalize(dest);
            return;
        }

        dest->digit_count = LC_unsigned_max(op1->digit_count, op2->digit_count);
        dest->digits      = ALLOC_DIGITS(dest->digit_count);

        size_t i = 0;
        for (; i < op1->digit_count && i < op2->digit_count; i += 1) {
            dest->digits[i] = op1_digits[i] & op2_digits[i];
        }
        for (; i < dest->digit_count; i += 1) {
            dest->digits[i] = 0;
        }
        LC_normalize(dest);
    }
}

LC_FUNCTION void LC_Bigint_xor(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    if (op1->digit_count == 0) {
        LC_Bigint_init_bigint(dest, op2);
        return;
    }
    if (op2->digit_count == 0) {
        LC_Bigint_init_bigint(dest, op1);
        return;
    }
    if (op1->is_negative || op2->is_negative) {
        size_t big_bit_count = LC_size_max(LC_Bigint_bits_needed(op1), LC_Bigint_bits_needed(op2));

        LC_BigInt twos_comp_op1 = {0};
        LC_to_twos_complement(&twos_comp_op1, op1, big_bit_count);

        LC_BigInt twos_comp_op2 = {0};
        LC_to_twos_complement(&twos_comp_op2, op2, big_bit_count);

        LC_BigInt twos_comp_dest = {0};
        LC_Bigint_xor(&twos_comp_dest, &twos_comp_op1, &twos_comp_op2);

        LC_from_twos_complement(dest, &twos_comp_dest, big_bit_count, true);
    } else {
        dest->is_negative    = false;
        uint64_t *op1_digits = LC_Bigint_ptr(op1);
        uint64_t *op2_digits = LC_Bigint_ptr(op2);

        LC_ASSERT(NULL, op1->digit_count > 0 && op2->digit_count > 0);
        if (op1->digit_count == 1 && op2->digit_count == 1) {
            dest->digit_count = 1;
            dest->digit       = op1_digits[0] ^ op2_digits[0];
            LC_normalize(dest);
            return;
        }
        dest->digit_count = LC_unsigned_max(op1->digit_count, op2->digit_count);
        dest->digits      = ALLOC_DIGITS(dest->digit_count);
        size_t i          = 0;
        for (; i < op1->digit_count && i < op2->digit_count; i += 1) {
            dest->digits[i] = op1_digits[i] ^ op2_digits[i];
        }
        for (; i < dest->digit_count; i += 1) {
            if (i < op1->digit_count) {
                dest->digits[i] = op1_digits[i];
            } else if (i < op2->digit_count) {
                dest->digits[i] = op2_digits[i];
            } else {
                LC_ASSERT(NULL, !"Unreachable");
            }
        }
        LC_normalize(dest);
    }
}

LC_FUNCTION void LC_Bigint_shl(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    LC_ASSERT(NULL, !op2->is_negative);

    if (op2->digit_count == 0) {
        return;
    }
    if (op2->digit_count != 1) {
        LC_ASSERT(NULL, !"Unsupported: shift left by amount greater than 64 bit integer");
    }
    LC_Bigint_shl_int(dest, op1, LC_Bigint_as_unsigned(op2));
}

LC_FUNCTION void LC_Bigint_shl_int(LC_BigInt *dest, LC_BigInt *op1, uint64_t shift) {
    if (shift == 0) {
        LC_Bigint_init_bigint(dest, op1);
        return;
    }

    if (op1->digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }

    uint64_t *op1_digits = LC_Bigint_ptr(op1);

    if (op1->digit_count == 1 && shift < 64) {
        dest->digit = op1_digits[0] << shift;
        if (dest->digit > op1_digits[0]) {
            dest->digit_count = 1;
            dest->is_negative = op1->is_negative;
            return;
        }
    }

    uint64_t digit_shift_count    = shift / 64;
    uint64_t leftover_shift_count = shift % 64;

    dest->digits      = ALLOC_DIGITS(op1->digit_count + digit_shift_count + 1);
    dest->digit_count = (unsigned)digit_shift_count;
    uint64_t carry    = 0;
    for (size_t i = 0; i < op1->digit_count; i += 1) {
        uint64_t digit                  = op1_digits[i];
        dest->digits[dest->digit_count] = carry | (digit << leftover_shift_count);
        dest->digit_count++;
        if (leftover_shift_count > 0) {
            carry = digit >> (64 - leftover_shift_count);
        } else {
            carry = 0;
        }
    }
    dest->digits[dest->digit_count] = carry;
    dest->digit_count += 1;
    dest->is_negative = op1->is_negative;
    LC_normalize(dest);
}

LC_FUNCTION void LC_Bigint_shl_trunc(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2, size_t bit_count, bool is_signed) {
    LC_BigInt unwrapped = {0};
    LC_Bigint_shl(&unwrapped, op1, op2);
    LC_Bigint_truncate(dest, &unwrapped, bit_count, is_signed);
}

LC_FUNCTION void LC_Bigint_shr(LC_BigInt *dest, LC_BigInt *op1, LC_BigInt *op2) {
    LC_ASSERT(NULL, !op2->is_negative);

    if (op1->digit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }

    if (op2->digit_count == 0) {
        LC_Bigint_init_bigint(dest, op1);
        return;
    }

    if (op2->digit_count != 1) {
        LC_ASSERT(NULL, !"Unsupported: shift right by amount greater than 64 bit integer");
    }

    uint64_t *op1_digits = LC_Bigint_ptr(op1);
    uint64_t  shift_amt  = LC_Bigint_as_unsigned(op2);

    if (op1->digit_count == 1) {
        dest->digit       = shift_amt < 64 ? op1_digits[0] >> shift_amt : 0;
        dest->digit_count = 1;
        dest->is_negative = op1->is_negative;
        LC_normalize(dest);
        return;
    }

    uint64_t digit_shift_count    = shift_amt / 64;
    uint64_t leftover_shift_count = shift_amt % 64;

    if (digit_shift_count >= op1->digit_count) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }

    dest->digit_count = (unsigned)(op1->digit_count - digit_shift_count);
    uint64_t *digits;
    if (dest->digit_count == 1) {
        digits = &dest->digit;
    } else {
        digits       = ALLOC_DIGITS(dest->digit_count);
        dest->digits = digits;
    }

    uint64_t carry = 0;
    for (size_t op_digit_index = op1->digit_count - 1;;) {
        uint64_t digit            = op1_digits[op_digit_index];
        size_t   dest_digit_index = op_digit_index - digit_shift_count;
        digits[dest_digit_index]  = carry | (digit >> leftover_shift_count);
        carry                     = digit << (64 - leftover_shift_count);

        if (dest_digit_index == 0) break;
        op_digit_index -= 1;
    }
    dest->is_negative = op1->is_negative;
    LC_normalize(dest);
}

LC_FUNCTION void LC_Bigint_not(LC_BigInt *dest, LC_BigInt *op, size_t bit_count, bool is_signed) {
    if (bit_count == 0) {
        LC_Bigint_init_unsigned(dest, 0);
        return;
    }

    if (is_signed) {
        LC_BigInt twos_comp = {0};
        LC_to_twos_complement(&twos_comp, op, bit_count);

        LC_BigInt inverted = {0};
        LC_Bigint_not(&inverted, &twos_comp, bit_count, false);

        LC_from_twos_complement(dest, &inverted, bit_count, true);
        return;
    }

    LC_ASSERT(NULL, !op->is_negative);

    dest->is_negative   = false;
    uint64_t *op_digits = LC_Bigint_ptr(op);
    if (bit_count <= 64) {
        dest->digit_count = 1;
        if (op->digit_count == 0) {
            if (bit_count == 64) {
                dest->digit = UINT64_MAX;
            } else {
                dest->digit = (1ULL << bit_count) - 1;
            }
        } else if (op->digit_count == 1) {
            dest->digit = ~op_digits[0];
            if (bit_count != 64) {
                uint64_t
                    mask = (1ULL << bit_count) - 1;
                dest->digit &= mask;
            }
        }
        LC_normalize(dest);
        return;
    }
    dest->digit_count = (unsigned int)((bit_count + 63) / 64);
    LC_ASSERT(NULL, dest->digit_count >= op->digit_count);
    dest->digits = ALLOC_DIGITS(dest->digit_count);
    size_t i     = 0;
    for (; i < op->digit_count; i += 1) {
        dest->digits[i] = ~op_digits[i];
    }
    for (; i < dest->digit_count; i += 1) {
        dest->digits[i] = 0xffffffffffffffffULL;
    }
    size_t digit_index     = dest->digit_count - 1;
    size_t digit_bit_index = bit_count % 64;
    if (digit_bit_index != 0) {
        uint64_t
            mask = (1ULL << digit_bit_index) - 1;
        dest->digits[digit_index] &= mask;
    }
    LC_normalize(dest);
}

LC_FUNCTION void LC_Bigint_truncate(LC_BigInt *dst, LC_BigInt *op, size_t bit_count, bool is_signed) {
    LC_BigInt twos_comp;
    LC_to_twos_complement(&twos_comp, op, bit_count);
    LC_from_twos_complement(dst, &twos_comp, bit_count, is_signed);
}

LC_FUNCTION LC_CmpRes LC_Bigint_cmp(LC_BigInt *op1, LC_BigInt *op2) {
    if (op1->is_negative && !op2->is_negative) return LC_CmpRes_LT;
    if (!op1->is_negative && op2->is_negative) return LC_CmpRes_GT;
    if (op1->digit_count > op2->digit_count) return op1->is_negative ? LC_CmpRes_LT : LC_CmpRes_GT;
    if (op2->digit_count > op1->digit_count) return op1->is_negative ? LC_CmpRes_GT : LC_CmpRes_LT;
    if (op1->digit_count == 0) return LC_CmpRes_EQ;

    uint64_t *op1_digits = LC_Bigint_ptr(op1);
    uint64_t *op2_digits = LC_Bigint_ptr(op2);
    for (unsigned i = op1->digit_count - 1;; i--) {
        uint64_t op1_digit = op1_digits[i];
        uint64_t op2_digit = op2_digits[i];

        if (op1_digit > op2_digit) {
            return op1->is_negative ? LC_CmpRes_LT : LC_CmpRes_GT;
        }
        if (op1_digit < op2_digit) {
            return op1->is_negative ? LC_CmpRes_GT : LC_CmpRes_LT;
        }
        if (i == 0) {
            return LC_CmpRes_EQ;
        }
    }
}

LC_FUNCTION char *LC_Bigint_str(LC_BigInt *bigint, uint64_t base) {
    LC_StringList out = {0};
    if (bigint->digit_count == 0) {
        return "0";
    }
    if (bigint->is_negative) {
        LC_Addf(L->arena, &out, "-");
    }
    if (bigint->digit_count == 1 && base == 10) {
        LC_Addf(L->arena, &out, "%llu", (unsigned long long)bigint->digit);
    } else {
        size_t len   = bigint->digit_count * 64;
        char  *start = (char *)malloc_arena(len);
        char  *buf   = start;

        LC_BigInt  digit_bi = {0};
        LC_BigInt  a1       = {0};
        LC_BigInt  a2       = {0};
        LC_BigInt  base_bi  = {0};
        LC_BigInt *a        = &a1;
        LC_BigInt *other_a  = &a2;

        LC_Bigint_init_bigint(a, bigint);
        LC_Bigint_init_unsigned(&base_bi, base);

        for (;;) {
            LC_Bigint_rem(&digit_bi, a, &base_bi);
            uint8_t digit = (uint8_t)LC_Bigint_as_unsigned(&digit_bi);
            *(buf++)      = LC_digit_to_char(digit, false);
            LC_Bigint_div_trunc(other_a, a, &base_bi);
            {
                LC_BigInt *tmp = a;
                a              = other_a;
                other_a        = tmp;
            }
            if (LC_Bigint_cmp_zero(a) == LC_CmpRes_EQ) {
                break;
            }
        }

        // reverse

        for (char *ptr = buf - 1; ptr >= start; ptr--) {
            LC_Addf(L->arena, &out, "%c", *ptr);
        }
    }
    LC_String s = LC_MergeString(L->arena, out);
    return s.str;
}

LC_FUNCTION int64_t LC_Bigint_as_signed(LC_BigInt *bigint) {
    if (bigint->digit_count == 0) return 0;
    if (bigint->digit_count != 1) {
        LC_ASSERT(NULL, !"LC_BigInt larger than i64");
    }

    if (bigint->is_negative) {
        // TODO this code path is untested
        if (bigint->digit <= 9223372036854775808ULL) {
            return (-((int64_t)(bigint->digit - 1))) - 1;
        }
        LC_ASSERT(NULL, !"LC_BigInt does not fit in i64");
    }
    return (int64_t)bigint->digit;
}

LC_FUNCTION LC_CmpRes LC_Bigint_cmp_zero(LC_BigInt *op) {
    if (op->digit_count == 0) {
        return LC_CmpRes_EQ;
    }
    return op->is_negative ? LC_CmpRes_LT : LC_CmpRes_GT;
}

LC_FUNCTION double LC_Bigint_as_float(LC_BigInt *bigint) {
    if (LC_Bigint_fits_in_bits(bigint, 64, bigint->is_negative)) {
        return bigint->is_negative ? (double)LC_Bigint_as_signed(bigint) : (double)LC_Bigint_as_unsigned(bigint);
    }
    LC_BigInt div;
    uint64_t  mult = 0x100000000000ULL;
    double    mul  = 1;
    LC_Bigint_init_unsigned(&div, mult);
    LC_BigInt current;
    LC_Bigint_init_bigint(&current, bigint);
    double f = 0;
    do {
        LC_BigInt temp;
        LC_Bigint_mod(&temp, &current, &div);
        f += LC_Bigint_as_signed(&temp) * mul;
        mul *= mult;
        LC_Bigint_div_trunc(&temp, &current, &div);
        current = temp;
    } while (current.digit_count > 0);
    return f;
}
LC_Operand LC_OPNull;

LC_FUNCTION LC_Operand LC_OPError(void) {
    LC_Operand result = {LC_OPF_Error};
    return result;
}

LC_FUNCTION LC_Operand LC_OPConstType(LC_Type *type) {
    LC_Operand result = {LC_OPF_UTConst | LC_OPF_Const};
    result.type       = type;
    return result;
}

LC_FUNCTION LC_Operand LC_OPDecl(LC_Decl *decl) {
    LC_Operand result = {0};
    result.decl       = decl;
    return result;
}

LC_FUNCTION LC_Operand LC_OPType(LC_Type *type) {
    LC_Operand result = {0};
    result.type       = type;
    return result;
}

LC_FUNCTION LC_Operand LC_OPLValueAndType(LC_Type *type) {
    LC_Operand result = LC_OPType(type);
    result.flags      = LC_OPF_LValue;
    return result;
}

LC_FUNCTION LC_Operand LC_ReportASTError(LC_AST *n, const char *str, ...) {
    LC_FORMAT(L->arena, str, s8);
    LC_SendErrorMessage(n ? n->pos : NULL, s8);
    L->errors += 1;
    return LC_OPError();
}

LC_FUNCTION LC_Operand LC_ReportASTErrorEx(LC_AST *n1, LC_AST *n2, const char *str, ...) {
    LC_FORMAT(L->arena, str, s8);
    LC_SendErrorMessage(n1->pos, s8);
    LC_SendErrorMessage(n2->pos, s8);
    L->errors += 1;
    return LC_OPError();
}

LC_FUNCTION LC_Operand LC_ConstCastFloat(LC_AST *pos, LC_Operand op) {
    LC_ASSERT(pos, LC_IsUTConst(op));
    LC_ASSERT(pos, LC_IsUntyped(op.type));
    if (LC_IsUTInt(op.type)) op.val.d = LC_Bigint_as_float(&op.val.i);
    if (LC_IsUTStr(op.type)) return LC_ReportASTError(pos, "Trying to convert '%s' to float", LC_GenLCType(op.type));
    op.type = L->tuntypedfloat;
    return op;
}

LC_FUNCTION LC_Operand LC_ConstCastInt(LC_AST *pos, LC_Operand op) {
    LC_ASSERT(pos, LC_IsUTConst(op));
    LC_ASSERT(pos, LC_IsUntyped(op.type));
    if (LC_IsUTFloat(op.type)) {
        double v = op.val.d; // add rounding?
        LC_Bigint_init_signed(&op.val.i, (int64_t)v);
    }
    if (LC_IsUTStr(op.type)) return LC_ReportASTError(pos, "Trying to convert %s to int", LC_GenLCType(op.type));
    op.type = L->tuntypedint;
    return op;
}

LC_FUNCTION LC_Operand LC_OPInt(int64_t v) {
    LC_Operand op = {0};
    op.type       = L->tuntypedint;
    op.flags |= LC_OPF_UTConst | LC_OPF_Const;
    LC_Bigint_init_signed(&op.v.i, v);
    return op;
}

LC_FUNCTION LC_Operand LC_OPIntT(LC_Type *type, int64_t v) {
    LC_ASSERT(NULL, LC_IsInt(type));
    LC_Operand op = LC_OPInt(v);
    op.type       = type;
    return op;
}

LC_FUNCTION LC_Operand LC_OPModDefaultUT(LC_Operand val) {
    if (LC_IsUntyped(val.type)) {
        val.type = val.type->tbase;
    }
    return val;
}

LC_FUNCTION LC_Operand LC_OPModType(LC_Operand op, LC_Type *type) {
    if (LC_IsUTConst(op)) {
        if (LC_IsUTInt(op.type) && LC_IsFloat(type)) {
            op = LC_ConstCastFloat(NULL, op);
        }
    }
    op.type = type;
    return op;
}

LC_FUNCTION LC_Operand LC_OPModBool(LC_Operand op) {
    op.type = L->tuntypedbool;
    return op;
}

LC_FUNCTION LC_Operand LC_OPModBoolV(LC_Operand op, int v) {
    op.type = L->tuntypedbool;
    LC_Bigint_init_signed(&op.v.i, v);
    return op;
}

LC_FUNCTION LC_Operand LC_EvalBinary(LC_AST *pos, LC_Operand a, LC_TokenKind op, LC_Operand b) {
    LC_ASSERT(pos, LC_IsUTConst(a));
    LC_ASSERT(pos, LC_IsUTConst(b));
    LC_ASSERT(pos, LC_IsUntyped(a.type));
    LC_ASSERT(pos, LC_IsUntyped(b.type));
    LC_ASSERT(pos, a.type->kind == b.type->kind);

    LC_Operand c = LC_OPConstType(a.type);
    if (LC_IsUTStr(a.type)) {
        return LC_ReportASTError(pos, "invalid operand %s for binary expr of type untyped string", LC_TokenKindToString(op));
    }
    if (LC_IsUTFloat(a.type)) {
        switch (op) {
        case LC_TokenKind_Add: c.v.d = a.v.d + b.v.d; break;
        case LC_TokenKind_Sub: c.v.d = a.v.d - b.v.d; break;
        case LC_TokenKind_Mul: c.v.d = a.v.d * b.v.d; break;
        case LC_TokenKind_Div: {
            if (b.v.d == 0.0) return LC_ReportASTError(pos, "division by 0");
            c.v.d = a.v.d / b.v.d;
        } break;
        case LC_TokenKind_LesserThenEq: c = LC_OPModBoolV(c, a.v.d <= b.v.d); break;
        case LC_TokenKind_GreaterThenEq: c = LC_OPModBoolV(c, a.v.d >= b.v.d); break;
        case LC_TokenKind_GreaterThen: c = LC_OPModBoolV(c, a.v.d > b.v.d); break;
        case LC_TokenKind_LesserThen: c = LC_OPModBoolV(c, a.v.d < b.v.d); break;
        case LC_TokenKind_Equals: c = LC_OPModBoolV(c, a.v.d == b.v.d); break;
        case LC_TokenKind_NotEquals: c = LC_OPModBoolV(c, a.v.d != b.v.d); break;
        default: return LC_ReportASTError(pos, "invalid operand %s for binary expr of type untyped float", LC_TokenKindToString(op));
        }
    }
    if (LC_IsUTInt(a.type)) {
        switch (op) {
        case LC_TokenKind_BitXor: LC_Bigint_xor(&c.v.i, &a.v.i, &b.v.i); break;
        case LC_TokenKind_BitAnd: LC_Bigint_and(&c.v.i, &a.v.i, &b.v.i); break;
        case LC_TokenKind_BitOr: LC_Bigint_or(&c.v.i, &a.v.i, &b.v.i); break;
        case LC_TokenKind_Add: LC_Bigint_add(&c.v.i, &a.v.i, &b.v.i); break;
        case LC_TokenKind_Sub: LC_Bigint_sub(&c.v.i, &a.v.i, &b.v.i); break;
        case LC_TokenKind_Mul: LC_Bigint_mul(&c.v.i, &a.v.i, &b.v.i); break;
        case LC_TokenKind_Div: {
            if (b.v.i.digit_count == 0) return LC_ReportASTError(pos, "division by zero in constant expression");
            LC_Bigint_div_floor(&c.v.i, &a.v.i, &b.v.i);
        } break;
        case LC_TokenKind_Mod: {
            if (b.v.i.digit_count == 0) return LC_ReportASTError(pos, "modulo by zero in constant expression");
            LC_Bigint_mod(&c.v.i, &a.v.i, &b.v.i);
        } break;
        case LC_TokenKind_LeftShift: LC_Bigint_shl(&c.v.i, &a.v.i, &b.v.i); break;
        case LC_TokenKind_RightShift: LC_Bigint_shr(&c.v.i, &a.v.i, &b.v.i); break;
        case LC_TokenKind_And: {
            int left  = LC_Bigint_cmp_zero(&a.v.i) != LC_CmpRes_EQ;
            int right = LC_Bigint_cmp_zero(&b.v.i) != LC_CmpRes_EQ;
            c         = LC_OPModBoolV(c, left && right);
        } break;
        case LC_TokenKind_Or: {
            int left  = LC_Bigint_cmp_zero(&a.v.i) != LC_CmpRes_EQ;
            int right = LC_Bigint_cmp_zero(&b.v.i) != LC_CmpRes_EQ;
            c         = LC_OPModBoolV(c, left || right);
        } break;
        default: {
            LC_CmpRes cmp = LC_Bigint_cmp(&a.v.i, &b.v.i);
            switch (op) {
            case LC_TokenKind_LesserThenEq: c = LC_OPModBoolV(c, (cmp == LC_CmpRes_LT) || (cmp == LC_CmpRes_EQ)); break;
            case LC_TokenKind_GreaterThenEq: c = LC_OPModBoolV(c, (cmp == LC_CmpRes_GT) || (cmp == LC_CmpRes_EQ)); break;
            case LC_TokenKind_GreaterThen: c = LC_OPModBoolV(c, cmp == LC_CmpRes_GT); break;
            case LC_TokenKind_LesserThen: c = LC_OPModBoolV(c, cmp == LC_CmpRes_LT); break;
            case LC_TokenKind_Equals: c = LC_OPModBoolV(c, cmp == LC_CmpRes_EQ); break;
            case LC_TokenKind_NotEquals: c = LC_OPModBoolV(c, cmp != LC_CmpRes_EQ); break;
            default: return LC_ReportASTError(pos, "invalid operand %s for binary expr of type untyped int", LC_TokenKindToString(op));
            }
        }
        }
    }

    return c;
}

LC_FUNCTION LC_Operand LC_EvalUnary(LC_AST *pos, LC_TokenKind op, LC_Operand a) {
    LC_ASSERT(pos, LC_IsUTConst(a));
    LC_ASSERT(pos, LC_IsUntyped(a.type));
    LC_Operand c = a;

    if (LC_IsUTStr(a.type)) {
        return LC_ReportASTError(pos, "invalid operand %s for unary expr of type untyped string", LC_TokenKindToString(op));
    }
    if (LC_IsUTFloat(a.type)) {
        switch (op) {
        case LC_TokenKind_Sub: c.v.d = -a.v.d; break;
        case LC_TokenKind_Add: c.v.d = +a.v.d; break;
        default: return LC_ReportASTError(pos, "invalid operand %s for unary expr of type untyped float", LC_TokenKindToString(op));
        }
    }
    if (LC_IsUTInt(a.type)) {
        switch (op) {
        case LC_TokenKind_Not: c = LC_OPModBoolV(c, LC_Bigint_cmp_zero(&a.v.i) == LC_CmpRes_EQ); break;
        case LC_TokenKind_Sub: LC_Bigint_negate(&c.v.i, &a.v.i); break;
        case LC_TokenKind_Add: c = a; break;
        case LC_TokenKind_Neg: LC_Bigint_not(&c.v.i, &a.v.i, a.type->tbase->size * 8, !a.type->tbase->is_unsigned); break;
        default: return LC_ReportASTError(pos, "invalid operand %s for unary expr of type untyped int", LC_TokenKindToString(op));
        }
    }

    return c;
}

LC_FUNCTION bool LC_BigIntFits(LC_BigInt i, LC_Type *type) {
    LC_ASSERT(NULL, LC_IsInt(type));
    if (!LC_Bigint_fits_in_bits(&i, type->size * 8, !type->is_unsigned)) {
        return false;
    }
    return true;
}

LC_FUNCTION LC_OPResult LC_IsBinaryExprValidForType(LC_TokenKind op, LC_Type *type) {
    if (LC_IsFloat(type)) {
        switch (op) {
        case LC_TokenKind_Add: return LC_OPResult_Ok; break;
        case LC_TokenKind_Sub: return LC_OPResult_Ok; break;
        case LC_TokenKind_Mul: return LC_OPResult_Ok; break;
        case LC_TokenKind_Div: return LC_OPResult_Ok; break;
        case LC_TokenKind_LesserThenEq: return LC_OPResult_Bool; break;
        case LC_TokenKind_GreaterThenEq: return LC_OPResult_Bool; break;
        case LC_TokenKind_GreaterThen: return LC_OPResult_Bool; break;
        case LC_TokenKind_LesserThen: return LC_OPResult_Bool; break;
        case LC_TokenKind_Equals: return LC_OPResult_Bool; break;
        case LC_TokenKind_NotEquals: return LC_OPResult_Bool; break;
        default: return LC_OPResult_Error;
        }
    }
    if (LC_IsInt(type)) {
        switch (op) {
        case LC_TokenKind_BitXor: return LC_OPResult_Ok; break;
        case LC_TokenKind_BitAnd: return LC_OPResult_Ok; break;
        case LC_TokenKind_BitOr: return LC_OPResult_Ok; break;
        case LC_TokenKind_Add: return LC_OPResult_Ok; break;
        case LC_TokenKind_Sub: return LC_OPResult_Ok; break;
        case LC_TokenKind_Mul: return LC_OPResult_Ok; break;
        case LC_TokenKind_Div: return LC_OPResult_Ok; break;
        case LC_TokenKind_Mod: return LC_OPResult_Ok; break;
        case LC_TokenKind_LeftShift: return LC_OPResult_Ok; break;
        case LC_TokenKind_RightShift: return LC_OPResult_Ok; break;
        case LC_TokenKind_And: return LC_OPResult_Bool; break;
        case LC_TokenKind_Or: return LC_OPResult_Bool; break;
        case LC_TokenKind_LesserThenEq: return LC_OPResult_Bool; break;
        case LC_TokenKind_GreaterThenEq: return LC_OPResult_Bool; break;
        case LC_TokenKind_GreaterThen: return LC_OPResult_Bool; break;
        case LC_TokenKind_LesserThen: return LC_OPResult_Bool; break;
        case LC_TokenKind_Equals: return LC_OPResult_Bool; break;
        case LC_TokenKind_NotEquals: return LC_OPResult_Bool; break;
        default: return LC_OPResult_Error;
        }
    }
    if (LC_IsPtrLike(type)) {
        switch (op) {
        case LC_TokenKind_And: return LC_OPResult_Bool; break;
        case LC_TokenKind_Or: return LC_OPResult_Bool; break;
        case LC_TokenKind_LesserThenEq: return LC_OPResult_Bool; break;
        case LC_TokenKind_GreaterThenEq: return LC_OPResult_Bool; break;
        case LC_TokenKind_GreaterThen: return LC_OPResult_Bool; break;
        case LC_TokenKind_LesserThen: return LC_OPResult_Bool; break;
        case LC_TokenKind_Equals: return LC_OPResult_Bool; break;
        case LC_TokenKind_NotEquals: return LC_OPResult_Bool; break;
        default: return LC_OPResult_Error;
        }
    }

    return LC_OPResult_Error;
}

LC_FUNCTION LC_OPResult LC_IsUnaryOpValidForType(LC_TokenKind op, LC_Type *type) {
    if (LC_IsFloat(type)) {
        if (op == LC_TokenKind_Sub) return LC_OPResult_Ok;
        if (op == LC_TokenKind_Add) return LC_OPResult_Ok;
    }
    if (LC_IsInt(type)) {
        if (op == LC_TokenKind_Not) return LC_OPResult_Bool;
        if (op == LC_TokenKind_Sub) return LC_OPResult_Ok;
        if (op == LC_TokenKind_Add) return LC_OPResult_Ok;
        if (op == LC_TokenKind_Neg) return LC_OPResult_Ok;
    }
    if (LC_IsPtrLike(type)) {
        if (op == LC_TokenKind_Not) return LC_OPResult_Bool;
    }
    return LC_OPResult_Error;
}

LC_FUNCTION LC_OPResult LC_IsAssignValidForType(LC_TokenKind op, LC_Type *type) {
    if (op == LC_TokenKind_Assign) return LC_OPResult_Ok;
    if (LC_IsInt(type)) {
        switch (op) {
        case LC_TokenKind_DivAssign: return LC_OPResult_Ok;
        case LC_TokenKind_MulAssign: return LC_OPResult_Ok;
        case LC_TokenKind_ModAssign: return LC_OPResult_Ok;
        case LC_TokenKind_SubAssign: return LC_OPResult_Ok;
        case LC_TokenKind_AddAssign: return LC_OPResult_Ok;
        case LC_TokenKind_BitAndAssign: return LC_OPResult_Ok;
        case LC_TokenKind_BitOrAssign: return LC_OPResult_Ok;
        case LC_TokenKind_BitXorAssign: return LC_OPResult_Ok;
        case LC_TokenKind_LeftShiftAssign: return LC_OPResult_Ok;
        case LC_TokenKind_RightShiftAssign: return LC_OPResult_Ok;
        default: return LC_OPResult_Error;
        }
    }
    if (LC_IsFloat(type)) {
        switch (op) {
        case LC_TokenKind_DivAssign: return LC_OPResult_Ok;
        case LC_TokenKind_MulAssign: return LC_OPResult_Ok;
        case LC_TokenKind_SubAssign: return LC_OPResult_Ok;
        case LC_TokenKind_AddAssign: return LC_OPResult_Ok;
        default: return LC_OPResult_Error;
        }
    }
    return LC_OPResult_Error;
}

LC_FUNCTION int LC_GetLevelsOfIndirection(LC_Type *type) {
    if (type->kind == LC_TypeKind_Pointer) return LC_GetLevelsOfIndirection(type->tbase) + 1;
    if (type->kind == LC_TypeKind_Array) return LC_GetLevelsOfIndirection(type->tbase) + 1;
    return 0;
}

LC_FUNCTION LC_AST *LC_CreateAST(LC_Token *pos, LC_ASTKind kind) {
    LC_AST *n = LC_PushStruct(L->ast_arena, LC_AST);
    n->id     = ++L->ast_count;
    n->kind   = kind;
    n->pos    = pos;
    if (L->parser == &L->quick_parser) n->pos = &L->BuiltinToken;
    return n;
}

LC_FUNCTION LC_AST *LC_CreateUnary(LC_Token *pos, LC_TokenKind op, LC_AST *expr) {
    LC_AST *r      = LC_CreateAST(pos, LC_ASTKind_ExprUnary);
    r->eunary.op   = op;
    r->eunary.expr = expr;
    return r;
}

LC_FUNCTION LC_AST *LC_CreateBinary(LC_Token *pos, LC_AST *left, LC_AST *right, LC_TokenKind op) {
    LC_AST *r        = LC_CreateAST(pos, LC_ASTKind_ExprBinary);
    r->ebinary.op    = op;
    r->ebinary.left  = left;
    r->ebinary.right = right;
    return r;
}

LC_FUNCTION LC_AST *LC_CreateIndex(LC_Token *pos, LC_AST *left, LC_AST *index) {
    LC_AST *r       = LC_CreateAST(pos, LC_ASTKind_ExprIndex);
    r->eindex.index = index;
    r->eindex.base  = left;
    return r;
}

LC_FUNCTION void LC_SetPointerSizeAndAlign(int size, int align) {
    L->pointer_align = align;
    L->pointer_size  = size;
    if (L->tpvoid) {
        L->tpvoid->size  = size;
        L->tpvoid->align = align;
    }
    if (L->tpchar) {
        L->tpchar->size  = size;
        L->tpchar->align = align;
    }
}

LC_FUNCTION LC_Type *LC_CreateType(LC_TypeKind kind) {
    LC_Type *r = LC_PushStruct(L->type_arena, LC_Type);
    L->type_count += 1;
    r->kind = kind;
    r->id   = ++L->typeids;
    if (r->kind == LC_TypeKind_Proc || r->kind == LC_TypeKind_Pointer) {
        r->size        = L->pointer_size;
        r->align       = L->pointer_align;
        r->is_unsigned = true;
    }
    return r;
}

LC_FUNCTION LC_Type *LC_CreateTypedef(LC_Decl *decl, LC_Type *base) {
    LC_Type *n                      = LC_CreateType(base->kind);
    *n                              = *base;
    decl->typedef_renamed_type_decl = base->decl;
    n->decl                         = decl;
    return n;
}

LC_FUNCTION LC_Type *LC_CreatePointerType(LC_Type *type) {
    uint64_t key   = (uint64_t)type;
    LC_Type *entry = (LC_Type *)LC_MapGetU64(&L->type_map, key);
    if (entry) {
        return entry;
    }
    LC_Type *n = LC_CreateType(LC_TypeKind_Pointer);
    n->tbase   = type;
    LC_MapInsertU64(&L->type_map, key, n);
    return n;
}

LC_FUNCTION LC_Type *LC_CreateArrayType(LC_Type *type, int size) {
    uint64_t size_key = LC_HashBytes(&size, sizeof(size));
    uint64_t type_key = LC_HashBytes(type, sizeof(*type));
    uint64_t key      = LC_HashMix(size_key, type_key);
    LC_Type *entry    = (LC_Type *)LC_MapGetU64(&L->type_map, key);
    if (entry) {
        return entry;
    }
    LC_Type *n     = LC_CreateType(LC_TypeKind_Array);
    n->tbase       = type;
    n->tarray.size = size;
    n->size        = type->size * size;
    n->align       = type->align;
    LC_MapInsertU64(&L->type_map, key, n);
    return n;
}

LC_FUNCTION LC_Type *LC_CreateProcType(LC_TypeMemberList args, LC_Type *ret, bool has_vargs, bool has_vargs_any_promotion) {
    LC_ASSERT(NULL, ret);
    uint64_t key      = LC_HashBytes(ret, sizeof(*ret));
    key               = LC_HashMix(key, LC_HashBytes(&has_vargs, sizeof(has_vargs)));
    key               = LC_HashMix(key, LC_HashBytes(&has_vargs_any_promotion, sizeof(has_vargs_any_promotion)));
    int procarg_count = 0;
    LC_TypeFor(it, args.first) {
        key = LC_HashMix(LC_HashBytes(it->type, sizeof(it->type[0])), key);
        key = LC_HashMix(LC_HashBytes((char *)it->name, LC_StrLen((char *)it->name)), key);
        if (it->default_value_expr) {
            key = LC_HashMix(LC_HashBytes(&it->default_value_expr, sizeof(LC_AST *)), key);
        }
        procarg_count += 1;
    }
    LC_Type *n = (LC_Type *)LC_MapGetU64(&L->type_map, key);
    if (n) return n;

    n                            = LC_CreateType(LC_TypeKind_Proc);
    n->tproc.args                = args;
    n->tproc.vargs               = has_vargs;
    n->tproc.vargs_any_promotion = has_vargs_any_promotion;
    n->tproc.ret                 = ret;
    LC_MapInsertU64(&L->type_map, key, n);
    return n;
}

LC_FUNCTION LC_Type *LC_CreateIncompleteType(LC_Decl *decl) {
    LC_Type *n = LC_CreateType(LC_TypeKind_Incomplete);
    n->decl    = decl;
    return n;
}

LC_FUNCTION LC_Type *LC_CreateUntypedIntEx(LC_Type *base, LC_Decl *decl) {
    uint64_t hash_base   = LC_HashBytes(base, sizeof(*base));
    uint64_t untyped_int = LC_TypeKind_UntypedInt;
    uint64_t key         = LC_HashMix(hash_base, untyped_int);
    LC_Type *n           = (LC_Type *)LC_MapGetU64(&L->type_map, key);
    if (n) return n;

    n        = LC_CreateType(LC_TypeKind_UntypedInt);
    n->tbase = base;
    n->decl  = decl;
    return n;
}

LC_FUNCTION LC_Type *LC_CreateUntypedInt(LC_Type *base) {
    LC_Decl *decl = LC_CreateDecl(LC_DeclKind_Type, LC_ILit("UntypedInt"), &L->NullAST);
    LC_Type *n    = LC_CreateUntypedIntEx(base, decl);
    return n;
}

LC_FUNCTION LC_TypeMember *LC_AddTypeToList(LC_TypeMemberList *list, LC_Intern name, LC_Type *type, LC_AST *ast) {
    LC_TypeFor(it, list->first) {
        if (name == it->name) {
            return NULL;
        }
    }

    LC_TypeMember *r = LC_PushStruct(L->arena, LC_TypeMember);
    r->name          = name;
    r->type          = type;
    r->ast           = ast;
    LC_DLLAdd(list->first, list->last, r);
    list->count += 1;
    return r;
}

LC_FUNCTION LC_Type *LC_StripPointer(LC_Type *type) {
    if (type->kind == LC_TypeKind_Pointer) {
        return type->tbase;
    }
    return type;
}

LC_FUNCTION void LC_ReserveAST(LC_ASTArray *stack, int size) {
    if (size > stack->cap) {
        LC_AST **new_stack = LC_PushArray(stack->arena, LC_AST *, size);

        LC_MemoryCopy(new_stack, stack->data, stack->len * sizeof(LC_AST *));
        stack->data = new_stack;
        stack->cap  = size;
    }
}

LC_FUNCTION void LC_PushAST(LC_ASTArray *stack, LC_AST *ast) {
    LC_ASSERT(NULL, stack->len <= stack->cap);
    LC_ASSERT(NULL, stack->arena);
    if (stack->len == stack->cap) {
        int      new_cap   = stack->cap < 16 ? 16 : stack->cap * 2;
        LC_AST **new_stack = LC_PushArray(stack->arena, LC_AST *, new_cap);

        LC_MemoryCopy(new_stack, stack->data, stack->len * sizeof(LC_AST *));
        stack->data = new_stack;
        stack->cap  = new_cap;
    }
    stack->data[stack->len++] = ast;
}

LC_FUNCTION void LC_PopAST(LC_ASTArray *stack) {
    LC_ASSERT(NULL, stack->arena);
    LC_ASSERT(NULL, stack->len > 0);
    stack->len -= 1;
}

LC_FUNCTION LC_AST *LC_GetLastAST(LC_ASTArray *arr) {
    LC_ASSERT(NULL, arr->len > 0);
    LC_AST *result = arr->data[arr->len - 1];
    return result;
}

LC_FUNCTION void LC_WalkAST(LC_ASTWalker *ctx, LC_AST *n) {
    if (!ctx->depth_first) {
        ctx->proc(ctx, n);
    }

    if (ctx->dont_recurse) {
        ctx->dont_recurse = false;
        return;
    }

    LC_PushAST(&ctx->stack, n);
    switch (n->kind) {
    case LC_ASTKind_TypespecIdent:
    case LC_ASTKind_ExprIdent:
    case LC_ASTKind_ExprString:
    case LC_ASTKind_ExprInt:
    case LC_ASTKind_ExprFloat:
    case LC_ASTKind_GlobImport:
    case LC_ASTKind_ExprBool:
    case LC_ASTKind_StmtBreak:
    case LC_ASTKind_StmtContinue: break;

    case LC_ASTKind_Package: {
        LC_ASTFor(it, n->apackage.ffile) LC_WalkAST(ctx, it);
    } break;

    case LC_ASTKind_File: {
        LC_ASTFor(it, n->afile.fimport) LC_WalkAST(ctx, it);
        LC_ASTFor(it, n->afile.fdecl) {
            if (ctx->visit_notes == false && it->kind == LC_ASTKind_DeclNote) continue;
            LC_WalkAST(ctx, it);
        }
    } break;

    case LC_ASTKind_DeclProc: {
        LC_WalkAST(ctx, n->dproc.type);
        if (n->dproc.body) LC_WalkAST(ctx, n->dproc.body);
    } break;
    case LC_ASTKind_NoteList: {
        if (ctx->visit_notes) LC_ASTFor(it, n->anote_list.first) LC_WalkAST(ctx, it);
    } break;
    case LC_ASTKind_TypespecProcArg: {
        LC_WalkAST(ctx, n->tproc_arg.type);
        if (n->tproc_arg.expr) LC_WalkAST(ctx, n->tproc_arg.expr);
    } break;
    case LC_ASTKind_TypespecAggMem: {
        LC_WalkAST(ctx, n->tproc_arg.type);
    } break;

    case LC_ASTKind_ExprNote: {
        ctx->inside_note += 1;
        if (ctx->visit_notes) LC_WalkAST(ctx, n->enote.expr);
        ctx->inside_note -= 1;
    } break;

    case LC_ASTKind_StmtSwitch: {
        LC_WalkAST(ctx, n->sswitch.expr);
        LC_ASTFor(it, n->sswitch.first) LC_WalkAST(ctx, it);
    } break;

    case LC_ASTKind_StmtSwitchCase: {
        LC_ASTFor(it, n->scase.first) LC_WalkAST(ctx, it);
        LC_WalkAST(ctx, n->scase.body);
    } break;
    case LC_ASTKind_StmtSwitchDefault: {
        LC_ASTFor(it, n->scase.first) LC_WalkAST(ctx, it);
        LC_WalkAST(ctx, n->scase.body);
    } break;

    case LC_ASTKind_StmtIf: {
        LC_WalkAST(ctx, n->sif.expr);
        LC_WalkAST(ctx, n->sif.body);
        LC_ASTFor(it, n->sif.first) LC_WalkAST(ctx, it);
    } break;
    case LC_ASTKind_StmtElse:
    case LC_ASTKind_StmtElseIf: {
        if (n->sif.expr) LC_WalkAST(ctx, n->sif.expr);
        LC_WalkAST(ctx, n->sif.body);
    } break;

    case LC_ASTKind_DeclNote: {
        ctx->inside_note += 1;
        if (ctx->visit_notes) LC_WalkAST(ctx, n->dnote.expr);
        ctx->inside_note -= 1;
    } break;
    case LC_ASTKind_DeclUnion:
    case LC_ASTKind_DeclStruct: {
        LC_ASTFor(it, n->dagg.first) LC_WalkAST(ctx, it);
    } break;

    case LC_ASTKind_DeclVar: {
        if (n->dvar.type) LC_WalkAST(ctx, n->dvar.type);
        if (n->dvar.expr) LC_WalkAST(ctx, n->dvar.expr);
    } break;
    case LC_ASTKind_DeclConst: {
        if (n->dconst.expr) LC_WalkAST(ctx, n->dconst.expr);
    } break;
    case LC_ASTKind_DeclTypedef: {
        LC_WalkAST(ctx, n->dtypedef.type);
    } break;
    case LC_ASTKind_TypespecField: {
        LC_WalkAST(ctx, n->efield.left);
    } break;

    case LC_ASTKind_TypespecPointer: {
        LC_WalkAST(ctx, n->tpointer.base);
    } break;
    case LC_ASTKind_TypespecArray: {
        LC_WalkAST(ctx, n->tarray.base);
        if (n->tarray.index) LC_WalkAST(ctx, n->tarray.index);
    } break;
    case LC_ASTKind_TypespecProc: {
        LC_ASTFor(it, n->tproc.first) LC_WalkAST(ctx, it);
        if (n->tproc.ret) LC_WalkAST(ctx, n->tproc.ret);
    } break;
    case LC_ASTKind_StmtBlock: {
        LC_ASTFor(it, n->sblock.first) LC_WalkAST(ctx, it);
        // hmm should we inline defers or maybe remove them from
        // the stmt list?
        // LC_ASTFor(it, n->sblock.first_defer) LC_WalkAST(ctx, it);
    } break;

    case LC_ASTKind_StmtNote: {
        ctx->inside_note += 1;
        if (ctx->visit_notes) LC_WalkAST(ctx, n->snote.expr);
        ctx->inside_note -= 1;
    } break;
    case LC_ASTKind_StmtReturn: {
        if (n->sreturn.expr) LC_WalkAST(ctx, n->sreturn.expr);
    } break;

    case LC_ASTKind_StmtDefer: {
        LC_WalkAST(ctx, n->sdefer.body);
    } break;
    case LC_ASTKind_StmtFor: {
        if (n->sfor.init) LC_WalkAST(ctx, n->sfor.init);
        if (n->sfor.cond) LC_WalkAST(ctx, n->sfor.cond);
        if (n->sfor.inc) LC_WalkAST(ctx, n->sfor.inc);
        LC_WalkAST(ctx, n->sfor.body);
    } break;

    case LC_ASTKind_StmtAssign: {
        LC_WalkAST(ctx, n->sassign.left);
        LC_WalkAST(ctx, n->sassign.right);
    } break;
    case LC_ASTKind_StmtExpr: {
        LC_WalkAST(ctx, n->sexpr.expr);
    } break;
    case LC_ASTKind_StmtVar: {
        if (n->svar.type) LC_WalkAST(ctx, n->svar.type);
        if (n->svar.expr) LC_WalkAST(ctx, n->svar.expr);
    } break;
    case LC_ASTKind_StmtConst: {
        LC_WalkAST(ctx, n->sconst.expr);
    } break;

    case LC_ASTKind_ExprType: {
        LC_WalkAST(ctx, n->etype.type);
    } break;
    case LC_ASTKind_ExprAddPtr:
    case LC_ASTKind_ExprBinary: {
        LC_WalkAST(ctx, n->ebinary.left);
        LC_WalkAST(ctx, n->ebinary.right);
    } break;
    case LC_ASTKind_ExprGetPointerOfValue:
    case LC_ASTKind_ExprGetValueOfPointer:
    case LC_ASTKind_ExprUnary: {
        LC_WalkAST(ctx, n->eunary.expr);
    } break;
    case LC_ASTKind_ExprCompoundItem:
    case LC_ASTKind_ExprCallItem: {
        if (n->ecompo_item.index) LC_WalkAST(ctx, n->ecompo_item.index);
        LC_WalkAST(ctx, n->ecompo_item.expr);
    } break;
    case LC_ASTKind_Note: {
        ctx->inside_note += 1;
        if (n->ecompo.name) LC_WalkAST(ctx, n->ecompo.name);
        LC_ASTFor(it, n->ecompo.first) LC_WalkAST(ctx, it);
        ctx->inside_note -= 1;
    } break;
    case LC_ASTKind_ExprBuiltin: {
        ctx->inside_builtin += 1;
        if (n->ecompo.name) LC_WalkAST(ctx, n->ecompo.name);
        LC_ASTFor(it, n->ecompo.first) LC_WalkAST(ctx, it);
        ctx->inside_builtin -= 1;
    } break;
    case LC_ASTKind_ExprCall:
    case LC_ASTKind_ExprCompound: {
        if (n->ecompo.name) LC_WalkAST(ctx, n->ecompo.name);
        LC_ASTFor(it, n->ecompo.first) LC_WalkAST(ctx, it);
    } break;
    case LC_ASTKind_ExprCast: {
        LC_WalkAST(ctx, n->ecast.type);
        LC_WalkAST(ctx, n->ecast.expr);
    } break;
    case LC_ASTKind_ExprField: {
        LC_WalkAST(ctx, n->efield.left);
    } break;
    case LC_ASTKind_ExprIndex: {
        LC_WalkAST(ctx, n->eindex.index);
        LC_WalkAST(ctx, n->eindex.base);
    } break;

    case LC_ASTKind_Ignore:
    case LC_ASTKind_Error:
    default: LC_ReportASTError(n, "internal compiler error: got invalid ast kind during ast walk: %s", LC_ASTKindToString(n->kind));
    }

    if (ctx->visit_notes && n->notes) {
        LC_WalkAST(ctx, n->notes);
    }
    LC_PopAST(&ctx->stack);

    if (ctx->depth_first) {
        ctx->proc(ctx, n);
    }
}

LC_FUNCTION LC_ASTWalker LC_GetDefaultWalker(LC_Arena *arena, LC_ASTWalkProc *proc) {
    LC_ASTWalker result = {0};
    result.stack.arena  = arena;
    result.proc         = proc;
    result.depth_first  = true;
    return result;
}

LC_FUNCTION void WalkAndFlattenAST(LC_ASTWalker *ctx, LC_AST *n) {
    LC_ASTArray *array = (LC_ASTArray *)ctx->user_data;
    LC_PushAST(array, n);
}

LC_FUNCTION LC_ASTArray LC_FlattenAST(LC_Arena *arena, LC_AST *n) {
    LC_ASTArray  array  = {arena};
    LC_ASTWalker walker = LC_GetDefaultWalker(arena, WalkAndFlattenAST);
    walker.user_data    = (void *)&array;
    LC_WalkAST(&walker, n);
    return array;
}

LC_FUNCTION void WalkToFindSizeofLike(LC_ASTWalker *w, LC_AST *n) {
    if (n->kind == LC_ASTKind_ExprBuiltin) {
        LC_ASSERT(n, n->ecompo.name->kind == LC_ASTKind_ExprIdent);
        if (n->ecompo.name->eident.name == L->isizeof || n->ecompo.name->eident.name == L->ialignof || n->ecompo.name->eident.name == L->ioffsetof) {
            ((bool *)w->user_data)[0] = true;
        }
    }
}

LC_FUNCTION bool LC_ContainsCBuiltin(LC_AST *n) {
    LC_TempArena checkpoint = LC_BeginTemp(L->arena);
    bool         found      = false;
    {
        LC_ASTWalker walker = LC_GetDefaultWalker(L->arena, WalkToFindSizeofLike);
        walker.depth_first  = false;
        walker.user_data    = (void *)&found;
        LC_WalkAST(&walker, n);
    }
    LC_EndTemp(checkpoint);
    return found;
}

LC_FUNCTION void SetASTPosOnAll_Walk(LC_ASTWalker *ctx, LC_AST *n) {
    n->pos = (LC_Token *)ctx->user_data;
}

LC_FUNCTION void LC_SetASTPosOnAll(LC_AST *n, LC_Token *pos) {
    LC_TempArena check  = LC_BeginTemp(L->arena);
    LC_ASTWalker walker = LC_GetDefaultWalker(L->arena, SetASTPosOnAll_Walk);
    walker.user_data    = (void *)pos;
    LC_WalkAST(&walker, n);
    LC_EndTemp(check);
}
LC_FUNCTION LC_AST *LC_CopyAST(LC_Arena *arena, LC_AST *n) {
    if (n == NULL) return NULL;

    LC_AST *result = LC_PushStruct(arena, LC_AST);
    result->kind   = n->kind;
    result->id     = ++L->ast_count;
    result->notes  = LC_CopyAST(arena, n->notes);
    result->pos    = n->pos;

    switch (n->kind) {
    case LC_ASTKind_File: {
        result->afile.x = n->afile.x;

        LC_ASTFor(it, n->afile.fimport) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->afile.fimport, result->afile.limport, it_copy);
        }
        LC_ASTFor(it, n->afile.fdecl) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->afile.fdecl, result->afile.ldecl, it_copy);
        }
    } break;

    case LC_ASTKind_DeclProc: {
        result->dbase.name = n->dbase.name;
        result->dproc.body = LC_CopyAST(arena, n->dproc.body);
        result->dproc.type = LC_CopyAST(arena, n->dproc.type);
    } break;

    case LC_ASTKind_NoteList: {
        LC_ASTFor(it, n->anote_list.first) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->anote_list.first, result->anote_list.last, it_copy);
        }
    } break;

    case LC_ASTKind_TypespecAggMem:
    case LC_ASTKind_TypespecProcArg: {
        result->tproc_arg.name = n->tproc_arg.name;
        result->tproc_arg.type = LC_CopyAST(arena, n->tproc_arg.type);
        result->tproc_arg.expr = LC_CopyAST(arena, n->tproc_arg.expr);
    } break;

    case LC_ASTKind_ExprNote: {
        result->enote.expr = LC_CopyAST(arena, n->enote.expr);
    } break;

    case LC_ASTKind_StmtSwitch: {
        LC_ASTFor(it, n->sswitch.first) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->sswitch.first, result->sswitch.last, it_copy);
        }
        result->sswitch.total_switch_case_count = n->sswitch.total_switch_case_count;
        result->sswitch.expr                    = LC_CopyAST(arena, n->sswitch.expr);
    } break;

    case LC_ASTKind_StmtSwitchCase: {
        LC_ASTFor(it, n->scase.first) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->scase.first, result->scase.last, it_copy);
        }
        result->scase.body = LC_CopyAST(arena, n->scase.body);
    } break;

    case LC_ASTKind_StmtSwitchDefault: {
        LC_ASTFor(it, n->scase.first) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->scase.first, result->scase.last, it_copy);
        }
        result->scase.body = LC_CopyAST(arena, n->scase.body);
    } break;

    case LC_ASTKind_StmtIf: {
        LC_ASTFor(it, n->sif.first) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->sswitch.first, result->sswitch.last, it_copy);
        }
        result->sif.expr = LC_CopyAST(arena, n->sif.expr);
        result->sif.body = LC_CopyAST(arena, n->sif.body);
    } break;

    case LC_ASTKind_StmtElse:
    case LC_ASTKind_StmtElseIf: {
        result->sif.expr = LC_CopyAST(arena, n->sif.expr);
        result->sif.body = LC_CopyAST(arena, n->sif.body);
    } break;

    case LC_ASTKind_GlobImport: {
        result->gimport.name = n->gimport.name;
        result->gimport.path = n->gimport.path;
    } break;

    case LC_ASTKind_DeclNote: {
        result->dnote.expr = LC_CopyAST(arena, n->dnote.expr);
    } break;

    case LC_ASTKind_DeclUnion:
    case LC_ASTKind_DeclStruct: {
        result->dbase.name = n->dbase.name;
        LC_ASTFor(it, n->dagg.first) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->dagg.first, result->dagg.last, it_copy);
        }
    } break;

    case LC_ASTKind_DeclVar: {
        result->dbase.name = n->dbase.name;
        result->dvar.type  = LC_CopyAST(arena, n->dvar.type);
        result->dvar.expr  = LC_CopyAST(arena, n->dvar.expr);
    } break;

    case LC_ASTKind_DeclConst: {
        result->dbase.name  = n->dbase.name;
        result->dconst.expr = LC_CopyAST(arena, n->dconst.expr);
    } break;

    case LC_ASTKind_DeclTypedef: {
        result->dbase.name    = n->dbase.name;
        result->dtypedef.type = LC_CopyAST(arena, n->dtypedef.type);
    } break;

    case LC_ASTKind_ExprField:
    case LC_ASTKind_TypespecField: {
        result->efield.left  = LC_CopyAST(arena, n->efield.left);
        result->efield.right = n->efield.right;
    } break;

    case LC_ASTKind_TypespecIdent: {
        result->eident.name = n->eident.name;
    } break;

    case LC_ASTKind_TypespecPointer: {
        result->tpointer.base = LC_CopyAST(arena, n->tpointer.base);
    } break;

    case LC_ASTKind_TypespecArray: {
        result->tarray.base  = LC_CopyAST(arena, n->tarray.base);
        result->tarray.index = LC_CopyAST(arena, n->tarray.index);
    } break;

    case LC_ASTKind_TypespecProc: {
        LC_ASTFor(it, n->tproc.first) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->tproc.first, result->tproc.last, it_copy);
        }
        result->tproc.ret   = LC_CopyAST(arena, n->tproc.ret);
        result->tproc.vargs = n->tproc.vargs;
    } break;

    case LC_ASTKind_StmtBlock: {
        LC_ASTFor(it, n->sblock.first) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->sblock.first, result->sblock.last, it_copy);

            if (it_copy->kind == LC_ASTKind_StmtDefer) {
                LC_SLLStackAddMod(result->sblock.first_defer, it_copy, sdefer.next);
            }
        }
        if (n->sblock.first_defer) {
            LC_ASSERT(result, result->sblock.first_defer);
        }
        result->sblock.kind = n->sblock.kind;
        result->sblock.name = n->sblock.name;
    } break;

    case LC_ASTKind_StmtNote: {
        result->snote.expr = LC_CopyAST(arena, n->snote.expr);
    } break;

    case LC_ASTKind_StmtReturn: {
        result->sreturn.expr = LC_CopyAST(arena, n->sreturn.expr);
    } break;

    case LC_ASTKind_StmtBreak: {
        result->sbreak.name = n->sbreak.name;
    } break;

    case LC_ASTKind_StmtContinue: {
        result->scontinue.name = n->scontinue.name;
    } break;

    case LC_ASTKind_StmtDefer: {
        result->sdefer.body = LC_CopyAST(arena, n->sdefer.body);
    } break;

    case LC_ASTKind_StmtFor: {
        result->sfor.init = LC_CopyAST(arena, n->sfor.init);
        result->sfor.cond = LC_CopyAST(arena, n->sfor.cond);
        result->sfor.inc  = LC_CopyAST(arena, n->sfor.inc);
        result->sfor.body = LC_CopyAST(arena, n->sfor.body);
    } break;

    case LC_ASTKind_StmtAssign: {
        result->sassign.op    = n->sassign.op;
        result->sassign.left  = LC_CopyAST(arena, n->sassign.left);
        result->sassign.right = LC_CopyAST(arena, n->sassign.right);
    } break;

    case LC_ASTKind_StmtExpr: {
        result->sexpr.expr = LC_CopyAST(arena, n->sexpr.expr);
    } break;

    case LC_ASTKind_StmtVar: {
        result->svar.type = LC_CopyAST(arena, n->svar.type);
        result->svar.expr = LC_CopyAST(arena, n->svar.expr);
        result->svar.name = n->svar.name;
    } break;

    case LC_ASTKind_StmtConst: {
        result->sconst.expr = LC_CopyAST(arena, n->sconst.expr);
        result->sconst.name = n->sconst.name;
    } break;

    case LC_ASTKind_ExprIdent: {
        result->eident.name = n->eident.name;
    } break;

    case LC_ASTKind_ExprBool:
    case LC_ASTKind_ExprFloat:
    case LC_ASTKind_ExprInt:
    case LC_ASTKind_ExprString: {
        result->eatom = n->eatom;
    } break;

    case LC_ASTKind_ExprType: {
        result->etype.type = LC_CopyAST(arena, n->etype.type);
    } break;

    case LC_ASTKind_ExprAddPtr:
    case LC_ASTKind_ExprBinary: {
        result->ebinary.op    = n->ebinary.op;
        result->ebinary.left  = LC_CopyAST(arena, n->ebinary.left);
        result->ebinary.right = LC_CopyAST(arena, n->ebinary.right);
    } break;

    case LC_ASTKind_ExprGetPointerOfValue:
    case LC_ASTKind_ExprGetValueOfPointer:
    case LC_ASTKind_ExprUnary: {
        result->eunary.op   = n->eunary.op;
        result->eunary.expr = LC_CopyAST(arena, n->eunary.expr);
    } break;

    case LC_ASTKind_ExprCompoundItem:
    case LC_ASTKind_ExprCallItem: {
        result->ecompo_item.name  = n->ecompo_item.name;
        result->ecompo_item.index = LC_CopyAST(arena, n->ecompo_item.index);
        result->ecompo_item.expr  = LC_CopyAST(arena, n->ecompo_item.expr);
    } break;

    case LC_ASTKind_ExprBuiltin:
    case LC_ASTKind_Note:
    case LC_ASTKind_ExprCall:
    case LC_ASTKind_ExprCompound: {
        LC_ASTFor(it, n->ecompo.first) {
            LC_AST *it_copy = LC_CopyAST(arena, it);
            LC_DLLAdd(result->ecompo.first, result->ecompo.last, it_copy);
        }

        result->ecompo.size = n->ecompo.size;
        result->ecompo.name = LC_CopyAST(arena, n->ecompo.name);
    } break;

    case LC_ASTKind_ExprCast: {
        result->ecast.type = LC_CopyAST(arena, n->ecast.type);
        result->ecast.expr = LC_CopyAST(arena, n->ecast.expr);
    } break;

    case LC_ASTKind_ExprIndex: {
        result->eindex.index = LC_CopyAST(arena, n->eindex.index);
        result->eindex.base  = LC_CopyAST(arena, n->eindex.base);
    } break;

    default: LC_ReportASTError(n, "internal compiler error: failed to LC_CopyAST, got invalid ast kind: %s", LC_ASTKindToString(n->kind));
    }

    return result;
}

// clang-format off
#define LC_PUSH_COMP_ARRAY_SIZE(SIZE) int PREV_SIZE = L->resolver.compo_context_array_size; L->resolver.compo_context_array_size = SIZE;
#define LC_POP_COMP_ARRAY_SIZE() L->resolver.compo_context_array_size = PREV_SIZE
#define LC_PUSH_SCOPE(SCOPE) DeclScope *PREV_SCOPE = L->resolver.active_scope; L->resolver.active_scope = SCOPE
#define LC_POP_SCOPE() L->resolver.active_scope = PREV_SCOPE
#define LC_PUSH_LOCAL_SCOPE() int LOCAL_LEN = L->resolver.locals.len
#define LC_POP_LOCAL_SCOPE() L->resolver.locals.len = LOCAL_LEN
#define LC_PUSH_PACKAGE(PKG) LC_AST *PREV_PKG = L->resolver.package; L->resolver.package = PKG; LC_PUSH_SCOPE(PKG->apackage.ext->scope)
#define LC_POP_PACKAGE() L->resolver.package = PREV_PKG; LC_POP_SCOPE()
#define LC_PROP_ERROR(OP, n, ...)  OP = __VA_ARGS__; if (LC_IsError(OP)) { n->kind = LC_ASTKind_Error; return OP; }
#define LC_DECL_PROP_ERROR(OP, ...) OP = __VA_ARGS__; if (LC_IsError(OP)) { LC_MarkDeclError(decl); return OP; }

#define LC_IF(COND, N, ...) if (COND) { LC_Operand R_ = LC_ReportASTError(N, __VA_ARGS__); N->kind = LC_ASTKind_Error; return R_; }
#define LC_DECL_IF(COND, ...) if (COND) { LC_MarkDeclError(decl); return LC_ReportASTError(__VA_ARGS__); }
#define LC_TYPE_IF(COND, ...) if (COND) { LC_MarkDeclError(decl); type->kind = LC_TypeKind_Error; return LC_ReportASTError(__VA_ARGS__); }
// clang-format on

LC_FUNCTION void LC_AddDecl(LC_DeclStack *scope, LC_Decl *decl) {
    if (scope->len + 1 > scope->cap) {
        LC_ASSERT(NULL, scope->cap);
        int       new_cap   = scope->cap * 2;
        LC_Decl **new_stack = LC_PushArray(L->arena, LC_Decl *, new_cap);
        LC_MemoryCopy(new_stack, scope->stack, scope->len * sizeof(LC_Decl *));
        scope->stack = new_stack;
        scope->cap   = new_cap;
    }
    scope->stack[scope->len++] = decl;
}

LC_FUNCTION void LC_InitDeclStack(LC_DeclStack *stack, int size) {
    stack->stack = LC_PushArray(L->arena, LC_Decl *, size);
    stack->cap   = size;
}

LC_FUNCTION LC_DeclStack *LC_CreateDeclStack(int size) {
    LC_DeclStack *stack = LC_PushStruct(L->arena, LC_DeclStack);
    LC_InitDeclStack(stack, size);
    return stack;
}

LC_FUNCTION LC_Decl *LC_FindDeclOnStack(LC_DeclStack *scp, LC_Intern name) {
    for (int i = 0; i < scp->len; i += 1) {
        LC_Decl *it = scp->stack[i];
        if (it->name == name) {
            return it;
        }
    }
    return NULL;
}

LC_FUNCTION void LC_MarkDeclError(LC_Decl *decl) {
    if (decl) {
        decl->kind  = LC_DeclKind_Error;
        decl->state = LC_DeclState_Error;
        if (decl->ast) decl->ast->kind = LC_ASTKind_Error;
    }
}

LC_FUNCTION LC_Decl *LC_CreateDecl(LC_DeclKind kind, LC_Intern name, LC_AST *n) {
    LC_Decl *decl = LC_PushStruct(L->decl_arena, LC_Decl);
    L->decl_count += 1;

    decl->name    = name;
    decl->kind    = kind;
    decl->ast     = n;
    decl->package = L->resolver.package;
    LC_ASSERT(n, decl->package);

    LC_AST *note = LC_HasNote(n, L->iforeign);
    if (note) {
        decl->is_foreign = true;
        if (note->anote.first) {
            if (note->anote.size != 1) LC_ReportASTError(note, "invalid format of @foreign(...), more then 1 argument");
            LC_AST *expr = note->anote.first->ecompo_item.expr;
            if (expr->kind == LC_ASTKind_ExprIdent) decl->foreign_name = expr->eident.name;
            if (expr->kind != LC_ASTKind_ExprIdent) LC_ReportASTError(note, "invalid format of @foreign(...), expected identifier");
        }
    }
    if (!decl->foreign_name) decl->foreign_name = decl->name;
    return decl;
}

LC_FUNCTION LC_Operand LC_ThereIsNoDecl(DeclScope *scp, LC_Decl *decl, bool check_locals) {
    LC_Decl *r = (LC_Decl *)LC_MapGetU64(scp, decl->name);
    if (check_locals && !r) {
        r = LC_FindDeclOnStack(&L->resolver.locals, decl->name);
    }
    if (r) {
        LC_MarkDeclError(r);
        LC_MarkDeclError(decl);
        return LC_ReportASTErrorEx(decl->ast, r->ast, "there are 2 decls with the same name '%s'", decl->name);
    }
    return LC_OPNull;
}

LC_FUNCTION LC_Operand LC_AddDeclToScope(DeclScope *scp, LC_Decl *decl) {
    LC_Operand LC_DECL_PROP_ERROR(op, LC_ThereIsNoDecl(scp, decl, false));
    LC_MapInsertU64(scp, decl->name, decl);
    return LC_OPDecl(decl);
}

LC_FUNCTION DeclScope *LC_CreateScope(int size) {
    DeclScope *scope = LC_PushStruct(L->arena, DeclScope);
    scope->arena     = L->arena;
    LC_MapReserve(scope, size);
    return scope;
}

LC_FUNCTION LC_Decl *LC_FindDeclInScope(DeclScope *scope, LC_Intern name) {
    LC_Decl *decl = (LC_Decl *)LC_MapGetU64(scope, name);
    return decl;
}

LC_FUNCTION LC_Decl *LC_GetLocalOrGlobalDecl(LC_Intern name) {
    LC_Decl *decl = LC_FindDeclInScope(L->resolver.active_scope, name);
    if (!decl && L->resolver.package->apackage.ext->scope == L->resolver.active_scope) {
        decl = LC_FindDeclOnStack(&L->resolver.locals, name);
    }
    return decl;
}

LC_FUNCTION LC_Operand LC_PutGlobalDecl(LC_Decl *decl) {
    LC_Operand LC_DECL_PROP_ERROR(op, LC_AddDeclToScope(L->resolver.package->apackage.ext->scope, decl));

    // :Mangle global scope name
    if (!decl->is_foreign && decl->package != L->builtin_package) {
        bool mangle = true;
        if (LC_HasNote(decl->ast, L->idont_mangle)) mangle = false;
        if (LC_HasNote(decl->ast, L->iapi)) mangle = false;
        if (decl->name == L->imain) {
            if (L->first_package) {
                if (L->first_package == decl->package->apackage.name) {
                    mangle = false;
                }
            } else mangle = false;
        }
        if (mangle) {
            LC_String name     = LC_Format(L->arena, "lc_%s_%s", (char *)decl->package->apackage.name, (char *)decl->name);
            decl->foreign_name = LC_InternStrLen(name.str, (int)name.len);
        }
    }

    LC_Decl *conflict = (LC_Decl *)LC_MapGetU64(&L->foreign_names, decl->foreign_name);
    if (conflict && !decl->is_foreign) {
        LC_ReportASTErrorEx(decl->ast, conflict->ast, "found two global declarations with the same foreign name: %s", decl->foreign_name);
    } else {
        LC_MapInsertU64(&L->foreign_names, decl->foreign_name, decl);
    }

    return op;
}

LC_FUNCTION LC_Operand LC_CreateLocalDecl(LC_DeclKind kind, LC_Intern name, LC_AST *ast) {
    LC_Decl *decl = LC_CreateDecl(kind, name, ast);
    decl->state   = LC_DeclState_Resolving;
    LC_Operand LC_DECL_PROP_ERROR(operr0, LC_ThereIsNoDecl(L->resolver.package->apackage.ext->scope, decl, true));
    LC_AddDecl(&L->resolver.locals, decl);
    return LC_OPDecl(decl);
}

LC_FUNCTION LC_Decl *LC_AddConstIntDecl(char *key, int64_t value) {
    LC_Intern intern = LC_ILit(key);
    LC_Decl  *decl   = LC_CreateDecl(LC_DeclKind_Const, intern, &L->NullAST);
    decl->state      = LC_DeclState_Resolved;
    decl->type       = L->tuntypedint;
    LC_Bigint_init_signed(&decl->v.i, value);
    LC_AddDeclToScope(L->resolver.package->apackage.ext->scope, decl);
    return decl;
}

LC_FUNCTION LC_Decl *LC_GetBuiltin(LC_Intern name) {
    LC_Decl *decl = (LC_Decl *)LC_MapGetU64(L->builtin_package->apackage.ext->scope, name);
    return decl;
}

LC_FUNCTION void LC_AddBuiltinConstInt(char *key, int64_t value) {
    LC_PUSH_PACKAGE(L->builtin_package);
    LC_AddConstIntDecl(key, value);
    LC_POP_PACKAGE();
}

LC_FUNCTION LC_AST *LC_HasNote(LC_AST *ast, LC_Intern i) {
    if (ast && ast->notes) {
        LC_ASTFor(it, ast->notes->anote_list.first) {
            LC_ASSERT(it, "internal compiler error: note is not an identifier");
            if (it->anote.name->eident.name == i) {
                return it;
            }
        }
    }
    return NULL;
}

LC_FUNCTION void LC_RegisterDeclsFromFile(LC_AST *file) {
    LC_ASTFor(n, file->afile.fdecl) {
        if (n->dbase.resolved_decl) continue;
        if (n->kind == LC_ASTKind_DeclNote) continue;
        LC_Decl *decl = LC_CreateDecl(LC_DeclKind_Type, n->dbase.name, n);
        switch (n->kind) {
        case LC_ASTKind_DeclStruct:
        case LC_ASTKind_DeclUnion:
            decl->type  = LC_CreateIncompleteType(decl);
            decl->state = LC_DeclState_Resolved;
            decl->kind  = LC_DeclKind_Type;
            break;
        case LC_ASTKind_DeclTypedef: decl->kind = LC_DeclKind_Type; break;
        case LC_ASTKind_DeclProc: decl->kind = LC_DeclKind_Proc; break;
        case LC_ASTKind_DeclConst: decl->kind = LC_DeclKind_Const; break;
        case LC_ASTKind_DeclVar: decl->kind = LC_DeclKind_Var; break;
        default: LC_ReportASTError(n, "internal compiler error: got unhandled ast declaration kind in %s", __FUNCTION__);
        }
        LC_PutGlobalDecl(decl);
        n->dbase.resolved_decl = decl;
    }
}

LC_FUNCTION void LC_ResolveDeclsFromFile(LC_AST *file) {
    LC_ASTFor(n, file->afile.fdecl) {
        if (n->kind == LC_ASTKind_DeclNote) {
            LC_ResolveNote(n, false);
        } else {
            LC_ResolveName(n, n->dbase.name);
        }
    }
}

LC_FUNCTION void LC_PackageDecls(LC_AST *package) {
    LC_PUSH_PACKAGE(package);

    // Register top level declarations
    LC_ASTFor(file, package->apackage.ffile) {
        LC_ASTFor(import, file->afile.fimport) {
            if (import->gimport.resolved == false) LC_ReportASTError(import, "internal compiler error: unresolved import got into typechecking stage");
        }
        LC_RegisterDeclsFromFile(file);
    }

    // Resolve declarations by name
    LC_ASTFor(file, package->apackage.ffile) {
        LC_ResolveDeclsFromFile(file);
    }

    LC_POP_PACKAGE();
}

LC_FUNCTION void LC_ResolveProcBodies(LC_AST *package) {
    LC_PUSH_PACKAGE(package);

    LC_ASTFor(file, package->apackage.ffile) {
        LC_ASTFor(n, file->afile.fdecl) {
            if (n->kind == LC_ASTKind_DeclNote) continue;

            LC_Decl *decl = n->dbase.resolved_decl;
            if (decl->kind == LC_DeclKind_Proc) {
                LC_Operand op = LC_ResolveProcBody(decl);
                if (LC_IsError(op)) LC_MarkDeclError(decl);
            }
        }
    }

    LC_POP_PACKAGE();
}

LC_FUNCTION void LC_ResolveIncompleteTypes(LC_AST *package) {
    LC_PUSH_PACKAGE(package);

    LC_ASTFor(file, package->apackage.ffile) {
        LC_ASTFor(n, file->afile.fdecl) {
            if (n->kind == LC_ASTKind_DeclNote) continue;

            LC_Decl *decl = n->dbase.resolved_decl;
            if (decl->kind == LC_DeclKind_Type) {
                LC_ResolveTypeAggregate(decl->ast, decl->type);
            }
        }
    }

    LC_POP_PACKAGE();
}

LC_FUNCTION LC_Operand LC_ResolveNote(LC_AST *n, bool is_decl) {
    LC_AST *note = n->dnote.expr;
    if (n->kind == LC_ASTKind_ExprNote) note = n->enote.expr;
    else if (n->kind == LC_ASTKind_StmtNote) note = n->snote.expr;
    else {
        LC_ASSERT(n, n->kind == LC_ASTKind_DeclNote);
        if (n->dnote.processed) return LC_OPNull;
    }

    if (note->ecompo.name->eident.name == L->istatic_assert) {
        LC_IF(is_decl, note, "#static_assert cant be used as variable initializer");
        LC_Operand LC_PROP_ERROR(op, n, LC_ExpectBuiltinWithOneArg(note));
        LC_IF(!LC_IsUTConst(op) || !LC_IsUTInt(op.type), n, "static assert requires constant untyped integer value");
        int val = (int)LC_Bigint_as_signed(&op.val.i);
        LC_IF(!val, note, "#static_assert failed !");
        n->dnote.processed = true;
    }

    return LC_OPNull;
}

void SetConstVal(LC_AST *n, LC_TypeAndVal val) {
    LC_ASSERT(n, LC_IsUntyped(val.type));
    n->const_val = val;
}

LC_FUNCTION LC_Operand LC_ResolveProcBody(LC_Decl *decl) {
    if (decl->state == LC_DeclState_Error) return LC_OPError();
    if (decl->state == LC_DeclState_ResolvedBody) return LC_OPNull;
    LC_ASSERT(decl->ast, decl->state == LC_DeclState_Resolved);

    LC_AST *n = decl->ast;
    if (n->dproc.body == NULL) return LC_OPNull;
    L->resolver.locals.len = 0;
    LC_ASTFor(it, n->dproc.type->tproc.first) {
        if (it->kind == LC_ASTKind_Error) {
            LC_MarkDeclError(decl);
            return LC_OPError();
        }
        LC_ASSERT(it, it->type);
        LC_Operand LC_DECL_PROP_ERROR(op, LC_CreateLocalDecl(LC_DeclKind_Var, it->tproc_arg.name, it));
        op.decl->type               = it->type;
        op.decl->state              = LC_DeclState_Resolved;
        it->tproc_arg.resolved_decl = op.decl;
    }

    int errors_before = L->errors;
    LC_ASSERT(n, decl->type->tproc.ret);
    L->resolver.expected_ret_type = decl->type->tproc.ret;
    LC_Operand LC_DECL_PROP_ERROR(op, LC_ResolveStmtBlock(n->dproc.body));
    L->resolver.locals.len = 0;

    if (errors_before == L->errors && decl->type->tproc.ret != L->tvoid && !(op.flags & LC_OPF_Returned)) {
        LC_ReportASTError(n, "you can get through this procedure without hitting a return stmt, add a return to cover all control paths");
    }
    decl->state = LC_DeclState_ResolvedBody;
    if (L->on_proc_body_resolved) L->on_proc_body_resolved(decl);
    return LC_OPNull;
}

LC_FUNCTION LC_ResolvedCompoItem *LC_AddResolvedCallItem(LC_ResolvedCompo *list, LC_TypeMember *t, LC_AST *comp, LC_AST *expr) {
    LC_AST *duplicate1 = (LC_AST *)LC_MapGetP(&L->resolver.duplicate_map, list);
    if (t && !duplicate1) {
        for (LC_ResolvedCompoItem *it = list->first; it; it = it->next) {
            if (t == it->t) {
                LC_MapInsertP(&L->resolver.duplicate_map, list, comp);
                LC_MapInsertP(&L->resolver.duplicate_map, comp, it->comp); // duplicate2
                break;
            }
        }
    }
    LC_ResolvedCompoItem *match = LC_PushStruct(L->arena, LC_ResolvedCompoItem);
    list->count += 1;
    match->t    = t;
    match->expr = expr;
    match->comp = comp;
    LC_AddSLL(list->first, list->last, match);
    return match;
}

LC_FUNCTION LC_Operand LC_ResolveCompoCall(LC_AST *n, LC_Type *type) {
    LC_ASSERT(n, type->kind == LC_TypeKind_Proc);
    LC_IF(type->tproc.vargs && type->tagg.mems.count > n->ecompo.size, n, "calling procedure with invalid argument count, expected at least %d args, got %d, the procedure type is: %s", type->tagg.mems.count, n->ecompo.size, LC_GenLCType(type));

    bool named_field_appeared = false;
    LC_ASTFor(it, n->ecompo.first) {
        LC_IF(type->tproc.vargs && it->ecompo_item.name, it, "variadic procedures cannot have named arguments");
        LC_IF(it->ecompo_item.index, it, "index inside a call is not allowed");
        LC_IF(named_field_appeared && it->ecompo_item.name == 0, it, "mixing named and positional arguments is illegal");
        if (it->ecompo_item.name) named_field_appeared = true;
    }

    LC_ResolvedCompo *matches = LC_PushStruct(L->arena, LC_ResolvedCompo);
    LC_TypeMember    *type_it = type->tproc.args.first;
    LC_AST           *npos_it = n->ecompo.first;

    // greedy match unnamed arguments
    for (; type_it; type_it = type_it->next, npos_it = npos_it->next) {
        if (npos_it == NULL || npos_it->ecompo_item.name) break;
        LC_AddResolvedCallItem(matches, type_it, npos_it, npos_it->ecompo_item.expr);
    }

    // greedy match variadic arguments
    if (type->tproc.vargs) {
        for (; npos_it; npos_it = npos_it->next) {
            LC_ResolvedCompoItem *m = LC_AddResolvedCallItem(matches, NULL, npos_it, npos_it->ecompo_item.expr);
            m->varg                 = true;
        }
    }

    // for every required proc type argument we seek a named argument
    // in either default proc values or passed in call arguments
    for (; type_it; type_it = type_it->next) {
        LC_ASTFor(n_it, npos_it) {
            if (type_it->name == n_it->ecompo_item.name) {
                LC_AddResolvedCallItem(matches, type_it, n_it, n_it->ecompo_item.expr);
                goto end_of_outer_loop;
            }
        }

        if (type_it->default_value_expr) {
            LC_ResolvedCompoItem *m = LC_AddResolvedCallItem(matches, type_it, NULL, type_it->default_value_expr);
            m->defaultarg           = true;
        }
    end_of_outer_loop:;
    }

    // make sure we matched every item in call
    LC_ASTFor(n_it, n->ecompo.first) {
        LC_AST *expr     = n_it->ecompo_item.expr;
        bool    included = false;
        for (LC_ResolvedCompoItem *it = matches->first; it; it = it->next) {
            if (it->expr == expr) {
                included = true;
                break;
            }
        }

        LC_IF(!included, expr, "unknown argument to a procedure call, couldn't match it with any of the declared arguments, the procedure type is: %s", LC_GenLCType(type));
    }

    LC_IF(!type->tproc.vargs && matches->count != type->tproc.args.count, n, "invalid argument count passed in to procedure call, expected: %d, matched: %d, the procedure type is: %s", type->tproc.args.count, matches->count, LC_GenLCType(type));

    // error on duplicates
    LC_AST *duplicate1 = (LC_AST *)LC_MapGetP(&L->resolver.duplicate_map, matches);
    LC_AST *duplicate2 = duplicate1 ? (LC_AST *)LC_MapGetP(&L->resolver.duplicate_map, duplicate1) : NULL;
    LC_MapClear(&L->resolver.duplicate_map);
    if (duplicate1) {
        LC_Operand err = LC_ReportASTErrorEx(duplicate1, duplicate2, "two call items match the same procedure argument");
        n->kind        = LC_ASTKind_Error;
        return err;
    }

    // resolve
    for (LC_ResolvedCompoItem *it = matches->first; it; it = it->next) {
        if (it->varg) {
            if (type->tproc.vargs_any_promotion) {
                LC_Operand LC_PROP_ERROR(opexpr, it->expr, LC_ResolveExprAndPushCompoContext(it->expr, L->tany));
                LC_TryTyping(it->expr, LC_OPType(L->tany));
            } else {
                LC_Operand LC_PROP_ERROR(opexpr, it->expr, LC_ResolveExpr(it->expr));
                LC_Operand LC_PROP_ERROR(op, it->expr, LC_ResolveTypeVargs(it->expr, opexpr));
                LC_TryTyping(it->expr, op);
            }
            continue;
        }
        if (it->defaultarg) {
            continue;
        }
        LC_Operand LC_PROP_ERROR(opexpr, it->expr, LC_ResolveExprAndPushCompoContext(it->expr, it->t->type));
        LC_Operand LC_PROP_ERROR(op, it->expr, LC_ResolveTypeVarDecl(it->expr, LC_OPType(it->t->type), opexpr));
        LC_TryTyping(it->expr, op);
    }

    n->ecompo.resolved_items = matches;
    LC_Operand result        = LC_OPLValueAndType(type->tproc.ret);
    return result;
}

LC_FUNCTION LC_Operand LC_ResolveCompoAggregate(LC_AST *n, LC_Type *type) {
    LC_ASSERT(n, type->kind == LC_TypeKind_Union || type->kind == LC_TypeKind_Struct);
    LC_IF(n->ecompo.size > 1 && type->kind == LC_TypeKind_Union, n, "too many union initializers, expected 1 or 0 got %d", n->ecompo.size);
    LC_IF(n->ecompo.size > type->tagg.mems.count, n, "too many struct initializers, expected less then %d got instead %d", type->tagg.mems.count, n->ecompo.size);

    bool named_field_appeared = false;
    LC_ASTFor(it, n->ecompo.first) {
        LC_IF(type->kind == LC_TypeKind_Union && it->ecompo_item.name == 0, it, "unions can only be initialized using named arguments");
        LC_IF(named_field_appeared && it->ecompo_item.name == 0, it, "mixing named and positional arguments is illegal");
        LC_IF(it->ecompo_item.index, it, "index specifier in non array compound is illegal");
        if (it->ecompo_item.name) named_field_appeared = true;
    }

    LC_ResolvedCompo *matches = LC_PushStruct(L->arena, LC_ResolvedCompo);
    LC_TypeMember    *type_it = type->tagg.mems.first;
    LC_AST           *npos_it = n->ecompo.first;

    // greedy match unnamed arguments
    for (; type_it; type_it = type_it->next, npos_it = npos_it->next) {
        if (npos_it == NULL || npos_it->ecompo_item.name) break;
        LC_AddResolvedCallItem(matches, type_it, npos_it, npos_it->ecompo_item.expr);
    }

    // match named arguments
    for (; npos_it; npos_it = npos_it->next) {
        bool found = false;
        LC_TypeFor(type_it, type->tagg.mems.first) {
            if (type_it->name == npos_it->ecompo_item.name) {
                LC_AddResolvedCallItem(matches, type_it, npos_it, npos_it->ecompo_item.expr);
                found = true;
                break;
            }
        }

        LC_IF(!found, npos_it, "no matching declaration with name '%s' in type '%s'", npos_it->ecompo_item.name, LC_GenLCType(type));
    }

    // error on duplicates
    LC_AST *duplicate1 = (LC_AST *)LC_MapGetP(&L->resolver.duplicate_map, matches);
    LC_AST *duplicate2 = duplicate1 ? (LC_AST *)LC_MapGetP(&L->resolver.duplicate_map, duplicate1) : NULL;
    if (duplicate1) {
        LC_Operand err = LC_ReportASTErrorEx(duplicate1, duplicate2, "two compound items match the same struct variable");
        n->kind        = LC_ASTKind_Error;
        return err;
    }

    // resolve
    LC_Operand result = LC_OPLValueAndType(type);
    result.flags |= LC_OPF_Const;
    for (LC_ResolvedCompoItem *it = matches->first; it; it = it->next) {
        LC_Operand LC_PROP_ERROR(opexpr, it->expr, LC_ResolveExprAndPushCompoContext(it->expr, it->t->type));
        LC_Operand LC_PROP_ERROR(op, it->expr, LC_ResolveTypeVarDecl(it->expr, LC_OPType(it->t->type), opexpr));
        LC_TryTyping(it->expr, op);

        if (!(opexpr.flags & LC_OPF_Const)) result.flags &= ~LC_OPF_Const;
    }

    n->ecompo.resolved_items = matches;
    return result;
}

LC_FUNCTION LC_ResolvedCompoArrayItem *LC_AddResolvedCompoArrayItem(LC_ResolvedArrayCompo *arr, int index, LC_AST *comp) {
    LC_AST *duplicate1 = (LC_AST *)LC_MapGetP(&L->resolver.duplicate_map, arr);
    if (!duplicate1) {
        for (LC_ResolvedCompoArrayItem *it = arr->first; it; it = it->next) {
            if (index == it->index) {
                LC_MapInsertP(&L->resolver.duplicate_map, arr, comp);
                LC_MapInsertP(&L->resolver.duplicate_map, comp, it->comp);
                break;
            }
        }
    }
    LC_ResolvedCompoArrayItem *result = LC_PushStruct(L->arena, LC_ResolvedCompoArrayItem);
    result->index                     = index;
    result->comp                      = comp;
    arr->count += 1;
    LC_AddSLL(arr->first, arr->last, result);
    return result;
}

LC_FUNCTION LC_Operand LC_ResolveCompoArray(LC_AST *n, LC_Type *type) {
    LC_ASSERT(n, type->kind == LC_TypeKind_Array);
    LC_IF(n->ecompo.size > type->tarray.size, n, "too many array intializers, array is of size '%d', got '%d'", type->tarray.size, n->ecompo.size);

    bool index_appeared = false;
    LC_ASTFor(it, n->ecompo.first) {
        LC_IF(index_appeared && it->ecompo_item.index == NULL, it, "mixing indexed and positional arguments is illegal");
        LC_IF(it->ecompo_item.name, it, "named arguments are invalid in array compound literal");
        if (it->ecompo_item.index) index_appeared = true;
    }

    LC_ResolvedArrayCompo *matches = LC_PushStruct(L->arena, LC_ResolvedArrayCompo);
    LC_AST                *npos_it = n->ecompo.first;
    int                    index   = 0;

    // greedy match unnamed arguments
    for (; npos_it; npos_it = npos_it->next) {
        if (npos_it->ecompo_item.index) break;
        LC_ASSERT(n, index < type->tarray.size);
        LC_AddResolvedCompoArrayItem(matches, index++, npos_it);
    }

    // match indexed arguments
    for (; npos_it; npos_it = npos_it->next) {
        uint64_t   val = 0;
        LC_Operand LC_PROP_ERROR(op, npos_it, LC_ResolveConstInt(npos_it->ecompo_item.index, L->tint, &val));
        LC_IF(val > type->tarray.size, npos_it, "array index out of bounds, array is of size %d", type->tarray.size);
        LC_AddResolvedCompoArrayItem(matches, (int)val, npos_it);
    }

    // error on duplicates
    LC_AST *duplicate1 = (LC_AST *)LC_MapGetP(&L->resolver.duplicate_map, matches);
    LC_AST *duplicate2 = duplicate1 ? (LC_AST *)LC_MapGetP(&L->resolver.duplicate_map, duplicate1) : NULL;
    if (duplicate1) {
        LC_Operand err = LC_ReportASTErrorEx(duplicate1, duplicate2, "two items in compound array literal match the same index");
        n->kind        = LC_ASTKind_Error;
        return err;
    }

    // resolve
    LC_Operand result = LC_OPLValueAndType(type);
    result.flags |= LC_OPF_Const;
    for (LC_ResolvedCompoArrayItem *it = matches->first; it; it = it->next) {
        LC_AST    *expr = it->comp->ecompo_item.expr;
        LC_Operand LC_PROP_ERROR(opexpr, expr, LC_ResolveExprAndPushCompoContext(expr, type->tbase));
        LC_Operand LC_PROP_ERROR(op, expr, LC_ResolveTypeVarDecl(expr, LC_OPType(type->tbase), opexpr));
        LC_TryTyping(expr, op);

        if (!(opexpr.flags & LC_OPF_Const)) result.flags &= ~LC_OPF_Const;
    }

    n->ecompo.resolved_array_items = matches;
    return result;
}

LC_FUNCTION LC_Operand LC_ResolveTypeOrExpr(LC_AST *n) {
    if (n->kind == LC_ASTKind_ExprType) {
        LC_Operand LC_PROP_ERROR(result, n, LC_ResolveType(n->etype.type));
        n->type = result.type;
        return result;
    } else {
        LC_Operand LC_PROP_ERROR(result, n, LC_ResolveExpr(n));
        return result;
    }
}

LC_FUNCTION LC_Operand LC_ExpectBuiltinWithOneArg(LC_AST *n) {
    LC_IF(n->ecompo.size != 1, n, "expected 1 argument to builtin procedure, got: %d", n->ecompo.size);
    LC_IF(n->ecompo.first->ecompo_item.name, n, "named arguments in this builtin procedure are illegal");
    LC_AST    *expr = n->ecompo.first->ecompo_item.expr;
    LC_Operand LC_PROP_ERROR(op, expr, LC_ResolveTypeOrExpr(expr));
    LC_Operand LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(n, op.type));
    expr->type = op.type;
    return op;
}

LC_FUNCTION LC_Operand LC_ResolveBuiltin(LC_AST *n) {
    LC_Operand result = {0};
    if (n->ecompo.name == 0 || n->ecompo.name->kind != LC_ASTKind_ExprIdent) return result;
    LC_Intern ident = n->ecompo.name->eident.name;

    if (ident == L->ilengthof) {
        LC_Operand LC_PROP_ERROR(op, n, LC_ExpectBuiltinWithOneArg(n));
        if (LC_IsArray(op.type)) {
            result = LC_OPInt(op.type->tarray.size);
        } else if (LC_IsUTStr(op.type)) {
            int64_t length = LC_StrLen((char *)op.v.name);
            result         = LC_OPInt(length);
        } else LC_IF(1, n, "expected array or constant string type, got instead '%s'", LC_GenLCType(op.type));
        n->kind = LC_ASTKind_ExprBuiltin;
    } else if (ident == L->isizeof) {
        LC_Operand LC_PROP_ERROR(op, n, LC_ExpectBuiltinWithOneArg(n));
        LC_IF(LC_IsUntyped(op.type), n, "cannot get sizeof a value that is untyped: '%s'", LC_GenLCType(op.type));
        result  = LC_OPInt(op.type->size);
        n->kind = LC_ASTKind_ExprBuiltin;
    } else if (ident == L->ialignof) {
        LC_Operand LC_PROP_ERROR(op, n, LC_ExpectBuiltinWithOneArg(n));
        LC_IF(LC_IsUntyped(op.type), n, "cannot get alignof a value that is untyped: '%s'", LC_GenLCType(op.type));

        LC_AST *expr = n->ecompo.first->ecompo_item.expr;
        LC_IF(expr->kind != LC_ASTKind_ExprType, expr, "argument should be a type, instead it's '%s'", LC_ASTKindToString(expr->kind));

        result  = LC_OPInt(op.type->align);
        n->kind = LC_ASTKind_ExprBuiltin;
    } else if (ident == L->itypeof) {
        LC_Operand LC_PROP_ERROR(op, n, LC_ExpectBuiltinWithOneArg(n));
        LC_IF(LC_IsUntyped(op.type), n, "cannot get typeof a value that is untyped: '%s'", LC_GenLCType(op.type));
        result  = LC_OPInt(op.type->id);
        n->kind = LC_ASTKind_ExprBuiltin;
    } else if (ident == L->ioffsetof) {
        LC_IF(n->ecompo.size != 2, n, "expected 2 arguments to builtin procedure 'offsetof', got: %d", n->ecompo.size);
        LC_AST *a1 = n->ecompo.first;
        LC_AST *a2 = a1->next;
        LC_IF(a1->ecompo_item.name, a1, "named arguments in this builtin procedure are illegal");
        LC_IF(a2->ecompo_item.name, a2, "named arguments in this builtin procedure are illegal");
        LC_AST *a1e = a1->ecompo_item.expr;
        LC_AST *a2e = a2->ecompo_item.expr;
        LC_IF(a1e->kind != LC_ASTKind_ExprType, a1e, "first argument should be a type, instead it's '%s'", LC_ASTKindToString(a1e->kind));
        LC_Operand LC_PROP_ERROR(optype, a1e, LC_ResolveType(a1e->etype.type));
        LC_Operand LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(a1e, optype.type));
        LC_IF(!LC_IsAggType(optype.type), a1e, "expected aggregate type in first parameter of 'offsetof', instead got '%s'", LC_GenLCType(optype.type));
        LC_IF(a2e->kind != LC_ASTKind_ExprIdent, a2e, "expected identifier as second parameter to 'offsetof', instead got '%s'", LC_ASTKindToString(a2e->kind));
        a1e->type = optype.type;

        LC_Type       *type       = optype.type;
        LC_TypeMember *found_type = NULL;
        LC_TypeFor(it, type->tagg.mems.first) {
            if (it->name == a2e->eident.name) {
                found_type = it;
                break;
            }
        }

        LC_ASSERT(n, type->decl);
        LC_IF(!found_type, n, "field '%s' not found in '%s'", a2e->eident.name, type->decl->name);
        result  = LC_OPInt(found_type->offset);
        n->kind = LC_ASTKind_ExprBuiltin;
    }

    if (LC_IsUTConst(result)) {
        n->const_val = result.val;
    }

    return result;
}

LC_FUNCTION bool LC_TryTyping(LC_AST *n, LC_Operand op) {
    LC_ASSERT(n, n->type);

    if (LC_IsUntyped(n->type)) {
        if (LC_IsUTInt(n->type) && LC_IsFloat(op.type)) {
            LC_Operand in = {LC_OPF_UTConst | LC_OPF_Const};
            in.val        = n->const_val;
            LC_Operand op = LC_ConstCastFloat(NULL, in);
            SetConstVal(n, op.val);
        }
        if (L->tany == op.type) op = LC_OPModDefaultUT(LC_OPType(n->type));
        n->type = op.type;

        // Bounds check
        if (n->const_val.type && LC_IsUTInt(n->const_val.type)) {
            if (LC_IsInt(n->type) && !LC_BigIntFits(n->const_val.i, n->type)) {
                const char *val = LC_Bigint_str(&n->const_val.i, 10);
                LC_ReportASTError(n, "value '%s', doesn't fit into type '%s'", val, LC_GenLCType(n->type));
            }
        }
    }

    // I think it returns true to do this: (a, b)
    return true;
}

LC_FUNCTION bool LC_TryDefaultTyping(LC_AST *n, LC_Operand *o) {
    LC_ASSERT(n, n->type);
    if (LC_IsUntyped(n->type)) {
        n->type = n->type->tbase;
        if (o) o->type = n->type;
    }
    return true;
}

LC_FUNCTION LC_Operand LC_ResolveNameInScope(LC_AST *n, LC_Decl *parent_decl) {
    LC_ASSERT(n, n->kind == LC_ASTKind_ExprField || n->kind == LC_ASTKind_TypespecField);
    LC_PUSH_SCOPE(parent_decl->scope);
    LC_Operand op = LC_ResolveName(n, n->efield.right);
    LC_POP_SCOPE();
    if (LC_IsError(op)) {
        n->kind = LC_ASTKind_Error;
        return op;
    }

    LC_ASSERT(n, op.decl);
    n->efield.resolved_decl = op.decl;
    n->efield.parent_decl   = parent_decl;

    LC_ASSERT(n, op.decl->kind != LC_DeclKind_Import);
    return op;
}

LC_FUNCTION LC_Operand LC_ResolveExpr(LC_AST *expr) {
    return LC_ResolveExprAndPushCompoContext(expr, NULL);
}

LC_FUNCTION LC_Operand LC_ResolveExprAndPushCompoContext(LC_AST *expr, LC_Type *type) {
    LC_Type *save                  = L->resolver.compo_context_type;
    L->resolver.compo_context_type = type;
    LC_Operand LC_PROP_ERROR(result, expr, LC_ResolveExprEx(expr));
    L->resolver.compo_context_type = save;
    return result;
}

LC_FUNCTION LC_Operand LC_ResolveExprEx(LC_AST *n) {
    LC_Operand result = {0};
    LC_ASSERT(n, LC_IsExpr(n));

    switch (n->kind) {
    case LC_ASTKind_ExprFloat: {
        result.flags    = LC_OPF_UTConst | LC_OPF_Const;
        result.val.d    = n->eatom.d;
        result.val.type = L->tuntypedfloat;
        SetConstVal(n, result.val);
    } break;
    case LC_ASTKind_ExprInt: {
        result.flags    = LC_OPF_UTConst | LC_OPF_Const;
        result.val.i    = n->eatom.i;
        result.val.type = L->tuntypedint;
        SetConstVal(n, result.val);
    } break;
    case LC_ASTKind_ExprBool: {
        result.flags    = LC_OPF_UTConst | LC_OPF_Const;
        result.val.i    = n->eatom.i;
        result.val.type = L->tuntypedbool;
        SetConstVal(n, result.val);
    } break;
    case LC_ASTKind_ExprString: {
        result.flags    = LC_OPF_LValue | LC_OPF_UTConst | LC_OPF_Const;
        result.val.name = n->eatom.name;
        result.val.type = L->tuntypedstring;
        SetConstVal(n, result.val);
    } break;
    case LC_ASTKind_ExprType: {
        return LC_ReportASTError(n, "cannot use type as value");
    } break;

    case LC_ASTKind_ExprIdent: {
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveName(n, n->eident.name));
        LC_IF(op.decl->kind == LC_DeclKind_Type, n, "declaration is type, unexpected inside expression");
        LC_IF(op.decl->kind == LC_DeclKind_Import, n, "declaration is import, unexpected usage");

        n->eident.resolved_decl = op.decl;
        result.val              = op.decl->val;
        if (op.decl->kind == LC_DeclKind_Const) {
            result.flags |= LC_OPF_UTConst | LC_OPF_Const;
            SetConstVal(n, result.val);
        } else {
            result.flags |= LC_OPF_LValue | LC_OPF_Const;
        }
    } break;

    case LC_ASTKind_ExprCast: {
        LC_Operand LC_PROP_ERROR(optype, n, LC_ResolveType(n->ecast.type));
        LC_Operand LC_PROP_ERROR(opexpr, n, LC_ResolveExpr(n->ecast.expr));
        // :ConstantFold
        // the idea is that this will convert the literal into corresponding
        // type. In c :uint(32) will become 32u. This way we can avoid doing
        // typed arithmetic and let the backend handle it.
        if (LC_IsUTConst(opexpr) && (LC_IsNum(optype.type) || LC_IsStr(optype.type))) {
            if (LC_IsFloat(optype.type)) {
                LC_PROP_ERROR(opexpr, n, LC_ConstCastFloat(n, opexpr));
                SetConstVal(n, opexpr.val);
            } else if (LC_IsInt(optype.type)) {
                LC_PROP_ERROR(opexpr, n, LC_ConstCastInt(n, opexpr));
                SetConstVal(n, opexpr.val);
            } else if (LC_IsStr(optype.type)) {
                LC_IF(!LC_IsUTStr(opexpr.type), n, "cannot cast constant expression of type '%s' to '%s'", LC_GenLCType(opexpr.type), LC_GenLCType(optype.type));
                SetConstVal(n, opexpr.val);
            } else LC_IF(1, n, "cannot cast constant expression of type '%s' to '%s'", LC_GenLCType(opexpr.type), LC_GenLCType(optype.type));
            result.type = optype.type;
        } else {
            LC_PROP_ERROR(result, n, LC_ResolveTypeCast(n, optype, opexpr));
            LC_TryTyping(n->ecast.expr, result);
        }
        result.flags |= (opexpr.flags & LC_OPF_Const);
    } break;

    case LC_ASTKind_ExprUnary: {
        LC_PROP_ERROR(result, n, LC_ResolveExpr(n->eunary.expr));

        if (LC_IsUTConst(result)) {
            LC_PROP_ERROR(result, n, LC_EvalUnary(n, n->eunary.op, result));
            SetConstVal(n, result.val);
        } else {
            LC_OPResult r = LC_IsUnaryOpValidForType(n->eunary.op, result.type);
            LC_IF(r == LC_OPResult_Error, n, "invalid unary operation for type '%s'", LC_GenLCType(result.type));
            if (r == LC_OPResult_Bool) result = LC_OPModBool(result);
        }
    } break;

    case LC_ASTKind_ExprBinary: {
        LC_Operand LC_PROP_ERROR(left, n, LC_ResolveExpr(n->ebinary.left));
        LC_Operand LC_PROP_ERROR(right, n, LC_ResolveExpr(n->ebinary.right));
        LC_PROP_ERROR(result, n, LC_ResolveBinaryExpr(n, left, right));
    } break;

    case LC_ASTKind_ExprAddPtr: {
        LC_Operand LC_PROP_ERROR(left, n, LC_ResolveExpr(n->ebinary.left));
        LC_Operand LC_PROP_ERROR(right, n, LC_ResolveExpr(n->ebinary.right));
        LC_IF(!LC_IsInt(right.type), n, "trying to addptr non integer value of type '%s'", LC_GenLCType(left.type));
        if (LC_IsUTStr(left.type)) LC_TryDefaultTyping(n->ebinary.left, &left);
        LC_IF(!LC_IsPtr(left.type) && !LC_IsArray(left.type), n, "left type is required to be a pointer or array, instead got '%s'", LC_GenLCType(left.type));
        result = left;
        if (!LC_IsUTConst(right)) result.flags &= ~LC_OPF_Const;
        if (LC_IsArray(result.type)) result.type = LC_CreatePointerType(result.type->tbase);
        LC_TryTyping(n->ebinary.right, result);
    } break;

    case LC_ASTKind_ExprIndex: {
        LC_Operand LC_PROP_ERROR(opindex, n, LC_ResolveExpr(n->eindex.index));
        LC_Operand LC_PROP_ERROR(opexpr, n, LC_ResolveExpr(n->eindex.base));
        LC_IF(!LC_IsInt(opindex.type), n, "indexing with non integer value of type '%s'", LC_GenLCType(opindex.type));
        if (LC_IsUTStr(opexpr.type)) LC_TryDefaultTyping(n->eindex.base, &opexpr);
        LC_IF(!LC_IsPtr(opexpr.type) && !LC_IsArray(opexpr.type), n, "trying to index non indexable type '%s'", LC_GenLCType(opexpr.type));
        LC_IF(LC_IsVoidPtr(opexpr.type), n, "void is non indexable");
        LC_TryDefaultTyping(n->eindex.index, &opindex);
        result.type = LC_GetBase(opexpr.type);
        result.flags |= LC_OPF_LValue;
        LC_Operand LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(n, result.type));
    } break;

    case LC_ASTKind_ExprGetPointerOfValue: {
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveExpr(n->eunary.expr));
        LC_IF(!LC_IsLValue(op), n, "trying to access address of a temporal object");
        result.type = LC_CreatePointerType(op.type);
        result.flags |= (op.flags & LC_OPF_Const);
    } break;

    case LC_ASTKind_ExprGetValueOfPointer: {
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveExpr(n->eunary.expr));
        LC_IF(!LC_IsPtr(op.type), n, "trying to get value of non pointer type: '%s'", LC_GenLCType(op.type));
        result.type = LC_GetBase(op.type);
        result.flags |= LC_OPF_LValue;
        result.flags &= ~LC_OPF_Const;
        LC_Operand LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(n, result.type));
    } break;

    case LC_ASTKind_ExprField: {
        bool resolved_import = false;

        LC_Operand op = {0};
        if (n->efield.left->kind == LC_ASTKind_ExprIdent) {
            LC_AST    *nf = n->efield.left;
            LC_Operand LC_PROP_ERROR(op_name, nf, LC_ResolveName(nf, nf->eident.name));

            if (op_name.decl->kind == LC_DeclKind_Import) {
                resolved_import          = true;
                nf->eident.resolved_decl = op_name.decl;
                nf->type                 = L->tvoid;

                LC_PROP_ERROR(op, n, LC_ResolveNameInScope(n, op_name.decl));
            }
        }

        if (!resolved_import) {
            LC_ASTKind left_kind = n->efield.left->kind;
            LC_PROP_ERROR(op, n, LC_ResolveExpr(n->efield.left));

            LC_Type   *type = LC_StripPointer(op.type);
            LC_Operand LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(n, type));
            LC_IF(!LC_IsAggType(type), n->efield.left, "invalid operation, expected aggregate type, '%s' is not an aggregate", LC_GenLCType(type));
            LC_PROP_ERROR(op, n, LC_ResolveNameInScope(n, type->decl));
            LC_ASSERT(n, op.decl->kind == LC_DeclKind_Var);
            result.flags |= LC_OPF_LValue | LC_OPF_Const;
            result.val = op.decl->val;
        } else {
            result.val = op.decl->val;
            // @copy_paste from ExprIdent
            if (op.decl->kind == LC_DeclKind_Const) {
                result.flags |= LC_OPF_UTConst | LC_OPF_Const;
                SetConstVal(n, result.val);
            } else {
                result.flags |= LC_OPF_LValue | LC_OPF_Const;
            }
        }
    } break;

    case LC_ASTKind_ExprCall: {
        LC_ASSERT(n, n->ecompo.name);
        LC_PROP_ERROR(result, n, LC_ResolveBuiltin(n));
        if (!result.type) {
            LC_Operand LC_PROP_ERROR(left, n, LC_ResolveExpr(n->ecompo.name));
            LC_IF(!LC_IsProc(left.type), n, "trying to call value of invalid type '%s', not a procedure", LC_GenLCType(left.type));
            if (L->before_call_args_resolved) L->before_call_args_resolved(n, left.type);
            LC_PROP_ERROR(result, n, LC_ResolveCompoCall(n, left.type));
        }
    } break;

    case LC_ASTKind_ExprCompound: {
        LC_Type *type = NULL;
        if (n->ecompo.name) {
            LC_PUSH_COMP_ARRAY_SIZE(n->ecompo.size);
            LC_Operand LC_PROP_ERROR(left, n, LC_ResolveTypeOrExpr(n->ecompo.name));
            type = left.type;
            LC_POP_COMP_ARRAY_SIZE();
        }
        if (!n->ecompo.name) type = L->resolver.compo_context_type;
        LC_IF(!type, n, "failed to deduce type of compound expression");
        LC_Operand LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(n, type));

        if (LC_IsAggType(type)) {
            LC_PROP_ERROR(result, n, LC_ResolveCompoAggregate(n, type));
        } else if (LC_IsArray(type)) {
            LC_PROP_ERROR(result, n, LC_ResolveCompoArray(n, type));
        } else {
            LC_IF(1, n, "compound of type '%s' is illegal, expected array, struct or union type", LC_GenLCType(type));
        }
    } break;

    default: LC_IF(1, n, "internal compiler error: unhandled expression kind '%s'", LC_ASTKindToString(n->kind));
    }

    n->type = result.type;
    if (n->type != L->tany && L->resolver.compo_context_type == L->tany) {
        LC_MapInsertP(&L->implicit_any, n, (void *)(intptr_t)1);
        result.flags &= ~LC_OPF_Const;
    }

    if (L->on_expr_resolved) L->on_expr_resolved(n, &result);
    return result;
}

LC_FUNCTION LC_Operand LC_ResolveStmtBlock(LC_AST *n) {
    LC_ASSERT(n, n->kind == LC_ASTKind_StmtBlock);

    LC_PUSH_LOCAL_SCOPE();
    LC_PushAST(&L->resolver.stmt_block_stack, n);

    LC_Operand result = {0};
    for (LC_AST *it = n->sblock.first; it; it = it->next) {
        LC_Operand op = LC_ResolveStmt(it);

        // We don't want to whine about non returned procedures if we spotted any errors
        // inside of it.
        if (LC_IsError(op) || (op.flags & LC_OPF_Returned)) result.flags |= LC_OPF_Returned;
    }

    LC_PopAST(&L->resolver.stmt_block_stack);
    LC_POP_LOCAL_SCOPE();

    if (L->on_stmt_resolved) L->on_stmt_resolved(n);
    return result;
}

LC_FUNCTION LC_Operand LC_ResolveVarDecl(LC_Decl *decl) {
    LC_ASSERT(decl->ast, decl->kind == LC_DeclKind_Var);
    LC_Operand result = {0};
    result.flags |= LC_OPF_Const;

    LC_AST    *n      = decl->ast;
    LC_Operand optype = {0};
    LC_Operand opexpr = {0};

    LC_AST *expr = n->dvar.expr;
    LC_AST *type = n->dvar.type;
    if (n->kind == LC_ASTKind_StmtVar) {
        expr = n->svar.expr;
        type = n->svar.type;
    } else {
        LC_ASSERT(n, n->kind == LC_ASTKind_DeclVar);
    }

    // special case := #c(``)
    if (expr && expr->kind == LC_ASTKind_ExprNote) {
        LC_Operand LC_PROP_ERROR(opnote, expr, LC_ResolveNote(expr, true));
        LC_IF(type == NULL, n, "invalid usage of unknown type, need to add type annotation");
        LC_DECL_PROP_ERROR(optype, LC_ResolveType(type));
        decl->type = optype.type;
    } else {
        if (type) {
            if (expr && expr->kind == LC_ASTKind_ExprCompound) {
                LC_PUSH_COMP_ARRAY_SIZE(expr->ecompo.size);
                LC_DECL_PROP_ERROR(optype, LC_ResolveType(type));
                LC_POP_COMP_ARRAY_SIZE();
            } else {
                LC_DECL_PROP_ERROR(optype, LC_ResolveType(type));
            }
        }
        if (expr) {
            LC_DECL_PROP_ERROR(opexpr, LC_ResolveExprAndPushCompoContext(expr, optype.type));
            if (!(opexpr.flags & LC_OPF_Const)) result.flags &= ~LC_OPF_Const;
        }

        LC_Operand LC_DECL_PROP_ERROR(opcast, LC_ResolveTypeVarDecl(n, optype, opexpr));
        if (expr) LC_TryTyping(expr, opcast);
        decl->val = opcast.val;
    }

    LC_ASSERT(n, decl->type);
    LC_Operand LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(n, decl->type));
    result.type = decl->type;
    return result;
}

LC_FUNCTION LC_Operand LC_MakeSureNoDeferBlock(LC_AST *n, char *str) {
    for (int i = L->resolver.stmt_block_stack.len - 1; i >= 0; i -= 1) {
        LC_AST *it = L->resolver.stmt_block_stack.data[i];
        if (it->kind == LC_ASTKind_Error) return LC_OPError();
        LC_ASSERT(it, it->kind == LC_ASTKind_StmtBlock);
        LC_IF(it->sblock.kind == SBLK_Defer, n, str);
    }
    return LC_OPNull;
}

LC_FUNCTION LC_Operand LC_MakeSureInsideLoopBlock(LC_AST *n, char *str) {
    bool loop_found = false;
    for (int i = L->resolver.stmt_block_stack.len - 1; i >= 0; i -= 1) {
        LC_AST *it = L->resolver.stmt_block_stack.data[i];
        if (it->kind == LC_ASTKind_Error) return LC_OPError();
        LC_ASSERT(it, it->kind == LC_ASTKind_StmtBlock);
        if (it->sblock.kind == SBLK_Loop) {
            loop_found = true;
            break;
        }
    }
    LC_IF(!loop_found, n, str);
    return LC_OPNull;
}

LC_FUNCTION LC_Operand LC_MatchLabeledBlock(LC_AST *n) {
    if (n->sbreak.name) {
        bool found = false;
        for (int i = L->resolver.stmt_block_stack.len - 1; i >= 0; i -= 1) {
            LC_AST *it = L->resolver.stmt_block_stack.data[i];
            if (it->kind == LC_ASTKind_Error) return LC_OPError();
            if (it->sblock.name == n->sbreak.name) {
                found = true;
                break;
            }
        }
        LC_IF(!found, n, "no label with name '%s'", n->sbreak.name);
    }
    return LC_OPNull;
}

LC_FUNCTION void WalkToFindCall(LC_ASTWalker *w, LC_AST *n) {
    if (n->kind == LC_ASTKind_ExprCall || n->kind == LC_ASTKind_ExprBuiltin) ((bool *)w->user_data)[0] = true;
}

LC_FUNCTION bool LC_ContainsCallExpr(LC_AST *ast) {
    LC_TempArena checkpoint = LC_BeginTemp(L->arena);
    bool         found_call = false;
    {
        LC_ASTWalker walker = LC_GetDefaultWalker(L->arena, WalkToFindCall);
        walker.depth_first  = false;
        walker.user_data    = (void *)&found_call;
        LC_WalkAST(&walker, ast);
    }
    LC_EndTemp(checkpoint);
    return found_call;
}

LC_FUNCTION LC_Operand LC_ResolveStmt(LC_AST *n) {
    LC_ASSERT(n, LC_IsStmt(n));

    LC_Operand result = {0};
    switch (n->kind) {
    case LC_ASTKind_StmtVar: {
        LC_Operand LC_PROP_ERROR(opdecl, n, LC_CreateLocalDecl(LC_DeclKind_Var, n->svar.name, n));
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveVarDecl(opdecl.decl));
        opdecl.decl->state    = LC_DeclState_Resolved;
        n->svar.resolved_decl = opdecl.decl;
        n->type               = op.type;
    } break;

    case LC_ASTKind_StmtConst: {
        LC_Operand LC_PROP_ERROR(opdecl, n, LC_CreateLocalDecl(LC_DeclKind_Const, n->sconst.name, n));
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveConstDecl(opdecl.decl));
        opdecl.decl->state = LC_DeclState_Resolved;
        n->type            = op.type;
    } break;

    case LC_ASTKind_StmtAssign: {
        LC_Operand LC_PROP_ERROR(left, n, LC_ResolveExpr(n->sassign.left));
        LC_IF(!LC_IsLValue(left), n, "assigning value to a temporal object (lvalue)");
        LC_Type *type = left.type;

        LC_OPResult valid = LC_IsAssignValidForType(n->sassign.op, type);
        LC_IF(valid == LC_OPResult_Error, n, "invalid assignment operation '%s' for type '%s'", LC_TokenKindToString(n->sassign.op), LC_GenLCType(type));

        LC_Operand LC_PROP_ERROR(right, n, LC_ResolveExprAndPushCompoContext(n->sassign.right, type));
        LC_PROP_ERROR(result, n, LC_ResolveTypeVarDecl(n, left, right));
        LC_TryTyping(n->sassign.left, result);
        LC_TryTyping(n->sassign.right, result);
        n->type = result.type;
    } break;

    case LC_ASTKind_StmtExpr: {
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveExpr(n->sexpr.expr));
        LC_TryDefaultTyping(n->sexpr.expr, &op);
        n->type               = op.type;
        bool    contains_call = LC_ContainsCallExpr(n->sexpr.expr);
        LC_AST *note          = LC_HasNote(n, L->iunused);
        LC_IF(!note && !contains_call, n, "very likely a bug, expression statement doesn't contain any calls so it doesn't do anything");
    } break;

    case LC_ASTKind_StmtReturn: {
        LC_PROP_ERROR(result, n, LC_MakeSureNoDeferBlock(n, "returning from defer block is illegal"));
        LC_Operand op = LC_OPType(L->tvoid);
        if (n->sreturn.expr) {
            LC_PROP_ERROR(op, n, LC_ResolveExprAndPushCompoContext(n->sreturn.expr, L->resolver.expected_ret_type));
        }

        if (!(op.type == L->resolver.expected_ret_type && op.type == L->tvoid)) {
            LC_PROP_ERROR(op, n, LC_ResolveTypeVarDecl(n, LC_OPType(L->resolver.expected_ret_type), op));
            if (n->sreturn.expr) LC_TryTyping(n->sreturn.expr, op);
        }
        result.flags |= LC_OPF_Returned;
    } break;

    case LC_ASTKind_StmtNote: LC_PROP_ERROR(result, n, LC_ResolveNote(n, false)); break;
    case LC_ASTKind_StmtContinue:
        LC_PROP_ERROR(result, n, LC_MakeSureNoDeferBlock(n, "continue inside of defer is illegal"));
        LC_PROP_ERROR(result, n, LC_MakeSureInsideLoopBlock(n, "continue outside of a for loop is illegal"));
    case LC_ASTKind_StmtBreak: {
        LC_PROP_ERROR(result, n, LC_MakeSureNoDeferBlock(n, "break inside of defer is illegal"));
        LC_PROP_ERROR(result, n, LC_MakeSureInsideLoopBlock(n, "break outside of a for loop is illegal"));
        LC_PROP_ERROR(result, n, LC_MatchLabeledBlock(n));
    } break;

    case LC_ASTKind_StmtDefer: {
        LC_PROP_ERROR(result, n, LC_MakeSureNoDeferBlock(n, "defer inside of defer is illegal"));
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveStmtBlock(n->sdefer.body));
        LC_AST    *parent_block = LC_GetLastAST(&L->resolver.stmt_block_stack);
        LC_ASSERT(n, parent_block->kind == LC_ASTKind_StmtBlock);
        LC_SLLStackAddMod(parent_block->sblock.first_defer, n, sdefer.next);
    } break;

    case LC_ASTKind_StmtSwitch: {
        LC_Operand LC_PROP_ERROR(opfirst, n, LC_ResolveExpr(n->sswitch.expr));
        LC_IF(!LC_IsInt(opfirst.type), n, "invalid type in switch condition '%s', it should be an integer", LC_GenLCType(opfirst.type));
        LC_TryDefaultTyping(n->sswitch.expr, &opfirst);

        bool all_returned = true;
        bool has_default  = n->sswitch.last && n->sswitch.last->kind == LC_ASTKind_StmtSwitchDefault;

        LC_Operand *ops = LC_PushArray(L->arena, LC_Operand, n->sswitch.total_switch_case_count);
        int         opi = 0;
        LC_ASTFor(case_it, n->sswitch.first) {
            if (case_it->kind == LC_ASTKind_StmtSwitchCase) {
                LC_ASTFor(case_expr_it, case_it->scase.first) {
                    LC_Operand LC_PROP_ERROR(opcase, n, LC_ResolveExpr(case_expr_it));
                    LC_IF(!LC_IsUTConst(opcase), case_expr_it, "expected an untyped constant");
                    ops[opi++] = opcase;

                    LC_Operand LC_PROP_ERROR(o, n, LC_ResolveTypeVarDecl(case_expr_it, opfirst, opcase));
                    LC_TryTyping(case_expr_it, o);
                }
            }
            LC_Operand LC_PROP_ERROR(opbody, case_it, LC_ResolveStmtBlock(case_it->scase.body));
            if (!(opbody.flags & LC_OPF_Returned)) all_returned = false;
        }
        LC_ASSERT(n, opi == n->sswitch.total_switch_case_count);

        for (int i = 0; i < opi; i += 1) {
            LC_Operand a = ops[i];
            for (int j = 0; j < opi; j += 1) {
                if (i == j) continue;
                LC_Operand b = ops[j];

                // bounds check error is thrown in LC_ResolveTypeVarDecl
                if (LC_BigIntFits(a.v.i, opfirst.type) && LC_BigIntFits(b.v.i, opfirst.type)) {
                    uint64_t au = LC_Bigint_as_unsigned(&a.v.i);
                    uint64_t bu = LC_Bigint_as_unsigned(&b.v.i);
                    LC_IF(au == bu, n, "duplicate fields, with value: %llu, in a switch statement", au);
                }
            }
        }

        if (all_returned && has_default) result.flags |= LC_OPF_Returned;
    } break;

    case LC_ASTKind_StmtFor: {
        LC_StmtFor *sfor = &n->sfor;
        LC_PUSH_LOCAL_SCOPE();

        if (sfor->init) {
            LC_Operand opinit = LC_ResolveStmt(sfor->init);
            if (LC_IsError(opinit)) {
                n->kind = LC_ASTKind_Error;
                LC_POP_LOCAL_SCOPE();
                return opinit;
            }

            LC_TryDefaultTyping(sfor->init, &opinit);
        }
        if (sfor->cond) {
            LC_Operand opcond = LC_ResolveExpr(sfor->cond);
            if (LC_IsError(opcond)) {
                n->kind = LC_ASTKind_Error;
                LC_POP_LOCAL_SCOPE();
                return opcond;
            }

            LC_TryDefaultTyping(sfor->cond, &opcond);
            if (!LC_IsIntLike(opcond.type)) {
                n->kind = LC_ASTKind_Error;
                LC_POP_LOCAL_SCOPE();
                return LC_ReportASTError(n, "invalid type in for condition '%s', it should be an integer or pointer", LC_GenLCType(opcond.type));
            }
        }
        if (sfor->inc) {
            LC_Operand opinc = LC_ResolveStmt(sfor->inc);
            if (LC_IsError(opinc)) {
                n->kind = LC_ASTKind_Error;
                LC_POP_LOCAL_SCOPE();
                return opinc;
            }
        }
        result = LC_ResolveStmtBlock(sfor->body);
        if (LC_IsError(result)) {
            n->kind = LC_ASTKind_Error;
            LC_POP_LOCAL_SCOPE();
            return result;
        }

        LC_POP_LOCAL_SCOPE();
    } break;

    case LC_ASTKind_StmtBlock: {
        // we don't handle errors here explicitly
        LC_Operand op = LC_ResolveStmtBlock(n);
        if (op.flags & LC_OPF_Returned) result.flags |= LC_OPF_Returned;
    } break;

    case LC_ASTKind_StmtIf: {
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveExpr(n->sif.expr));
        LC_TryDefaultTyping(n->sif.expr, &op);
        LC_IF(!LC_IsIntLike(op.type), n, "invalid type in if clause expression %s it should be an integer or pointer", LC_GenLCType(op.type));

        bool all_returned = true;
        bool has_else     = n->sif.last && n->sif.last->kind == LC_ASTKind_StmtElse;

        LC_Operand LC_PROP_ERROR(opbody, n, LC_ResolveStmtBlock(n->sif.body));
        if (!(opbody.flags & LC_OPF_Returned)) all_returned = false;

        LC_ASTFor(it, n->sif.first) {
            if (it->kind == LC_ASTKind_StmtElseIf) {
                LC_Operand LC_PROP_ERROR(op, it, LC_ResolveExpr(it->sif.expr));
                LC_TryDefaultTyping(it->sif.expr, &op);
                LC_IF(!LC_IsIntLike(op.type), n, "invalid type in if clause expression %s it should be an integer or pointer", LC_GenLCType(op.type));
            }
            LC_Operand LC_PROP_ERROR(opbody, it, LC_ResolveStmtBlock(it->sif.body));
            if (!(opbody.flags & LC_OPF_Returned)) all_returned = false;
        }

        if (all_returned && has_else) result.flags |= LC_OPF_Returned;
    } break;
    default: LC_IF(1, n, "internal compiler error: unhandled statement kind '%s'", LC_ASTKindToString(n->kind));
    }

    if (L->on_stmt_resolved) L->on_stmt_resolved(n);
    return result;
}

LC_FUNCTION LC_Operand LC_ResolveConstDecl(LC_Decl *decl) {
    LC_ASSERT(decl->ast, decl->kind == LC_DeclKind_Const);
    LC_AST *n    = decl->ast;
    LC_AST *expr = n->dconst.expr;
    if (n->kind == LC_ASTKind_StmtConst) {
        expr = n->sconst.expr;
    } else {
        LC_ASSERT(n, n->kind == LC_ASTKind_DeclConst);
    }

    LC_Operand LC_DECL_PROP_ERROR(opexpr, LC_ResolveExpr(expr));
    LC_DECL_IF(!LC_IsUTConst(opexpr), n, "expected an untyped constant");
    LC_DECL_IF(!LC_IsUntyped(opexpr.type), n, "type of constant expression is not a simple type");
    decl->val = opexpr.val;
    return opexpr;
}

LC_FUNCTION LC_Operand LC_ResolveName(LC_AST *pos, LC_Intern intern) {
    LC_Decl *decl = LC_GetLocalOrGlobalDecl(intern);
    LC_DECL_IF(!decl, pos, "undeclared identifier '%s'", intern);
    LC_DECL_IF(decl->state == LC_DeclState_Resolving, pos, "cyclic dependency %s", intern);
    if (decl->state == LC_DeclState_Error) return LC_OPError();
    if (decl->state == LC_DeclState_Resolved || decl->state == LC_DeclState_ResolvedBody) return LC_OPDecl(decl);
    LC_ASSERT(pos, decl->state == LC_DeclState_Unresolved);
    decl->state = LC_DeclState_Resolving;

    LC_AST *n = decl->ast;
    switch (decl->kind) {
    case LC_DeclKind_Const: {
        LC_Operand LC_DECL_PROP_ERROR(op, LC_ResolveConstDecl(decl));
    } break;
    case LC_DeclKind_Var: {
        LC_Operand LC_DECL_PROP_ERROR(op, LC_ResolveVarDecl(decl));
        LC_DECL_IF(!(op.flags & LC_OPF_Const), n, "non constant global declarations are illegal");
    } break;
    case LC_DeclKind_Proc: {
        LC_Operand LC_DECL_PROP_ERROR(optype, LC_ResolveType(n->dproc.type));
        decl->type       = optype.type;
        decl->type->decl = decl;
    } break;
    case LC_DeclKind_Import: {
        LC_ASSERT(n, decl->scope);
    } break;
    case LC_DeclKind_Type: {
        LC_ASSERT(n, n->kind == LC_ASTKind_DeclTypedef);
        LC_Operand LC_DECL_PROP_ERROR(op, LC_ResolveType(n->dtypedef.type));
        decl->val = op.val;

        // I have decided that aggregates cannot be hard typedefed.
        // It brings issues to LC_ResolveTypeAggregate and is not needed, what's needed
        // is typedef on numbers and pointers that create distinct new
        // types. I have never had a use for typedefing a struct to make
        // it more typesafe etc.
        LC_AST *is_weak = LC_HasNote(n, L->iweak);
        bool    is_agg  = op.type->decl && LC_IsAgg(op.type->decl->ast);
        if (!is_weak && !is_agg) decl->type = LC_CreateTypedef(decl, decl->type);
        LC_DECL_IF(is_weak && is_agg, n, "@weak doesn't work on aggregate types");
    } break;
    default: LC_DECL_IF(1, n, "internal compiler error: unhandled LC_DeclKind: '%s'", LC_DeclKindToString(decl->kind))
    }
    decl->state = LC_DeclState_Resolved;

    if (L->on_decl_type_resolved) L->on_decl_type_resolved(decl);
    LC_AST *pkg = decl->package;
    LC_DLLAdd(pkg->apackage.ext->first_ordered, pkg->apackage.ext->last_ordered, decl);
    return LC_OPDecl(decl);
}

LC_FUNCTION LC_Operand LC_ResolveConstInt(LC_AST *n, LC_Type *int_type, uint64_t *out_size) {
    LC_Operand LC_PROP_ERROR(op, n, LC_ResolveExpr(n));
    LC_IF(!LC_IsUTConst(op), n, "expected a constant untyped int");
    LC_IF(!LC_IsUTInt(op.type), n, "expected untyped int constant instead: '%s'", LC_GenLCType(op.type));
    LC_IF(!LC_BigIntFits(op.val.i, int_type), n, "constant value: '%s', doesn't fit in type '%s'", LC_GenLCTypeVal(op.val), LC_GenLCType(int_type));
    if (out_size) *out_size = LC_Bigint_as_unsigned(&op.val.i);
    LC_TryTyping(n, LC_OPType(int_type));
    return op;
}

LC_FUNCTION LC_Operand LC_ResolveType(LC_AST *n) {
    LC_ASSERT(n, LC_IsType(n));
    LC_Operand result = {0};

    switch (n->kind) {
    case LC_ASTKind_TypespecField: {
        LC_ASSERT(n, n->efield.left->kind == LC_ASTKind_TypespecIdent);
        LC_Operand LC_PROP_ERROR(l, n, LC_ResolveName(n, n->efield.left->eident.name));
        LC_IF(l.decl->kind != LC_DeclKind_Import, n, "only accessing '.' imports in type definitions is valid, you are trying to access: '%s'", LC_DeclKindToString(l.decl->kind));
        n->efield.left->eident.resolved_decl = l.decl;
        n->efield.left->type                 = L->tvoid;

        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveNameInScope(n, l.decl));
        LC_IF(op.decl->kind != LC_DeclKind_Type, n, "expected reference to type, instead it's: '%s'", LC_DeclKindToString(op.decl->kind));
        result.type = op.decl->type;
    } break;

    case LC_ASTKind_TypespecIdent: {
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveName(n, n->eident.name));
        LC_IF(op.decl->kind != LC_DeclKind_Type, n, "identifier is not a type");
        result.type             = op.decl->type;
        n->eident.resolved_decl = op.decl;
    } break;

    case LC_ASTKind_TypespecPointer: {
        LC_Operand LC_PROP_ERROR(op, n, LC_ResolveType(n->tpointer.base));
        result.type = LC_CreatePointerType(op.type);
    } break;

    case LC_ASTKind_TypespecArray: {
        LC_Operand LC_PROP_ERROR(opbase, n, LC_ResolveType(n->tarray.base));
        uint64_t   size = L->resolver.compo_context_array_size;
        if (n->tarray.index) {
            LC_Operand LC_PROP_ERROR(opindex, n, LC_ResolveConstInt(n->tarray.index, L->tint, &size));
        }
        LC_IF(size == 0, n, "failed to deduce array size");
        LC_Operand LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(n, opbase.type));

        result.type = LC_CreateArrayType(opbase.type, (int)size);
    } break;

    case LC_ASTKind_TypespecProc: {
        LC_Type *ret = L->tvoid;
        if (n->tproc.ret) {
            LC_Operand LC_PROP_ERROR(op, n->tproc.ret, LC_ResolveType(n->tproc.ret));
            ret = op.type;
        }
        LC_Operand        LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(n, ret));
        LC_TypeMemberList typelist = {0};
        LC_ASTFor(it, n->tproc.first) {
            LC_Operand LC_PROP_ERROR(op, it, LC_ResolveType(it->tproc_arg.type));
            LC_Operand LC_PROP_ERROR(maybe_err, n, LC_ResolveTypeAggregate(it, op.type));

            LC_AST *expr = it->tproc_arg.expr;
            if (expr) {
                LC_Operand LC_PROP_ERROR(opexpr, expr, LC_ResolveExprAndPushCompoContext(expr, op.type));
                LC_Operand LC_PROP_ERROR(opfin, expr, LC_ResolveTypeVarDecl(expr, op, opexpr));
                LC_TryTyping(expr, opfin);
            }

            LC_TypeMember *mem = LC_AddTypeToList(&typelist, it->tproc_arg.name, op.type, it);
            LC_IF(!mem, it, "duplicate proc argument '%s'", it->tproc_arg.name);
            mem->default_value_expr = expr;
            it->type                = op.type;
        }

        LC_TypeFor(i, typelist.first) {
            LC_TypeFor(j, typelist.first) {
                LC_IF(i != j && i->name == j->name, i->ast, "procedure has 2 arguments with the same name");
            }
        }
        result.type = LC_CreateProcType(typelist, ret, n->tproc.vargs, n->tproc.vargs_any_promotion);
    } break;

    default: LC_IF(1, n, "internal compiler error: unhandled kind in LC_ResolveType '%s'", LC_ASTKindToString(n->kind));
    }

    n->type = result.type;
    LC_ASSERT(n, result.type);
    return result;
}

// clang-format off
// NA - Not applicable
// LC_RPS - OK if right side int of pointer size
// LC_LPS
// LC_TEQ - OK if types equal
// LC_RO0 - OK if right value is int equal nil
// LT - Left untyped to typed
// RT
// LF - Left to float
// RF
// LC_SR - String
enum { LC_INT, LC_FLOAT, LC_UT_INT, LC_UT_FLOAT, LC_UT_STR, LC_PTR, LC_VOID_PTR, LC_PROC, LC_AGG, LC_ARRAY, LC_ANY, LC_VOID, LC_TYPE_COUNT };
typedef enum { LC_NO, LC_OK, LC_LPS, LC_RPS, LC_TEQ, LC_NA, LC_RO0, LC_LT, LC_RT, LC_LF, LC_RF, LC_SR } LC_TypeRule;

LC_TypeRule CastingRules[LC_TYPE_COUNT][LC_TYPE_COUNT] = {
//\/:tgt( src)>   LC_INT      , LC_FLOAT , LC_UT_INT , LC_UT_FLOAT , LC_UT_STR , LC_PTR , LC_VOID_PTR , LC_PROC , LC_AGG , LC_ARRAY
/*[LC_INT] =        */{LC_OK  , LC_OK    , LC_OK     , LC_OK       , LC_NO     , LC_LPS , LC_LPS      , LC_NO   , LC_NO  , LC_NO}   ,
/*[LC_FLOAT] =      */{LC_OK  , LC_OK    , LC_OK     , LC_OK       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO}   ,
/*[LC_UT_INT] =     */{LC_NA  , LC_NA    , LC_NA     , LC_NA       , LC_NO     , LC_NA  , LC_NA       , LC_NA   , LC_NA  , LC_NA}   ,
/*[LC_UT_FLOAT] =   */{LC_NA  , LC_NA    , LC_NA     , LC_NA       , LC_NO     , LC_NA  , LC_NA       , LC_NA   , LC_NA  , LC_NA}   ,
/*[LC_UT_STR] =     */{LC_NA  , LC_NA    , LC_NA     , LC_NA       , LC_NO     , LC_NA  , LC_NA       , LC_NA   , LC_NA  , LC_NA}   ,
/*[LC_PTR] =        */{LC_RPS , LC_NO    , LC_OK     , LC_NO       , LC_SR     , LC_OK  , LC_OK       , LC_OK   , LC_NO  , LC_NO}   ,
/*[LC_VOID_PTR] =   */{LC_RPS , LC_NO    , LC_OK     , LC_NO       , LC_OK     , LC_OK  , LC_OK       , LC_OK   , LC_NO  , LC_NO}   ,
/*[LC_PROC] =       */{LC_NO  , LC_NO    , LC_NO     , LC_NO       , LC_NO     , LC_OK  , LC_OK       , LC_OK   , LC_NO  , LC_NO}   ,
/*[LC_AGG] =        */{LC_NO  , LC_NO    , LC_NO     , LC_NO       , LC_SR     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO}   ,
/*[LC_ARRAY] =      */{LC_NO  , LC_NO    , LC_NO     , LC_NO       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO}   ,
};

LC_TypeRule AssignRules[LC_TYPE_COUNT][LC_TYPE_COUNT] = {
//\/l r>            LC_INT    , LC_FLOAT , LC_UT_INT , LC_UT_FLOAT , LC_UT_STR , LC_PTR , LC_VOID_PTR , LC_PROC , LC_AGG , LC_ARRAY , LC_ANY
/*[LC_INT] =        */{LC_TEQ , LC_NO    , LC_OK     , LC_NO       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO    , LC_NO}  ,
/*[LC_FLOAT] =      */{LC_NO  , LC_TEQ   , LC_OK     , LC_OK       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO    , LC_NO}  ,
/*[LC_UT_INT] =     */{LC_NA  , LC_NA    , LC_NA     , LC_NA       , LC_NA     , LC_NA  , LC_NA       , LC_NA   , LC_NA  , LC_NA    , LC_NO}  ,
/*[LC_UT_FLOAT] =   */{LC_NA  , LC_NA    , LC_NA     , LC_NA       , LC_NA     , LC_NA  , LC_NA       , LC_NA   , LC_NA  , LC_NA    , LC_NO}  ,
/*[LC_UT_STR] =     */{LC_NA  , LC_NA    , LC_NA     , LC_NA       , LC_NA     , LC_NO  , LC_NA       , LC_NA   , LC_NA  , LC_NA    , LC_NO}  ,
/*[LC_PTR] =        */{LC_NO  , LC_NO    , LC_RO0    , LC_NO       , LC_SR     , LC_TEQ , LC_OK       , LC_NO   , LC_NO  , LC_NO    , LC_NO}  ,
/*[LC_VOID_PTR] =   */{LC_NO  , LC_NO    , LC_RO0    , LC_NO       , LC_OK     , LC_OK  , LC_OK       , LC_OK   , LC_NO  , LC_NO    , LC_NO}  ,
/*[LC_PROC] =       */{LC_NO  , LC_NO    , LC_RO0    , LC_NO       , LC_NO     , LC_NO  , LC_OK       , LC_TEQ  , LC_NO  , LC_NO    , LC_NO}  ,
/*[LC_AGG] =        */{LC_NO  , LC_NO    , LC_NO     , LC_NO       , LC_SR     , LC_NO  , LC_NO       , LC_NO   , LC_TEQ , LC_NO    , LC_TEQ} ,
/*[LC_ARRAY] =      */{LC_NO  , LC_NO    , LC_NO     , LC_NO       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_TEQ   , LC_NO}  ,
/*[LC_ANY] =        */{LC_OK  , LC_OK    , LC_OK     , LC_OK       , LC_OK     , LC_OK  , LC_NO       , LC_OK   , LC_OK  , LC_OK    , LC_OK}  ,
};

LC_TypeRule BinaryRules[LC_TYPE_COUNT][LC_TYPE_COUNT] = {
//\/l r>           LC_INT     , LC_FLOAT , LC_UT_INT , LC_UT_FLOAT , LC_UT_STR , LC_PTR , LC_VOID_PTR , LC_PROC , LC_AGG , LC_ARRAY
/*[LC_INT]     =   */{LC_TEQ  , LC_NO    , LC_RT     , LC_NO       , LC_NO     , LC_NO  , LC_OK       , LC_OK   , LC_NO  , LC_NO    , }       ,
/*[LC_FLOAT]   =   */{LC_NO   , LC_TEQ   , LC_RT     , LC_RT       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO    , }       ,
/*[LC_UT_INT]  =   */{LC_LT   , LC_LT    , LC_OK     , LC_LF       , LC_NO     , LC_OK  , LC_OK       , LC_OK   , LC_NO  , LC_NO    , }       ,
/*[LC_UT_FLOAT]=   */{LC_NO   , LC_LT    , LC_RF     , LC_OK       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO    , }       ,
/*[LC_UT_STR] =    */{LC_NO   , LC_NO    , LC_NO     , LC_NO       , LC_OK     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO    , }       ,
/*[LC_PTR]     =   */{LC_OK   , LC_NO    , LC_OK     , LC_NO       , LC_NO     , LC_OK  , LC_OK       , LC_NO   , LC_NO  , LC_NO    , }       ,
/*[LC_VOID_PTR]=   */{LC_OK   , LC_NO    , LC_OK     , LC_NO       , LC_NO     , LC_OK  , LC_OK       , LC_NO   , LC_NO  , LC_NO    , }       ,
/*[LC_PROC]    =   */{LC_OK   , LC_NO    , LC_OK     , LC_NO       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO    , }       ,
/*[LC_AGG]     =   */{LC_NO   , LC_NO    , LC_NO     , LC_NO       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO    , }       ,
/*[LC_ARRAY]   =   */{LC_NO   , LC_NO    , LC_NO     , LC_NO       , LC_NO     , LC_NO  , LC_NO       , LC_NO   , LC_NO  , LC_NO    , }       ,
};
// clang-format on

int GetTypeCategory(LC_Type *x) {
    if (x->kind >= LC_TypeKind_char && x->kind <= LC_TypeKind_ullong) return LC_INT;
    if ((x->kind == LC_TypeKind_float) || (x->kind == LC_TypeKind_double)) return LC_FLOAT;
    if (x->kind == LC_TypeKind_UntypedInt) return LC_UT_INT;
    if (x->kind == LC_TypeKind_UntypedFloat) return LC_UT_FLOAT;
    if (x->kind == LC_TypeKind_UntypedString) return LC_UT_STR;
    if (x == L->tpvoid) return LC_VOID_PTR;
    if (x == L->tany) return LC_ANY;
    if (x->kind == LC_TypeKind_Pointer) return LC_PTR;
    if (x->kind == LC_TypeKind_Proc) return LC_PROC;
    if (LC_IsAggType(x)) return LC_AGG;
    if (LC_IsArray(x)) return LC_ARRAY;
    return LC_VOID;
}

LC_FUNCTION LC_Operand LC_ResolveBinaryExpr(LC_AST *n, LC_Operand l, LC_Operand r) {
    bool isconst = LC_IsConst(l) && LC_IsConst(r);

    LC_TypeRule rule = BinaryRules[GetTypeCategory(l.type)][GetTypeCategory(r.type)];
    LC_IF(rule == LC_NO, n, "cannot perform binary operation, types don't qualify for it, left: '%s' right: '%s'", LC_GenLCType(l.type), LC_GenLCType(r.type));
    LC_IF(rule == LC_TEQ && l.type != r.type, n, "cannot perform binary operation, types are incompatible, left: '%s' right: '%s'", LC_GenLCType(l.type), LC_GenLCType(r.type));
    if (rule == LC_LT) l = LC_OPModType(l, r.type);
    if (rule == LC_RT) r = LC_OPModType(r, l.type);
    if (rule == LC_LF) l = LC_ConstCastFloat(n, l);
    if (rule == LC_RF) r = LC_ConstCastFloat(n, r);
    LC_ASSERT(n, rule == LC_LT || rule == LC_RT || rule == LC_LF || rule == LC_RF || rule == LC_OK || rule == LC_TEQ);

    // WARNING: if we allow for more then boolean operations on pointers then
    // we need to fix the propagated type here, we are counting on it getting
    // modified to bool.
    LC_Operand op = LC_OPType(l.type);
    if (isconst) op.flags |= LC_OPF_Const;
    if (LC_IsUTConst(l) && LC_IsUTConst(r)) {
        LC_PROP_ERROR(op, n, LC_EvalBinary(n, l, n->ebinary.op, r));
        SetConstVal(n, op.val);
    } else {
        LC_OPResult r = LC_IsBinaryExprValidForType(n->ebinary.op, op.type);
        LC_IF(r == LC_OPResult_Error, n, "invalid binary operation for type '%s'", LC_GenLCType(op.type));
        (LC_TryTyping(n->ebinary.left, op), LC_TryTyping(n->ebinary.right, op));
        if (r == LC_OPResult_Bool) op = LC_OPModBool(op);
    }

    return op;
}

LC_FUNCTION LC_Operand LC_ResolveTypeVargs(LC_AST *pos, LC_Operand v) {
    if (LC_IsUntyped(v.type)) v = LC_OPModDefaultUT(v);            // untyped => typed
    if (LC_IsSmallerThenInt(v.type)) v = LC_OPModType(v, L->tint); // c int promotion
    if (v.type == L->tfloat) v = LC_OPModType(v, L->tdouble);      // c int promotion
    return v;
}

LC_FUNCTION LC_Operand LC_ResolveTypeCast(LC_AST *pos, LC_Operand t, LC_Operand v) {
    LC_TypeRule rule = CastingRules[GetTypeCategory(t.type)][GetTypeCategory(v.type)];
    LC_IF(rule == LC_NO, pos, "cannot cast, types are incompatible, left: '%s' right: '%s'", LC_GenLCType(t.type), LC_GenLCType(v.type));
    LC_IF(rule == LC_RPS && v.type->size != L->tpvoid->size, pos, "cannot cast, integer type on right is not big enough to hold a pointer, left: '%s' right: '%s'", LC_GenLCType(t.type), LC_GenLCType(v.type));
    LC_IF(rule == LC_LPS && t.type->size != L->tpvoid->size, pos, "cannot cast, integer type on left is not big enough to hold a pointer, left: '%s' right: '%s'", LC_GenLCType(t.type), LC_GenLCType(v.type));
    LC_IF(rule == LC_SR && !LC_IsStr(t.type), pos, "cannot cast untyped string to non string type: '%s'", LC_GenLCType(t.type));
    LC_ASSERT(pos, rule == LC_LPS || rule == LC_RPS || rule == LC_OK);

    LC_Operand op = LC_OPType(t.type);
    op.flags      = (v.flags & LC_OPF_LValue) | (v.flags & LC_OPF_Const);
    return op;
}

LC_FUNCTION LC_Operand LC_ResolveTypeVarDecl(LC_AST *pos, LC_Operand t, LC_Operand v) {
    t.v = v.v;
    LC_IF(t.type && t.type->kind == LC_TypeKind_void, pos, "cannot create a variable of type void");
    LC_IF(v.type && v.type->kind == LC_TypeKind_void, pos, "cannot assign void expression to a variable");
    if (v.type && t.type) {
        LC_TypeRule rule = AssignRules[GetTypeCategory(t.type)][GetTypeCategory(v.type)];
        LC_IF(rule == LC_NO, pos, "cannot assign, types are incompatible, variable type: '%s' expression type: '%s'", LC_GenLCType(t.type), LC_GenLCType(v.type));
        LC_IF(rule == LC_RPS && v.type->size != L->tpvoid->size, pos, "cannot assign, integer type of expression is not big enough to hold a pointer, left: '%s' right: '%s'", LC_GenLCType(t.type), LC_GenLCType(v.type));
        LC_IF(rule == LC_LPS && t.type->size != L->tpvoid->size, pos, "cannot assign, integer type of variable is not big enough to hold a pointer, left: '%s' right: '%s'", LC_GenLCType(t.type), LC_GenLCType(v.type));
        LC_IF(rule == LC_TEQ && t.type != v.type, pos, "cannot assign, types require explicit cast, variable type: '%s' expression type: '%s'", LC_GenLCType(t.type), LC_GenLCType(v.type));
        LC_IF(rule == LC_RO0 && v.type != L->tuntypednil, pos, "cannot assign, can assign only const integer equal to 0, variable type: '%s' expression type: '%s'", LC_GenLCType(t.type), LC_GenLCType(v.type));
        LC_IF(rule == LC_SR && !LC_IsStr(t.type), pos, "cannot assign untyped string to non string type: '%s'", LC_GenLCType(t.type));
        LC_ASSERT(pos, rule == LC_LPS || rule == LC_RPS || rule == LC_OK || rule == LC_TEQ || rule == LC_RO0 || rule == LC_SR);
        return t;
    }

    if (v.type) return LC_OPModDefaultUT(v); // NULL := untyped => default
    if (t.type) return t;                    // T := NULL => T
    return LC_ReportASTError(pos, "internal compiler error: failed to resolve type of variable, both type and expression are null");
}

LC_FUNCTION LC_Operand LC_ResolveTypeAggregate(LC_AST *pos, LC_Type *type) {
    LC_Decl *decl = type->decl;
    if (type->kind == LC_TypeKind_Error) return LC_OPError();
    LC_TYPE_IF(type->kind == LC_TypeKind_Completing, pos, "cyclic dependency in type '%s'", type->decl->name);
    if (type->kind != LC_TypeKind_Incomplete) return LC_OPNull;
    LC_PUSH_SCOPE(L->resolver.package->apackage.ext->scope);

    LC_AST *n = decl->ast;
    LC_ASSERT(n, decl);
    LC_ASSERT(n, n->kind == LC_ASTKind_DeclStruct || n->kind == LC_ASTKind_DeclUnion);
    int decl_stack_size = 0;

    type->kind = LC_TypeKind_Completing;
    LC_ASTFor(it, n->dagg.first) {
        LC_Intern name = it->tagg_mem.name;

        LC_Operand op = LC_ResolveType(it->tagg_mem.type);
        if (LC_IsError(op)) {
            LC_MarkDeclError(decl);
            type->kind = LC_TypeKind_Error;
            continue; // handle error after we go through all fields
        }

        LC_Operand opc = LC_ResolveTypeAggregate(it, op.type);
        if (LC_IsError(opc)) {
            LC_MarkDeclError(decl);
            type->kind = LC_TypeKind_Error;
            continue; // handle error after we go through all fields
        }

        LC_TypeMember *mem = LC_AddTypeToList(&type->tagg.mems, name, op.type, it);
        LC_TYPE_IF(mem == NULL, it, "duplicate field '%s' in aggregate type '%s'", name, decl->name);
    }
    if (type->kind == LC_TypeKind_Error) {
        return LC_OPError();
    }
    LC_TYPE_IF(type->tagg.mems.count == 0, n, "aggregate type '%s' has no fields", decl->name);
    decl_stack_size += type->tagg.mems.count;

    LC_AST *packed = LC_HasNote(n, L->ipacked);
    if (n->kind == LC_ASTKind_DeclStruct) {
        type->kind      = LC_TypeKind_Struct;
        int field_sizes = 0;
        LC_TypeFor(it, type->tagg.mems.first) {
            int mem_align = packed ? 1 : it->type->align;
            LC_ASSERT(n, LC_IS_POW2(mem_align));
            type->size = (int)LC_AlignUp(type->size, mem_align);
            it->offset = type->size;
            field_sizes += it->type->size;
            type->align = LC_MAX(type->align, mem_align);
            type->size  = it->type->size + (int)LC_AlignUp(type->size, mem_align);
        }
        type->size    = (int)LC_AlignUp(type->size, type->align);
        type->padding = type->size - field_sizes;
    }

    if (n->kind == LC_ASTKind_DeclUnion) {
        type->kind = LC_TypeKind_Union;
        if (packed) LC_ReportASTError(packed, "@packed on union is invalid");
        LC_TypeFor(it, type->tagg.mems.first) {
            LC_ASSERT(n, LC_IS_POW2(it->type->align));
            type->size  = LC_MAX(type->size, it->type->size);
            type->align = LC_MAX(type->align, it->type->align);
        }
        type->size = (int)LC_AlignUp(type->size, type->align);
    }

    int map_size = LC_NextPow2(decl_stack_size * 2 + 1);
    decl->scope  = LC_CreateScope(map_size);

    LC_TypeFor(it, type->tagg.mems.first) {
        LC_Decl *d     = LC_CreateDecl(LC_DeclKind_Var, it->name, it->ast);
        d->state       = LC_DeclState_Resolved;
        d->type        = it->type;
        d->type_member = it;
        LC_AddDeclToScope(decl->scope, d);
    }

    LC_ASSERT(n, decl->scope->cap == map_size);

    if (L->on_decl_type_resolved) L->on_decl_type_resolved(decl);
    LC_AST *pkg = decl->package;
    LC_DLLAdd(pkg->apackage.ext->first_ordered, pkg->apackage.ext->last_ordered, decl);
    LC_POP_SCOPE();
    return LC_OPNull;
}

#undef LC_IF
#undef LC_DECL_IF
#undef LC_PROP_ERROR
#undef LC_DECL_PROP_ERROR
const int ParseStmtBlock_AllowSingleStmt = 1;

LC_FUNCTION LC_Parser LC_MakeParser(LC_Lex *x) {
    LC_Parser p = {0};
    p.at        = x->tokens;
    p.begin     = x->tokens;
    p.end       = x->tokens + x->token_count;
    p.x         = x;
    return p;
}

LC_FUNCTION LC_Parser *LC_MakeParserQuick(char *str) {
    LC_Lex *x = LC_LexStream("quick_lex", str, 0);
    LC_InternTokens(x);

    L->quick_parser = LC_MakeParser(x);
    L->parser       = &L->quick_parser;
    return L->parser;
}

LC_FUNCTION LC_AST *LC_ReportParseError(LC_Token *pos, const char *str, ...) {
    LC_FORMAT(L->arena, str, n);
    LC_SendErrorMessage(pos, n);
    L->errors += 1;
    LC_AST *r = LC_CreateAST(pos, LC_ASTKind_Error);
    return r;
}

LC_FUNCTION LC_Token *LC_Next(void) {
    if (L->parser->at < L->parser->end) {
        LC_Token *result = L->parser->at;
        L->parser->at += 1;
        return result;
    }
    return &L->NullToken;
}

LC_FUNCTION LC_Token *LC_Get(void) {
    if (L->parser->at < L->parser->end) {
        return L->parser->at;
    }
    return &L->NullToken;
}

LC_FUNCTION LC_Token *LC_GetI(int i) {
    LC_Token *result = L->parser->at + i;
    if (result >= L->parser->begin && result < L->parser->end) {
        return result;
    }
    return &L->NullToken;
}

LC_FUNCTION LC_Token *LC_Is(LC_TokenKind kind) {
    LC_Token *t = LC_Get();
    if (t->kind == kind) {
        return t;
    }
    return 0;
}

LC_FUNCTION LC_Token *LC_IsKeyword(LC_Intern intern) {
    LC_Token *t = LC_Get();
    if (t->kind == LC_TokenKind_Keyword && t->ident == intern) {
        return t;
    }
    return 0;
}

LC_FUNCTION LC_Token *LC_Match(LC_TokenKind kind) {
    LC_Token *t = LC_Get();
    if (t->kind == kind) {
        LC_Next();
        return t;
    }
    return 0;
}

LC_FUNCTION LC_Token *LC_MatchKeyword(LC_Intern intern) {
    LC_Token *t = LC_Get();
    if (t->kind == LC_TokenKind_Keyword && t->ident == intern) {
        LC_Next();
        return t;
    }
    return 0;
}

#define LC_EXPECT(token, KIND, context)                                                                                                                                   \
    LC_Token *token = LC_Match(KIND);                                                                                                                                     \
    if (!token) {                                                                                                                                                         \
        LC_Token *t = LC_Get();                                                                                                                                           \
        return LC_ReportParseError(t, "expected %s got instead %s, this happened while parsing: %s", LC_TokenKindToString(KIND), LC_TokenKindToString(t->kind), context); \
    }

#define LC_PROP_ERROR(expr, ...)              \
    expr = __VA_ARGS__;                       \
    do {                                      \
        if (expr->kind == LC_ASTKind_Error) { \
            return expr;                      \
        }                                     \
    } while (0)

// Pratt expression parser
// Based on this really good article: https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html
// clang-format off
LC_FUNCTION LC_Precedence LC_MakePrecedence(int left, int right) {
    LC_Precedence result = {left, right};
    return result;
}

LC_FUNCTION LC_Precedence LC_GetPrecedence(LC_PrecedenceKind p, LC_TokenKind kind) {
    if (p == LC_PrecedenceKind_Prefix) goto Prefix;
    if (p == LC_PrecedenceKind_Infix) goto Infix;
    if (p == LC_PrecedenceKind_Postfix) goto Postfix;
    LC_ASSERT(NULL, !"invalid codepath");

Prefix:
    switch (kind) {
        case LC_TokenKind_OpenBracket: return LC_MakePrecedence(-2, 22);
        case LC_TokenKind_Mul: case LC_TokenKind_BitAnd: case LC_TokenKind_Keyword: case LC_TokenKind_OpenParen:
        case LC_TokenKind_Sub: case LC_TokenKind_Add: case LC_TokenKind_Neg: case LC_TokenKind_Not: case LC_TokenKind_OpenBrace: return LC_MakePrecedence(-2, 20);
        default: return LC_MakePrecedence(-1, -1);
    }
Infix:
    switch (kind) {
        case LC_TokenKind_Or: return LC_MakePrecedence(9, 10);
        case LC_TokenKind_And: return LC_MakePrecedence(11, 12);
        case LC_TokenKind_Equals: case LC_TokenKind_NotEquals: case LC_TokenKind_GreaterThen:
        case LC_TokenKind_GreaterThenEq: case LC_TokenKind_LesserThen: case LC_TokenKind_LesserThenEq: return LC_MakePrecedence(13, 14);
        case LC_TokenKind_Sub: case LC_TokenKind_Add: case LC_TokenKind_BitOr: case LC_TokenKind_BitXor: return LC_MakePrecedence(15, 16);
        case LC_TokenKind_RightShift: case LC_TokenKind_LeftShift: case LC_TokenKind_BitAnd:
        case LC_TokenKind_Mul: case LC_TokenKind_Div: case LC_TokenKind_Mod: return LC_MakePrecedence(17, 18);
        default: return LC_MakePrecedence(0, 0);
    }
Postfix:
    switch (kind) {
        case LC_TokenKind_Dot: case LC_TokenKind_OpenBracket: case LC_TokenKind_OpenParen: return LC_MakePrecedence(21, -2);
        default: return LC_MakePrecedence(-1, -1);
    }
}

LC_FUNCTION LC_AST *LC_ParseExprEx(int min_bp) {
    LC_AST *left = NULL;
    LC_Token *prev = LC_GetI(-1);
    LC_Token *t = LC_Next();
    LC_Precedence prefixbp = LC_GetPrecedence(LC_PrecedenceKind_Prefix, t->kind);

    // parse prefix expression
    switch (t->kind) {
        case LC_TokenKind_RawString:
        case LC_TokenKind_String: { left = LC_CreateAST(t, LC_ASTKind_ExprString); left->eatom.name = t->ident; } break;
        case LC_TokenKind_Ident: { left = LC_CreateAST(t, LC_ASTKind_ExprIdent); left->eident.name = t->ident; } break;
        case LC_TokenKind_Int: { left = LC_CreateAST(t, LC_ASTKind_ExprInt); left->eatom.i = t->i; } break;
        case LC_TokenKind_Float: { left = LC_CreateAST(t, LC_ASTKind_ExprFloat); left->eatom.d = t->f64; } break;
        case LC_TokenKind_Unicode: { left = LC_CreateAST(t, LC_ASTKind_ExprInt); left->eatom.i = t->i; } break;

        case LC_TokenKind_Keyword: {
            if (t->ident == L->kfalse) {
                left = LC_CreateAST(t, LC_ASTKind_ExprBool);
                LC_Bigint_init_signed(&left->eatom.i, false);
            } else if (t->ident == L->ktrue) {
                left = LC_CreateAST(t, LC_ASTKind_ExprBool);
                LC_Bigint_init_signed(&left->eatom.i, true);
            } else {
                return LC_ReportParseError(t, "got unexpected keyword '%s' while parsing expression", t->ident);
            }
        } break;

        case LC_TokenKind_AddPtr: {
            LC_EXPECT(open_paren, LC_TokenKind_OpenParen, "addptr");
            LC_AST *LC_PROP_ERROR(ptr, LC_ParseExprEx(0));
            LC_EXPECT(comma, LC_TokenKind_Comma, "addptr");
            LC_AST *LC_PROP_ERROR(offset, LC_ParseExprEx(0));
            LC_EXPECT(close_paren, LC_TokenKind_CloseParen, "addptr");
            left = LC_CreateBinary(t, ptr, offset, LC_TokenKind_EOF);
            left->kind = LC_ASTKind_ExprAddPtr;
        } break;

        case LC_TokenKind_Colon: {
            left = LC_CreateAST(t, LC_ASTKind_ExprType);
            LC_PROP_ERROR(left->etype.type, LC_ParseType());

            LC_Token *open = LC_Get();
            if (LC_Match(LC_TokenKind_OpenBrace)) {
                left = LC_ParseCompo(open, left);
                if (left->kind == LC_ASTKind_Error) {
                    L->parser->at = t;
                    return left;
                }
            } else if (LC_Match(LC_TokenKind_OpenParen)) {
                LC_AST *type = left;
                left = LC_CreateAST(open, LC_ASTKind_ExprCast);
                left->ecast.type = type->etype.type;
                type->kind = LC_ASTKind_Ignore;

                LC_PROP_ERROR(left->ecast.expr, LC_ParseExpr());
                LC_EXPECT(close_paren, LC_TokenKind_CloseParen, "cast expression");
            }
        } break;

        case LC_TokenKind_OpenBrace: {
            left = LC_ParseCompo(t, left);
            if (left->kind == LC_ASTKind_Error) {
                L->parser->at = prev;
                return left;
            }
        } break;

        case LC_TokenKind_Not: case LC_TokenKind_Neg: case LC_TokenKind_Add: case LC_TokenKind_Sub: {
            LC_AST *LC_PROP_ERROR(expr, LC_ParseExprEx(prefixbp.right));
            left = LC_CreateUnary(t, t->kind, expr);
        } break;

        case LC_TokenKind_BitAnd: {
            LC_AST *LC_PROP_ERROR(expr, LC_ParseExprEx(prefixbp.right));
            left = LC_CreateUnary(t, t->kind, expr);
            left->kind = LC_ASTKind_ExprGetPointerOfValue;
        } break;

        case LC_TokenKind_Mul: {
            LC_AST *LC_PROP_ERROR(expr, LC_ParseExprEx(prefixbp.right));
            left = LC_CreateUnary(t, t->kind, expr);
            left->kind = LC_ASTKind_ExprGetValueOfPointer;
        } break;

        case LC_TokenKind_OpenParen: {
            LC_AST *LC_PROP_ERROR(expr, LC_ParseExprEx(0));
            left = expr;
            LC_EXPECT(_c, LC_TokenKind_CloseParen, "expression");
        } break;

        default: return LC_ReportParseError(prev, "got invalid token: %s, while parsing expression", LC_TokenKindToString(t->kind));
    }

    for (;;) {
        t = LC_Get();

        // lets say [+] is left:1, right:2 and we parse 2+3+4
        // We pass min_bp of 2 to the next recursion
        // in recursion we check if left(1) > min_bp(2)
        // it's not so we don't recurse - we break
        // We do the for loop instead

        LC_Precedence postfix_bp = LC_GetPrecedence(LC_PrecedenceKind_Postfix, t->kind);
        LC_Precedence infix_bp = LC_GetPrecedence(LC_PrecedenceKind_Infix, t->kind);

        // parse postfix expression
        if (postfix_bp.left > min_bp) {
            LC_Next();
            switch (t->kind) {
                case LC_TokenKind_OpenBracket: {
                    LC_AST *LC_PROP_ERROR(index, LC_ParseExprEx(0));
                    LC_EXPECT(close_bracket, LC_TokenKind_CloseBracket, "index expression");
                    left = LC_CreateIndex(t, left, index);
                } break;
                case LC_TokenKind_OpenParen: {
                    LC_PROP_ERROR(left, LC_ParseCompo(t, left));
                } break;
                case LC_TokenKind_Dot: {
                    LC_EXPECT(ident, LC_TokenKind_Ident, "field access expression");
                    LC_AST *field = LC_CreateAST(t, LC_ASTKind_ExprField);
                    field->efield.left = left;
                    field->efield.right = ident->ident;
                    left = field;
                } break;
                default: {}
            }
        }

        // parse infix expression
        else if (infix_bp.left > min_bp) {
            t = LC_Next();
            LC_AST *LC_PROP_ERROR(right, LC_ParseExprEx(infix_bp.right));
            left = LC_CreateBinary(t, left, right, t->kind);
        }

        else break;
    }

    if (L->on_expr_parsed) L->on_expr_parsed(left);
    return left;
}
// clang-format on

LC_FUNCTION LC_AST *LC_ParseCompo(LC_Token *pos, LC_AST *left) {
    if (pos->kind != LC_TokenKind_OpenBrace && pos->kind != LC_TokenKind_OpenParen) {
        return LC_ReportParseError(pos, "internal compiler error: expected open brace or open paren in %s", __FUNCTION__);
    }

    LC_ASTKind   kind       = pos->kind == LC_TokenKind_OpenBrace ? LC_ASTKind_ExprCompound : LC_ASTKind_ExprCall;
    LC_TokenKind close_kind = pos->kind == LC_TokenKind_OpenBrace ? LC_TokenKind_CloseBrace : LC_TokenKind_CloseParen;
    LC_ASTKind   item_kind  = pos->kind == LC_TokenKind_OpenBrace ? LC_ASTKind_ExprCompoundItem : LC_ASTKind_ExprCallItem;

    LC_AST *n      = LC_CreateAST(pos, kind);
    n->ecompo.name = left;

    while (!LC_Is(close_kind)) {
        LC_Token *vpos = LC_Get();
        LC_AST   *v    = LC_CreateAST(vpos, item_kind);

        if (LC_Match(LC_TokenKind_OpenBracket)) {
            if (kind == LC_ASTKind_ExprCall) return LC_ReportParseError(vpos, "procedure calls cant have indexed arguments");
            LC_PROP_ERROR(v->ecompo_item.index, LC_ParseExpr());
            LC_EXPECT(close_bracket, LC_TokenKind_CloseBracket, "compo expression");
            LC_EXPECT(assign, LC_TokenKind_Assign, "compo expression");
            LC_PROP_ERROR(v->ecompo_item.expr, LC_ParseExpr());
        } else {
            LC_PROP_ERROR(v->ecompo_item.expr, LC_ParseExpr());
            if (LC_Match(LC_TokenKind_Assign)) {
                LC_AST *e = v->ecompo_item.expr;
                if (e->kind != LC_ASTKind_ExprIdent) return LC_ReportParseError(LC_GetI(-1), "named argument is required to be an identifier");
                LC_PROP_ERROR(v->ecompo_item.expr, LC_ParseExpr());
                v->ecompo_item.name = e->eident.name;
                e->kind             = LC_ASTKind_Ignore; // :FreeAST
            }
        }

        n->ecompo.size += 1;
        LC_DLLAdd(n->ecompo.first, n->ecompo.last, v);
        if (!LC_Match(LC_TokenKind_Comma)) break;
    }
    LC_EXPECT(close_token, close_kind, "compo expression");
    return n;
}

LC_FUNCTION LC_AST *LC_ParseExpr(void) {
    return LC_ParseExprEx(0);
}

LC_FUNCTION LC_AST *LC_ParseProcType(LC_Token *pos) {
    LC_AST *n = LC_CreateAST(pos, LC_ASTKind_TypespecProc);
    LC_EXPECT(open_paren, LC_TokenKind_OpenParen, "procedure typespec");
    if (!LC_Match(LC_TokenKind_CloseParen)) {
        for (;;) {
            if (LC_Match(LC_TokenKind_ThreeDots)) {
                n->tproc.vargs = true;

                LC_Token *any = LC_Get();
                if (any->kind == LC_TokenKind_Ident && any->ident == L->iAny) {
                    n->tproc.vargs_any_promotion = true;
                    LC_Next();
                }
                break;
            }
            LC_EXPECT(ident, LC_TokenKind_Ident, "procedure typespec argument list");
            LC_AST *v         = LC_CreateAST(ident, LC_ASTKind_TypespecProcArg);
            v->tproc_arg.name = ident->ident;

            LC_EXPECT(colon, LC_TokenKind_Colon, "procedure typespec argument list");
            LC_PROP_ERROR(v->tproc_arg.type, LC_ParseType());

            if (LC_Match(LC_TokenKind_Assign)) {
                LC_PROP_ERROR(v->tproc_arg.expr, LC_ParseExpr());
            }

            v->notes = LC_ParseNotes();
            LC_DLLAdd(n->tproc.first, n->tproc.last, v);

            if (!LC_Match(LC_TokenKind_Comma)) break;
        }
        LC_EXPECT(close_paren, LC_TokenKind_CloseParen, "procedure typespec argument list");
    }
    if (LC_Match(LC_TokenKind_Colon)) {
        LC_PROP_ERROR(n->tproc.ret, LC_ParseType());
    }
    return n;
}

LC_FUNCTION LC_AST *LC_ParseType(void) {
    LC_AST   *n = NULL;
    LC_Token *t = LC_Next();
    if (t->kind == LC_TokenKind_Ident) {
        n              = LC_CreateAST(t, LC_ASTKind_TypespecIdent);
        n->eident.name = t->ident;
        LC_Token *dot  = LC_Match(LC_TokenKind_Dot);
        if (dot) {
            LC_AST *field      = LC_CreateAST(t, LC_ASTKind_TypespecField);
            field->efield.left = n;
            LC_EXPECT(ident, LC_TokenKind_Ident, "field access typespec");
            field->efield.right = ident->ident;
            return field;
        }
    } else if (t->kind == LC_TokenKind_Mul) {
        n = LC_CreateAST(t, LC_ASTKind_TypespecPointer);
        LC_PROP_ERROR(n->tpointer.base, LC_ParseType());
    } else if (t->kind == LC_TokenKind_OpenBracket) {
        n = LC_CreateAST(t, LC_ASTKind_TypespecArray);
        if (!LC_Match(LC_TokenKind_CloseBracket)) {
            LC_PROP_ERROR(n->tarray.index, LC_ParseExpr());
            LC_EXPECT(close_bracket, LC_TokenKind_CloseBracket, "array typespec");
        }
        LC_PROP_ERROR(n->tarray.base, LC_ParseType());
    } else if (t->kind == LC_TokenKind_Keyword && t->ident == L->kproc) {
        LC_PROP_ERROR(n, LC_ParseProcType(t));
    } else {
        return LC_ReportParseError(t, "failed to parse typespec, invalid token %s", LC_TokenKindToString(t->kind));
    }

    if (L->on_typespec_parsed) L->on_typespec_parsed(n);
    return n;
}

LC_FUNCTION LC_AST *LC_ParseForStmt(LC_Token *pos) {
    LC_AST *n = LC_CreateAST(pos, LC_ASTKind_StmtFor);

    if (LC_Get()->kind != LC_TokenKind_OpenBrace) {
        if (!LC_Is(LC_TokenKind_Semicolon)) {
            LC_PROP_ERROR(n->sfor.init, LC_ParseStmt(false));
            if (n->sfor.init->kind == LC_ASTKind_StmtExpr) {
                n->sfor.cond       = n->sfor.init->sexpr.expr;
                n->sfor.init->kind = LC_ASTKind_Ignore; // :FreeAST
                n->sfor.init       = NULL;
                goto skip_to_last;
            } else if (n->sfor.init->kind != LC_ASTKind_StmtVar && n->sfor.init->kind != LC_ASTKind_StmtAssign) {
                return LC_ReportParseError(n->sfor.init->pos, "invalid for loop syntax, expected variable intializer or assignment");
            }
        }

        if (LC_Match(LC_TokenKind_Semicolon) && !LC_Is(LC_TokenKind_Semicolon)) {
            LC_PROP_ERROR(n->sfor.cond, LC_ParseExpr());
        }

    skip_to_last:;
        if (LC_Match(LC_TokenKind_Semicolon)) {
            LC_PROP_ERROR(n->sfor.inc, LC_ParseStmt(false));
            if (n->sfor.inc->kind != LC_ASTKind_StmtAssign && n->sfor.inc->kind != LC_ASTKind_StmtExpr) {
                return LC_ReportParseError(n->sfor.inc->pos, "invalid for loop syntax, expected assignment or expression");
            }
        }
    }

    LC_PROP_ERROR(n->sfor.body, LC_ParseStmtBlock(ParseStmtBlock_AllowSingleStmt));
    n->sfor.body->sblock.kind = SBLK_Loop;
    return n;
}

LC_FUNCTION LC_AST *LC_ParseSwitchStmt(LC_Token *pos) {
    LC_AST *n = LC_CreateAST(pos, LC_ASTKind_StmtSwitch);
    LC_PROP_ERROR(n->sswitch.expr, LC_ParseExpr());
    LC_EXPECT(open_brace, LC_TokenKind_OpenBrace, "switch statement");
    for (;;) {
        LC_Token *pos = LC_Get();
        if (LC_MatchKeyword(L->kcase)) {
            LC_AST *v = LC_CreateAST(pos, LC_ASTKind_StmtSwitchCase);
            do {
                LC_AST *LC_PROP_ERROR(expr, LC_ParseExpr());
                LC_DLLAdd(v->scase.first, v->scase.last, expr);
                n->sswitch.total_switch_case_count += 1;
            } while (LC_Match(LC_TokenKind_Comma));
            LC_EXPECT(colon, LC_TokenKind_Colon, "switch statement case");
            LC_PROP_ERROR(v->scase.body, LC_ParseStmtBlock(ParseStmtBlock_AllowSingleStmt));
            v->notes = LC_ParseNotes();
            LC_DLLAdd(n->sswitch.first, n->sswitch.last, v);
        } else if (LC_MatchKeyword(L->kdefault)) {
            LC_EXPECT(colon, LC_TokenKind_Colon, "switch statement default case");
            LC_AST *v = LC_CreateAST(pos, LC_ASTKind_StmtSwitchDefault);
            LC_PROP_ERROR(v->scase.body, LC_ParseStmtBlock(ParseStmtBlock_AllowSingleStmt));
            LC_EXPECT(close_brace, LC_TokenKind_CloseBrace, "switch statement default case");
            v->notes = LC_ParseNotes();
            LC_DLLAdd(n->sswitch.first, n->sswitch.last, v);
            break;
        } else {
            return LC_ReportParseError(LC_Get(), "invalid token while parsing switch statement");
        }

        if (LC_Match(LC_TokenKind_EOF)) return LC_ReportParseError(pos, "Unclosed '}' switch stmt, reached end of file");
        if (LC_Match(LC_TokenKind_CloseBrace)) break;
    }
    return n;
}

LC_FUNCTION LC_AST *LC_ParseStmt(bool check_semicolon) {
    LC_AST   *n    = 0;
    LC_Token *pos  = LC_Get();
    LC_Token *pos1 = LC_GetI(1);
    if (LC_MatchKeyword(L->kreturn)) {
        n = LC_CreateAST(pos, LC_ASTKind_StmtReturn);
        if (LC_Get()->kind != LC_TokenKind_Semicolon) {
            LC_PROP_ERROR(n->sreturn.expr, LC_ParseExpr());
        }
    }

    else if (LC_MatchKeyword(L->kbreak)) {
        n               = LC_CreateAST(pos, LC_ASTKind_StmtBreak);
        LC_Token *ident = LC_Match(LC_TokenKind_Ident);
        if (ident) n->sbreak.name = ident->ident;
    }

    else if (LC_MatchKeyword(L->kcontinue)) {
        n               = LC_CreateAST(pos, LC_ASTKind_StmtContinue);
        LC_Token *ident = LC_Match(LC_TokenKind_Ident);
        if (ident) n->scontinue.name = ident->ident;
    }

    else if (LC_MatchKeyword(L->kdefer)) {
        check_semicolon = false;
        n               = LC_CreateAST(pos, LC_ASTKind_StmtDefer);
        LC_PROP_ERROR(n->sdefer.body, LC_ParseStmtBlock(ParseStmtBlock_AllowSingleStmt));
        n->sdefer.body->sblock.kind = SBLK_Defer;
    }

    else if (LC_MatchKeyword(L->kfor)) {
        LC_PROP_ERROR(n, LC_ParseForStmt(pos));
        check_semicolon = false;
    }

    else if (LC_MatchKeyword(L->kswitch)) {
        LC_PROP_ERROR(n, LC_ParseSwitchStmt(pos));
        check_semicolon = false;
    }

    else if (LC_MatchKeyword(L->kif)) {
        n = LC_CreateAST(pos, LC_ASTKind_StmtIf);
        LC_PROP_ERROR(n->sif.expr, LC_ParseExpr());
        LC_PROP_ERROR(n->sif.body, LC_ParseStmtBlock(ParseStmtBlock_AllowSingleStmt));
        for (;;) {
            if (!LC_MatchKeyword(L->kelse)) break;

            LC_AST *v = LC_CreateAST(LC_GetI(-1), LC_ASTKind_StmtElse);
            if (LC_MatchKeyword(L->kif)) {
                v->kind = LC_ASTKind_StmtElseIf;
                LC_PROP_ERROR(v->sif.expr, LC_ParseExpr());
            }
            LC_PROP_ERROR(v->sif.body, LC_ParseStmtBlock(ParseStmtBlock_AllowSingleStmt));
            LC_DLLAdd(n->sif.first, n->sif.last, v);
        }
        check_semicolon = false;
    }

    else if (LC_Match(LC_TokenKind_Hash)) { // #c(``);
        n = LC_CreateAST(LC_Get(), LC_ASTKind_StmtNote);
        LC_PROP_ERROR(n->snote.expr, LC_ParseNote());
    } else if (pos->kind == LC_TokenKind_OpenBrace) { // { block }
        n               = LC_ParseStmtBlock(0);
        check_semicolon = false;
    }

    else if (pos->kind == LC_TokenKind_Ident && pos1->kind == LC_TokenKind_Colon) { // Name: ...
        LC_Next();
        LC_Next();

        if (LC_MatchKeyword(L->kfor)) {
            LC_PROP_ERROR(n, LC_ParseForStmt(LC_GetI(-1)));
            n->sfor.body->sblock.name = pos->ident;
            check_semicolon           = false;
        } else {
            n              = LC_CreateAST(pos, LC_ASTKind_StmtVar);
            LC_Intern name = pos->ident;
            if (LC_Match(LC_TokenKind_Assign)) {
                LC_PROP_ERROR(n->svar.expr, LC_ParseExpr());
                n->svar.name = name;
            } else if (LC_Match(LC_TokenKind_Colon)) {
                n->kind = LC_ASTKind_StmtConst;
                LC_PROP_ERROR(n->sconst.expr, LC_ParseExpr());
                n->sconst.name = name;
            } else {
                n->svar.name = name;
                LC_PROP_ERROR(n->svar.type, LC_ParseType());
                if (LC_Match(LC_TokenKind_Assign)) {
                    if (LC_Match(LC_TokenKind_Hash)) {
                        LC_AST *note = LC_CreateAST(LC_Get(), LC_ASTKind_ExprNote);
                        LC_PROP_ERROR(note->enote.expr, LC_ParseNote());
                        n->svar.expr = note;
                    } else {
                        LC_PROP_ERROR(n->svar.expr, LC_ParseExpr());
                    }
                }
            }
        }
    } else {
        n = LC_CreateAST(pos, LC_ASTKind_StmtExpr);
        LC_PROP_ERROR(n->sexpr.expr, LC_ParseExpr());

        LC_Token *t = LC_Get();
        if (LC_IsAssign(t->kind)) {
            LC_Next();
            LC_AST *left = n->sexpr.expr;

            n->kind = LC_ASTKind_StmtAssign;
            LC_PROP_ERROR(n->sassign.right, LC_ParseExpr());
            n->sassign.left = left;
            n->sassign.op   = t->kind;
        }
    }

    if (check_semicolon) {
        if (!LC_Match(LC_TokenKind_Semicolon)) return LC_ReportParseError(LC_GetI(-1), "statement lacks a semicolon at the end");
    }

    n->notes = LC_ParseNotes();
    return n;
}

LC_FUNCTION LC_AST *LC_ParseStmtBlock(int flags) {
    LC_AST *n = LC_CreateAST(LC_Get(), LC_ASTKind_StmtBlock);

    bool single_stmt = false;
    if (flags & ParseStmtBlock_AllowSingleStmt) {
        if (!LC_Is(LC_TokenKind_OpenBrace)) {
            LC_AST *LC_PROP_ERROR(v, LC_ParseStmt(true));
            LC_DLLAdd(n->sblock.first, n->sblock.last, v);
            single_stmt = true;
        }
    }

    if (!single_stmt) {
        LC_EXPECT(open_brace, LC_TokenKind_OpenBrace, "statement block");
        if (!LC_Match(LC_TokenKind_CloseBrace)) {
            for (;;) {
                LC_AST *v = LC_ParseStmt(true);

                // Eat until next statement in case of error
                if (v->kind == LC_ASTKind_Error) {
                    for (;;) {
                        if (LC_Is(LC_TokenKind_EOF) || LC_Is(LC_TokenKind_OpenBrace) || LC_Match(LC_TokenKind_CloseBrace)) return v;
                        if (LC_Match(LC_TokenKind_Semicolon)) break;
                        LC_Next();
                    }
                }

                if (L->on_stmt_parsed) L->on_stmt_parsed(v);
                LC_DLLAdd(n->sblock.first, n->sblock.last, v);
                if (LC_Match(LC_TokenKind_EOF)) return LC_ReportParseError(open_brace, "Unclosed '}' stmt list, reached end of file");
                if (LC_Match(LC_TokenKind_CloseBrace)) break;
            }
        }
    }

    if (L->on_stmt_parsed) L->on_stmt_parsed(n);
    return n;
}

LC_FUNCTION LC_AST *LC_ParseProcDecl(LC_Token *name) {
    LC_AST *n     = LC_CreateAST(name, LC_ASTKind_DeclProc);
    n->dbase.name = name->ident;
    LC_PROP_ERROR(n->dproc.type, LC_ParseProcType(name));

    LC_Token *ob = LC_Get();
    if (ob->kind == LC_TokenKind_OpenBrace) {
        // Here I added additional error handling which slows down compilation a bit.
        // We can for sure deduce where procs end and where they begin because of the syntaxes
        // nature - so to avoid any error spills from one procedure to another and I
        // seek for the last brace of procedure and set 'end' on parser to 1 after that token.
        LC_Token *cb              = ob;
        LC_Token *last_open_brace = ob;
        int       pair_counter    = 0;

        // Seek for the last '}' close brace of procedure
        for (;;) {
            LC_Token *d = LC_GetI(3);
            if (LC_GetI(0)->kind == LC_TokenKind_Ident && LC_GetI(1)->kind == LC_TokenKind_Colon && LC_GetI(2)->kind == LC_TokenKind_Colon && d->kind == LC_TokenKind_Keyword) {
                if (d->ident == L->kproc || d->ident == L->kstruct || d->ident == L->kunion || d->ident == L->ktypedef) {
                    break;
                }
            }

            LC_Token *token = LC_Next();
            if (token == &L->NullToken) break;
            if (token->kind == LC_TokenKind_OpenBrace) pair_counter += 1;
            if (token->kind == LC_TokenKind_OpenBrace) last_open_brace = token;
            if (token->kind == LC_TokenKind_CloseBrace) pair_counter -= 1;
            if (token->kind == LC_TokenKind_CloseBrace) cb = token;
        }
        if (pair_counter != 0) return LC_ReportParseError(last_open_brace, "unclosed open brace '{' inside this procedure");
        L->parser->at = ob;

        // Set the parsing boundary to one after the last close brace
        LC_Token *save_end = L->parser->end;
        L->parser->end     = cb + 1;
        n->dproc.body      = LC_ParseStmtBlock(0);
        L->parser->end     = save_end;
        if (n->dproc.body->kind == LC_ASTKind_Error) return n->dproc.body;

        n->dproc.body->sblock.kind = SBLK_Proc;
    } else {
        LC_EXPECT(semicolon, LC_TokenKind_Semicolon, "procedure declaration");
    }

    return n;
}

LC_FUNCTION LC_AST *LC_ParseStruct(LC_ASTKind kind, LC_Token *ident) {
    LC_AST *n     = LC_CreateAST(ident, kind);
    n->dbase.name = ident->ident;
    LC_EXPECT(open_brace, LC_TokenKind_OpenBrace, "struct declaration");
    for (;;) {
        LC_AST *v = LC_CreateAST(ident, LC_ASTKind_TypespecAggMem);
        LC_EXPECT(ident, LC_TokenKind_Ident, "struct member");
        v->tagg_mem.name = ident->ident;
        LC_EXPECT(colon, LC_TokenKind_Colon, "struct member");
        LC_PROP_ERROR(v->tagg_mem.type, LC_ParseType());
        LC_EXPECT(semicolon, LC_TokenKind_Semicolon, "struct member");

        v->notes = LC_ParseNotes();
        LC_DLLAdd(n->dagg.first, n->dagg.last, v);
        if (LC_Match(LC_TokenKind_EOF)) return LC_ReportParseError(ident, "Unclosed '}' struct, reached end of file");
        if (LC_Match(LC_TokenKind_CloseBrace)) break;
    }

    return n;
}

LC_FUNCTION LC_AST *LC_ParseTypedef(LC_Token *ident) {
    LC_AST *n     = LC_CreateAST(ident, LC_ASTKind_DeclTypedef);
    n->dbase.name = ident->ident;
    LC_PROP_ERROR(n->dtypedef.type, LC_ParseType());
    if (!LC_Match(LC_TokenKind_Semicolon)) return LC_ReportParseError(LC_GetI(-1), "expected semicolon ';' after typedef declaration, got instead %s", LC_TokenKindToString(LC_GetI(-1)->kind));
    return n;
}

LC_FUNCTION LC_AST *LC_CreateNote(LC_Token *pos, LC_Intern ident) {
    LC_AST *n = LC_CreateAST(pos, LC_ASTKind_Note);

    LC_AST *astident      = LC_CreateAST(pos, LC_ASTKind_ExprIdent);
    astident->eident.name = ident;
    n->ecompo.name        = astident;

    return n;
}

LC_FUNCTION LC_AST *LC_ParseNote(void) {
    LC_AST *n = NULL;

    // syntactic sugar
    // #`stuff` => #c(`stuff`)
    LC_Token *str_token = LC_Match(LC_TokenKind_RawString);
    if (str_token) {
        n = LC_CreateNote(str_token, L->ic);

        // Add CallItem
        {
            LC_AST *astcallitem                       = LC_CreateAST(str_token, LC_ASTKind_ExprCallItem);
            astcallitem->ecompo_item.expr             = LC_CreateAST(str_token, LC_ASTKind_ExprString);
            astcallitem->ecompo_item.expr->eatom.name = str_token->ident;
            LC_DLLAdd(n->ecompo.first, n->ecompo.last, astcallitem);
        }
    } else {

        LC_EXPECT(ident, LC_TokenKind_Ident, "note");
        if (!LC_IsNoteDeclared(ident->ident)) {
            LC_ReportParseError(ident, "unregistered note name: '%s'", ident->ident);
        }

        LC_AST *astident      = LC_CreateAST(ident, LC_ASTKind_ExprIdent);
        astident->eident.name = ident->ident;

        LC_Token *open_paren = LC_Match(LC_TokenKind_OpenParen);
        if (open_paren) {
            n = LC_ParseCompo(open_paren, astident);
        } else {
            n = LC_CreateAST(ident, LC_ASTKind_Note);
        }
        n->ecompo.name = astident;
        n->kind        = LC_ASTKind_Note;
    }
    return n;
}

LC_FUNCTION LC_AST *LC_ParseNotes(void) {
    LC_Token *pos   = LC_Get();
    LC_AST   *first = 0;
    LC_AST   *last  = 0;
    for (;;) {
        LC_Token *t = LC_Match(LC_TokenKind_Note);
        if (!t) break;
        LC_AST *n = LC_ParseNote();
        if (n->kind == LC_ASTKind_Error) continue;
        LC_DLLAdd(first, last, n);
    }

    if (first) {
        LC_AST *n           = LC_CreateAST(pos, LC_ASTKind_NoteList);
        n->anote_list.first = first;
        n->anote_list.last  = last;
        return n;
    }
    return 0;
}

LC_FUNCTION bool ParseHashBuildIf(LC_AST *n) {
    LC_Token *t0 = LC_GetI(0);
    LC_Token *t1 = LC_GetI(1);
    if (t0->kind == LC_TokenKind_Hash && t1->kind == LC_TokenKind_Ident && t1->ident == L->ibuild_if) {
        LC_Next();

        LC_AST *note = LC_ParseNote();
        if (note->kind == LC_ASTKind_Error) {
            LC_EatUntilNextValidDecl();
            return true;
        }

        if (!LC_Match(LC_TokenKind_Semicolon)) {
            LC_ReportParseError(LC_GetI(-1), "expected ';' semicolon");
            LC_EatUntilNextValidDecl();
            return true;
        }

        LC_AST *note_list = LC_CreateAST(t0, LC_ASTKind_NoteList);
        LC_DLLAdd(note_list->anote_list.first, note_list->anote_list.last, note);
        n->notes = note_list;

        return LC_ResolveBuildIf(note);
    }
    return true;
}

LC_FUNCTION bool LC_ResolveBuildIf(LC_AST *build_if) {
    LC_ExprCompo *note = &build_if->anote;
    if (note->size != 1) {
        LC_ReportParseError(LC_GetI(-1), "invalid argument count for #build_if directive, expected 1, got %d", note->size);
        return true;
    }

    LC_ExprCompoItem *item = &note->first->ecompo_item;
    if (item->index != NULL || item->name != 0) {
        LC_ReportParseError(LC_GetI(-1), "invalid syntax, you have passed in a named or indexed argument to #build_if");
        return true;
    }

    LC_PUSH_PACKAGE(L->builtin_package);
    LC_Operand op = LC_ResolveExpr(item->expr);
    LC_POP_PACKAGE();
    if (!LC_IsUTConst(op)) {
        LC_ReportParseError(LC_GetI(-1), "expected #build_if to have an untyped constant expcession");
        return true;
    }
    if (!LC_IsUTInt(op.type)) {
        LC_ReportParseError(LC_GetI(-1), "expected #build_if to have expression of type untyped int");
        return true;
    }

    int64_t result = LC_Bigint_as_signed(&op.v.i);
    return (bool)result;
}

LC_FUNCTION LC_AST *LC_ParseDecl(LC_AST *file) {
    LC_AST   *n           = 0;
    LC_Token *doc_comment = LC_Match(LC_TokenKind_DocComment);
    LC_Token *ident       = LC_Get();

    if (LC_Match(LC_TokenKind_Ident)) {
        if (LC_Match(LC_TokenKind_Colon)) {
            if (LC_Match(LC_TokenKind_Colon)) {
                if (LC_MatchKeyword(L->kproc)) {
                    LC_PROP_ERROR(n, LC_ParseProcDecl(ident));
                } else if (LC_MatchKeyword(L->kstruct)) {
                    LC_PROP_ERROR(n, LC_ParseStruct(LC_ASTKind_DeclStruct, ident));
                } else if (LC_MatchKeyword(L->kunion)) {
                    LC_PROP_ERROR(n, LC_ParseStruct(LC_ASTKind_DeclUnion, ident));
                } else if (LC_MatchKeyword(L->ktypedef)) {
                    LC_PROP_ERROR(n, LC_ParseTypedef(ident));
                } else {
                    n             = LC_CreateAST(ident, LC_ASTKind_DeclConst);
                    n->dbase.name = ident->ident;
                    if (LC_Match(LC_TokenKind_BitXor)) {
                        LC_AST *last_decl = file->afile.ldecl;
                        if (!last_decl || last_decl->kind != LC_ASTKind_DeclConst) return LC_ReportParseError(LC_GetI(-1), "invalid usage, there is no constant declaration preceding '^', this operator implies - PREV_CONST + 1");
                        LC_AST *left      = LC_CreateAST(n->pos, LC_ASTKind_ExprIdent);
                        left->eident.name = last_decl->dbase.name;
                        LC_AST *right     = LC_CreateAST(n->pos, LC_ASTKind_ExprInt);
                        right->eatom.i    = LC_Bigint_u64(1);

                        n->dconst.expr = LC_CreateBinary(n->pos, left, right, LC_TokenKind_Add);
                    } else if (LC_Match(LC_TokenKind_LeftShift)) {
                        LC_AST *last_decl = file->afile.ldecl;
                        if (!last_decl || last_decl->kind != LC_ASTKind_DeclConst) return LC_ReportParseError(LC_GetI(-1), "invalid usage, there is no constant declaration preceding '^', this operator implies - PREV_CONST << 1");
                        LC_AST *left      = LC_CreateAST(n->pos, LC_ASTKind_ExprIdent);
                        left->eident.name = last_decl->dbase.name;
                        LC_AST *right     = LC_CreateAST(n->pos, LC_ASTKind_ExprInt);
                        right->eatom.i    = LC_Bigint_u64(1);

                        n->dconst.expr = LC_CreateBinary(n->pos, left, right, LC_TokenKind_LeftShift);
                    } else {
                        LC_PROP_ERROR(n->dconst.expr, LC_ParseExpr());
                    }

                    if (!LC_Match(LC_TokenKind_Semicolon)) return LC_ReportParseError(LC_GetI(-1), "expected ';' semicolon");
                }
            } else if (LC_Match(LC_TokenKind_Assign)) {
                n = LC_CreateAST(ident, LC_ASTKind_DeclVar);
                LC_PROP_ERROR(n->dvar.expr, LC_ParseExpr());
                n->dbase.name = ident->ident;
                if (!LC_Match(LC_TokenKind_Semicolon)) return LC_ReportParseError(LC_GetI(-1), "expected ';' semicolon");
            } else {
                n             = LC_CreateAST(ident, LC_ASTKind_DeclVar);
                n->dbase.name = ident->ident;

                LC_PROP_ERROR(n->dvar.type, LC_ParseType());
                if (LC_Match(LC_TokenKind_Assign)) {
                    if (LC_Match(LC_TokenKind_Hash)) {
                        LC_AST *note = LC_CreateAST(LC_Get(), LC_ASTKind_ExprNote);
                        LC_PROP_ERROR(note->enote.expr, LC_ParseNote());
                        n->dvar.expr = note;
                    } else {
                        LC_PROP_ERROR(n->dvar.expr, LC_ParseExpr());
                    }
                }
                if (!LC_Match(LC_TokenKind_Semicolon)) return LC_ReportParseError(LC_GetI(-1), "expected ';' semicolon");
            }
        } else return LC_ReportParseError(ident, "got unexpected token: %s, while parsing declaration", LC_TokenKindToString(ident->kind));
    } else if (LC_Match(LC_TokenKind_Hash)) {
        n = LC_CreateAST(ident, LC_ASTKind_DeclNote);
        LC_PROP_ERROR(n->dnote.expr, LC_ParseNote());
        if (!LC_Match(LC_TokenKind_Semicolon)) return LC_ReportParseError(LC_GetI(-1), "expected ';' semicolon");
    } else if (LC_MatchKeyword(L->kimport)) {
        return LC_ReportParseError(LC_Get(), "imports can only appear at the top level");
    } else if (ident->kind == LC_TokenKind_EOF) return NULL;
    else return LC_ReportParseError(ident, "got unexpected token: %s, while parsing declaration", LC_TokenKindToString(ident->kind));

    LC_AST *notes = LC_ParseNotes();
    if (n) {
        n->notes             = notes;
        n->dbase.doc_comment = doc_comment;
    }
    return n;
}

LC_FUNCTION bool LC_EatUntilNextValidDecl(void) {
    for (;;) {
        LC_Token *a = LC_GetI(0);
        if (a->kind == LC_TokenKind_Keyword && a->ident == L->kimport) {
            return true;
        }

        LC_Token *d = LC_GetI(3);
        if (a->kind == LC_TokenKind_Ident && LC_GetI(1)->kind == LC_TokenKind_Colon && LC_GetI(2)->kind == LC_TokenKind_Colon && d->kind == LC_TokenKind_Keyword) {
            if (d->ident == L->kproc || d->ident == L->kstruct || d->ident == L->kunion || d->ident == L->ktypedef) {
                return false;
            }
        }

        LC_Token *token = LC_Next();
        if (token == &L->NullToken) {
            return false;
        }
    }
}

LC_FUNCTION LC_AST *LC_ParseImport(void) {
    LC_AST   *n      = NULL;
    LC_Token *import = LC_MatchKeyword(L->kimport);
    if (import) {
        n = LC_CreateAST(import, LC_ASTKind_GlobImport);

        LC_Token *ident = LC_Match(LC_TokenKind_Ident);
        if (ident) n->gimport.name = ident->ident;

        LC_Token *path = LC_Match(LC_TokenKind_String);
        if (!path) return LC_ReportParseError(LC_GetI(-1), "expected string after an import, instead got %s", LC_TokenKindToString(LC_Get()->kind));

        n->gimport.path = path->ident;
        if (!LC_Match(LC_TokenKind_Semicolon)) return LC_ReportParseError(LC_GetI(-1), "expected ';' semicolon");
    }
    return n;
}

LC_FUNCTION void LC_AddFileToPackage(LC_AST *pkg, LC_AST *f) {
    f->afile.package = pkg;
    LC_DLLAdd(pkg->apackage.ffile, pkg->apackage.lfile, f);
}

LC_FUNCTION LC_AST *LC_ParseFileEx(LC_AST *package) {
    LC_Token *package_doc_comment = LC_Match(LC_TokenKind_PackageDocComment);

    LC_AST *n            = LC_CreateAST(LC_Get(), LC_ASTKind_File);
    n->afile.x           = L->parser->x;
    n->afile.doc_comment = LC_Match(LC_TokenKind_FileDocComment);
    ParseHashBuildIf(n);

    // Parse imports
    while (!LC_Is(LC_TokenKind_EOF)) {
        LC_AST *import = LC_ParseImport();
        if (!import) break;

        if (import->kind == LC_ASTKind_Error) {
            bool is_import = LC_EatUntilNextValidDecl();
            if (!is_import) break;
        } else {
            LC_DLLAdd(n->afile.fimport, n->afile.limport, import);
        }
    }

    // Parse top level decls
    while (!LC_Is(LC_TokenKind_EOF)) {
        LC_AST *decl = LC_ParseDecl(n);
        if (!decl) continue;

        if (decl->kind == LC_ASTKind_Error) {
            LC_EatUntilNextValidDecl();
        } else {
            LC_DLLAdd(n->afile.fdecl, n->afile.ldecl, decl);
            if (L->on_decl_parsed) L->on_decl_parsed(decl);
        }
    }

    if (package) {
        if (package->apackage.ext->doc_comment) LC_ReportParseError(package_doc_comment, "there are more then 1 package doc comments in %s package", (char *)package->apackage.name);
        package->apackage.ext->doc_comment = package_doc_comment;
        LC_AddFileToPackage(package, n);
    }

    return n;
}

LC_FUNCTION LC_AST *LC_ParseTokens(LC_AST *package, LC_Lex *x) {
    LC_Parser p  = LC_MakeParser(x);
    L->parser    = &p;
    LC_AST *file = LC_ParseFileEx(package);
    return L->errors ? NULL : file;
}

LC_FUNCTION LC_AST *LC_ParseFile(LC_AST *package, char *filename, char *content, int line) {
    if (content == NULL) {
        LC_SendErrorMessagef(NULL, NULL, "internal compiler error: file passed to %s is null", __FUNCTION__);
        return NULL;
    }
    if (filename == NULL) {
        LC_SendErrorMessagef(NULL, NULL, "internal compiler error: filename passed to %s is null", __FUNCTION__);
        return NULL;
    }

    LC_Lex *x = LC_LexStream(filename, content, line);
    if (L->errors) return NULL;
    LC_InternTokens(x);

    LC_AST *file = LC_ParseTokens(package, x);
    if (!file) return NULL;
    return file;
}

LC_FUNCTION LC_AST *LC_ParseStmtf(const char *str, ...) {
    LC_FORMAT(L->arena, str, s8);
    LC_Parser *old    = L->parser;
    LC_Parser *p      = LC_MakeParserQuick(s8.str);
    LC_AST    *result = LC_ParseStmt(false);
    L->parser         = old;
    return result;
}

LC_FUNCTION LC_AST *LC_ParseExprf(const char *str, ...) {
    LC_FORMAT(L->arena, str, s8);
    LC_Parser *old    = L->parser;
    LC_Parser *p      = LC_MakeParserQuick(s8.str);
    LC_AST    *result = LC_ParseExpr();
    L->parser         = old;
    return result;
}

LC_FUNCTION LC_AST *LC_ParseDeclf(const char *str, ...) {
    LC_FORMAT(L->arena, str, s8);
    LC_Parser *old    = L->parser;
    LC_Parser *p      = LC_MakeParserQuick(s8.str);
    LC_AST    *result = LC_ParseDecl(&L->NullAST);
    L->parser         = old;
    return result;
}

#undef LC_EXPECT
#undef LC_PROP_ERROR
LC_FUNCTION LC_StringList *LC_BeginStringGen(LC_Arena *arena) {
    L->printer.list          = LC_MakeEmptyList();
    L->printer.arena         = arena;
    L->printer.last_filename = 0;
    L->printer.last_line_num = 0;
    L->printer.indent        = 0;
    return &L->printer.list;
}

LC_FUNCTION LC_String LC_EndStringGen(LC_Arena *arena) {
    LC_String result = LC_MergeString(arena, L->printer.list);
    return result;
}

LC_FUNCTION void LC_GenIndent(void) {
    LC_String s = LC_Lit("    ");
    for (int i = 0; i < L->printer.indent; i++) {
        LC_AddNode(L->printer.arena, &L->printer.list, s);
    }
}

LC_FUNCTION char *LC_Strf(const char *str, ...) {
    LC_FORMAT(L->arena, str, s8);
    return s8.str;
}

LC_FUNCTION void LC_GenLine(void) {
    LC_Genf("\n");
    LC_GenIndent();
}

LC_FUNCTION char *LC_GenLCType(LC_Type *type) {
    LC_StringList out = {0};
    for (LC_Type *it = type; it;) {
        if (it->kind == LC_TypeKind_Pointer) {
            LC_Addf(L->arena, &out, "*");
            it = it->tptr.base;
        } else if (it->kind == LC_TypeKind_Array) {
            LC_Addf(L->arena, &out, "[%d]", it->tarray.size);
            it = it->tarray.base;
        } else if (it->kind == LC_TypeKind_Proc) {
            LC_Addf(L->arena, &out, "proc(");
            LC_TypeFor(mem, it->tproc.args.first) {
                LC_Addf(L->arena, &out, "%s: %s", (char *)mem->name, LC_GenLCType(mem->type));
                if (mem->default_value_expr) LC_Addf(L->arena, &out, "/*has default value*/");
                if (mem->next) LC_Addf(L->arena, &out, ", ");
            }
            if (it->tproc.vargs) LC_Addf(L->arena, &out, "..");
            LC_Addf(L->arena, &out, ")");
            if (it->tproc.ret->kind != LC_TypeKind_void) LC_Addf(L->arena, &out, ": %s", LC_GenLCType(it->tproc.ret));
            break;
        } else if (it->decl) {
            LC_Decl *decl = it->decl;
            LC_ASSERT(decl->ast, decl);
            LC_Addf(L->arena, &out, "%s", (char *)decl->name);
            break;
        } else {
            LC_SendErrorMessagef(NULL, NULL, "internal compiler error: unhandled type kind in %s", __FUNCTION__);
        }
    }
    LC_String s = LC_MergeString(L->arena, out);
    return s.str;
}

LC_FUNCTION char *LC_GenLCTypeVal(LC_TypeAndVal v) {
    if (LC_IsInt(v.type) || LC_IsPtr(v.type) || LC_IsProc(v.type)) {
        return LC_Bigint_str(&v.i, 10);
    }
    if (LC_IsFloat(v.type)) {
        LC_String s = LC_Format(L->arena, "%f", v.d);
        return s.str;
    }
    LC_ASSERT(NULL, !"invalid codepath");
    return "";
}

LC_FUNCTION char *LC_GenLCAggName(LC_Type *t) {
    if (t->kind == LC_TypeKind_Struct) return "struct";
    if (t->kind == LC_TypeKind_Union) return "union";
    return NULL;
}

LC_FUNCTION void LC_GenLCNode(LC_AST *n) {
    switch (n->kind) {
    case LC_ASTKind_Package: {
        LC_ASTFor(it, n->apackage.ffile) {
            LC_GenLCNode(it);
        }
    } break;

    case LC_ASTKind_File: {
        LC_ASTFor(it, n->afile.fimport) {
            LC_GenLCNode(it);
        }

        LC_ASTFor(it, n->afile.fdecl) {
            LC_GenLCNode(it);
        }
        // @todo: we need to do something with notes so we can generate them in order!

    } break;

    case LC_ASTKind_GlobImport: {
        LC_GenLinef("import %s \"%s\";", (char *)n->gimport.name, (char *)n->gimport.path);
    } break;

    case LC_ASTKind_DeclProc: {
        LC_GenLinef("%s :: ", (char *)n->dbase.name);
        LC_GenLCNode(n->dproc.type);
        if (n->dproc.body) {
            LC_Genf(" ");
            LC_GenLCNode(n->dproc.body);
        } else {
            LC_Genf(";");
        }
    } break;

    case LC_ASTKind_DeclUnion:
    case LC_ASTKind_DeclStruct: {
        const char *agg = n->kind == LC_ASTKind_DeclUnion ? "union" : "struct";
        LC_GenLinef("%s :: %s {", (char *)n->dbase.name, agg);
        L->printer.indent += 1;
        LC_ASTFor(it, n->dagg.first) {
            LC_GenLine();
            LC_GenLCNode(it);
            LC_Genf(";");
        }
        L->printer.indent -= 1;
        LC_GenLinef("}");
    } break;

    case LC_ASTKind_TypespecAggMem: {
        LC_Genf("%s: ", (char *)n->tagg_mem.name);
        LC_GenLCNode(n->tagg_mem.type);
    } break;

    case LC_ASTKind_DeclVar: {
        LC_GenLinef("%s ", (char *)n->dbase.name);
        if (n->dvar.type) {
            LC_Genf(": ");
            LC_GenLCNode(n->dvar.type);
            if (n->dvar.expr) {
                LC_Genf("= ");
                LC_GenLCNode(n->dvar.expr);
            }
        } else {
            LC_Genf(":= ");
            LC_GenLCNode(n->dvar.expr);
        }
        LC_Genf(";");
    } break;

    case LC_ASTKind_DeclConst: {
        LC_GenLinef("%s :: ", (char *)n->dbase.name);
        LC_GenLCNode(n->dconst.expr);
        LC_Genf(";");
    } break;

    case LC_ASTKind_DeclTypedef: {
        LC_GenLinef("%s :: typedef ", (char *)n->dbase.name);
        LC_GenLCNode(n->dtypedef.type);
        LC_Genf(";");
    } break;

    case LC_ASTKind_ExprIdent:
    case LC_ASTKind_TypespecIdent: {
        LC_Genf("%s", (char *)n->eident.name);
    } break;

    case LC_ASTKind_ExprField:
    case LC_ASTKind_TypespecField: {
        LC_GenLCNode(n->efield.left);
        LC_Genf(".%s", (char *)n->efield.right);
    } break;

    case LC_ASTKind_TypespecPointer: {
        LC_Genf("*");
        LC_GenLCNode(n->tpointer.base);
    } break;

    case LC_ASTKind_TypespecArray: {
        LC_Genf("[");
        if (n->tarray.index) LC_GenLCNode(n->tarray.index);
        LC_Genf("]");
        LC_GenLCNode(n->tpointer.base);
    } break;

    case LC_ASTKind_TypespecProc: {
        LC_Genf("proc(");
        LC_ASTFor(it, n->tproc.first) {
            LC_GenLCNode(it);
            if (it != n->tproc.last) LC_Genf(", ");
        }
        if (n->tproc.vargs) {
            LC_Genf(", ...");
            if (n->tproc.vargs_any_promotion) LC_Genf("Any");
        }
        LC_Genf(")");
        if (n->tproc.ret) {
            LC_Genf(": ");
            LC_GenLCNode(n->tproc.ret);
        }
    } break;

    case LC_ASTKind_TypespecProcArg: {
        LC_Genf("%s: ", (char *)n->tproc_arg.name);
        LC_GenLCNode(n->tproc_arg.type);
        if (n->tproc_arg.expr) {
            LC_Genf(" = ");
            LC_GenLCNode(n->tproc_arg.expr);
        }
    } break;

    case LC_ASTKind_StmtBlock: {
        if (n->sblock.name && n->sblock.kind != SBLK_Loop) LC_Genf("%s: ", (char *)n->sblock.name);
        LC_Genf("{");
        L->printer.indent += 1;
        LC_ASTFor(it, n->sblock.first) {
            LC_GenLine();
            LC_GenLCNode(it);
            if (it->kind != LC_ASTKind_StmtBlock && it->kind != LC_ASTKind_StmtDefer && it->kind != LC_ASTKind_StmtFor && it->kind != LC_ASTKind_StmtIf && it->kind != LC_ASTKind_StmtSwitch) LC_Genf(";");
        }
        L->printer.indent -= 1;
        LC_GenLinef("}");
    } break;

    case LC_ASTKind_StmtReturn: {
        LC_Genf("return");
        if (n->sreturn.expr) {
            LC_Genf(" ");
            LC_GenLCNode(n->sreturn.expr);
        }
    } break;

    case LC_ASTKind_StmtBreak: {
        LC_Genf("break");
        if (n->sbreak.name) LC_Genf(" %s", (char *)n->sbreak.name);
    } break;

    case LC_ASTKind_StmtContinue: {
        LC_Genf("continue");
        if (n->scontinue.name) LC_Genf(" %s", (char *)n->scontinue.name);
    } break;

    case LC_ASTKind_StmtDefer: {
        LC_Genf("defer ");
        LC_GenLCNode(n->sdefer.body);
    } break;

    case LC_ASTKind_StmtFor: {
        LC_StmtBlock *sblock = &n->sfor.body->sblock;
        if (sblock->name && sblock->kind == SBLK_Loop) {
            LC_Genf("%s: ", (char *)sblock->name);
        }

        LC_Genf("for ");
        if (n->sfor.init) {
            LC_GenLCNode(n->sfor.init);
            if (n->sfor.cond) LC_Genf("; ");
        }

        if (n->sfor.cond) {
            LC_GenLCNode(n->sfor.cond);
            if (n->sfor.inc) {
                LC_Genf("; ");
                LC_GenLCNode(n->sfor.inc);
            }
        }

        LC_Genf(" ");
        LC_GenLCNode(n->sfor.body);
    } break;

    case LC_ASTKind_StmtElseIf:
        LC_Genf("else ");
    case LC_ASTKind_StmtIf: {
        LC_Genf("if ");
        LC_GenLCNode(n->sif.expr);
        LC_GenLCNode(n->sif.body);
        LC_ASTFor(it, n->sif.first) {
            LC_GenLCNode(it);
        }
    } break;

    case LC_ASTKind_StmtElse: {
        LC_Genf("else ");
        LC_GenLCNode(n->sif.body);
    } break;

    case LC_ASTKind_StmtSwitch: {
        LC_Genf("switch ");
        LC_GenLCNode(n->sswitch.expr);
        LC_Genf("{");
        L->printer.indent += 1;
        LC_ASTFor(it, n->sswitch.first) {
            LC_GenLine();
            LC_GenLCNode(it);
        }
        L->printer.indent -= 1;
        LC_Genf("}");
    } break;

    case LC_ASTKind_StmtSwitchCase: {
        LC_Genf("case ");
        LC_ASTFor(it, n->scase.first) {
            LC_GenLCNode(it);
            if (it != n->scase.last) LC_Genf(", ");
        }
        LC_Genf(": ");
        LC_GenLCNode(n->scase.body);
    } break;
    case LC_ASTKind_StmtSwitchDefault: {
        LC_Genf("default: ");
        LC_GenLCNode(n->scase.body);
    } break;

    case LC_ASTKind_StmtAssign: {
        LC_GenLCNode(n->sassign.left);
        LC_Genf(" %s ", LC_TokenKindToOperator(n->sassign.op));
        LC_GenLCNode(n->sassign.right);
    } break;

    case LC_ASTKind_StmtExpr: {
        LC_GenLCNode(n->sexpr.expr);
    } break;

    case LC_ASTKind_StmtVar: {
        LC_Genf("%s", (char *)n->svar.name);
        if (n->svar.type) {
            LC_Genf(": ");
            LC_GenLCNode(n->svar.type);
            if (n->svar.expr) {
                LC_Genf(" = ");
                LC_GenLCNode(n->svar.expr);
            }
        } else {
            LC_Genf(" := ");
            LC_GenLCNode(n->svar.expr);
        }
    } break;

    case LC_ASTKind_StmtConst: {
        LC_GenLinef("%s :: ", (char *)n->sconst.name);
        LC_GenLCNode(n->sconst.expr);
    } break;

    case LC_ASTKind_ExprString: {
        LC_Genf("`%s`", (char *)n->eatom.name);
    } break;

    case LC_ASTKind_ExprInt: {
        LC_Genf("%s", LC_Bigint_str(&n->eatom.i, 10));
    } break;

    case LC_ASTKind_ExprFloat: {
        LC_Genf("%f", n->eatom.d);
    } break;

    case LC_ASTKind_ExprBool: {
        int64_t value = LC_Bigint_as_unsigned(&n->eatom.i);
        if (value) {
            LC_Genf("true");
        } else {
            LC_Genf("false");
        }
    } break;

    case LC_ASTKind_ExprType: {
        LC_Genf(":");
        LC_GenLCNode(n->etype.type);
    } break;

    case LC_ASTKind_ExprBinary: {
        LC_Genf("(");
        LC_GenLCNode(n->ebinary.left);
        LC_Genf("%s", LC_TokenKindToOperator(n->ebinary.op));
        LC_GenLCNode(n->ebinary.right);
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprUnary: {
        LC_Genf("%s(", LC_TokenKindToOperator(n->eunary.op));
        LC_GenLCNode(n->eunary.expr);
        LC_Genf(")");
    } break;

    case LC_ASTKind_StmtNote: {
        LC_Genf("#");
        LC_GenLCNode(n->snote.expr);
    } break;

    case LC_ASTKind_ExprNote: {
        LC_Genf("#");
        LC_GenLCNode(n->enote.expr);
    } break;

    case LC_ASTKind_DeclNote: {
        LC_GenLinef("#");
        LC_GenLCNode(n->dnote.expr);
        LC_Genf(";");
    } break;

    case LC_ASTKind_Note:
    case LC_ASTKind_ExprBuiltin:
    case LC_ASTKind_ExprCall: {
        LC_GenLCNode(n->ecompo.name);
        LC_Genf("(");
        LC_ASTFor(it, n->ecompo.first) {
            LC_GenLCNode(it);
            if (it != n->ecompo.last) LC_Genf(", ");
        }
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprCompoundItem:
    case LC_ASTKind_ExprCallItem: {
        if (n->ecompo_item.name) {
            LC_Genf("%s = ", (char *)n->ecompo_item.name);
        }
        if (n->ecompo_item.index) {
            LC_Genf("[");
            LC_GenLCNode(n->ecompo_item.index);
            LC_Genf("] = ");
        }
        LC_GenLCNode(n->ecompo_item.expr);
    } break;

    case LC_ASTKind_ExprCompound: {
        if (n->ecompo.name) LC_GenLCNode(n->ecompo.name);
        LC_Genf("{");
        LC_ASTFor(it, n->ecompo.first) {
            LC_GenLCNode(it);
            if (it != n->ecompo.last) LC_Genf(", ");
        }
        LC_Genf("}");
    } break;

    case LC_ASTKind_ExprCast: {
        LC_Genf(":");
        LC_GenLCNode(n->ecast.type);
        LC_Genf("(");
        LC_GenLCNode(n->ecast.expr);
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprIndex: {
        LC_Genf("(");
        LC_GenLCNode(n->eindex.base);
        LC_Genf("[");
        LC_GenLCNode(n->eindex.index);
        LC_Genf("]");
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprAddPtr: {
        LC_Genf("addptr(");
        LC_GenLCNode(n->ebinary.left);
        LC_Genf(", ");
        LC_GenLCNode(n->ebinary.right);
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprGetValueOfPointer: {
        LC_Genf("*(");
        LC_GenLCNode(n->eunary.expr);
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprGetPointerOfValue: {
        LC_Genf("&(");
        LC_GenLCNode(n->eunary.expr);
        LC_Genf(")");
    } break;

    default: LC_ReportASTError(n, "internal compiler error: unhandled ast kind in %s", __FUNCTION__);
    }
}

const bool LC_GenCInternalGenerateSizeofs = true;

LC_FUNCTION void LC_GenCLineDirective(LC_AST *node) {
    if (L->emit_line_directives) {
        L->printer.last_line_num = node->pos->line;
        LC_GenLinef("#line %d", L->printer.last_line_num);
        LC_Intern file = node->pos->lex->file;
        if (file != L->printer.last_filename) {
            L->printer.last_filename = file;
            LC_Genf(" \"%s\"", (char *)L->printer.last_filename);
        }
    }
}

LC_FUNCTION void LC_GenLastCLineDirective(void) {
    if (L->emit_line_directives) {
        LC_Genf("#line %d", L->printer.last_line_num);
    }
}

LC_FUNCTION void LC_GenCLineDirectiveNum(int num) {
    if (L->emit_line_directives) {
        LC_Genf("#line %d", num);
    }
}

LC_FUNCTION char *LC_GenCTypeParen(char *str, char c) {
    return c && c != '[' ? LC_Strf("(%s)", str) : str;
}

LC_FUNCTION char *LC_GenCType(LC_Type *type, char *str) {
    switch (type->kind) {
    case LC_TypeKind_Pointer: {
        return LC_GenCType(type->tptr.base, LC_GenCTypeParen(LC_Strf("*%s", str), *str));
    } break;
    case LC_TypeKind_Array: {
        if (type->tarray.size == 0) {
            return LC_GenCType(type->tarray.base, LC_GenCTypeParen(LC_Strf("%s[]", str), *str));
        } else {
            return LC_GenCType(type->tarray.base, LC_GenCTypeParen(LC_Strf("%s[%d]", str, type->tarray.size), *str));
        }

    } break;
    case LC_TypeKind_Proc: {
        LC_StringList out = {0};
        LC_Addf(L->arena, &out, "(*%s)", str);
        LC_Addf(L->arena, &out, "(");
        if (type->tagg.mems.count == 0) {
            LC_Addf(L->arena, &out, "void");
        } else {
            int i = 0;
            for (LC_TypeMember *it = type->tproc.args.first; it; it = it->next) {
                LC_Addf(L->arena, &out, "%s%s", i == 0 ? "" : ", ", LC_GenCType(it->type, ""));
                i += 1;
            }
        }
        if (type->tproc.vargs) {
            LC_Addf(L->arena, &out, ", ...");
        }
        LC_Addf(L->arena, &out, ")");
        char *front  = LC_MergeString(L->arena, out).str;
        char *result = LC_GenCType(type->tproc.ret, front);
        return result;
    } break;
    default: return LC_Strf("%s%s%s", type->decl->foreign_name, str[0] ? " " : "", str);
    }
}

LC_FUNCTION LC_Intern LC_GetStringFromSingleArgNote(LC_AST *note) {
    LC_ASSERT(note, note->kind == LC_ASTKind_Note);
    LC_ASSERT(note, note->ecompo.first == note->ecompo.last);
    LC_AST *arg = note->ecompo.first;
    LC_ASSERT(note, arg->kind == LC_ASTKind_ExprCallItem);
    LC_AST *str = arg->ecompo_item.expr;
    LC_ASSERT(note, str->kind == LC_ASTKind_ExprString);
    return str->eatom.name;
}

LC_FUNCTION void LC_GenCCompound(LC_AST *n) {
    LC_Type *type = n->type;
    if (LC_IsAggType(type)) {
        LC_ResolvedCompo *rd = n->ecompo.resolved_items;
        LC_Genf("{");
        if (rd->first == NULL) LC_Genf("0");
        for (LC_ResolvedCompoItem *it = rd->first; it; it = it->next) {
            LC_Genf(".%s = ", (char *)it->t->name);
            LC_GenCExpr(it->expr);
            if (it->next) LC_Genf(", ");
        }
        LC_Genf("}");
    } else if (LC_IsArray(type)) {
        LC_ResolvedArrayCompo *rd = n->ecompo.resolved_array_items;
        LC_Genf("{");
        for (LC_ResolvedCompoArrayItem *it = rd->first; it; it = it->next) {
            LC_Genf("[%d] = ", it->index);
            LC_GenCExpr(it->comp->ecompo_item.expr);
            if (it->next) LC_Genf(", ");
        }
        LC_Genf("}");
    } else {
        LC_ReportASTError(n, "internal compiler error: got unhandled case in %s", __FUNCTION__);
    }
}

LC_THREAD_LOCAL bool GC_SpecialCase_GlobalScopeStringDecl;

LC_FUNCTION void LC_GenCString(char *s, LC_Type *type) {
    if (type == L->tstring) {
        if (!GC_SpecialCase_GlobalScopeStringDecl) LC_Genf("(LC_String)");
        LC_Genf("{ ");
    }
    LC_Genf("\"");
    for (int i = 0; s[i]; i += 1) {
        LC_String escape = LC_GetEscapeString(s[i]);
        if (escape.len) {
            LC_Genf("%.*s", LC_Expand(escape));
        } else {
            LC_Genf("%c", s[i]);
        }
    }
    LC_Genf("\"");
    if (type == L->tstring) LC_Genf(", %d }", (int)LC_StrLen(s));
}

LC_FUNCTION char *LC_GenCVal(LC_TypeAndVal v, LC_Type *type) {
    char *str = LC_GenLCTypeVal(v);
    switch (type->kind) {
    case LC_TypeKind_uchar:
    case LC_TypeKind_ushort:
    case LC_TypeKind_uint: str = LC_Strf("%su", str); break;
    case LC_TypeKind_ulong: str = LC_Strf("%sul", str); break;
    case LC_TypeKind_Pointer:
    case LC_TypeKind_Proc:
    case LC_TypeKind_ullong: str = LC_Strf("%sull", str); break;
    case LC_TypeKind_long: str = LC_Strf("%sull", str); break;
    case LC_TypeKind_llong: str = LC_Strf("%sull", str); break;
    case LC_TypeKind_float: str = LC_Strf("%sf", str); break;
    case LC_TypeKind_UntypedFloat: str = LC_Strf(" /*utfloat*/%s", str); break;
    case LC_TypeKind_UntypedInt: str = LC_Strf(" /*utint*/%sull", str); break;
    default: {
    }
    }
    if (LC_IsUTInt(v.type) && !LC_IsUntyped(type) && type->size < 4) {
        str = LC_Strf("(%s)%s", LC_GenCType(type, ""), str);
    }
    return str;
}

LC_FUNCTION void LC_GenCExpr(LC_AST *n) {
    LC_ASSERT(n, LC_IsExpr(n));
    intptr_t is_any = (intptr_t)LC_MapGetP(&L->implicit_any, n);
    if (is_any) LC_Genf("(LC_Any){%d, (%s[]){", n->type->id, LC_GenCType(n->type, ""));

    if (n->const_val.type) {
        bool contains_sizeof_like = LC_GenCInternalGenerateSizeofs ? LC_ContainsCBuiltin(n) : false;
        if (!contains_sizeof_like) {
            if (LC_IsUTStr(n->const_val.type)) {
                LC_GenCString((char *)n->const_val.name, n->type);
            } else {
                char *val = LC_GenCVal(n->const_val, n->type);
                LC_Genf("%s", val);
            }
            if (is_any) LC_Genf("}}");
            return;
        }
    }

    LC_Type *type = n->type;
    switch (n->kind) {
    case LC_ASTKind_ExprIdent: {
        LC_Genf("%s", (char *)n->eident.resolved_decl->foreign_name);
    } break;

    case LC_ASTKind_ExprCast: {
        LC_Genf("(");
        LC_Genf("(%s)", LC_GenCType(type, ""));
        LC_Genf("(");
        LC_GenCExpr(n->ecast.expr);
        LC_Genf(")");
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprUnary: {
        LC_Genf("%s(", LC_TokenKindToOperator(n->eunary.op));
        LC_GenCExpr(n->eunary.expr);
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprAddPtr: {
        LC_Genf("(");
        LC_GenCExpr(n->ebinary.left);
        LC_Genf("+");
        LC_GenCExpr(n->ebinary.right);
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprBinary: {
        LC_Genf("(");
        LC_GenCExpr(n->ebinary.left);
        LC_Genf("%s", LC_TokenKindToOperator(n->ebinary.op));
        LC_GenCExpr(n->ebinary.right);
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprIndex: {
        LC_Genf("(");
        LC_GenCExpr(n->eindex.base);
        LC_Genf("[");
        LC_GenCExpr(n->eindex.index);
        LC_Genf("]");
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprGetValueOfPointer: {
        LC_Genf("(*(");
        LC_GenCExpr(n->eunary.expr);
        LC_Genf("))");
    } break;

    case LC_ASTKind_ExprGetPointerOfValue: {
        LC_Genf("(&(");
        LC_GenCExpr(n->eunary.expr);
        LC_Genf("))");
    } break;

    case LC_ASTKind_ExprField: {
        if (n->efield.parent_decl->kind != LC_DeclKind_Import) {
            LC_Type *left_type = n->efield.left->type;
            LC_GenCExpr(n->efield.left);
            if (LC_IsPtr(left_type)) LC_Genf("->");
            else LC_Genf(".");
            LC_Genf("%s", (char *)n->efield.right);
        } else {
            LC_Genf("%s", (char *)n->efield.resolved_decl->foreign_name);
        }
    } break;

    case LC_ASTKind_ExprCall: {
        LC_ResolvedCompo *rd = n->ecompo.resolved_items;
        LC_GenCExpr(n->ecompo.name);
        LC_Genf("(");
        for (LC_ResolvedCompoItem *it = rd->first; it; it = it->next) {
            LC_GenCExpr(it->expr);
            if (it->next) LC_Genf(", ");
        }
        LC_Genf(")");
    } break;

    case LC_ASTKind_ExprCompound: {
        LC_Genf("(%s)", LC_GenCType(type, ""));
        LC_GenCCompound(n);
    } break;

    case LC_ASTKind_ExprBuiltin: {
        LC_ASSERT(n, n->ecompo.name->kind == LC_ASTKind_ExprIdent);
        if (n->ecompo.name->eident.name == L->isizeof) {
            LC_Genf("sizeof(");
            LC_AST *expr = n->ecompo.first->ecompo_item.expr;
            if (expr->kind == LC_ASTKind_ExprType) {
                LC_Genf("%s", LC_GenCType(expr->type, ""));
            } else {
                LC_GenCExpr(expr);
            }
            LC_Genf(")");
        } else if (n->ecompo.name->eident.name == L->ialignof) {
            LC_Genf("LC_Alignof(");
            LC_AST *expr = n->ecompo.first->ecompo_item.expr;
            if (expr->kind == LC_ASTKind_ExprType) {
                LC_Genf("%s", LC_GenCType(expr->type, ""));
            } else {
                LC_GenCExpr(expr);
            }
            LC_Genf(")");
        } else if (n->ecompo.name->eident.name == L->ioffsetof) {
            LC_AST *i1 = n->ecompo.first->ecompo_item.expr;
            LC_AST *i2 = n->ecompo.first->next->ecompo_item.expr;
            LC_Genf("offsetof(%s, %s)", LC_GenCType(i1->type, ""), (char *)i2->eident.name);
        } else {
            LC_ReportASTError(n, "internal compiler error: got unhandled case in %s / LC_ASTKind_ExprBuiltin", __FUNCTION__);
        }
    } break;

    default: LC_ReportASTError(n, "internal compiler error: got unhandled case in %s", __FUNCTION__);
    }

    if (is_any) LC_Genf("}}");
}

const int GC_Stmt_OmitSemicolonAndNewLine = 1;

LC_FUNCTION void LC_GenCNote(LC_AST *note) {
    if (note->ecompo.name->eident.name == L->ic) {
        LC_Genf("%s", (char *)LC_GetStringFromSingleArgNote(note));
    }
}

LC_FUNCTION void LC_GenCVarExpr(LC_AST *n, bool is_declaration) {
    if (LC_HasNote(n, L->inot_init)) return;

    LC_AST *e = n->dvar.expr;
    if (n->kind == LC_ASTKind_StmtVar) e = n->svar.expr;
    if (e) {
        LC_Genf(" = ");
        if (e->kind == LC_ASTKind_ExprNote) {
            LC_GenCNote(e->enote.expr);
        } else if (is_declaration && e->kind == LC_ASTKind_ExprCompound) {
            LC_GenCCompound(e);
        } else {
            LC_GenCExpr(e);
        }
    } else {
        LC_Genf(" = {0}");
    }
}

LC_FUNCTION void LC_GenCDefers(LC_AST *block) {
    LC_AST *first = block->sblock.first_defer;
    if (first == NULL) return;

    int save = L->printer.last_line_num;
    LC_GenLine();
    LC_GenLastCLineDirective();

    LC_GenLinef("/*defer*/");
    for (LC_AST *it = first; it; it = it->sdefer.next) {
        LC_GenCStmtBlock(it->sdefer.body);
    }

    L->printer.last_line_num = save + 1;
    LC_GenLine();
    LC_GenLastCLineDirective();
}

LC_FUNCTION void LC_GenCDefersLoopBreak(LC_AST *n) {
    LC_ASSERT(n, n->kind == LC_ASTKind_StmtBreak || n->kind == LC_ASTKind_StmtContinue);
    LC_AST *it = NULL;
    for (int i = L->printer.out_block_stack.len - 1; i >= 0; i -= 1) {
        it = L->printer.out_block_stack.data[i];
        LC_GenCDefers(it);
        LC_ASSERT(it, it->sblock.kind != SBLK_Proc);
        if (it->sblock.kind == SBLK_Loop) {
            if (!n->sbreak.name) break;
            if (n->sbreak.name && it->sblock.name == n->sbreak.name) break;
        }
    }
    LC_ASSERT(it, it->sblock.kind == SBLK_Loop);
}

LC_FUNCTION void LC_GenCDefersReturn(LC_AST *n) {
    LC_ASSERT(n, n->kind == LC_ASTKind_StmtReturn);
    LC_AST *it = NULL;
    for (int i = L->printer.out_block_stack.len - 1; i >= 0; i -= 1) {
        it = L->printer.out_block_stack.data[i];
        LC_GenCDefers(it);
        if (it->sblock.kind == SBLK_Proc) {
            break;
        }
    }
    LC_ASSERT(it, it);
    LC_ASSERT(it, it->sblock.kind == SBLK_Proc);
}

LC_FUNCTION void LC_GenCStmt2(LC_AST *n, int flags) {
    LC_ASSERT(n, LC_IsStmt(n));
    bool semicolon = !(flags & GC_Stmt_OmitSemicolonAndNewLine);

    if (semicolon) {
        LC_GenLine();
    }

    switch (n->kind) {
    case LC_ASTKind_StmtVar: {
        LC_Type *type = n->type;
        LC_Genf("%s", LC_GenCType(type, (char *)n->svar.name));
        LC_GenCVarExpr(n, true);
    } break;
    case LC_ASTKind_StmtExpr: LC_GenCExpr(n->sexpr.expr); break;

    case LC_ASTKind_StmtAssign: {
        // Assigning to array doesn't work in C so we need to handle that
        // specific compo case here. :CompoArray
        if (LC_IsArray(n->type) && n->sassign.right->kind == LC_ASTKind_ExprCompound) {
            LC_ASSERT(n, n->sassign.op == LC_TokenKind_Assign);
            LC_AST *expr = n->sassign.right;
            LC_Genf("memset(");
            LC_GenCExpr(n->sassign.left);
            LC_Genf(", 0, sizeof(");
            LC_GenCExpr(n->sassign.left);
            LC_Genf("));");

            LC_ResolvedArrayCompo *rd = expr->ecompo.resolved_array_items;
            for (LC_ResolvedCompoArrayItem *it = rd->first; it; it = it->next) {
                LC_GenCExpr(n->sassign.left);
                LC_Genf("[%d] = ", it->index);
                LC_GenCExpr(it->comp->ecompo_item.expr);
                LC_Genf(";");
            }

        } else {
            LC_GenCExpr(n->sassign.left);
            LC_Genf(" %s ", LC_TokenKindToOperator(n->sassign.op));
            LC_GenCExpr(n->sassign.right);
        }
    } break;
    default: LC_ReportASTError(n, "internal compiler error: got unhandled case in %s", __FUNCTION__);
    }

    if (semicolon) LC_Genf(";");
}

LC_FUNCTION void LC_GenCStmt(LC_AST *n) {
    LC_ASSERT(n, LC_IsStmt(n));
    LC_GenCLineDirective(n);
    switch (n->kind) {
    case LC_ASTKind_StmtConst:
    case LC_ASTKind_StmtDefer: break;
    case LC_ASTKind_StmtNote: {
        LC_GenLine();
        LC_GenCNote(n->snote.expr);
        LC_Genf(";");
    } break;

    case LC_ASTKind_StmtReturn: {
        LC_GenCDefersReturn(n);
        LC_GenLinef("return");
        if (n->sreturn.expr) {
            LC_Genf(" ");
            LC_GenCExpr(n->sreturn.expr);
        }
        LC_Genf(";");
    } break;

    case LC_ASTKind_StmtContinue:
    case LC_ASTKind_StmtBreak: {
        const char *stmt = n->kind == LC_ASTKind_StmtBreak ? "break" : "continue";
        LC_GenCDefersLoopBreak(n);
        if (n->sbreak.name) {
            LC_GenLinef("goto %s_%s;", (char *)n->sbreak.name, stmt);
        } else {
            LC_GenLinef("%s;", stmt);
        }
    } break;

    case LC_ASTKind_StmtBlock: {
        LC_GenLinef("/*block*/");
        LC_GenCStmtBlock(n);
    } break;

    case LC_ASTKind_StmtSwitch: {
        LC_GenLinef("switch(");
        LC_GenCExpr(n->sswitch.expr);
        LC_Genf(") {");

        L->printer.indent += 1;
        LC_ASTFor(it, n->sswitch.first) {
            LC_GenCLineDirective(it);
            if (it->kind == LC_ASTKind_StmtSwitchCase) {
                LC_ASTFor(label_it, it->scase.first) {
                    LC_GenLinef("case ");
                    LC_GenCExpr(label_it);
                    LC_Genf(":");
                }
            }
            if (it->kind == LC_ASTKind_StmtSwitchDefault) {
                LC_GenLinef("default:");
            }
            LC_GenCStmtBlock(it->scase.body);
            if (LC_HasNote(it, L->ifallthrough)) {
                LC_Genf(" /*@fallthough*/");
            } else {
                LC_Genf(" break;");
            }
        }
        L->printer.indent -= 1;
        LC_GenLinef("}");
    } break;

    case LC_ASTKind_StmtFor: {
        LC_GenLinef("for (");
        if (n->sfor.init) LC_GenCStmt2(n->sfor.init, GC_Stmt_OmitSemicolonAndNewLine);
        LC_Genf(";");
        if (n->sfor.cond) {
            LC_Genf(" ");
            LC_GenCExpr(n->sfor.cond);
        }
        LC_Genf(";");
        if (n->sfor.inc) {
            LC_Genf(" ");
            LC_GenCStmt2(n->sfor.inc, GC_Stmt_OmitSemicolonAndNewLine);
        }
        LC_Genf(")");
        LC_GenCStmtBlock(n->sfor.body);
    } break;

    case LC_ASTKind_StmtIf: {
        LC_GenLinef("if ");
        LC_GenCExprParen(n->sif.expr);
        LC_GenCStmtBlock(n->sif.body);
        LC_ASTFor(it, n->sif.first) {
            LC_GenCLineDirective(it);
            LC_GenLinef("else");
            if (it->kind == LC_ASTKind_StmtElseIf) {
                LC_Genf(" if ");
                LC_GenCExprParen(it->sif.expr);
            }
            LC_GenCStmtBlock(it->sif.body);
        }
    } break;

    default: LC_GenCStmt2(n, 0);
    }
}

LC_FUNCTION void LC_GenCExprParen(LC_AST *expr) {
    bool paren = expr->kind != LC_ASTKind_ExprBinary;
    if (paren) LC_Genf("(");
    LC_GenCExpr(expr);
    if (paren) LC_Genf(")");
}

LC_FUNCTION void LC_GenCStmtBlock(LC_AST *n) {
    LC_PushAST(&L->printer.out_block_stack, n);
    LC_ASSERT(n, n->kind == LC_ASTKind_StmtBlock);
    LC_Genf(" {");
    L->printer.indent += 1;
    LC_ASTFor(it, n->sblock.first) {
        LC_GenCStmt(it);
    }
    LC_GenCDefers(n);
    if (n->sblock.name) LC_GenLinef("%s_continue:;", (char *)n->sblock.name);
    L->printer.indent -= 1;
    LC_GenLinef("}");
    if (n->sblock.name) LC_GenLinef("%s_break:;", (char *)n->sblock.name);
    LC_PopAST(&L->printer.out_block_stack);
}

LC_FUNCTION void LC_GenCProcDecl(LC_Decl *decl) {
    LC_StringList out      = {0};
    LC_Type      *type     = decl->type;
    LC_AST       *n        = decl->ast;
    LC_AST       *typespec = n->dproc.type;

    LC_Addf(L->arena, &out, "%s(", (char *)decl->foreign_name);
    if (type->tagg.mems.count == 0) {
        LC_Addf(L->arena, &out, "void");
    } else {
        int i = 0;
        LC_ASTFor(it, typespec->tproc.first) {
            LC_Type *type = it->type;
            LC_Addf(L->arena, &out, "%s%s", i == 0 ? "" : ", ", LC_GenCType(type, (char *)it->tproc_arg.name));
            i += 1;
        }
    }
    if (type->tproc.vargs) {
        LC_Addf(L->arena, &out, ", ...");
    }
    LC_Addf(L->arena, &out, ")");
    char *front  = LC_MergeString(L->arena, out).str;
    char *result = LC_GenCType(type->tproc.ret, front);

    LC_GenLine();
    bool is_public = LC_HasNote(n, L->iapi) || decl->foreign_name == L->imain;
    if (!is_public) LC_Genf("static ");
    LC_Genf("%s", result);
}

LC_FUNCTION void LC_GenCAggForwardDecl(LC_Decl *decl) {
    LC_ASSERT(decl->ast, LC_IsAgg(decl->ast));
    char *agg = LC_GenLCAggName(decl->type);
    LC_GenLinef("typedef %s %s %s;", agg, (char *)decl->foreign_name, (char *)decl->foreign_name);
}

LC_FUNCTION void LC_GenCTypeDecl(LC_Decl *decl) {
    LC_AST *n = decl->ast;
    LC_ASSERT(n, decl->kind == LC_DeclKind_Type);
    if (n->kind == LC_ASTKind_DeclTypedef) {
        LC_Type *type = decl->typedef_renamed_type_decl ? decl->typedef_renamed_type_decl->type : decl->type;
        LC_GenLinef("typedef %s;", LC_GenCType(type, (char *)decl->foreign_name));
    } else {
        LC_Type  *type = decl->type;
        LC_Intern name = decl->foreign_name;
        {
            bool packed = LC_HasNote(n, L->ipacked) ? true : false;
            if (packed) LC_GenLinef("#pragma pack(push, 1)");

            LC_GenLinef("%s %s {", LC_GenLCAggName(type), name ? (char *)name : "");
            L->printer.indent += 1;
            for (LC_TypeMember *it = type->tagg.mems.first; it; it = it->next) {
                LC_GenLinef("%s;", LC_GenCType(it->type, (char *)it->name));
            }
            L->printer.indent -= 1;
            LC_GenLinef("};");
            if (packed) LC_GenLinef("#pragma pack(pop)");
            LC_GenLine();
        }
    }
}

LC_FUNCTION void LC_GenCVarFDecl(LC_Decl *decl) {
    if (!LC_HasNote(decl->ast, L->iapi)) return;
    LC_Type *type = decl->type; // make string arrays assignable
    LC_GenLinef("extern ");
    if (LC_HasNote(decl->ast, L->ithread_local)) LC_Genf("_Thread_local ");
    LC_Genf("%s;", LC_GenCType(type, (char *)decl->foreign_name));
}

LC_FUNCTION void LC_GenCHeader(LC_AST *package) {
    // C notes
    LC_ASTFor(file, package->apackage.ffile) {
        LC_ASTFor(it, file->afile.fdecl) {
            if (it->kind != LC_ASTKind_DeclNote) continue;

            LC_AST *note = it->dnote.expr;
            if (note->ecompo.name->eident.name == L->ic) {
                LC_GenLinef("%s", (char *)LC_GetStringFromSingleArgNote(note));
            }
        }
    }

    // struct forward decls
    LC_DeclFor(decl, package->apackage.ext->first_ordered) {
        if (decl->is_foreign) continue;
        LC_AST *n = decl->ast;
        if (decl->kind == LC_DeclKind_Type && LC_IsAgg(n)) LC_GenCAggForwardDecl(decl);
    }

    // type decls
    LC_GenLine();
    LC_DeclFor(decl, package->apackage.ext->first_ordered) {
        if (decl->is_foreign) continue;
        LC_AST *n = decl->ast;
        if (decl->kind == LC_DeclKind_Type) LC_GenCTypeDecl(decl);
    }

    // proc and var forward decls
    LC_DeclFor(decl, package->apackage.ext->first_ordered) {
        if (decl->is_foreign) continue;
        LC_AST *n = decl->ast;
        if (decl->kind == LC_DeclKind_Var) {
            LC_GenCVarFDecl(decl);
        } else if (decl->kind == LC_DeclKind_Proc) {
            LC_GenCProcDecl(decl);
            LC_Genf(";");
        }
    }
}

LC_FUNCTION void LC_GenCImpl(LC_AST *package) {
    // implementation of vars
    LC_DeclFor(decl, package->apackage.ext->first_ordered) {
        if (decl->kind == LC_DeclKind_Var && !decl->is_foreign) {
            LC_AST  *n    = decl->ast;
            LC_Type *type = decl->type; // make string arrays assignable
            LC_GenLine();
            if (!LC_HasNote(n, L->iapi)) LC_Genf("static ");
            if (LC_HasNote(n, L->ithread_local)) LC_Genf("_Thread_local ");
            LC_Genf("%s", LC_GenCType(type, (char *)decl->foreign_name));

            GC_SpecialCase_GlobalScopeStringDecl = true;
            LC_GenCVarExpr(n, true);
            GC_SpecialCase_GlobalScopeStringDecl = false;
            LC_Genf(";");
            LC_GenLine();
        }
    }

    // implementation of procs
    LC_DeclFor(decl, package->apackage.ext->first_ordered) {
        LC_AST *n = decl->ast;
        if (decl->kind == LC_DeclKind_Proc && n->dproc.body && !decl->is_foreign) {
            LC_GenCLineDirective(n);
            LC_GenCProcDecl(decl);
            LC_GenCStmtBlock(n->dproc.body);
            LC_GenLine();
        }
    }
}

LC_FUNCTION void WalkAndCountDeclRefs(LC_ASTWalker *ctx, LC_AST *n) {
    LC_Decl *decl = NULL;
    if (n->kind == LC_ASTKind_ExprIdent || n->kind == LC_ASTKind_TypespecIdent) {
        if (n->eident.resolved_decl) decl = n->eident.resolved_decl;
    }
    if (n->kind == LC_ASTKind_ExprField) {
        if (n->efield.resolved_decl) decl = n->efield.resolved_decl;
    }
    if (decl) {
        LC_Map  *map_of_visits = (LC_Map *)ctx->user_data;
        intptr_t visited       = (intptr_t)LC_MapGetP(map_of_visits, decl);
        LC_MapInsertP(map_of_visits, decl, (void *)(visited + 1));
        if (visited == 0 && decl->ast->kind != LC_ASTKind_Null) {
            LC_WalkAST(ctx, decl->ast);
        }
    }
}

LC_FUNCTION void LC_CountDeclRefs(LC_Arena *arena, LC_Map *map, LC_AST *ast) {
    LC_ASTWalker walker = LC_GetDefaultWalker(arena, WalkAndCountDeclRefs);
    walker.user_data    = (void *)map;
    walker.visit_notes  = true;
    LC_WalkAST(&walker, ast);
}

LC_FUNCTION LC_Decl *LC_RemoveUnreferencedGlobalDeclsPass(LC_Map *map_of_visits) {
    LC_Decl *first_removed_decl = NULL;
    LC_Decl *last_removed_decl  = NULL;
    for (LC_ASTRef *it = L->ordered_packages.first; it; it = it->next) {
        for (LC_Decl *decl = it->ast->apackage.ext->first_ordered; decl;) {
            intptr_t ref_count = (intptr_t)LC_MapGetP(map_of_visits, decl);

            LC_Decl *remove = decl;
            decl            = decl->next;
            if (ref_count == 0 && remove->foreign_name != LC_ILit("main")) {
                LC_DLLRemove(it->ast->apackage.ext->first_ordered, it->ast->apackage.ext->last_ordered, remove);
                LC_DLLAdd(first_removed_decl, last_removed_decl, remove);
            }
        }
    }
    return first_removed_decl;
}

LC_FUNCTION void LC_ErrorOnUnreferencedLocalsPass(LC_Map *map_of_visits) {
    LC_Decl *first = (LC_Decl *)L->decl_arena->memory.data;
    for (int i = 0; i < L->decl_count; i += 1) {
        LC_Decl *decl = first + i;
        if (decl->package == L->builtin_package) {
            continue;
        }

        intptr_t ref_count = (intptr_t)LC_MapGetP(map_of_visits, decl);
        if (ref_count == 0) {
            if (LC_IsStmt(decl->ast)) {
                if (!LC_HasNote(decl->ast, L->iunused)) LC_ReportASTError(decl->ast, "unused local variable '%s'", decl->name);
            }
        }
    }
}

LC_FUNCTION void LC_FindUnusedLocalsAndRemoveUnusedGlobalDeclsPass(void) {
    if (L->errors) return;
    LC_TempArena check = LC_BeginTemp(L->arena);

    LC_AST *package = LC_GetPackageByName(L->first_package);
    LC_Map  map     = {check.arena};
    LC_MapReserve(&map, 512);

    LC_CountDeclRefs(check.arena, &map, package);
    LC_Decl *first_removed_decl = LC_RemoveUnreferencedGlobalDeclsPass(&map);
    for (LC_Decl *it = first_removed_decl; it; it = it->next) {
        LC_CountDeclRefs(check.arena, &map, it->ast);
    }
    LC_ErrorOnUnreferencedLocalsPass(&map);

    LC_EndTemp(check);
}
LC_FUNCTION LC_Operand LC_ImportPackage(LC_AST *import, LC_AST *dst, LC_AST *src) {
    DeclScope *dst_scope  = dst->apackage.ext->scope;
    int        scope_size = LC_NextPow2(src->apackage.ext->scope->len * 2 + 1);
    if (import && import->gimport.name) {
        LC_PUSH_PACKAGE(dst);
        LC_Decl *decl = LC_CreateDecl(LC_DeclKind_Import, import->gimport.name, import);
        decl->scope   = LC_CreateScope(scope_size);
        LC_PutGlobalDecl(decl);
        import->gimport.resolved_decl = decl;
        LC_POP_PACKAGE();
        dst_scope = decl->scope;
    }

    for (int i = 0; i < src->apackage.ext->scope->cap; i += 1) {
        LC_MapEntry entry = src->apackage.ext->scope->entries[i];
        if (entry.key != 0) {
            LC_Decl *decl = (LC_Decl *)entry.value;
            if (decl->package != src) continue;
            LC_Decl *existing = (LC_Decl *)LC_MapGetU64(dst_scope, decl->name);
            if (existing && decl->package == L->builtin_package) {
                continue;
            }
            if (existing) {
                LC_MarkDeclError(existing);
                LC_MarkDeclError(decl);
                return LC_ReportASTErrorEx(decl->ast, existing->ast, "name colission while importing '%s' into '%s', there are 2 decls with the same name '%s'", src->apackage.name, dst->apackage.name, decl->name);
            }
            LC_MapInsertU64(dst_scope, decl->name, decl);
        }
    }

    if (import && import->gimport.name) {
        LC_ASSERT(import, dst_scope->cap == scope_size);
    }

    if (import) import->gimport.resolved = true;
    return LC_OPNull;
}

LC_FUNCTION LC_Intern LC_MakePackageNameFromPath(LC_String path) {
    if (path.str[path.len - 1] == '/') path = LC_Chop(path, 1);
    LC_String s8name = LC_SkipToLastSlash(path);
    if (!LC_IsDir(L->arena, path) && LC_EndsWith(path, LC_Lit(".lc"), true)) {
        s8name = LC_ChopLastPeriod(s8name);
    }
    if (s8name.len == 0) {
        L->errors += 1;
        LC_SendErrorMessagef(NULL, NULL, "failed to extract name from path %.*s", LC_Expand(path));
        return LC_GetUniqueIntern("invalid_package_name");
    }
    LC_Intern result = LC_InternStrLen(s8name.str, (int)s8name.len);
    return result;
}

LC_FUNCTION bool LC_PackageNameValid(LC_Intern name) {
    char *str = (char *)name;
    if (LC_IsDigit(str[0])) return false;
    for (int i = 0; str[i]; i += 1) {
        bool is_valid = LC_IsIdent(str[i]) || LC_IsDigit(str[i]);
        if (!is_valid) return false;
    }
    return true;
}

LC_FUNCTION bool LC_PackageNameDuplicate(LC_Intern name) {
    LC_ASTFor(it, L->fpackage) {
        if (it->apackage.name == name) return true;
    }
    return false;
}

LC_FUNCTION void LC_AddPackageToList(LC_AST *n) {
    LC_Intern name = n->apackage.name;
    if (LC_PackageNameDuplicate(name)) {
        LC_SendErrorMessagef(NULL, NULL, "found 2 packages with the same name: '%s' / '%.*s'\n", name, LC_Expand(n->apackage.ext->path));
        L->errors += 1;
        return;
    }
    if (!LC_PackageNameValid(name)) {
        LC_SendErrorMessagef(NULL, NULL, "invalid package name, please change the name of the package directory: '%s'\n", name);
        L->errors += 1;
        return;
    }
    LC_DLLAdd(L->fpackage, L->lpackage, n);
}

LC_FUNCTION LC_AST *LC_RegisterPackage(LC_String path) {
    LC_ASSERT(NULL, path.len != 0);
    LC_AST *n             = LC_CreateAST(NULL, LC_ASTKind_Package);
    n->apackage.ext       = LC_PushStruct(L->arena, LC_ASTPackageExt);
    n->apackage.name      = LC_MakePackageNameFromPath(path);
    n->apackage.ext->path = path;
    LC_AddPackageToList(n);
    return n;
}

LC_FUNCTION LC_AST *LC_FindImportInRefList(LC_ASTRefList *arr, LC_Intern path) {
    for (LC_ASTRef *it = arr->first; it; it = it->next) {
        if (it->ast->gimport.path == path) return it->ast;
    }
    return NULL;
}

LC_FUNCTION void LC_AddASTToRefList(LC_ASTRefList *refs, LC_AST *ast) {
    LC_ASTRef *ref = LC_PushStruct(L->arena, LC_ASTRef);
    ref->ast       = ast;
    LC_DLLAdd(refs->first, refs->last, ref);
}

LC_FUNCTION LC_ASTRefList LC_GetPackageImports(LC_AST *package) {
    LC_ASSERT(package, package->kind == LC_ASTKind_Package);

    LC_ASTRefList refs = {0};
    LC_ASTFor(file, package->apackage.ffile) {
        LC_ASTFor(import, file->afile.fimport) {
            LC_AST *found = LC_FindImportInRefList(&refs, import->gimport.path);
            if (found) {
                LC_ReportASTErrorEx(import, found, "duplicate import of: '%s', into package '%s'\n", import->gimport.path, package->apackage.name);
                continue;
            }
            LC_AddASTToRefList(&refs, import);
        }
    }

    return refs;
}

LC_FUNCTION void LC_RegisterPackageDir(char *dir) {
    LC_String sdir = LC_MakeFromChar(dir);
    if (!LC_IsDir(L->arena, sdir)) {
        LC_SendErrorMessagef(NULL, NULL, "dir with name '%s', doesn't exist\n", dir);
        return;
    }
    LC_AddNode(L->arena, &L->package_dirs, sdir);
}

LC_FUNCTION LC_AST *LC_GetPackageByName(LC_Intern name) {
    LC_ASTFor(it, L->fpackage) {
        if (it->apackage.name == name) return it;
    }

    LC_AST *result = NULL;
    for (LC_StringNode *it = L->package_dirs.first; it; it = it->next) {
        LC_String s    = it->string;
        LC_String path = LC_Format(L->arena, "%.*s/%s", LC_Expand(s), (char *)name);
        if (LC_IsDir(L->arena, path)) {
            if (result != NULL) {
                LC_SendErrorMessagef(NULL, NULL, "found 2 directories with the same name: '%.*s', '%.*s'\n", LC_Expand(path), LC_Expand(result->apackage.ext->path));
                L->errors += 1;
                break;
            }
            result = LC_RegisterPackage(path);
        }
    }

    return result;
}

LC_FUNCTION LC_StringList LC_ListFilesInPackage(LC_Arena *arena, LC_String path) {
    LC_StringList result = LC_MakeEmptyList();
    for (LC_FileIter it = LC_IterateFiles(arena, path); LC_IsValid(it); LC_Advance(&it)) {
        if (LC_EndsWith(it.absolute_path, LC_Lit(".lc"), LC_IgnoreCase)) {
            LC_AddNode(arena, &result, it.absolute_path);
        }
    }
    return result;
}

LC_FUNCTION LoadedFile LC_ReadFileHook(LC_AST *package, LC_String path) {
    LoadedFile result = {path};
    if (L->on_file_load) {
        L->on_file_load(package, &result);
    } else {
        result.content = LC_ReadFile(L->arena, result.path);
    }

    return result;
}

LC_FUNCTION void LC_ParsePackage(LC_AST *n) {
    LC_ASSERT(n, n->kind == LC_ASTKind_Package);
    LC_ASSERT(n, n->apackage.ext->scope == NULL);
    n->apackage.ext->scope = LC_CreateScope(256);

    LC_StringList files = n->apackage.ext->injected_filepaths;
    if (files.node_count == 0) {
        files = LC_ListFilesInPackage(L->arena, n->apackage.ext->path);
        if (files.first == NULL) {
            LC_SendErrorMessagef(NULL, NULL, "no valid .lc files in '%.*s'", LC_Expand(n->apackage.ext->path));
            n->apackage.ext->state = LC_DeclState_Error;
            L->errors += 1;
            return;
        }
    }

    for (LC_StringNode *it = files.first; it; it = it->next) {
        LoadedFile file = LC_ReadFileHook(n, it->string);
        if (file.content.str == NULL) file.content.str = "";

        LC_AST *ast_file = LC_ParseFile(n, file.path.str, file.content.str, file.line);
        if (!ast_file) {
            n->apackage.ext->state = LC_DeclState_Error;
            return;
        }
    }
}

LC_FUNCTION void LC_ParsePackagesPass(LC_Intern name) {
    LC_AST *n = LC_GetPackageByName(name);
    if (!n) {
        LC_SendErrorMessagef(NULL, NULL, "no package with name '%s'\n", name);
        L->errors += 1;
        return;
    }
    if (n->apackage.ext->scope) {
        return;
    }
    LC_ParsePackage(n);
    LC_ASTRefList imports = LC_GetPackageImports(n);
    for (LC_ASTRef *it = imports.first; it; it = it->next) {
        LC_ParsePackagesPass(it->ast->gimport.path);
    }
}

LC_FUNCTION void LC_BuildIfPass(void) {
    LC_ASTFor(n, L->fpackage) {
        for (LC_AST *fit = n->apackage.ffile; fit;) {
            LC_AST *next = fit->next;

            LC_AST *build_if = LC_HasNote(fit, L->ibuild_if);
            if (build_if) {
                if (!LC_ResolveBuildIf(build_if)) {
                    LC_DLLRemove(n->apackage.ffile, n->apackage.lfile, fit);
                    LC_AddASTToRefList(&L->discarded, fit);
                    fit = next;
                    continue;
                }
            }

            for (LC_AST *dit = fit->afile.fdecl; dit; dit = dit->next) {
                LC_AST *build_if = LC_HasNote(dit, L->ibuild_if);
                if (build_if) {
                    if (!LC_ResolveBuildIf(build_if)) {
                        LC_DLLRemove(fit->afile.fdecl, fit->afile.ldecl, dit);
                        LC_AddASTToRefList(&L->discarded, dit);
                    }
                }
            }

            fit = next;
        }
    }
}

LC_FUNCTION void LC_AddOrderedPackageToRefList(LC_AST *n) {
    LC_ASTRefList *ordered = &L->ordered_packages;
    for (LC_ASTRef *it = ordered->first; it; it = it->next) {
        if (it->ast->apackage.name == n->apackage.name) {
            return;
        }
    }
    LC_AddASTToRefList(ordered, n);
}

// Here we use import statements to produce a list of ordered packages.
// While we are at it we also resolve most top level declarations. I say
// most because aggregations are handled a bit differently, their resolution
// is deffered. This is added because a pointer doesn't require full typeinfo of
// an aggregate. It's just a number.
LC_FUNCTION LC_AST *LC_OrderPackagesAndBasicResolve(LC_AST *pos, LC_Intern name) {
    LC_AST *n = LC_GetPackageByName(name);
    if (n->apackage.ext->state == LC_DeclState_Error) {
        return NULL;
    }
    if (n->apackage.ext->state == LC_DeclState_Resolved) {
        // This function can be called multiple times, I assume user might
        // want to use type information to generate something. Pattern:
        // typecheck -> generate -> typecheck is expected!
        LC_PackageDecls(n);
        return n;
    }
    if (n->apackage.ext->state == LC_DeclState_Resolving) {
        LC_ReportASTError(pos, "circular import '%s'", name);
        n->apackage.ext->state = LC_DeclState_Error;
        return NULL;
    }
    LC_ASSERT(pos, n->apackage.ext->state == LC_DeclState_Unresolved);
    n->apackage.ext->state = LC_DeclState_Resolving;

    LC_Operand op = LC_ImportPackage(NULL, n, L->builtin_package);
    LC_ASSERT(pos, !LC_IsError(op));

    // Resolve all imports regardless of errors.
    // If current package has wrong import it means it's also
    // wrong but it should still look into all imports
    // despite this.
    int           wrong_import = 0;
    LC_ASTRefList refs         = LC_GetPackageImports(n);
    for (LC_ASTRef *it = refs.first; it; it = it->next) {
        LC_AST *import = LC_OrderPackagesAndBasicResolve(it->ast, it->ast->gimport.path);
        if (!import) {
            n->apackage.ext->state = LC_DeclState_Error;
            wrong_import += 1;
            continue;
        }

        LC_Operand op = LC_ImportPackage(it->ast, n, import);
        if (LC_IsError(op)) {
            n->apackage.ext->state = LC_DeclState_Error;
            wrong_import += 1;
            continue;
        }
    }

    if (wrong_import) return NULL;

    LC_PackageDecls(n);
    LC_AddOrderedPackageToRefList(n);
    n->apackage.ext->state = LC_DeclState_Resolved;
    return n;
}

LC_FUNCTION void LC_OrderAndResolveTopLevelPass(LC_Intern name) {
    L->first_package = name;
    LC_OrderPackagesAndBasicResolve(NULL, name);

    // Resolve still incomplete aggregate types, this operates on all packages
    // that didn't have errors so even if something broke in package ordering
    // it should still be fine to go forward with this and also proc body analysis
    for (LC_ASTRef *it = L->ordered_packages.first; it; it = it->next) {
        LC_AST *package = it->ast;
        LC_ASSERT(package, package->apackage.ext->state == LC_DeclState_Resolved);
        LC_ResolveIncompleteTypes(package);
    }
}

LC_FUNCTION void LC_ResolveProcBodiesPass(void) {
    // We don't need to check errors, only valid packages should have been put into
    // the list.
    for (LC_ASTRef *it = L->ordered_packages.first; it; it = it->next) {
        LC_AST *package = it->ast;
        LC_ASSERT(package, package->apackage.ext->state == LC_DeclState_Resolved);
        LC_ResolveProcBodies(package);
    }
}

LC_FUNCTION void LC_ParseAndResolve(LC_Intern name) {
    LC_ParsePackagesPass(name);
    LC_BuildIfPass();
    if (L->errors) return;

    LC_OrderAndResolveTopLevelPass(name);
    LC_ResolveProcBodiesPass();
}

LC_FUNCTION LC_String LC_GenerateUnityBuild(void) {
    if (L->errors) return LC_MakeEmptyString();
    LC_ASTRefList packages = L->ordered_packages;

    LC_BeginStringGen(L->arena);

    LC_GenLinef("#include <stdbool.h>");
    LC_GenLinef("#include <stddef.h>");
    LC_GenLinef("#ifndef LC_String_IMPL");
    LC_GenLinef("#define LC_String_IMPL");
    LC_GenLinef("typedef struct { char *str; long long len; } LC_String;");
    LC_GenLinef("#endif");
    LC_GenLinef("#ifndef LC_Any_IMPL");
    LC_GenLinef("#define LC_Any_IMPL");
    LC_GenLinef("typedef struct { int type; void *data; } LC_Any;");
    LC_GenLinef("#endif");

    LC_GenLinef("#ifndef LC_Alignof");
    LC_GenLinef("#if defined(__TINYC__)");
    LC_GenLinef("#define LC_Alignof(...) __alignof__(__VA_ARGS__)");
    LC_GenLinef("#else");
    LC_GenLinef("#define LC_Alignof(...) _Alignof(__VA_ARGS__)");
    LC_GenLinef("#endif");
    LC_GenLinef("#endif");
    LC_GenLinef("void *memset(void *, int, size_t);");

    for (LC_ASTRef *it = packages.first; it; it = it->next) LC_GenCHeader(it->ast);
    for (LC_ASTRef *it = packages.first; it; it = it->next) LC_GenCImpl(it->ast);
    LC_String s = LC_EndStringGen(L->arena);
    return s;
}

LC_FUNCTION void LC_AddSingleFilePackage(LC_Intern name, LC_String path) {
    LC_AST *n             = LC_CreateAST(0, LC_ASTKind_Package);
    n->apackage.ext       = LC_PushStruct(L->arena, LC_ASTPackageExt);
    n->apackage.name      = name;
    n->apackage.ext->path = path;
    LC_AddNode(L->arena, &n->apackage.ext->injected_filepaths, path);
    LC_AddPackageToList(n);
}
LC_FUNCTION LC_Lang *LC_LangAlloc(void) {
    LC_Arena *arena      = LC_BootstrapArena();
    LC_Arena *lex_arena  = LC_PushStruct(arena, LC_Arena);
    LC_Arena *decl_arena = LC_PushStruct(arena, LC_Arena);
    LC_Arena *ast_arena  = LC_PushStruct(arena, LC_Arena);
    LC_Arena *type_arena = LC_PushStruct(arena, LC_Arena);
    LC_InitArena(lex_arena);
    LC_InitArena(decl_arena);
    LC_InitArena(ast_arena);
    LC_InitArena(type_arena);

    LC_Lang *l    = LC_PushStruct(arena, LC_Lang);
    l->arena      = arena;
    l->lex_arena  = lex_arena;
    l->decl_arena = decl_arena;
    l->ast_arena  = ast_arena;
    l->type_arena = type_arena;

    l->emit_line_directives        = true;
    l->breakpoint_on_error         = true;
    l->use_colored_terminal_output = true;

    return l;
}

LC_FUNCTION void LC_LangEnd(LC_Lang *lang) {
    LC_DeallocateArena(lang->lex_arena);
    LC_DeallocateArena(lang->type_arena);
    LC_DeallocateArena(lang->decl_arena);
    LC_DeallocateArena(lang->ast_arena);
    LC_DeallocateArena(lang->arena);
    if (L == lang) L = NULL;
}

LC_FUNCTION void LC_LangBegin(LC_Lang *l) {
    L = l;

    // Init default target settings
    {
        if (L->os == LC_OS_Invalid) {
            L->os = LC_OS_LINUX;
#if LC_OPERATING_SYSTEM_WINDOWS
            L->os = LC_OS_WINDOWS;
#elif LC_OPERATING_SYSTEM_MAC
            L->os = LC_OS_MAC;
#endif
        }
        if (L->arch == LC_ARCH_Invalid) {
            L->arch = LC_ARCH_X64;
        }
        if (L->gen == LC_GEN_Invalid) {
            L->gen = LC_GEN_C;
        }
    }

    //
    // Init declared notes, interns and foreign names checker
    //
    {
        L->declared_notes.arena = L->arena;
        L->interns.arena        = L->arena;
        L->foreign_names.arena  = L->arena;
        L->implicit_any.arena   = L->arena;

        LC_MapReserve(&L->declared_notes, 128);
        LC_MapReserve(&L->interns, 4096);
        LC_MapReserve(&L->foreign_names, 256);
        LC_MapReserve(&L->implicit_any, 64);

#define X(x) l->k##x = LC_InternStrLen(#x, sizeof(#x) - 1);
        LC_LIST_KEYWORDS
#undef X
        l->first_keyword = l->kfor;
        l->last_keyword  = l->kfalse;

#define X(x, declare) l->i##x = LC_InternStrLen(#x, sizeof(#x) - 1);
        LC_LIST_INTERNS
#undef X
#define X(x, declare) \
    if (declare) LC_DeclareNote(L->i##x);
        LC_LIST_INTERNS
#undef X
    }

    // Nulls
    {
        L->NullLEX.begin    = "builtin declarations";
        L->NullLEX.file     = LC_ILit("builtin declarations");
        L->BuiltinToken.lex = &L->NullLEX;
        L->BuiltinToken.str = "builtin declarations";
        L->BuiltinToken.len = sizeof("builtin declarations") - 1;
        L->NullAST.pos      = &L->BuiltinToken;
    }

    {
        LC_AST *builtins              = LC_CreateAST(0, LC_ASTKind_Package);
        L->builtin_package            = builtins;
        builtins->apackage.ext        = LC_PushStruct(L->arena, LC_ASTPackageExt);
        builtins->apackage.name       = LC_ILit("builtins");
        builtins->apackage.ext->scope = LC_CreateScope(256);
        LC_AddPackageToList(builtins);
    }

    LC_InitDeclStack(&L->resolver.locals, 128);
    L->resolver.duplicate_map.arena = L->arena;
    LC_MapReserve(&L->resolver.duplicate_map, 32);

    L->resolver.stmt_block_stack.arena = L->arena;
    L->printer.out_block_stack.arena   = L->arena;

    LC_PUSH_PACKAGE(L->builtin_package);

    //
    // Init default type sizes using current platform
    //
    // Here we use the sizes of our current platform but
    // later on it gets swapped based on LC override global variables in
    // InitTarget
    //
    {
        l->type_map.arena = L->arena;
        LC_MapReserve(&l->type_map, 256);

        typedef long long          llong;
        typedef unsigned long long ullong;
        typedef unsigned long      ulong;
        typedef unsigned short     ushort;
        typedef unsigned char      uchar;
        typedef unsigned int       uint;

        L->pointer_align = LC_Alignof(void *);
        L->pointer_size  = sizeof(void *);

        int i = 0;
#define X(TNAME, IS_UNSIGNED)                      \
    l->types[i].kind        = LC_TypeKind_##TNAME; \
    l->types[i].size        = sizeof(TNAME);       \
    l->types[i].align       = LC_Alignof(TNAME);   \
    l->types[i].is_unsigned = IS_UNSIGNED;         \
    l->t##TNAME             = l->types + i++;

        LC_LIST_TYPES
#undef X

        //
        // Overwrite types with target
        //
        if (L->arch == LC_ARCH_X64) {
            LC_SetPointerSizeAndAlign(8, 8);
            if (L->os == LC_OS_WINDOWS) {
                L->tlong->size   = 4;
                L->tlong->align  = 4;
                L->tulong->size  = 4;
                L->tulong->align = 4;
            } else {
                L->tlong->size   = 8;
                L->tlong->align  = 8;
                L->tulong->size  = 8;
                L->tulong->align = 8;
            }
        } else if (L->arch == LC_ARCH_X86) {
            LC_SetPointerSizeAndAlign(4, 4);
            L->tlong->size   = 4;
            L->tlong->align  = 4;
            L->tulong->size  = 4;
            L->tulong->align = 4;
        }

        l->types[i].kind = LC_TypeKind_void;
        l->tvoid         = l->types + i++;

        // Init decls for types
        for (int i = 0; i < T_Count; i += 1) {
            char     *it     = (char *)LC_TypeKindToString((LC_TypeKind)i) + 12;
            LC_Intern intern = LC_ILit(it);
            LC_Type  *t      = l->types + i;
            t->id            = ++l->typeids;

            LC_Decl *decl = LC_CreateDecl(LC_DeclKind_Type, intern, &L->NullAST);
            decl->state   = LC_DeclState_Resolved;
            decl->type    = t;
            t->decl       = decl;
            LC_AddDeclToScope(L->builtin_package->apackage.ext->scope, decl);

            if (t->kind == LC_TypeKind_uchar) decl->foreign_name = LC_ILit("unsigned char");
            if (t->kind == LC_TypeKind_ushort) decl->foreign_name = LC_ILit("unsigned short");
            if (t->kind == LC_TypeKind_uint) decl->foreign_name = LC_ILit("unsigned");
            if (t->kind == LC_TypeKind_ulong) decl->foreign_name = LC_ILit("unsigned long");
            if (t->kind == LC_TypeKind_llong) decl->foreign_name = LC_ILit("long long");
            if (t->kind == LC_TypeKind_ullong) decl->foreign_name = LC_ILit("unsigned long long");
        }
    }

    l->tpvoid = LC_CreatePointerType(l->tvoid);
    l->tpchar = LC_CreatePointerType(l->tchar);

    {
        l->tuntypedint  = LC_CreateUntypedInt(L->tint);
        l->tuntypedbool = LC_CreateUntypedInt(L->tbool);
        l->tuntypednil  = LC_CreateUntypedInt(L->tullong);

        l->ttuntypedfloat.kind       = LC_TypeKind_UntypedFloat;
        l->ttuntypedfloat.id         = ++L->typeids;
        l->tuntypedfloat             = &L->ttuntypedfloat;
        l->tuntypedfloat->tutdefault = l->tdouble;
        l->tuntypedfloat->decl       = LC_CreateDecl(LC_DeclKind_Type, LC_ILit("UntypedFloat"), &L->NullAST);

        l->ttuntypedstring.kind       = LC_TypeKind_UntypedString;
        l->ttuntypedstring.id         = ++L->typeids;
        l->tuntypedstring             = &L->ttuntypedstring;
        l->tuntypedstring->tutdefault = l->tpchar;
        l->tuntypedstring->decl       = LC_CreateDecl(LC_DeclKind_Type, LC_ILit("UntypedString"), &L->NullAST);
    }

    // Add builtin "String" type
    {
        L->ttstring.kind = LC_TypeKind_Incomplete;
        L->ttstring.id   = ++L->typeids;
        L->tstring       = &L->ttstring;

        LC_AST  *ast       = LC_ParseDeclf("String :: struct { str: *char; len: int; }");
        LC_Decl *decl      = LC_CreateDecl(LC_DeclKind_Type, ast->dbase.name, ast);
        decl->foreign_name = LC_ILit("LC_String");
        decl->state        = LC_DeclState_Resolved;
        decl->type         = L->tstring;
        L->tstring->decl   = decl;
        LC_AddDeclToScope(L->builtin_package->apackage.ext->scope, decl);
        LC_Operand result = LC_ResolveTypeAggregate(ast, decl->type);
        LC_ASSERT(ast, !LC_IsError(result));
    }

    // Add builtin "Any" type
    {
        L->ttany.kind = LC_TypeKind_Incomplete;
        L->ttany.id   = ++L->typeids;
        L->tany       = &L->ttany;

        LC_AST  *ast       = LC_ParseDeclf("Any :: struct { type: int; data: *void; }");
        LC_Decl *decl      = LC_CreateDecl(LC_DeclKind_Type, ast->dbase.name, ast);
        decl->foreign_name = LC_ILit("LC_Any");
        decl->state        = LC_DeclState_Resolved;
        decl->type         = L->tany;
        L->tany->decl      = decl;
        LC_AddDeclToScope(L->builtin_package->apackage.ext->scope, decl);
        LC_Operand result = LC_ResolveTypeAggregate(ast, decl->type);
        LC_ASSERT(ast, !LC_IsError(result));
    }

    LC_Decl *decl_nil = LC_AddConstIntDecl("nil", 0);
    decl_nil->type    = L->tuntypednil;

    for (int i = LC_ARCH_X64; i < LC_ARCH_Count; i += 1) LC_AddBuiltinConstInt((char *)LC_ARCHToString((LC_ARCH)i), i);
    for (int i = LC_OS_WINDOWS; i < LC_OS_Count; i += 1) LC_AddBuiltinConstInt((char *)LC_OSToString((LC_OS)i), i);
    for (int i = LC_GEN_C; i < LC_GEN_Count; i += 1) LC_AddBuiltinConstInt((char *)LC_GENToString((LC_GEN)i), i);
    LC_AddBuiltinConstInt("LC_ARCH", L->arch);
    LC_AddBuiltinConstInt("LC_GEN", L->gen);
    LC_AddBuiltinConstInt("LC_OS", L->os);

    LC_POP_PACKAGE();
}


#if _WIN32
    typedef struct LC_Win32_FileIter {
    HANDLE           handle;
    WIN32_FIND_DATAW data;
} LC_Win32_FileIter;

LC_FUNCTION bool LC_IsDir(LC_Arena *arena, LC_String path) {
    wchar_t wbuff[1024];
    LC_CreateWidecharFromChar(wbuff, LC_StrLenof(wbuff), path.str, path.len);
    DWORD dwAttrib = GetFileAttributesW(wbuff);
    return dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY);
}

LC_FUNCTION LC_String LC_GetAbsolutePath(LC_Arena *arena, LC_String relative) {
    wchar_t wpath[1024];
    LC_CreateWidecharFromChar(wpath, LC_StrLenof(wpath), relative.str, relative.len);
    wchar_t wpath_abs[1024];
    DWORD   written = GetFullPathNameW((wchar_t *)wpath, LC_StrLenof(wpath_abs), wpath_abs, 0);
    if (written == 0)
        return LC_MakeEmptyString();
    LC_String path = LC_FromWidecharEx(arena, wpath_abs, written);
    LC_NormalizePathUnsafe(path);
    return path;
}

LC_FUNCTION bool LC_IsValid(LC_FileIter it) {
    return it.is_valid;
}

LC_FUNCTION void LC_Advance(LC_FileIter *it) {
    while (FindNextFileW(it->w32->handle, &it->w32->data) != 0) {
        WIN32_FIND_DATAW *data = &it->w32->data;

        // Skip '.' and '..'
        if (data->cFileName[0] == '.' && data->cFileName[1] == '.' && data->cFileName[2] == 0) continue;
        if (data->cFileName[0] == '.' && data->cFileName[1] == 0) continue;

        it->is_directory      = data->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
        it->filename          = LC_FromWidecharEx(it->arena, data->cFileName, LC_WideLength(data->cFileName));
        const char *is_dir    = it->is_directory ? "/" : "";
        const char *separator = it->path.str[it->path.len - 1] == '/' ? "" : "/";
        it->relative_path     = LC_Format(it->arena, "%.*s%s%.*s%s", LC_Expand(it->path), separator, LC_Expand(it->filename), is_dir);
        it->absolute_path     = LC_GetAbsolutePath(it->arena, it->relative_path);
        it->is_valid          = true;

        if (it->is_directory) {
            LC_ASSERT(NULL, it->relative_path.str[it->relative_path.len - 1] == '/');
            LC_ASSERT(NULL, it->absolute_path.str[it->absolute_path.len - 1] == '/');
        }
        return;
    }

    it->is_valid = false;
    DWORD error  = GetLastError();
    LC_ASSERT(NULL, error == ERROR_NO_MORE_FILES);
    FindClose(it->w32->handle);
}

LC_FUNCTION LC_FileIter LC_IterateFiles(LC_Arena *scratch_arena, LC_String path) {
    LC_FileIter it = {0};
    it.arena       = scratch_arena;
    it.path        = path;

    LC_String modified_path = LC_Format(it.arena, "%.*s\\*", LC_Expand(path));
    wchar_t  *wbuff         = LC_PushArray(it.arena, wchar_t, modified_path.len + 1);
    int64_t   wsize         = LC_CreateWidecharFromChar(wbuff, modified_path.len + 1, modified_path.str, modified_path.len);
    LC_ASSERT(NULL, wsize);

    it.w32         = LC_PushStruct(it.arena, LC_Win32_FileIter);
    it.w32->handle = FindFirstFileW(wbuff, &it.w32->data);
    if (it.w32->handle == INVALID_HANDLE_VALUE) {
        it.is_valid = false;
        return it;
    }

    LC_ASSERT(NULL, it.w32->data.cFileName[0] == '.' && it.w32->data.cFileName[1] == 0);
    LC_Advance(&it);
    return it;
}

LC_FUNCTION LC_String LC_ReadFile(LC_Arena *arena, LC_String path) {
    LC_String result = LC_MakeEmptyString();

    wchar_t wpath[1024];
    LC_CreateWidecharFromChar(wpath, LC_StrLenof(wpath), path.str, path.len);
    HANDLE handle = CreateFileW(wpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (handle != INVALID_HANDLE_VALUE) {
        LARGE_INTEGER file_size;
        if (GetFileSizeEx(handle, &file_size)) {
            if (file_size.QuadPart != 0) {
                result.len = (int64_t)file_size.QuadPart;
                result.str = (char *)LC_PushSizeNonZeroed(arena, result.len + 1);
                DWORD read;
                if (ReadFile(handle, result.str, (DWORD)result.len, &read, NULL)) { // @todo: can only read 32 byte size files?
                    if (read == result.len) {
                        result.str[result.len] = 0;
                    }
                }
            }
        }
        CloseHandle(handle);
    }

    return result;
}

LC_FUNCTION bool LC_EnableTerminalColors(void) {
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    if (hOut != INVALID_HANDLE_VALUE) {
        DWORD dwMode = 0;
        if (GetConsoleMode(hOut, &dwMode)) {
            dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
            if (SetConsoleMode(hOut, dwMode)) {
                return true;
            }
        }
    }
    return false;
}

#elif __linux__ || __APPLE__ || __unix__
    #include <sys/stat.h>
    #include <unistd.h>
    #include <limits.h>
    #include <time.h>
    #include <dirent.h>
    #include <sys/mman.h>

    LC_FUNCTION bool LC_EnableTerminalColors(void) {
    return true;
}

LC_FUNCTION bool LC_IsDir(LC_Arena *arena, LC_String path) {
    bool         result = false;
    LC_TempArena ch     = LC_BeginTemp(arena);
    LC_String    copy   = LC_CopyString(arena, path);

    struct stat s;
    if (stat(copy.str, &s) != 0) {
        result = false;
    } else {
        result = S_ISDIR(s.st_mode);
    }

    LC_EndTemp(ch);
    return result;
}

LC_FUNCTION LC_String LC_GetAbsolutePath(LC_Arena *arena, LC_String relative) {
    LC_String copy   = LC_CopyString(arena, relative);
    char     *buffer = (char *)LC_PushSizeNonZeroed(arena, PATH_MAX);
    realpath((char *)copy.str, buffer);
    LC_String result = LC_MakeFromChar(buffer);
    return result;
}

LC_FUNCTION bool LC_IsValid(LC_FileIter it) {
    return it.is_valid;
}

LC_FUNCTION void LC_Advance(LC_FileIter *it) {
    struct dirent *file = 0;
    while ((file = readdir((DIR *)it->dir)) != NULL) {
        if (file->d_name[0] == '.' && file->d_name[1] == '.' && file->d_name[2] == 0) continue;
        if (file->d_name[0] == '.' && file->d_name[1] == 0) continue;

        it->is_directory = file->d_type == DT_DIR;
        it->filename     = LC_CopyChar(it->arena, file->d_name);

        const char *dir_char_ending = it->is_directory ? "/" : "";
        const char *separator       = it->path.str[it->path.len - 1] == '/' ? "" : "/";
        it->relative_path           = LC_Format(it->arena, "%.*s%s%s%s", LC_Expand(it->path), separator, file->d_name, dir_char_ending);
        it->absolute_path           = LC_GetAbsolutePath(it->arena, it->relative_path);
        if (it->is_directory) it->absolute_path = LC_Format(it->arena, "%.*s/", LC_Expand(it->absolute_path));
        it->is_valid = true;
        return;
    }
    it->is_valid = false;
    closedir((DIR *)it->dir);
}

LC_FUNCTION LC_FileIter LC_IterateFiles(LC_Arena *arena, LC_String path) {
    LC_FileIter it = {0};
    it.arena       = arena;
    it.path = path = LC_CopyString(arena, path);
    it.dir         = (void *)opendir((char *)path.str);
    if (!it.dir) return it;

    LC_Advance(&it);
    return it;
}

LC_FUNCTION LC_String LC_ReadFile(LC_Arena *arena, LC_String path) {
    LC_String result = LC_MakeEmptyString();

    // ftell returns insane size if file is
    // a directory **on some machines** KEKW
    if (LC_IsDir(arena, path)) {
        return result;
    }

    path    = LC_CopyString(arena, path);
    FILE *f = fopen(path.str, "rb");
    if (f) {
        fseek(f, 0, SEEK_END);
        result.len = ftell(f);
        fseek(f, 0, SEEK_SET);

        result.str = (char *)LC_PushSizeNonZeroed(arena, result.len + 1);
        fread(result.str, result.len, 1, f);
        result.str[result.len] = 0;

        fclose(f);
    }

    return result;
}

#endif

#ifndef LC_USE_CUSTOM_ARENA
    #if defined(LC_USE_ADDRESS_SANITIZER)
    #include <sanitizer/asan_interface.h>
#endif

#if !defined(ASAN_POISON_MEMORY_REGION)
    #define LC_ASAN_POISON_MEMORY_REGION(addr, size) ((void)(addr), (void)(size))
    #define LC_ASAN_UNPOISON_MEMORY_REGION(addr, size) ((void)(addr), (void)(size))
#else
    #define LC_ASAN_POISON_MEMORY_REGION(addr, size) ASAN_POISON_MEMORY_REGION(addr, size)
    #define LC_ASAN_UNPOISON_MEMORY_REGION(addr, size) ASAN_UNPOISON_MEMORY_REGION(addr, size)
#endif

#define LC_DEFAULT_RESERVE_SIZE LC_GIB(1)
#define LC_DEFAULT_ALIGNMENT 8
#define LC_COMMIT_ADD_SIZE LC_MIB(4)

LC_FUNCTION uint8_t *LC_V_AdvanceCommit(LC_VMemory *m, size_t *commit_size, size_t page_size) {
    size_t aligned_up_commit                            = LC_AlignUp(*commit_size, page_size);
    size_t to_be_total_commited_size                    = aligned_up_commit + m->commit;
    size_t to_be_total_commited_size_clamped_to_reserve = LC_CLAMP_TOP(to_be_total_commited_size, m->reserve);
    size_t adjusted_to_boundary_commit                  = to_be_total_commited_size_clamped_to_reserve - m->commit;
    LC_Assertf(adjusted_to_boundary_commit, "Reached the virtual memory reserved boundary");
    *commit_size = adjusted_to_boundary_commit;

    if (adjusted_to_boundary_commit == 0) {
        return 0;
    }
    uint8_t *result = m->data + m->commit;
    return result;
}

LC_FUNCTION void LC_PopToPos(LC_Arena *arena, size_t pos) {
    LC_Assertf(arena->len >= arena->base_len, "Bug: arena->len shouldn't ever be smaller then arena->base_len");
    pos         = LC_CLAMP(pos, arena->base_len, arena->len);
    size_t size = arena->len - pos;
    arena->len  = pos;
    LC_ASAN_POISON_MEMORY_REGION(arena->memory.data + arena->len, size);
}

LC_FUNCTION void LC_PopSize(LC_Arena *arena, size_t size) {
    LC_PopToPos(arena, arena->len - size);
}

LC_FUNCTION void LC_DeallocateArena(LC_Arena *arena) {
    LC_VDeallocate(&arena->memory);
}

LC_FUNCTION void LC_ResetArena(LC_Arena *arena) {
    LC_PopToPos(arena, 0);
}

LC_FUNCTION size_t LC__AlignLen(LC_Arena *a) {
    size_t align_offset = a->alignment ? LC_GetAlignOffset((uintptr_t)a->memory.data + (uintptr_t)a->len, a->alignment) : 0;
    size_t aligned      = a->len + align_offset;
    return aligned;
}

LC_FUNCTION void *LC__PushSizeNonZeroed(LC_Arena *a, size_t size) {
    size_t align_offset        = a->alignment ? LC_GetAlignOffset((uintptr_t)a->memory.data + (uintptr_t)a->len, a->alignment) : 0;
    size_t aligned_len         = a->len + align_offset;
    size_t size_with_alignment = size + align_offset;

    if (a->len + size_with_alignment > a->memory.commit) {
        if (a->memory.reserve == 0) {
            LC_Assertf(0, "Pushing on uninitialized arena with zero initialization turned off");
        }
        bool result = LC_VCommit(&a->memory, size_with_alignment + LC_COMMIT_ADD_SIZE);
        LC_Assertf(result, "%s(%d): Failed to commit memory more memory! reserve: %zu commit: %zu len: %zu size_with_alignment: %zu", LC_BeginTempdSourceLoc.file, LC_BeginTempdSourceLoc.line, a->memory.reserve, a->memory.commit, a->len, size_with_alignment);
        (void)result;
    }

    uint8_t *result = a->memory.data + aligned_len;
    a->len += size_with_alignment;
    LC_Assertf(a->len <= a->memory.commit, "%s(%d): Reached commit boundary! reserve: %zu commit: %zu len: %zu base_len: %zu alignment: %d size_with_alignment: %zu", LC_BeginTempdSourceLoc.file, LC_BeginTempdSourceLoc.line, a->memory.reserve, a->memory.commit, a->len, a->base_len, a->alignment, size_with_alignment);
    LC_ASAN_UNPOISON_MEMORY_REGION(result, size);
    return (void *)result;
}

LC_FUNCTION void *LC__PushSize(LC_Arena *arena, size_t size) {
    void *result = LC__PushSizeNonZeroed(arena, size);
    LC_MemoryZero(result, size);
    return result;
}

LC_FUNCTION LC_Arena LC_PushArena(LC_Arena *arena, size_t size) {
    LC_Arena result;
    LC_MemoryZero(&result, sizeof(result));
    result.memory.data    = LC_PushArrayNonZeroed(arena, uint8_t, size);
    result.memory.commit  = size;
    result.memory.reserve = size;
    result.alignment      = arena->alignment;
    return result;
}

LC_FUNCTION LC_Arena *LC_PushArenaP(LC_Arena *arena, size_t size) {
    LC_Arena *result = LC_PushStruct(arena, LC_Arena);
    *result          = LC_PushArena(arena, size);
    return result;
}

LC_FUNCTION void LC_InitArenaEx(LC_Arena *a, size_t reserve) {
    a->memory = LC_VReserve(reserve);
    LC_ASAN_POISON_MEMORY_REGION(a->memory.data, a->memory.reserve);
    a->alignment = LC_DEFAULT_ALIGNMENT;
}

LC_FUNCTION void LC_InitArena(LC_Arena *a) {
    LC_InitArenaEx(a, LC_DEFAULT_RESERVE_SIZE);
}

LC_FUNCTION LC_Arena *LC_BootstrapArena(void) {
    LC_Arena bootstrap_arena = {0};
    LC_InitArena(&bootstrap_arena);

    LC_Arena *arena = LC_PushStruct(&bootstrap_arena, LC_Arena);
    *arena          = bootstrap_arena;
    arena->base_len = arena->len;
    return arena;
}

LC_FUNCTION void LC_InitArenaFromBuffer(LC_Arena *arena, void *buffer, size_t size) {
    arena->memory.data    = (uint8_t *)buffer;
    arena->memory.commit  = size;
    arena->memory.reserve = size;
    arena->alignment      = LC_DEFAULT_ALIGNMENT;
    LC_ASAN_POISON_MEMORY_REGION(arena->memory.data, arena->memory.reserve);
}

LC_FUNCTION LC_TempArena LC_BeginTemp(LC_Arena *arena) {
    LC_TempArena result;
    result.pos   = arena->len;
    result.arena = arena;
    return result;
}

LC_FUNCTION void LC_EndTemp(LC_TempArena checkpoint) {
    LC_PopToPos(checkpoint.arena, checkpoint.pos);
}
    #if _WIN32
        const size_t LC_V_WIN32_PAGE_SIZE = 4096;

LC_FUNCTION LC_VMemory LC_VReserve(size_t size) {
    LC_VMemory result;
    LC_MemoryZero(&result, sizeof(result));
    size_t adjusted_size = LC_AlignUp(size, LC_V_WIN32_PAGE_SIZE);
    result.data          = (uint8_t *)VirtualAlloc(0, adjusted_size, MEM_RESERVE, PAGE_READWRITE);
    LC_Assertf(result.data, "Failed to reserve virtual memory");
    result.reserve = adjusted_size;
    return result;
}

LC_FUNCTION bool LC_VCommit(LC_VMemory *m, size_t commit) {
    uint8_t *pointer = LC_V_AdvanceCommit(m, &commit, LC_V_WIN32_PAGE_SIZE);
    if (pointer) {
        void *result = VirtualAlloc(pointer, commit, MEM_COMMIT, PAGE_READWRITE);
        LC_Assertf(result, "Failed to commit more memory");
        if (result) {
            m->commit += commit;
            return true;
        }
    }
    return false;
}

LC_FUNCTION void LC_VDeallocate(LC_VMemory *m) {
    BOOL result = VirtualFree(m->data, 0, MEM_RELEASE);
    LC_Assertf(result != 0, "Failed to release LC_VMemory");
}

LC_FUNCTION bool LC_VDecommitPos(LC_VMemory *m, size_t pos) {
    size_t aligned          = LC_AlignDown(pos, LC_V_WIN32_PAGE_SIZE);
    size_t adjusted_pos     = LC_CLAMP_TOP(aligned, m->commit);
    size_t size_to_decommit = m->commit - adjusted_pos;
    if (size_to_decommit) {
        uint8_t *base_address = m->data + adjusted_pos;
        BOOL     result       = VirtualFree(base_address, size_to_decommit, MEM_DECOMMIT);
        if (result) {
            m->commit -= size_to_decommit;
            return true;
        }
    }
    return false;
}
    #elif __linux__ || __APPLE__ || __unix__
        #define LC_V_UNIX_PAGE_SIZE 4096

LC_FUNCTION LC_VMemory LC_VReserve(size_t size) {
    LC_VMemory result       = {};
    size_t     size_aligned = LC_AlignUp(size, LC_V_UNIX_PAGE_SIZE);
    result.data             = (uint8_t *)mmap(0, size_aligned, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    LC_Assertf(result.data, "Failed to reserve memory using mmap!!");
    if (result.data) {
        result.reserve = size_aligned;
    }
    return result;
}

LC_FUNCTION bool LC_VCommit(LC_VMemory *m, size_t commit) {
    uint8_t *pointer = LC_V_AdvanceCommit(m, &commit, LC_V_UNIX_PAGE_SIZE);
    if (pointer) {
        int mprotect_result = mprotect(pointer, commit, PROT_READ | PROT_WRITE);
        LC_Assertf(mprotect_result == 0, "Failed to commit more memory using mmap");
        if (mprotect_result == 0) {
            m->commit += commit;
            return true;
        }
    }
    return false;
}

LC_FUNCTION void LC_VDeallocate(LC_VMemory *m) {
    int result = munmap(m->data, m->reserve);
    LC_Assertf(result == 0, "Failed to release virtual memory using munmap");
}
    #endif
#endif

#if __clang__
    #pragma clang diagnostic pop
#endif

#endif // LIB_COMPILER_IMPLEMENTATION

/*
Copyright (c) 2024 Krzosa Karol

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
