extern "C" {
#include "module_manager.h"
#include "interpreter.h"
#include "runtime_externs.h"
#include "stack.h"
#include "utils.h"
}

#include "analyzer.hpp"
#include "parser.hpp"

#include <cstring>
#include <filesystem>
#include <iostream>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>

template <size_t N, bool return_value, typename... Args>
  requires(N == 0)
void call_func(void (*f)(), size_t n, Args... args) {
  asm volatile("movq %0, %%r11"
               : /* no outputs */
               : "m"(n));
  if constexpr (return_value) {
    s_push(((void *(*)(Args...))f)(args...));
  } else {
    ((void (*)(Args...))f)(args...);
    s_push(0);
  }
}

template <size_t N, bool return_value, typename... Args>
  requires(N != 0)
void call_func(void (*f)(), size_t n, Args... args) {
  void *arg = s_pop();
  call_func<N - 1, return_value, Args..., void *>(f, n, arg, args...);
}

template <size_t N, bool return_value, bool do_check = true>
void call_anyarg_func(void (*f)(), size_t n) {
  if constexpr (do_check) {
    if (n > N) {
      failure("too many function arguments (not supported): %zu > %zu\n", n, N);
    }
  }
  if (n == N) {
    call_func<N, return_value>(f, n);
  } else if constexpr (N > 0) {
    call_anyarg_func<N - 1, return_value, false>(f, n);
  }
}

// ---

struct Offsets {
  size_t strings;
  size_t globals;
  size_t code;
  size_t publics_num;
};

void rewrite_code_with_offsets(Bytefile *bytefile, const Offsets &offsets) {
  // rewrite publics
  for (size_t i = 0; i < bytefile->public_symbols_number; ++i) {
    set_public_name_offset_unsafe(get_public_name_offset_unsafe(bytefile, i) +
                                      offsets.strings,
                                  bytefile, i);
    set_public_offset_unsafe(
        get_public_offset_unsafe(bytefile, i) + offsets.code, bytefile, i);
  }

  // rewrite code
  char *ip = bytefile->code_ptr;
  while (ip - bytefile->code_ptr < bytefile->code_size) {
    char *instr_ip = ip;

#ifdef DEBUG_VERSION
    std::cout << ip - bytefile->code_ptr << ": ";
    const auto [cmd, l] = parse_command(&ip, bytefile, std::cout);
    std::cout << '\n';
#else
    const auto [cmd, l] = parse_command(&ip, bytefile);
#endif

    char *read_ip = instr_ip + 1;
    char *write_ip = instr_ip + 1;
    switch (cmd) {
    case Cmd::STRING:
    case Cmd::SEXP:
    case Cmd::TAG:
      ip_write_int_unsafe(write_ip,
                          ip_read_int_unsafe(&read_ip) + offsets.strings);
      break;
    case Cmd::JMP:
    case Cmd::CJMPnz:
    case Cmd::CJMPz:
    case Cmd::CALL:
      ip_write_int_unsafe(write_ip,
                          ip_read_int_unsafe(&read_ip) + offsets.code);
      break;
    case Cmd::CLOSURE: {
      aint offset = ip_read_int_unsafe(&read_ip);
      ip_write_int_unsafe(write_ip, offset + offsets.code);
      size_t args_count = ip_read_int_unsafe(&read_ip);
      for (size_t i = 0; i < args_count; ++i) {
        uint8_t arg_type = ip_read_byte_unsafe(&read_ip);
        aint id = ip_read_int_unsafe(&read_ip);
        if (to_var_category(arg_type) == VAR_GLOBAL) {
          write_ip = read_ip;
          ip_write_int_unsafe(write_ip, id + offsets.globals);
        }
      }
      break;
    }
    case Cmd::LD:
    case Cmd::LDA:
    case Cmd::ST:
      if (to_var_category(l) == VAR_GLOBAL) {
        aint id = ip_read_int_unsafe(&read_ip);
        ip_write_int_unsafe(write_ip, id + offsets.globals);
      }
      break;
    default:
      break;
    }
  }
}

struct BuiltinSubst {
  BUILTIN id;
  uint32_t args_count;

  auto operator<=>(const BuiltinSubst &) const = default;
  bool operator==(const BuiltinSubst &) const = default;
};

void print_subst_to_bytes(BuiltinSubst subst, char **loc) {
  static constexpr const uint8_t builtin_cmd =
      ((CMD_CTRL << 4) | CMD_CTRL_BUILTIN);

  **(uint8_t **)loc = builtin_cmd;
  *loc += sizeof(uint8_t);
  **(uint32_t **)loc = subst.id;
  *loc += sizeof(int32_t);
  **(uint32_t **)loc = subst.args_count;
  *loc += sizeof(int32_t);
}

using BuiltinSubstMap = std::map<BuiltinSubst,
                                 /*generated builtin offset*/ size_t>;

void add_subst_builtin_offsets(BuiltinSubstMap &subst_map,
                               const Bytefile *bytefile) {
  for (size_t i = 0; i < bytefile->substs_area_size; ++i) {
    if (i + sizeof(uint32_t) >= bytefile->substs_area_size) {
      failure("substitution %zu offset is out of area\n", i);
    }

    uint32_t offset = *(uint32_t *)(bytefile->substs_ptr + i);
    i += sizeof(uint32_t);
    const char *name = bytefile->substs_ptr + i;
    i += strlen(name);

#ifdef DEBUG_VERSION
    printf("subst: offset 0x%.8x, name %s\n", offset, name);
#endif

    if (i > bytefile->substs_area_size) {
      failure("substitution %zu name is out of area\n", i);
    }

    BUILTIN builtin = id_by_builtin(name);

    // NOTE: address is first argument of the call
    if (builtin != BUILTIN_NONE) {
      char *ip = bytefile->code_ptr + offset;
      ip_read_int_unsafe(&ip);                       // read ptr placeholder
      uint32_t args_count = ip_read_int_unsafe(&ip); // read args count
      subst_map[{builtin, args_count}] = 0;
    }
  }
}

// NOTE: unmanaged memory allocated
std::pair<char *, size_t> gen_builtins(size_t code_offset,
                                       BuiltinSubstMap &subst_map) {
  size_t code_size = subst_map.size() *
                     /*size of builtin command*/ (1 + 2 * sizeof(uint32_t));
  char *code = (char *)malloc(code_size);

  char *code_it = code;
  for (auto &subst : subst_map) {
    subst.second = code_it - code + code_offset;
    print_subst_to_bytes(subst.first, &code_it);
  }

  return {code, code_size};
}

void subst_in_code(Bytefile *bytefile,
                   const std::unordered_map<std::string, size_t> &publics,
                   const BuiltinSubstMap &builtins) {
  for (size_t i = 0; i < bytefile->substs_area_size; ++i) {
    if (i + sizeof(uint32_t) >= bytefile->substs_area_size) {
      failure("substitution %zu offset is out of area\n", i);
    }

    uint32_t offset = *(uint32_t *)(bytefile->substs_ptr + i);
    i += sizeof(uint32_t);
    const char *name = bytefile->substs_ptr + i;
    i += strlen(name);

#ifdef DEBUG_VERSION
    printf("subst: offset 0x%.8x, name %s\n", offset, name);
#endif

    if (i > bytefile->substs_area_size) {
      failure("substitution %zu name is out of area\n", i);
    }

    BUILTIN builtin_id = id_by_builtin(name);

    // NOTE: address is first argument of the call and closure, args count is
    // second argument
    if (builtin_id != BUILTIN_NONE) {
      uint32_t *val_ptr = (uint32_t *)(bytefile->code_ptr + offset);
      uint32_t args_count =
          *(uint32_t *)(bytefile->code_ptr + offset + sizeof(uint32_t));

      *val_ptr = builtins.at({.id = builtin_id, .args_count = args_count});
    } else {
      // NOTE: works with globals too
      const auto it = publics.find(name);
      if (it == publics.end()) {
        failure("public name for substitution is not found: <%s>\n", name);
      }

      *(uint32_t *)(bytefile->code_ptr + offset) = it->second;
    }
  }
}

Offsets initial_offsets() {
  return {.strings = 0,
          .globals = 1, // NOTE: V,sysargs from, Std
          .code = 0,
          .publics_num = 0};
}

Offsets calc_merge_sizes(const std::vector<Bytefile *> &bytefiles) {
  Offsets sizes = initial_offsets();
  for (size_t i = 0; i < bytefiles.size(); ++i) {
    sizes.strings += bytefiles[i]->stringtab_size;
    sizes.globals += bytefiles[i]->global_area_size;
    sizes.code += bytefiles[i]->code_size;
    sizes.publics_num += bytefiles[i]->public_symbols_number;
  }
  return sizes;
}

void init_result_bytefile(Bytefile *result, const Offsets &sizes) {
  result->code_size = sizes.code;
  result->stringtab_size = sizes.strings;
  result->global_area_size = sizes.globals;
  result->substs_area_size = 0;
  result->imports_number = 0;
  result->public_symbols_number = sizes.publics_num;

  result->main_offset = 0;
  result->public_ptr = (int *)result->buffer;
  result->string_ptr =
      (char *)result->public_ptr + calc_publics_size(sizes.publics_num);
  result->code_ptr = result->string_ptr + result->stringtab_size;
  result->imports_ptr = NULL;
  result->global_ptr = NULL;
  result->substs_ptr = NULL;
}

struct MergeResult {
  Bytefile *bf;
  std::vector<size_t> main_offsets;
};

MergeResult merge_files(std::vector<Bytefile *> &&bytefiles) {
  Offsets sizes = calc_merge_sizes(bytefiles);
  size_t public_symbols_size = calc_publics_size(sizes.publics_num);

  // find all builtin variations ad extract them
  BuiltinSubstMap builtins_map;
  for (const auto &bytefile : bytefiles) {
    add_subst_builtin_offsets(builtins_map, bytefile);
  }
  auto [builtins_code, builtins_code_size] =
      gen_builtins(sizes.code, builtins_map);
  sizes.code += builtins_code_size;

  Bytefile *result =
      (Bytefile *)malloc(sizeof(Bytefile) + sizes.strings + sizes.code +
                         public_symbols_size); // NOTE: globals are on the stack

  // collect publics
  std::unordered_map<std::string, size_t> publics;
  std::vector<size_t> main_offsets;
  {
    // NOTE: V,sysargs from Std
    publics.insert({"global_sysargs", 0});

    size_t code_offset = 0;
    size_t globals_offset = 1; // NOTE: V,sysargs from, Std
    for (size_t i = 0; i < bytefiles.size(); ++i) {
      for (size_t j = 0; j < bytefiles[i]->public_symbols_number; ++j) {
        const char *name = get_public_name_unsafe(bytefiles[i], j);

        size_t offset =
            get_public_offset_unsafe(bytefiles[i], j) +
            (std::memcmp(name, GLOBAL_VAR_TAG, GLOBAL_VAR_TAG_LEN) == 0
                 ? globals_offset // NOTE: is global id
                 : code_offset);  // NOTE: is function offset in code

        if (strcmp(name, "main") == 0) {
          main_offsets.push_back(offset);
        } else if (!publics.insert({name, offset}).second) {
          failure("public name found more then once: %s", name);
        }
      }
      code_offset += bytefiles[i]->code_size;
      globals_offset += bytefiles[i]->global_area_size;
    }
  }

  // init result
  init_result_bytefile(result, sizes);

  // update & merge code segments
  Offsets offsets = initial_offsets();
  for (size_t i = 0; i < bytefiles.size(); ++i) {
    rewrite_code_with_offsets(bytefiles[i], offsets);
    subst_in_code(bytefiles[i], publics, builtins_map);

    size_t publics_offset = calc_publics_size(offsets.publics_num);

    // copy data to merged file
    memcpy(result->string_ptr + offsets.strings, bytefiles[i]->string_ptr,
           bytefiles[i]->stringtab_size);
    memcpy(result->code_ptr + offsets.code, bytefiles[i]->code_ptr,
           bytefiles[i]->code_size);
    memcpy((char *)result->public_ptr + publics_offset,
           (char *)bytefiles[i]->public_ptr,
           calc_publics_size(bytefiles[i]->public_symbols_number));

    // update offsets
    offsets.strings += bytefiles[i]->stringtab_size;
    offsets.globals += bytefiles[i]->global_area_size;
    offsets.code += bytefiles[i]->code_size;
    offsets.publics_num += bytefiles[i]->public_symbols_number;

    free(bytefiles[i]);
  }

  memcpy(result->code_ptr + offsets.code, builtins_code, builtins_code_size);
  free(builtins_code);

#ifdef DEBUG_VERSION
  std::cout << "main offsets:\n";
  for (const auto &offset : main_offsets) {
    std::cout << offset << '\n';
  }

  std::cout << "- merged file:\n";
  print_file(*result, std::cout);
#endif
  return {result, main_offsets};
}

// ---

Bytefile *path_mod_load(std::filesystem::path &&path) {
  return read_file(path.c_str());
}

static std::vector<std::filesystem::path> search_paths;

extern "C" {
void mod_add_search_path(const char *path) { search_paths.emplace_back(path); }

Bytefile *mod_load(const char *name) {
  std::string full_name = std::string{name} + ".bc";

  if (std::filesystem::exists(full_name)) {
    return path_mod_load(full_name);
  }
  for (const auto &dir_path : search_paths) {
    auto path = dir_path / full_name;
    if (std::filesystem::exists(path)) {
      return path_mod_load(std::move(path));
    }
  }

  return NULL;
}

} // extern "C"

void mod_load_rec(Bytefile *mod,
                  std::unordered_map<std::string, Bytefile *> &loaded,
                  std::vector<Bytefile *> &loaded_ord) {
  for (size_t i = 0; i < mod->imports_number; ++i) {
    const char *import_str = get_import_safe(mod, i);
    if (loaded.count(import_str) == 0 &&
        strcmp(import_str, "Std") != 0) { // not loaded
      Bytefile *import_mod = mod_load(import_str);
      if (import_mod == NULL) {
        failure("module <%s> not found\n", import_str);
      }
      loaded.insert({import_str, import_mod});
      mod_load_rec(import_mod, loaded, loaded_ord);
    }
  }
  loaded_ord.push_back(mod);
}

MergeResult load_with_imports(Bytefile *root, bool do_verification) {
  std::unordered_map<std::string, Bytefile *> loaded;
  std::vector<Bytefile *> loaded_ord;
  mod_load_rec(root, loaded, loaded_ord);

  MergeResult result = merge_files(std::move(loaded_ord));

  if (do_verification) {
    analyze(result.bf, std::move(result.main_offsets));
  }
  return result;
}

extern "C" {
Bytefile *run_with_imports(Bytefile *root, int argc, char **argv,
                           bool do_verification) {

  MergeResult result = load_with_imports(root, do_verification);

  Bytefile *bf = result.bf;

  bf->main_offset = 0;
  prepare_state(bf, &s); // NOTE: for push_globals
  push_globals(&s);

  for (size_t i = 0; i < result.main_offsets.size(); ++i) {
    bf->main_offset = result.main_offsets[i];
    set_argc_argv(argc, argv); // args for module main
    run_main(bf, argc, argv);
  }

  cleanup_state(&s);

  return bf;
}
} // extern "C"

struct StdFunc {
  void (*ptr)();
  size_t args_count;
  bool is_args = false; // one var for all args
  bool is_vararg = false;
};

BUILTIN id_by_builtin(const char *name) {
  static const std::unordered_map<std::string, BUILTIN> std_func = {
      {"Luppercase", BUILTIN_Luppercase},
      {"Llowercase", BUILTIN_Llowercase},
      {"Lassert", BUILTIN_Lassert},
      {"Lstring", BUILTIN_Lstring},
      {"Llength", BUILTIN_Llength},
      {"LstringInt", BUILTIN_LstringInt},
      {"Lread", BUILTIN_Lread},
      {"Lwrite", BUILTIN_Lwrite},
      {"LmakeArray", BUILTIN_LmakeArray},
      {"LmakeString", BUILTIN_LmakeString},
      {"Lstringcat", BUILTIN_Lstringcat},
      {"LmatchSubString", BUILTIN_LmatchSubString},
      {"Lsprintf", BUILTIN_Lsprintf},
      {"Lsubstring", BUILTIN_Lsubstring},
      {"Li__Infix_4343", BUILTIN_Li__Infix_4343}, // ++
      {"Lclone", BUILTIN_Lclone},
      {"Lhash", BUILTIN_Lhash},
      {"LtagHash", BUILTIN_LtagHash},
      {"Lcompare", BUILTIN_Lcompare},
      {"LflatCompare", BUILTIN_LflatCompare},
      {"Lfst", BUILTIN_Lfst},
      {"Lsnd", BUILTIN_Lsnd},
      {"Lhd", BUILTIN_Lhd},
      {"Ltl", BUILTIN_Ltl},
      {"LreadLine", BUILTIN_LreadLine},
      {"Lprintf", BUILTIN_Lprintf},
      {"Lfopen", BUILTIN_Lfopen},
      {"Lfclose", BUILTIN_Lfclose},
      {"Lfread", BUILTIN_Lfread},
      {"Lfwrite", BUILTIN_Lfwrite},
      {"Lfexists", BUILTIN_Lfexists},
      {"Lfprintf", BUILTIN_Lfprintf},
      {"Lregexp", BUILTIN_Lregexp},
      {"LregexpMatch", BUILTIN_LregexpMatch},
      {"Lfailure", BUILTIN_Lfailure},
      {"Lsystem", BUILTIN_Lsystem},
      {"LgetEnv", BUILTIN_LgetEnv},
      {"Lrandom", BUILTIN_Lrandom},
      {"Ltime", BUILTIN_Ltime},
      {"Ls__Infix_58", BUILTIN_Ls__Infix_58},
      //
      {"LkindOf", BUILTIN_LkindOf},
      {"LcompareTags", BUILTIN_LcompareTags},
  //
#define BUILTIN_ELEM(name) {#name, BUILTIN_##name},
      FORALL_BINOP_FUNC(BUILTIN_ELEM)
#undef BUILTIN_ELEM
      //
      {".array", BUILTIN_Barray},
  };

  auto const it = std_func.find(name);

  return it == std_func.end() ? BUILTIN_NONE : it->second;
}

/* NOTE: from src/X86_64.ml: */
/* For vararg functions where we pass them in the stdlib function using
   va_list, we have to unbox values to print them correctly. For this we have
   special assemply functions in `printf.S`. We additionally pass them amount
   of arguments to unbox using register r11. */
void run_stdlib_func(BUILTIN id, size_t args_count) {
  void *ret = NULL;
  switch (id) {
  case BUILTIN_Luppercase:
    ret = (void *)Luppercase(*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Llowercase:
    ret = (void *)Llowercase(*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lassert:
    // NOTE: basic params: .args_count = 2, .is_vararg = true
    call_anyarg_func<20, false>((void (*)()) & Lassert, args_count);
    break;
  case BUILTIN_Lstring:
    ret = Lstring(s_nth_i(0)); // .is_args = true
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Llength:
    ret = (void *)Llength(*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_LstringInt:
    ret = (void *)LstringInt((char *)*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lread:
    printf(" ");
    ret = (void *)Lread();
    s_push(ret);
    break;
  case BUILTIN_Lwrite:
    ret = (void *)Lwrite(*s_nth_i(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_LmakeArray:
    ret = (void *)LmakeArray(*s_nth_i(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_LmakeString:
    ret = (void *)LmakeString(*s_nth_i(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lstringcat:
    ret = (void *)Lstringcat(s_nth_i(0)); // .is_args = true;
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_LmatchSubString:
    ret = (void *)LmatchSubString((char *)*s_nth(2), (char *)*s_nth(1),
                                  *s_nth_i(0));
    s_popn(3);
    s_push(ret);
    break;
  case BUILTIN_Lsprintf:
    // NOTE: basic params: .args_count = 1, .is_vararg = true
    call_anyarg_func<20, true>((void (*)()) & Lsprintf, args_count);
    break;
  case BUILTIN_Lsubstring:
    // std::cout << "substr\n";
    s_rotate_n(3);                        // NOTE: is fix ?
    ret = (void *)Lsubstring(s_nth_i(0)); // .is_args = true;
    s_popn(3);
    s_push(ret);
    break;
  case BUILTIN_Li__Infix_4343:
    s_rotate_n(2);                            // NOTE: is fix ?
    ret = (void *)Li__Infix_4343(s_nth_i(0)); // .is_args = true
    s_popn(2);
    s_push(ret);
    break; // ++
  case BUILTIN_Lclone:
    ret = (void *)Lclone(s_nth_i(0)); // .is_args = true;
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lhash:
    ret = (void *)Lhash(*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_LtagHash:
    ret = (void *)LtagHash((char *)*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lcompare:
    ret = (void *)Lcompare(*s_nth(1), *s_nth(0));
    s_popn(2);
    s_push(ret);
    break;
  case BUILTIN_LflatCompare:
    ret = (void *)LflatCompare(*s_nth(1), *s_nth(0));
    s_popn(2);
    s_push(ret);
    break;
  case BUILTIN_Lfst:
    ret = (void *)Lfst(*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lsnd:
    ret = (void *)Lsnd(*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lhd:
    ret = (void *)Lhd(*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Ltl:
    ret = (void *)Ltl(*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_LreadLine:
    ret = (void *)LreadLine();
    s_push(ret);
    break;
  case BUILTIN_Lprintf:
    // NOTE: basic params: .args_count = 1, .is_vararg = true
    call_anyarg_func<20, false>((void (*)()) & Lprintf, args_count);
    break;
  case BUILTIN_Lfopen:
    ret = (void *)Lfopen((char *)*s_nth(1), (char *)*s_nth(0));
    s_popn(2);
    s_push(ret);
    break;
  case BUILTIN_Lfclose:
    /*ret = (void *)*/ Lfclose((FILE *)*s_nth(0));
    s_popn(1);
    s_push(0); // NOTE: UB on use
    break;
  case BUILTIN_Lfread:
    ret = (void *)Lfread((char *)*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lfwrite:
    /*ret = (void *)*/ Lfwrite((char *)*s_nth(1), (char *)*s_nth(0));
    s_popn(2);
    s_push(0); // NOTE: UB on use
    break;
  case BUILTIN_Lfexists:
    ret = (void *)Lfexists((char *)*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lfprintf:
    // NOTE: basic params: .args_count = 2, .is_vararg = true
    call_anyarg_func<20, false>((void (*)()) & Lfprintf, args_count);
    break;
  case BUILTIN_Lregexp:
    ret = (void *)Lregexp((char *)*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_LregexpMatch:
    ret = (void *)LregexpMatch((struct re_pattern_buffer *)*s_nth(2),
                               (char *)*s_nth(1), *s_nth_i(0));
    s_popn(2);
    s_push(ret);
    break;
  case BUILTIN_Lfailure:
    // NOTE: basic params: .args_count = 1, .is_vararg = true
    call_anyarg_func<20, false>((void (*)()) & Lfailure, args_count);
    break;
  case BUILTIN_Lsystem:
    ret = (void *)Lsystem((char *)*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_LgetEnv:
    ret = (void *)LgetEnv((char *)*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Lrandom:
    ret = (void *)Lrandom(*s_nth_i(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_Ltime:
    ret = (void *)Ltime();
    s_push(ret);
    break;
  case BUILTIN_LkindOf:
    ret = (void *)LkindOf(*s_nth(0));
    s_popn(1);
    s_push(ret);
    break;
  case BUILTIN_LcompareTags:
    ret = (void *)LcompareTags((char *)*s_nth(1), (char *)*s_nth(0));
    s_popn(2);
    s_push(ret);
    break;
  case BUILTIN_Ls__Infix_58:
    s_rotate_n(2);                                   // NOTE: is fix ?
    ret = (void *)Ls__Infix_58((void **)s_nth_i(0)); // .is_args = true
    s_popn(2);
    s_push(ret);
    break;
#define BUILTIN_CASE(name)                                                     \
  case BUILTIN_##name:                                                         \
    ret = (void *)name((char *)*s_nth(1), (char *)*s_nth(0));                  \
    s_popn(2);                                                                 \
    s_push(ret);                                                               \
    break;
    FORALL_BINOP_FUNC(BUILTIN_CASE)
#undef BUILTIN_CASE
  default:
    failure("RUNTIME ERROR: stdlib function <%u> not found\n", id);
    break;
  }
}
