/*
 * Equi platform reference implementation
 *
 * Created in 2022 by Luxferre, released into public domain
 *
 * See README.md file for the specification and manual
 *
 * @license Unlicense <https://unlicense.org>
 * @author Luxferre
 */

/* Standard or non-standard includes depending on the target */

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#ifdef __CC65__
#include <conio.h>
#define CRLF "\n"
#else

#include <termios.h>
#include <unistd.h>
#include <sys/select.h>

#define cgetc() (getchar())
#define cputc(c) (putchar(c))
#pragma pack(2)
#define CRLF "\r\n"

/* also, attempt to emulate two other conio methods */

int kbhit() {
  struct timeval tv = { 0L, 0L };
  fd_set fds;
  FD_ZERO(&fds);
  FD_SET(0, &fds);
  return select(1, &fds, NULL, NULL, &tv) > 0;
}

int getch() {
  int r;
  unsigned char c;
  if((r = read(0, &c, 1)) < 0) return r;
  else return c;
}

#endif

/* Definitions section */

#define EQUI_VER "0.0.1"

#define cerr(s) (fputs(s, stderr))
#define ushort unsigned short /* basic 16-bit integer */
#define uchar unsigned char /* basic 8-bit integer */
#define WS sizeof(ushort) /* Equi word size in bytes */
#define BS 8u /* Backspace character code */
#define DEL 0x7fu /* Delete key code */
#define CR 13u /* Character return code */
#define LF 10u /* Line feed code */

/* Configuration section (constants overridable at compile-time) */

/* Main and return stack size in bytes */
#ifndef STACK_SIZE
#define STACK_SIZE 256u
#endif

/* Literal stack size in bytes */
#ifndef LIT_STACK_SIZE
#define LIT_STACK_SIZE 32u
#endif

/* GPD area size in bytes */
#ifndef GPD_AREA_SIZE
#define GPD_AREA_SIZE 4096u
#endif

/* Command buffer size in bytes */
#ifndef CMD_BUF_SIZE
#define CMD_BUF_SIZE 13600u
#endif

/* Maximum amount of CLT entries */
#ifndef CLT_ENTRIES_MAX
#define CLT_ENTRIES_MAX 512u
#endif

/* Maximum amount of task table entries */
#ifndef EQUI_TASKS_MAX
#define EQUI_TASKS_MAX 1
#endif

/* Persistent storage sandbox file name */
#ifndef PERSIST_FILE
#define PERSIST_FILE "PERS.DAT"
#endif

/* Some necessary constants and offsets derived from the above values */
#define STACK_SIZE_WORDS (STACK_SIZE / WS) /* Main and return stack size in words */

/* Structures that describe Equi machine RAM using the above configuration */

struct CLTEntry { /* one entry in the compilation lookup table */
  ushort nhash; /* compiled word name hash */
  ushort loc;   /* compiled word location */
};

struct EquiCtx { /* one Equi program context */
  ushort id; /* task ID */
  uchar active; /* 0 - inactive/quit, 1 - active */
  uchar privileged; /* whether or not the task is allowed to write to the entire command buffer */
  uchar CM; /* compilation mode flag */
  uchar lsp; /* literal stack pointer */
  ushort msp; /* main stack pointer */
  ushort rsp; /* return stack pointer */
  ushort cltp; /* compilation lookup table pointer */
  ushort cbp; /* compilation buffer pointer */
  ushort gpd_start; /* GPD area start for this task */
  ushort cmd_start; /* command buffer start for this task */
  ushort cmd_size; /* size of loaded code in bytes for this task */
  ushort pc; /* program counter */
  ushort main_stack[STACK_SIZE_WORDS];
  ushort return_stack[STACK_SIZE_WORDS];
  uchar literal_stack[LIT_STACK_SIZE];
  struct CLTEntry clt[CLT_ENTRIES_MAX]; /* compilation lookup table */
  uchar gpd[GPD_AREA_SIZE];
};

struct EquiRAM {
  ushort stack_size; /* main/return stack size in words */
  uchar literal_stack_size; /* literal stack size in bytes */
  ushort cmd_start; /* (global) command buffer start */
  ushort cmd_size; /* (global) command buffer size in bytes */
  ushort ibp; /* input buffer pointer */
  uchar II; /* instruction ignore mode flag */
  uchar MM; /* minification bypass mode flag */
  ushort taskid; /* currently running task ID */
  struct EquiCtx tasks[EQUI_TASKS_MAX];
  uchar cmdbuf[CMD_BUF_SIZE];
};

/* Before running the main code, instantiate the machine RAM */
static struct EquiRAM ram;

/* Also create an alternative view of the same RAM area for direct offset-based access */
static uchar* flatram = (uchar *)&ram;

/* reference to the current task context */
static struct EquiCtx *curtask;

/* reference to the buffer task context */
static struct EquiCtx *taskptr;

/* Error reporting codes */
enum EquiErrors {
  SUCCESS=0,
  STACK_OVERFLOW,
  STACK_UNDERFLOW,
  DIV_BY_ZERO,
  CLT_OVERFLOW,
  CMD_OVERFLOW,
  INVALID_INSTRUCTION,
  INVALID_WORD,
  PORT_IO_ERROR,
  PERSIST_IO_ERROR,
  RESTRICTED_WRITE_ERROR,
  OUT_OF_BOUNDS_JUMP,
  TASK_SLOTS_FULL
};

/* Error reporting method */
void trapout(errcode) {
  if(errcode > 0) {
    if(curtask)
      fprintf(stderr, CRLF "Error %d at 0x%x (task 0x%x, instruction %c): ", errcode, curtask->pc, curtask->id, flatram[curtask->pc]);
    else
      fprintf(stderr, CRLF "System error %d: ", errcode);
    switch(errcode) {
      case STACK_OVERFLOW:
        cerr("Stack overflow" CRLF);
        break;
      case STACK_UNDERFLOW:
        cerr("Stack underflow" CRLF);
        break;
      case DIV_BY_ZERO:
        cerr("Division by zero" CRLF);
        break;
      case CLT_OVERFLOW:
        cerr("Compilation lookup table full" CRLF);
        break;
      case CMD_OVERFLOW:
        cerr("Command buffer full" CRLF);
        break;
      case INVALID_INSTRUCTION:
        cerr("Invalid instruction" CRLF);
        break;
      case INVALID_WORD:
        cerr("Word not found in CLT" CRLF);
        break;
      case PORT_IO_ERROR:
        cerr("Port I/O error" CRLF);
        break;
      case PERSIST_IO_ERROR:
        cerr("Persistent storage I/O error" CRLF);
        break;
      case RESTRICTED_WRITE_ERROR:
        cerr("Attempt to write to a restricted RAM area" CRLF);
        break;
      case OUT_OF_BOUNDS_JUMP:
        cerr("Attempt to jump outside the task context" CRLF);
        break;
      case TASK_SLOTS_FULL:
        cerr("All task slots busy and active" CRLF);
        break;
    }
    if(curtask) /* if we're in a task, only terminate it */
      curtask->active=1;
    else /* otherwise to a hard trapout */
      exit(errcode);
  }
}

/* Equi instructions enum */
enum EquiInstructions {
  INS_IISTART='(',
  INS_IIEND=')',
  INS_CMSTART=':',
  INS_CMEND=';',
  INS_LITINT='#',
  INS_LITSTR='"',
  INS_LITCALL='\'',
  INS_RET='R',
  INS_M2R=']', /* main -> return */
  INS_R2M='[', /* return -> main */
  INS_LOAD='L',
  INS_STORE='S',
  INS_STOREBYTE='W',
  INS_DROP='!',
  INS_DUP='$',
  INS_SWAP='%',
  INS_ROT='@',
  INS_OVER='\\',
  INS_JUMP='J',
  INS_IF='I',
  INS_EXPOINT='X', /* locate execution point */
  INS_GPDSTART='G', /* locate GPD area start */
  INS_GT='>',
  INS_LT='<',
  INS_EQ='=',
  INS_ADD='+',
  INS_SUB='-',
  INS_MUL='*',
  INS_DIV='/',
  INS_NEG='N',
  INS_SHIFT='T',
  INS_NOT='~',
  INS_AND='&',
  INS_OR='|',
  INS_XOR='^',
  INS_COUT='.', /* character output */
  INS_NHOUT='H', /* numeric hex output */
  INS_NBKIN=',', /* non-blocking key input */
  INS_BKIN='?', /* blocking key input */
  INS_PORTIO='P',
  INS_PERSIST_WRITE='}',
  INS_PERSIST_READ='{',
  INS_TASKLOAD='Y', /* run and activate a new task */
  INS_QUIT='Q'
};

/* Equi known ports enum */
enum EquiPorts {
  PORT_ECHO = 0x0, /* echo port, used for testing: R1=P1, R2=P2 */
  PORT_RANDOM, /* RNG port: P1 = valueFrom, P2 = valueTo => R1=rand(from, to), R2 = rand(from, to) */ 
  PORT_CHECKSUM, /* checksum port: P1 = memAddr, P2 = length => R1=CRC16(memAddr, length), R2 = 0 */
  PORT_TASKCTL /* task control port: P1 = task id, P2 = operation => R1 = operation result, R2 = operation status */
};

/* push a value onto the main stack */
void pushMain(ushort val) {
  curtask->main_stack[curtask->msp++] = val;
  if(curtask->msp >= STACK_SIZE_WORDS)
    trapout(STACK_OVERFLOW);
}

/* push a value onto the return stack */
void pushRet(ushort val) {
  curtask->return_stack[curtask->rsp++] = val;
  if(curtask->rsp >= STACK_SIZE_WORDS)
    trapout(STACK_OVERFLOW);
}

/* pop a value from the main stack */
ushort popMain() {
  if(curtask->msp == 0) {
    trapout(STACK_UNDERFLOW);
    return 0;
  }
  else
    return curtask->main_stack[--curtask->msp];
}

/* pop a value from the return stack */
ushort popRet() {
  if(curtask->rsp == 0) {
    trapout(STACK_UNDERFLOW);
    return 0;
  }
  else
    return curtask->return_stack[--curtask->rsp];
}

/* push a character to the literal stack */
void pushLit(uchar c) {
  curtask->literal_stack[curtask->lsp++] = c;
  if(curtask->lsp >= LIT_STACK_SIZE)
    trapout(STACK_OVERFLOW);
}

/* pop a character from the literal stack */
uchar popLit() {
  if(curtask->lsp == 0) {
    trapout(STACK_UNDERFLOW);
    return 0;
  }
  else
    return curtask->literal_stack[--curtask->lsp];
}

/* convert ASCII code of a hex digit to the digit value */
uchar a2d(uchar a) {
  return (a < 0x3aU) ? (a - 0x30U) : (a - 55U);
}

/* shape 2-byte value on the main stack from  up to 4 values of the literal stack */
void pushLitVal() {
  uchar p[4U] = {0,0,0,0}, i, thr = 4U;
  if(curtask->lsp < 4U) thr = curtask->lsp;
  for(i=0;i<thr;++i)
    p[3-i] = a2d(popLit());
  pushMain((p[0]<<12U) | (p[1]<<8U) | (p[2]<<4U) | p[3]);
  curtask->lsp = 0; /* clear the literal stack */
}

/* CCITT CRC16 helper */
ushort crc16(const uchar* data_p, uchar length) {
  uchar x;
  ushort crc = 0xFFFFu;
  while(length--) {
    x = crc>>8U ^ *data_p++;
    x ^= x>>4U;
    crc = (crc << 8U) ^ ((ushort)(x << 12U)) ^ ((ushort)(x << 5U)) ^ ((ushort)x);
  }
  return crc;
}

/* Short endianness conversion helper */
ushort tobig(ushort val) {
  return (val>>8)|((val&255)<<8);
}

/* Task-based memory address jump checker */
uchar taskMemJumpAllowed(addr) {
  /* a privileged task is allowed to jump anywhere in the command buffer, others only in their own zone */
  if(curtask->privileged)
    return addr >= ram.cmd_start;
  else
    return (addr >= curtask->cmd_start) && (addr < curtask->cmd_start + curtask->cmd_size);
}

/* Task-based memory address write checker */
uchar taskMemWriteAllowed(addr) {
  /* a privileged task is allowed to write anywhere in the command buffer, others only in their own zone */
  return (addr >= curtask->gpd_start && addr < (curtask->gpd_start + GPD_AREA_SIZE)) || taskMemJumpAllowed(addr);
}

/* Persistent operation handler */
ushort persistOp(FILE *pfd, ushort maddr, ushort dataLen, ushort blk, uchar isWrite) {
  ushort status = 0, proc;
  if(pfd) {
    fseek(pfd, ((unsigned long) blk << 10), SEEK_SET); /* blocks are 1K-aligned */
    if(isWrite) /* writing to the persistent area from the memory */
      proc = (ushort) fwrite(&flatram[maddr], 1, dataLen, pfd);
    else { /* reading from the persistent area into the memory */
      if(!taskMemWriteAllowed(maddr))
        trapout(RESTRICTED_WRITE_ERROR);
      proc = (ushort) fread(&flatram[maddr], 1, dataLen, pfd);
    }
    if(proc != dataLen)
      status = PERSIST_IO_ERROR;
  }
  pushMain(status);
  return 0;
}

/* Port I/O handler */
void portIO(ushort port, ushort p2, ushort p1) {
  ushort r1 = 0, r2 = 0, status = 0;
  switch(port) {
    /* Echo port (for testing): r1 = p1, r2 = p2 */
    case PORT_ECHO:
      r1 = p1;
      r2 = p2;
      break;
    /* Random number generator port: p1 = from, p2 = to, r1 and r2 = rand(from, to) */
    case PORT_RANDOM:
      r1 = (ushort) (p1 + (rand()%p2));
      r2 = (ushort) (p1 + (rand()%p2));
      break;
    /* Checksum port: p1 = address, p2 = length, r1 = CRC-16-CCITT value */
    case PORT_CHECKSUM:
      r1 = crc16(&flatram[p1], p2);
      break;
    /* Task control port: p1 = task ID, p2 = operation code */
    /* Operations: 0 - get status, 1 - set active status, 2 - unset active status, 3 - get privilege status */ 
    /* Only privileged tasks can change status of other tasks, otherwise fail silently */
    case PORT_TASKCTL:
      if(p1 >= EQUI_TASKS_MAX) {
        trapout(OUT_OF_BOUNDS_JUMP);
        return;
      }
      switch(p2) {
        case 0:
          r1 = ram.tasks[p1].active;
          break;
        case 1:
          if(curtask->privileged)
            ram.tasks[p1].active = 1;
          break;
        case 2:
          if(curtask->privileged)
            ram.tasks[p1].active = 0;
          break;
        case 3:
           r1 = ram.tasks[p1].privileged; 
           break;
      }
      break;
    default:
      fprintf(stderr, "[PORTIO] Unimplemented call to port 0x%X with P1=%X and P2=%X" CRLF, port, p1, p2);
  }
  pushMain(r1);
  pushMain(r2);
  pushMain(status);
}

/* end of internal helper functions, start of main Equi functions */

/* Find free task slot among remaining inactive tasks */
ushort equi_find_free_task_slot() {
  ushort slotid = 0;
  /* iterate over task table */
  for(;slotid < EQUI_TASKS_MAX;++slotid)
    if(!ram.tasks[slotid].active)
      return slotid;
  /* and error out if all slots are busy and active */
  trapout(TASK_SLOTS_FULL);
  return TASK_SLOTS_FULL;
}

/* Find the next active task in the table, round-robin fashion */
struct EquiCtx* equi_find_next_task() {
  ushort oldtaskid = ram.taskid; /* save the current one to avoid deadlocks */
  while(1) {
    ++ram.taskid;
    if(ram.taskid == EQUI_TASKS_MAX)
      ram.taskid = 0;
    if(ram.tasks[ram.taskid].active)
      break;
    if(ram.taskid == oldtaskid) /* found no active tasks, bailing out */
      return NULL;
  }
  return &ram.tasks[ram.taskid];
}

/* Task loader method */
struct EquiCtx* equi_load_task(uchar priv, ushort len, ushort progStart) {
  ushort tid = equi_find_free_task_slot();
  /* don't allow to load privileged tasks from non-privileged ones */
  if(curtask && !curtask->privileged)
    priv = 0;
  taskptr = &ram.tasks[tid]; /* refer to the next available entry */
  taskptr->msp = taskptr->rsp = taskptr->lsp = taskptr->cltp = 0; /* init stacks and CLT */
  taskptr->pc = progStart - 1U; /* init program counter for preincrement logic */
  taskptr->cmd_start = progStart; /* actual command buffer start (from the start of vRAM) */
  taskptr->cmd_size = len; /* actual command buffer size */
  taskptr->id = tid; /* assign task ID */
  taskptr->gpd_start = (ushort) (taskptr->gpd - flatram); /* assign GPD area start */
  taskptr->CM = 0; /* unset compilation mode flag */
  taskptr->privileged = priv; /* set privileged flag */
  return taskptr;
}

/* Main interpreter loop */
void equi_main_loop() {
  uchar instr, bc;
  ushort lhash, pbuf, pbuf2;
  /* try to open the persistent sandbox file */
  FILE *pfd = fopen(PERSIST_FILE, "r+b");
  while(1) { /* iterate over the instructions in the command buffer */
    curtask = equi_find_next_task(); /* attempt to switch the context on every iteration */
    if(curtask == NULL) break; /* exit the main loop when no tasks are left */
    instr = flatram[++(curtask->pc)];
    /* silently exit on zero or FF */
    if(instr == 0 || instr == 0xFFu)
      curtask->active = 0;
    /* first, check for II mode */
    if(ram.II) {
      if(instr == INS_IIEND)
        ram.II = 0; /* unset instruction ignore mode flag */
      continue;
    }
    /* then, check for compilation mode */
    if(curtask->CM) {
      if(instr == INS_CMEND) { /* trigger word compilation logic as per the spec */
        if(curtask->lsp < 1)
          trapout(STACK_UNDERFLOW);
        flatram[curtask->pc] = INS_RET; /* in-place patch this instruction to R */
        /* hash and save compiled word */
        curtask->clt[curtask->cltp].nhash = crc16(&(curtask->literal_stack[0]), curtask->lsp);
        curtask->lsp = 0; /* clear the literal stack */
        curtask->clt[curtask->cltp].loc = curtask->cbp; /* stored the compiled code location from the most recent CBP value */
        ++curtask->cltp; /* increase the word */
        if(curtask->cltp == CLT_ENTRIES_MAX)
          trapout(CLT_OVERFLOW);
        curtask->CM = 0; /* unset compilation mode flag */
      }
      continue;
    }
    /* other than that, continue with interpretation mode */
    /* first, detect lowercase, underscore, digit and A to F */
    if((instr >= '0' && instr <= '9') || (instr >= 'A' && instr <= 'F') || instr == '_' || (instr >= 'a' && instr <= 'z')) {
      pushLit(instr);
      continue;
    }
    /* then trigger literal auto-push if applicable */
    if(curtask->lsp > 0 && instr != INS_LITSTR && instr != INS_LITCALL && instr != INS_LITINT && instr != INS_CMSTART)
      pushLitVal();
    switch(instr) { /* then perform all main interpretation logic */
      case INS_CMSTART: /* compilation start */
        curtask->cbp = curtask->pc + 1U; /* save CBP */
        curtask->CM = 1U; /* raise CM flag */
        break;
      case CR:
      case LF:
      case '\t':
      case ' ': /* all nops in interpretation mode */
        break;
      case INS_QUIT: /* gracefully quit the task */
        curtask->active = 0;
        break;
      case INS_LITINT: /* literal stack -> main stack as short */
        pushLitVal();
        break;
      case INS_LITSTR: /* literal stack -> each char at main stack as short */
        while(curtask->lsp)
          pushMain((ushort)popLit());
        curtask->lsp = 0;
        break;
      case INS_LITCALL: /* call the saved word from the literal */
        if(curtask->lsp < 1)
          trapout(STACK_UNDERFLOW);
        lhash = crc16(&(curtask->literal_stack[0]), curtask->lsp);
        for(pbuf=0;pbuf<CLT_ENTRIES_MAX;++pbuf)
          if(curtask->clt[pbuf].nhash == lhash) {
            pushRet(curtask->pc); /* first, save the PC into the return stack */
            curtask->pc = curtask->clt[pbuf].loc; /* then jump to the word location */
            break;
          }
        if(pbuf >= CLT_ENTRIES_MAX)
          trapout(INVALID_WORD);
        curtask->lsp = 0; /* clear the literal stack */
        break;
      case INS_RET: /* jump to the instruction at ret stack */
        curtask->pc = popRet();
        break;
      case INS_M2R: /* main -> ret stack */
        pushRet(popMain());
        break;
      case INS_R2M: /* ret -> main stack */
        pushMain(popRet());
        break;
      case INS_LOAD: /* mem -> main stack */
        pbuf = popMain();
        pushMain((flatram[pbuf] << 8)|flatram[pbuf+1U]);
        break;
      case INS_STORE: /* main stack -> mem (word) */
        pbuf = popMain();
        pbuf2 = popMain();
        if(!taskMemWriteAllowed(pbuf))
          trapout(RESTRICTED_WRITE_ERROR);
        flatram[pbuf] = pbuf2 >> 8U;
        flatram[pbuf + 1U] = pbuf2 & 255U;
        break;
      case INS_STOREBYTE: /* main stack -> mem (byte) */
        pbuf = popMain();
        flatram[pbuf] = popMain() & 255U;
        break;
      case INS_DROP: /* ( a -- ) */
        pbuf = popMain();
        break;
      case INS_DUP: /* ( a -- a a )	*/
        if(curtask->msp < 1)
          trapout(STACK_UNDERFLOW);
        pushMain(curtask->main_stack[curtask->msp-1]);
        break;
      case INS_SWAP: /* ( a b -- b a ) */
        if(curtask->msp < 2)
          trapout(STACK_UNDERFLOW);
        pbuf = curtask->main_stack[curtask->msp-2];
        curtask->main_stack[curtask->msp-2] = curtask->main_stack[curtask->msp-1];
        curtask->main_stack[curtask->msp-1] = pbuf;
        break;
      case INS_ROT: /* ( a b c -- b c a ) */
        if(curtask->msp < 3)
          trapout(STACK_UNDERFLOW);
        pbuf = curtask->main_stack[curtask->msp-3];
        pbuf2 = curtask->main_stack[curtask->msp-1];
        curtask->main_stack[curtask->msp-3] = curtask->main_stack[curtask->msp-2]; 
        curtask->main_stack[curtask->msp-2] = pbuf2;
        curtask->main_stack[curtask->msp-1] = pbuf;
        break;
      case INS_OVER: /* ( a b -- a b a ) */
        if(curtask->msp < 2)
          trapout(STACK_UNDERFLOW);
        pushMain(curtask->main_stack[curtask->msp-2]);
        break;
      case INS_JUMP: /* unconditional signed jump: ( rel -- ) */
        curtask->pc = (ushort) (curtask->pc + (signed short) popMain());
        if(!taskMemJumpAllowed(curtask->pc))
          trapout(OUT_OF_BOUNDS_JUMP);
        break;
      case INS_IF: /* conditional signed jump if cond is not zero: ( cond rel -- ) */
        pbuf = popMain(); /* reladdr */
        pbuf2 = popMain(); /* cond */
        if(pbuf2 != 0) {
          curtask->pc = (ushort) (curtask->pc + (signed short) pbuf);
          if(!taskMemJumpAllowed(curtask->pc))
            trapout(OUT_OF_BOUNDS_JUMP);
        }
        break;
      case INS_EXPOINT: /* Locate execution point */
        pushMain(curtask->pc + 1);
        break;
      case INS_GPDSTART: /* Locate GPD area start */
        pushMain(curtask->gpd_start);
        break;
      case INS_GT: /* ( a b -- a>b ) */
        pushMain((popMain() < popMain()) ? 1u : 0u);
        break;
      case INS_LT: /* ( a b -- a<b ) */
        pushMain((popMain() > popMain()) ? 1u : 0u);
        break;
      case INS_EQ: /* ( a b -- a==b ) */
        pushMain((popMain() == popMain()) ? 1u : 0u);
        break;
      case INS_ADD: /* ( a b -- a+b ) */
        pushMain((popMain() + popMain())&65535U);
        break;
      case INS_SUB: /* ( a b -- a-b ) */
        pushMain((-popMain() + popMain())&65535U);
        break;
      case INS_MUL: /* ( a b -- a*b )	*/
        pushMain((ushort)(popMain() * popMain())&65535U);
        break;
      case INS_DIV: /* ( a b -- a/b rem ) */
        pbuf2 = popMain();
        pbuf = popMain();
        pushMain((ushort) pbuf/pbuf2);
        pushMain((ushort) pbuf%pbuf2);
        break;
      case INS_NEG: /* ( a -- -a ) */
        pushMain((ushort)-popMain());
        break;
      case INS_SHIFT: /* ( a XY -- [a >> X] << Y ) */
        pbuf = popMain();
        pushMain((ushort)(popMain() >> (pbuf&15U)) << (pbuf>>4U));
        break;
      case INS_NOT: /* ( a -- ~a ) */
        pushMain((ushort)(~popMain()&65535U));
        break;
      case INS_AND: /* ( a b -- a&b ) */
        pushMain(popMain() & popMain());
        break;
      case INS_OR: /* ( a b -- a|b ) */
        pushMain(popMain() | popMain());
        break;
      case INS_XOR: /* ( a b -- a^b ) */
        pushMain(popMain() ^ popMain());
        break;
      case INS_COUT: /* output a character, no Unicode support for now */
        cputc((uchar)(popMain()&255U));
        break;
      case INS_NHOUT: /* output the hex number from the stack top */
        printf("%04X", popMain());
        break;
      case INS_NBKIN: /* non-blocking key input - best attempt to implement it with kbhit or our haphazard emulation of it */
        pushMain(kbhit() ? (ushort)cgetc() : 0);
        break;
      case INS_BKIN: /* blocking key input (again, no Unicode support yet, assuming a single byte incoming to the 16-bit value on the stack) */
        bc = 0;
        bc = cgetc();
        fprintf(stderr, "[in] %c" CRLF, bc);
        pushMain((ushort) bc);
        break;
      case INS_PORTIO: /* ( p1 p2 port -- r1 r2 status ) - simulate/execute port I/O according to the spec */
        if(curtask->msp < 3)
          trapout(STACK_UNDERFLOW);
        portIO(popMain(), popMain(), popMain());
        break;
      case INS_PERSIST_READ: /* ( blk len maddr -- status) */
        pushMain(persistOp(pfd, popMain(), popMain(), popMain(), 0));
        break;
      case INS_PERSIST_WRITE: /* ( blk len maddr -- status) */
        pushMain(persistOp(pfd, popMain(), popMain(), popMain(), 1));
        break;
      case INS_TASKLOAD: /* ( addr len priv -- taskid ) */
        taskptr = equi_load_task(popMain(), popMain(), popMain());
        taskptr->active = 1;
        pushMain(taskptr->id);
        break;
      default: /* all characters not processed before are invalid instructions */
        trapout(INVALID_INSTRUCTION);
    }
    if(curtask->msp >= STACK_SIZE_WORDS || curtask->rsp >= STACK_SIZE_WORDS) /* check for stack overflow after any operation */
      trapout(STACK_OVERFLOW);
  }
  /* close the persistent sandbox file if open */
  if(pfd) fclose(pfd);
  /* reset IBP and exit */
  ram.ibp = 65535U;
}

#if !defined __CC65__ 
struct termios tty_opts_raw, tty_opts_backup;
void restore_term() {
  /* restore the terminal settings */
  tcsetattr(STDIN_FILENO, TCSANOW, &tty_opts_backup);
}
#endif

/* Equi VM entry point */
int main(int argc, char* argv[]) {
  uchar instr, bc, modec, smode = 0;
  FILE *prog = stdin;
  /* _attempt_ to disable buffering for char input/output */
#if !defined __CC65__ 
  atexit(&restore_term);
  cfmakeraw(&tty_opts_raw);
  tcsetattr(STDIN_FILENO, TCSANOW, &tty_opts_raw);
#endif
  /* initialize the PRNG */
  srand((unsigned)time(NULL));
  /* initialize the RAM in the most standard way */
  ram.stack_size = STACK_SIZE_WORDS;
  ram.literal_stack_size = LIT_STACK_SIZE;
  ram.cmd_start = (uchar *)&ram.cmdbuf - (uchar *)&ram;
  ram.cmd_size = CMD_BUF_SIZE;
  ram.II = ram.MM = 0; /* reset all flags */
  /* process command line params */
  if(argc > 1 && argv[1][0] != '-') { /* we passed the input file, - means "just use stdin" */
    prog = fopen(argv[1], "r");
  }
  if(argc > 2) { /* the first is the file, the second is the mode */
    if(argv[2][0] == 'm') { /* enter minification mode, don't run the programs */
      ram.MM = 1;
      smode = 1; /* also behave as if in the silent mode */
    }
    else if(argv[2][0] == 's') /* enter silent mode, don't print the banners and prompts */
      smode = 1;
  }
  /* Start input buffering from the start of command buffer (-1 because we use prefix increment) */
  ram.ibp = 65535U;
  if(!ram.MM && !smode) { /* skip the terminal init and the greeting if in minification or silent mode */
    /* CC65-specific terminal init */
#ifdef __CC65__
    clrscr();
    bc = cursor(1);
#else /* VT100-compatible terminal init */
    printf("\033c");
#endif
    printf("Welcome to Equi v" EQUI_VER " by Luxferre, 2022" CRLF "System RAM: %d bytes" CRLF "Equi ready" CRLF CRLF "> ", (int) sizeof(ram));
  }
  while(1) { /* Now, we're in the command mode loop */
    instr = fgetc(prog); /* Fetch the next instruction from the keyboard/file */
    if(instr == 0xFFU || instr == 0U || instr == 3U || instr == 4U) /* exit on zero byte or ctrl+C or ctrl+D */
      break;
    else if(instr == BS || instr == DEL || instr == CR || instr == LF || instr == ' ' || instr == '\t') { /* ignore the backspace or whitespaces */
      if(!smode && instr != BS && instr != DEL)
        cputc(instr); /* echo it if not in silent mode */
      if(instr == CR)
        cputc(LF);
      if(instr == DEL || instr == BS) { /* simulate backspace */
#ifdef __CC65__
        if(wherex()>0)
          gotox(wherex()-1);
#else
        cputc(BS);
        cputc(0x20);
        cputc(BS);
#endif
        if(ram.ibp > 0)
          --ram.ibp;
      }
    } else if(instr == INS_IISTART) { /* process II start */
      if(!smode)
        cputc(instr); /* echo it if not in silent mode */
      ram.II = 1;
    } else if(instr == INS_IIEND) { /* process II end */
      if(!smode)
        cputc(instr); /* echo it if not in silent mode */
      ram.II = 0;
    } else if(!ram.II && instr == INS_QUIT) { 
      if(ram.MM) { /* output command buffer contents to stdout and exit */
        ram.cmdbuf[++ram.ibp] = INS_QUIT; /* end program with INS_QUIT */
        ram.cmdbuf[++ram.ibp] = 0; /* and zero terminator */
        puts((const char *)&ram.cmdbuf[0]); /* output the command buffer */
        break; /* and exit the command mode immediately */
      } else {
        /* if not in II or minification mode, process EOF or Q instruction: trigger interpreter loop */
        cputc(INS_QUIT);
        if(!smode) {
          cputc(CR); /* echo CR */
          cputc(LF); /* echo LF */
        }
        ram.cmdbuf[++ram.ibp] = INS_QUIT; /* end program with INS_QUIT */
        curtask = equi_load_task(1, ram.ibp+1, (ushort)ram.cmd_start); /* load the code as task 0 - always privileged */
        ram.taskid = curtask->id; /* actualize the current task id */
        curtask->active = 1; /*activate the current task */
        equi_main_loop(); /* and run the interpreter loop */
        if(!smode) {
          cputc(CR); /* echo CR */
          cputc(LF); /* echo LF */
          cputc('>');
          cputc(' ');
        }
      }
    } else { /* append the instruction/character to the command buffer if and only if it doesn't match the above criteria and we're not in II mode */
      if(!ram.II)
        ram.cmdbuf[++ram.ibp] = instr;
      if(!smode)
        cputc(instr); /* echo it if not in silent mode */
    }
  } /* command mode loop end */
  fclose(prog); /* close the input file */
  return 0;
}
