Ctrl K
ComunidadGuías…

selfie.c

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
Copyright (c) the Selfie Project authors. All rights reserved.
Please see the AUTHORS file for details. Use of this source code is
governed by a BSD license that can be found in the LICENSE file.

Selfie is a project of the Computational Systems Group at the
Department of Computer Sciences of the University of Salzburg
in Austria. For further information please refer to:

selfie.cs.uni-salzburg.at

The Selfie Project provides an educational platform for teaching
undergraduate and graduate students the design and implementation
of programming languages and runtime systems. The focus is on the
construction of compilers, libraries, operating systems, and even
virtual machine monitors. The common theme is to identify and
resolve self-reference in systems code which is seen as the key
challenge when teaching systems engineering, hence the name.

Selfie is a self-contained 64-bit, 12KLOC C implementation of:

1. a self-compiling compiler called starc that compiles
   a tiny but still fast subset of C called C Star (C*) to
   a tiny and easy-to-teach subset of RISC-V called RISC-U,
2. a self-executing emulator called mipster that executes
   RISC-U code including itself when compiled with starc,
3. a self-hosting hypervisor called hypster that provides
   RISC-U virtual machines that can host all of selfie,
   that is, starc, mipster, and hypster itself, and
5. a tiny C* library called libcstar utilized by selfie.

Selfie is implemented in a single (!) file and kept minimal for simplicity.
There is also a simple in-memory linker, a RISC-U disassembler, a garbage
collector, a profiler, and a debugger with replay as well as minimal
operating system support in the form of RISC-V system calls built into
the emulator and hypervisor. The garbage collector is conservative and
may operate as library in the same address space as the mutator and
as part of the emulator in the address space of the kernel.

C* is a tiny Turing-complete subset of C that includes dereferencing
(the * operator) but excludes composite data types, bitwise and Boolean
operators, and many other features. There are only unsigned 64-bit
integers and 64-bit pointers as well as character and string literals.
This choice turns out to be helpful for students to understand the
true role of composite data types such as arrays and records.
Bitwise operations are implemented in libcstar using unsigned integer
arithmetics helping students better understand arithmetic operators.
C* is supposed to be close to the minimum necessary for implementing
a self-compiling, single-pass, recursive-descent compiler. C* can be
taught in one to two weeks of classes depending on student background.

The compiler can readily be extended to compile features missing in C*
and to improve performance of the generated code. The compiler generates
RISC-U executables in ELF format that are compatible with the official
RISC-V toolchain. The mipster emulator can execute RISC-U executables
loaded from file but also from memory immediately after code generation
without going through the file system.

RISC-U is a tiny Turing-complete subset of the RISC-V instruction set.
It only features unsigned 64-bit integer arithmetic, double-word memory,
and simple control-flow instructions but neither bitwise nor byte- and
word-level instructions. RISC-U can be taught in one week of classes.

The emulator implements minimal operating system support that is meant
to be extended by students, first as part of the emulator, and then
ported to run on top of it, similar to an actual operating system or
virtual machine monitor. The fact that the emulator can execute itself
helps exposing the self-referential nature of that challenge. In fact,
selfie goes one step further by implementing microkernel functionality
as part of the emulator and a hypervisor that can run as part of the
emulator as well as on top of it, all with the same code.

Selfie is the result of many years of teaching systems engineering.
The design of the compiler is inspired by the Oberon compiler of
Professor Niklaus Wirth from ETH Zurich. RISC-U is inspired by the
RISC-V community around Professor David Patterson from UC Berkeley.
The design of the hypervisor is inspired by microkernels of Professor
Jochen Liedtke from University of Karlsruhe. The garbage collector
is inspired by the conservative garbage collector of Hans Boehm.

All of selfie including its source code is available at:

github.com/cksystemsteaching/selfie

*/

// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~
// -----------------------------------------------------------------
// ---------------------     L I B R A R Y     ---------------------
// -----------------------------------------------------------------
// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~

// -----------------------------------------------------------------
// ----------------------- BUILTIN PROCEDURES ----------------------
// -----------------------------------------------------------------

// selfie bootstraps int to uint64_t!
void exit(int code);

uint64_t read(uint64_t fd, uint64_t* buffer, uint64_t bytes_to_read);
uint64_t write(uint64_t fd, uint64_t* buffer, uint64_t bytes_to_write);

// selfie bootstraps char to uint64_t and ignores ellipsis!
uint64_t open(char* filename, uint64_t flags, ...);

// selfie bootstraps void* to uint64_t* and unsigned to uint64_t!
void* malloc(unsigned long);

// selfie bootstraps the following *printf procedures
int printf(const char* format, ...);
int sprintf(char* str, const char* format, ...);
int dprintf(int fd, const char* format, ...);

// -----------------------------------------------------------------
// ----------------------- LIBRARY PROCEDURES ----------------------
// -----------------------------------------------------------------

uint64_t is_true(uint64_t a);
uint64_t not(uint64_t a);
uint64_t and(uint64_t a, uint64_t b);
uint64_t or(uint64_t a, uint64_t b);
uint64_t xor(uint64_t a, uint64_t b);

uint64_t two_to_the_power_of(uint64_t p);
uint64_t log_two(uint64_t n);

uint64_t ten_to_the_power_of(uint64_t p);
uint64_t log_ten(uint64_t n);

uint64_t left_shift(uint64_t n, uint64_t b);
uint64_t right_shift(uint64_t n, uint64_t b);

uint64_t get_bits(uint64_t n, uint64_t i, uint64_t b);

uint64_t absolute(uint64_t n);
uint64_t max(uint64_t a, uint64_t b);

uint64_t signed_less_than(uint64_t a, uint64_t b);
uint64_t signed_division(uint64_t a, uint64_t b);

uint64_t is_signed_integer(uint64_t n, uint64_t b);
uint64_t sign_extend(uint64_t n, uint64_t b);
uint64_t sign_shrink(uint64_t n, uint64_t b);

char  load_character(char* s, uint64_t i);
char* store_character(char* s, uint64_t i, char c);

uint64_t is_letter(char c);
uint64_t is_digit(char c);

char*    string_alloc(uint64_t l);
uint64_t string_length(char* s);
char*    string_copy(char* s);
void     string_reverse(char* s);
uint64_t string_compare(char* s, char* t);

uint64_t atoi(char* s);
char*    itoa(uint64_t n, char* s, uint64_t b, uint64_t d, uint64_t a);

uint64_t fixed_point_ratio(uint64_t a, uint64_t b, uint64_t f);
uint64_t fixed_point_percentage(uint64_t a, uint64_t b, uint64_t f);

uint64_t fixed_point_integral(uint64_t a, uint64_t f);
uint64_t fixed_point_fractional(uint64_t a, uint64_t f);

uint64_t ratio_format_integral_2(uint64_t a, uint64_t b);
uint64_t ratio_format_fractional_2(uint64_t a, uint64_t b);

uint64_t percentage_format_integral_2(uint64_t a, uint64_t b);
uint64_t percentage_format_fractional_2(uint64_t a, uint64_t b);

uint64_t write_to_printf(uint64_t fd, uint64_t* buffer, uint64_t bytes_to_write);

void put_character(char c);

void print(char* s);
void println();

void print_character(char c);
void print_string(char* s);
void print_unsigned_integer(uint64_t n);
void print_integer(uint64_t n);
void print_fractional(uint64_t n, uint64_t p);
void unprint_integer(uint64_t n);
void print_hexadecimal_no_prefix(uint64_t n, uint64_t a);
void print_hexadecimal(uint64_t n, uint64_t a);
void print_octal_no_prefix(uint64_t n, uint64_t a);
void print_octal(uint64_t n, uint64_t a);
void print_binary_no_prefix(uint64_t n, uint64_t a);
void print_binary(uint64_t n, uint64_t a);

// printf

uint64_t print_format(char* s, uint64_t i, char* a);

uint64_t vdsprintf(uint64_t fd, char* buffer, char* format, uint64_t* args);

// selfie implementation of *printf procedures
uint64_t non_0_boot_level_printf(char* format, ...);
uint64_t non_0_boot_level_sprintf(char* str, char* format, ...);
uint64_t non_0_boot_level_dprintf(uint64_t fd, char* format, ...);

uint64_t printf_or_write(uint64_t length);

// selfie's malloc interface

uint64_t round_up(uint64_t n, uint64_t m);

uint64_t* smalloc(uint64_t size); // use this to allocate memory, not malloc
uint64_t* smalloc_system(uint64_t size); // internal use only!

void zero_memory(uint64_t* memory, uint64_t size);

uint64_t* zalloc(uint64_t size);  // internal use only!
uint64_t* zmalloc(uint64_t size); // use this to allocate zeroed memory

void assert(uint64_t assert_expr, uint64_t lineno, const char* procedure_name);

// ------------------------ GLOBAL CONSTANTS -----------------------

char* SELFIE_URL = (char*) 0;

uint64_t IS64BITSYSTEM = 1; // flag indicating 64-bit selfie
uint64_t IS64BITTARGET = 1; // flag indicating 64-bit target

uint64_t ISRISCU = 1; // flag indicating presence of starc-generated RISC-U binary

uint64_t SIZEOFUINT64INBITS     = 64; // sizeof(uint64_t) * 8
uint64_t SIZEOFUINT64STARINBITS = 64; // sizeof(uint64_t*) * 8

uint64_t* power_of_two_table;

uint64_t UINT64_MAX; // maximum numerical value of an unsigned 64-bit integer

uint64_t INT64_MAX; // maximum numerical value of a signed 64-bit integer
uint64_t INT64_MIN; // minimum numerical value of a signed 64-bit integer

uint64_t SINGLEWORDSIZE       = 4;  // single-word size in bytes
uint64_t SINGLEWORDSIZEINBITS = 32; // single-word size in bits

uint64_t SIZEOFUINT     = 8; // size of target-dependent unsigned integer in bytes
uint64_t SIZEOFUINTSTAR = 8; // size of target-dependent pointer to unsigned integer in bytes

uint64_t UINT_MAX; // maximum numerical value of target-dependent unsigned integer
uint64_t INT_MIN;  // minimum numerical value of target-dependent signed integer

uint64_t WORDSIZE       = 8;  // target-dependent word size in bytes
uint64_t WORDSIZEINBITS = 64; // WORDSIZE * 8

char* character_buffer; // buffer for reading and writing characters

char* integer_buffer; // buffer for formatting integers

char* string_buffer; // buffer for formatting strings

uint64_t MAX_FILENAME_LENGTH = 128;

char* filename_buffer; // buffer for opening files

char*     console_buffer; // buffer for console output
uint64_t* binary_buffer;  // buffer for binary I/O

uint64_t STDOUT_FD = 1; // standard output has file descriptor 1

// flags for opening read-only files
// LINUX:       0 = 0x0000 = O_RDONLY (0x0000)
// MAC:         0 = 0x0000 = O_RDONLY (0x0000)
// WINDOWS: 32768 = 0x8000 = _O_BINARY (0x8000) | _O_RDONLY (0x0000)
// since LINUX/MAC do not seem to mind about _O_BINARY set
// we use the WINDOWS flags as default
uint64_t O_RDONLY = 32768;

// flags for opening write-only files
// LINUX: 577 = 0x0241 = O_CREAT (0x0040) | O_TRUNC (0x0200) | O_WRONLY (0x0001)
uint64_t LINUX_O_CREAT_TRUNC_WRONLY = 577;

// MAC: 1537 = 0x0601 = O_CREAT (0x0200) | O_TRUNC (0x0400) | O_WRONLY (0x0001)
uint64_t MAC_O_CREAT_TRUNC_WRONLY = 1537;

// WINDOWS: 33537 = 0x8301 = _O_BINARY (0x8000) | _O_CREAT (0x0100) | _O_TRUNC (0x0200) | _O_WRONLY (0x0001)
uint64_t WINDOWS_O_BINARY_CREAT_TRUNC_WRONLY = 33537;

// default is LINUX, re-initialized in init_system()
uint64_t O_CREAT_TRUNC_WRONLY = 577; // write-only flags for host operating system

// flags for rw-r--r-- (text) file permissions
// 420 = 00644 = S_IRUSR (00400) | S_IWUSR (00200) | S_IRGRP (00040) | S_IROTH (00004)
// these flags seem to be working for LINUX, MAC, and WINDOWS
uint64_t S_IRUSR_IWUSR_IRGRP_IROTH = 420;

// flags for rwxr-xr-x (binary) file permissions
// 493 = 00755 = S_IRUSR_IWUSR_IRGRP_IROTH | S_IXUSR (00100) | S_IXGRP (00010) | S_IXOTH (00001)
// these flags also seem to be working for LINUX, MAC, and WINDOWS
uint64_t S_IRUSR_IWUSR_IXUSR_IRGRP_IXGRP_IROTH_IXOTH = 493;

// ------------------------ GLOBAL VARIABLES -----------------------

uint64_t number_of_written_characters = 0;

char*    output_name = (char*) 0;
uint64_t output_fd   = 1; // standard output file descriptor

char*    output_buffer = (char*) 0;
uint64_t output_cursor = 0; // cursor for output buffer

// ------------------------- INITIALIZATION ------------------------

void init_library() {
  uint64_t i;

  if (SELFIE_URL)
    // avoid repeated initialization in tools
    return;

  SELFIE_URL = "selfie.cs.uni-salzburg.at";

  SIZEOFUINT64INBITS     = sizeof(uint64_t) * 8;
  SIZEOFUINT64STARINBITS = sizeof(uint64_t*) * 8;

  // powers of two table with SIZEOFUINT64INBITS entries for 2^0 to 2^(SIZEOFUINT64INBITS - 1)
  power_of_two_table = smalloc(SIZEOFUINT64INBITS * sizeof(uint64_t));

  *power_of_two_table = 1; // 2^0 == 1

  i = 1;

  while (i < SIZEOFUINT64INBITS) {
    // compute powers of two incrementally using this recurrence relation
    *(power_of_two_table + i) = *(power_of_two_table + (i - 1)) * 2;

    i = i + 1;
  }

  // compute 64-bit unsigned integer range using signed integer arithmetic
  UINT64_MAX = -1;

  // compute 64-bit signed integer range using unsigned integer arithmetic
  INT64_MIN = two_to_the_power_of(SIZEOFUINT64INBITS - 1);
  INT64_MAX = INT64_MIN - 1;

  // target-dependent, see init_target()
  SIZEOFUINT     = sizeof(uint64_t);
  SIZEOFUINTSTAR = sizeof(uint64_t*);
  UINT_MAX       = UINT64_MAX;
  INT_MIN        = INT64_MIN;
  WORDSIZE       = sizeof(uint64_t);
  WORDSIZEINBITS = WORDSIZE * 8;

  // allocate and touch to make sure memory is mapped for read calls
  character_buffer  = (char*) smalloc(1);
  *character_buffer = (char) 0;

  // accommodate at least SIZEOFUINT64INBITS-numbers for itoa, no mapping needed
  integer_buffer = string_alloc(SIZEOFUINT64INBITS);

  // accommodate at least standard terminal line, no mapping needed
  string_buffer = string_alloc(80);

  // does not need to be mapped
  filename_buffer = string_alloc(MAX_FILENAME_LENGTH);

  // buffer for output of printf derivatives, does not need to be mapped
  console_buffer = string_alloc(255);

  // allocate and touch to make sure memory is mapped for one-word read calls
  binary_buffer  = smalloc(sizeof(uint64_t));
  *binary_buffer = 0;
}

void reset_library() {
  number_of_written_characters = 0;
}

// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~
// -----------------------------------------------------------------
// ---------------------    C O M P I L E R    ---------------------
// -----------------------------------------------------------------
// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~

// -----------------------------------------------------------------
// ---------------------------- SCANNER ----------------------------
// -----------------------------------------------------------------

void print_symbol(uint64_t symbol);
void print_line_number(char* message, uint64_t line);

void syntax_error_message(char* message);
void syntax_error_expected_character(char character);

void get_character();

uint64_t is_character_new_line();
uint64_t is_character_whitespace();

char find_next_character();

uint64_t is_character_letter_or_digit_or_underscore();
uint64_t is_character_not_double_quote_or_new_line_or_eof();

uint64_t identifier_string_match(uint64_t string_index);
uint64_t identifier_or_keyword();

void get_symbol();

void handle_escape_sequence();

// ------------------------ GLOBAL CONSTANTS -----------------------

char CHAR_EOF          =  -1; // end of file
char CHAR_BACKSPACE    =   8; // ASCII code 8  = backspace
char CHAR_TAB          =   9; // ASCII code 9  = tabulator
char CHAR_LF           =  10; // ASCII code 10 = line feed
char CHAR_CR           =  13; // ASCII code 13 = carriage return
char CHAR_SPACE        = ' ';
char CHAR_UNDERSCORE   = '_';
char CHAR_SINGLEQUOTE  =  39; // ASCII code 39 = '
char CHAR_DOUBLEQUOTE  = '"';
char CHAR_COMMA        = ',';
char CHAR_SEMICOLON    = ';';
char CHAR_LPARENTHESIS = '(';
char CHAR_RPARENTHESIS = ')';
char CHAR_LBRACE       = '{';
char CHAR_RBRACE       = '}';
char CHAR_PLUS         = '+';
char CHAR_DASH         = '-';
char CHAR_ASTERISK     = '*';
char CHAR_SLASH        = '/';
char CHAR_PERCENTAGE   = '%';
char CHAR_EQUAL        = '=';
char CHAR_EXCLAMATION  = '!';
char CHAR_LT           = '<';
char CHAR_GT           = '>';
char CHAR_BACKSLASH    =  92; // ASCII code 92 = backslash
char CHAR_DOT          = '.';

uint64_t SYM_EOF = -1; // end of file

// C* symbols

uint64_t SYM_INTEGER      = 0;  // integer
uint64_t SYM_CHARACTER    = 1;  // character
uint64_t SYM_STRING       = 2;  // string
uint64_t SYM_IDENTIFIER   = 3;  // identifier
uint64_t SYM_UINT64       = 4;  // uint64_t
uint64_t SYM_IF           = 5;  // if
uint64_t SYM_ELSE         = 6;  // else
uint64_t SYM_VOID         = 7;  // void
uint64_t SYM_RETURN       = 8;  // return
uint64_t SYM_WHILE        = 9;  // while
uint64_t SYM_SIZEOF       = 10; // sizeof
uint64_t SYM_COMMA        = 11; // ,
uint64_t SYM_SEMICOLON    = 12; // ;
uint64_t SYM_LPARENTHESIS = 13; // (
uint64_t SYM_RPARENTHESIS = 14; // )
uint64_t SYM_LBRACE       = 15; // {
uint64_t SYM_RBRACE       = 16; // }
uint64_t SYM_PLUS         = 17; // +
uint64_t SYM_MINUS        = 18; // -
uint64_t SYM_ASTERISK     = 19; // *
uint64_t SYM_DIVISION     = 20; // /
uint64_t SYM_REMAINDER    = 21; // %
uint64_t SYM_ASSIGN       = 22; // =
uint64_t SYM_EQUALITY     = 23; // ==
uint64_t SYM_NOTEQ        = 24; // !=
uint64_t SYM_LT           = 25; // <
uint64_t SYM_LEQ          = 26; // <=
uint64_t SYM_GT           = 27; // >
uint64_t SYM_GEQ          = 28; // >=
uint64_t SYM_ELLIPSIS     = 29; // ...

// symbols for bootstrapping

uint64_t SYM_INT      = 30; // int
uint64_t SYM_CHAR     = 31; // char
uint64_t SYM_UNSIGNED = 32; // unsigned
uint64_t SYM_CONST    = 33; // const

uint64_t* SYMBOLS; // strings representing symbols

uint64_t MAX_IDENTIFIER_LENGTH = 64;  // maximum number of characters in an identifier
uint64_t MAX_INTEGER_LENGTH    = 20;  // maximum number of characters in an unsigned integer
uint64_t MAX_STRING_LENGTH     = 128; // maximum number of characters in a string

// ------------------------ GLOBAL VARIABLES -----------------------

char character; // most recently read character

uint64_t number_of_read_characters = 0;

uint64_t line_number = 1; // current line number for error reporting

uint64_t number_of_ignored_characters = 0;
uint64_t number_of_comments           = 0;
uint64_t number_of_scanned_symbols    = 0;

uint64_t number_of_syntax_errors = 0; // the number of encountered syntax errors

char* macro_const = (char*) 0; // stores scanned macro constant as string
char* identifier  = (char*) 0; // stores scanned identifier as string
char* integer     = (char*) 0; // stores scanned integer as string
char* string      = (char*) 0; // stores scanned string

uint64_t literal = 0; // numerical value of most recently scanned integer or character

uint64_t integer_is_signed = 0; // enforce INT64_MIN limit if '-' was scanned before

uint64_t symbol; // most recently recognized symbol

char*    source_name = (char*) 0; // name of source file
uint64_t source_fd   = 0; // file descriptor of open source file

// ------------------------- INITIALIZATION ------------------------

void init_scanner () {
  SYMBOLS = smalloc((SYM_CONST + 1) * sizeof(uint64_t*));

  *(SYMBOLS + SYM_INTEGER)      = (uint64_t) "integer";
  *(SYMBOLS + SYM_CHARACTER)    = (uint64_t) "character";
  *(SYMBOLS + SYM_STRING)       = (uint64_t) "string";
  *(SYMBOLS + SYM_IDENTIFIER)   = (uint64_t) "identifier";
  *(SYMBOLS + SYM_UINT64)       = (uint64_t) "uint64_t";
  *(SYMBOLS + SYM_IF)           = (uint64_t) "if";
  *(SYMBOLS + SYM_ELSE)         = (uint64_t) "else";
  *(SYMBOLS + SYM_VOID)         = (uint64_t) "void";
  *(SYMBOLS + SYM_RETURN)       = (uint64_t) "return";
  *(SYMBOLS + SYM_WHILE)        = (uint64_t) "while";
  *(SYMBOLS + SYM_SIZEOF)       = (uint64_t) "sizeof";
  *(SYMBOLS + SYM_COMMA)        = (uint64_t) ",";
  *(SYMBOLS + SYM_SEMICOLON)    = (uint64_t) ";";
  *(SYMBOLS + SYM_LPARENTHESIS) = (uint64_t) "(";
  *(SYMBOLS + SYM_RPARENTHESIS) = (uint64_t) ")";
  *(SYMBOLS + SYM_LBRACE)       = (uint64_t) "{";
  *(SYMBOLS + SYM_RBRACE)       = (uint64_t) "}";
  *(SYMBOLS + SYM_PLUS)         = (uint64_t) "+";
  *(SYMBOLS + SYM_MINUS)        = (uint64_t) "-";
  *(SYMBOLS + SYM_ASTERISK)     = (uint64_t) "*";
  *(SYMBOLS + SYM_DIVISION)     = (uint64_t) "/";
  *(SYMBOLS + SYM_REMAINDER)    = (uint64_t) "%";
  *(SYMBOLS + SYM_ASSIGN)       = (uint64_t) "=";
  *(SYMBOLS + SYM_EQUALITY)     = (uint64_t) "==";
  *(SYMBOLS + SYM_NOTEQ)        = (uint64_t) "!=";
  *(SYMBOLS + SYM_LT)           = (uint64_t) "<";
  *(SYMBOLS + SYM_LEQ)          = (uint64_t) "<=";
  *(SYMBOLS + SYM_GT)           = (uint64_t) ">";
  *(SYMBOLS + SYM_GEQ)          = (uint64_t) ">=";
  *(SYMBOLS + SYM_ELLIPSIS)     = (uint64_t) "...";

  *(SYMBOLS + SYM_INT)      = (uint64_t) "int";
  *(SYMBOLS + SYM_CHAR)     = (uint64_t) "char";
  *(SYMBOLS + SYM_UNSIGNED) = (uint64_t) "unsigned";
  *(SYMBOLS + SYM_CONST)    = (uint64_t) "const";

  character = CHAR_EOF;
  symbol    = SYM_EOF;
}

void reset_scanner() {
  number_of_read_characters = 0;

  line_number = 1;

  get_character();

  number_of_ignored_characters = 0;
  number_of_comments           = 0;
  number_of_scanned_symbols    = 0;

  number_of_syntax_errors = 0;
}

// -----------------------------------------------------------------
// ------------------------- SYMBOL TABLE --------------------------
// -----------------------------------------------------------------

// symbol table entry
// +---+---------+
// | 0 | next    | pointer to next entry
// | 1 | string  | identifier string, big integer as string, string literal
// | 2 | line#   | source line number
// | 3 | class   | VARIABLE, BIGINT, STRING, PROCEDURE, MACRO
// | 4 | type    | UINT64_T, UINT64STAR_T, VOID_T, UNDECLARED_T
// | 5 | value   | VARIABLE: initial value, PROCEDURE: number of formal parameters
// | 6 | address | VARIABLE, BIGINT, STRING: offset, PROCEDURE: address
// | 7 | scope   | REG_GP (global), REG_S0 (local)
// +---+---------+

uint64_t* allocate_symbol_table_entry() {
  return smalloc(2 * sizeof(uint64_t*) + 6 * sizeof(uint64_t));
}

uint64_t* get_next_entry(uint64_t* entry)  { return (uint64_t*) *entry; }
char*     get_string(uint64_t* entry)      { return (char*)     *(entry + 1); }
uint64_t  get_line_number(uint64_t* entry) { return             *(entry + 2); }
uint64_t  get_class(uint64_t* entry)       { return             *(entry + 3); }
uint64_t  get_type(uint64_t* entry)        { return             *(entry + 4); }
uint64_t  get_value(uint64_t* entry)       { return             *(entry + 5); }
uint64_t  get_address(uint64_t* entry)     { return             *(entry + 6); }
uint64_t  get_scope(uint64_t* entry)       { return             *(entry + 7); }

void set_next_entry(uint64_t* entry, uint64_t* next) { *entry       = (uint64_t) next; }
void set_string(uint64_t* entry, char* identifier)   { *(entry + 1) = (uint64_t) identifier; }
void set_line_number(uint64_t* entry, uint64_t line) { *(entry + 2) = line; }
void set_class(uint64_t* entry, uint64_t class)      { *(entry + 3) = class; }
void set_type(uint64_t* entry, uint64_t type)        { *(entry + 4) = type; }
void set_value(uint64_t* entry, uint64_t value)      { *(entry + 5) = value; }
void set_address(uint64_t* entry, uint64_t address)  { *(entry + 6) = address; }
void set_scope(uint64_t* entry, uint64_t scope)      { *(entry + 7) = scope; }

uint64_t hash(uint64_t* key);

uint64_t* create_symbol_table_entry(uint64_t table, char* string,
  uint64_t line, uint64_t class, uint64_t type, uint64_t value, uint64_t address);

uint64_t* search_symbol_table(uint64_t* entry, char* string, uint64_t class);
uint64_t* search_global_symbol_table(char* string, uint64_t class);
uint64_t* search_local_symbol_table(char* string);
uint64_t* get_scoped_symbol_table_entry(char* string);

uint64_t is_undefined_procedure(uint64_t* entry);
uint64_t report_undefined_procedures();

// ------------------------ GLOBAL CONSTANTS -----------------------

// classes
uint64_t VARIABLE  = 1;
uint64_t BIGINT    = 2;
uint64_t STRING    = 3;
uint64_t PROCEDURE = 4;
uint64_t MACRO     = 5;

// types
uint64_t UINT64_T     = 1;
uint64_t UINT64STAR_T = 2;
uint64_t VOID_T       = 3;
uint64_t UNDECLARED_T = 4;

// symbol tables
uint64_t GLOBAL_TABLE = 1;
uint64_t LOCAL_TABLE  = 2;

// hash table size for global symbol table
uint64_t HASH_TABLE_SIZE = 1024;

// ------------------------ GLOBAL VARIABLES -----------------------

// table pointers

uint64_t* global_symbol_table = (uint64_t*) 0;
uint64_t* local_symbol_table  = (uint64_t*) 0;

uint64_t number_of_symbol_lookups  = 0;
uint64_t number_of_list_iterations = 0;

// ------------------------- INITIALIZATION ------------------------

void reset_symbol_tables() {
  global_symbol_table = (uint64_t*) zmalloc(HASH_TABLE_SIZE * sizeof(uint64_t*));
  local_symbol_table  = (uint64_t*) 0;

  number_of_symbol_lookups  = 0;
  number_of_list_iterations = 0;
}

// -----------------------------------------------------------------
// ---------------------- REGISTER ALLOCATOR -----------------------
// -----------------------------------------------------------------

void     talloc();
uint64_t current_temporary();
uint64_t previous_temporary();
uint64_t next_temporary();
void     tfree(uint64_t number_of_temporaries);

// -----------------------------------------------------------------
// ---------------------------- PARSER -----------------------------
// -----------------------------------------------------------------

uint64_t is_type();
uint64_t is_value();
uint64_t is_expression();
uint64_t is_comparison();
uint64_t is_plus_or_minus();
uint64_t is_mult_or_div_or_rem();
uint64_t is_factor();
uint64_t is_literal();

uint64_t is_neither_rbrace_nor_eof();
uint64_t is_possibly_parameter(uint64_t is_already_variadic);

uint64_t is_neither_type_nor_void();
uint64_t is_not_statement();
uint64_t is_not_factor();

void syntax_error_expected_symbol(uint64_t expected);
void syntax_error_unexpected_symbol();

uint64_t get_expected_symbol(uint64_t expected_symbol);
void     get_required_symbol(uint64_t required_symbol);

void check_variable_redefinition(uint64_t scope, char* variable);
void syntax_error_undeclared_identifier(char* name);
void syntax_error_unexpected_identifier(char* expected);

void print_type(uint64_t type);
void type_warning(uint64_t expected, uint64_t found);

void compile_cstar(); // grammar top symbol, parser entry

uint64_t* compile_variable(char* variable, uint64_t type, uint64_t offset); // returns variable entry

uint64_t compile_type(); // returns type

uint64_t compile_initialize(uint64_t type); // returns initial value
uint64_t compile_cast(uint64_t type); // returns cast type
uint64_t compile_value(); // returns value

void compile_statement();

uint64_t load_upper_value(uint64_t reg, uint64_t value);
uint64_t load_upper_address(uint64_t* entry);

uint64_t load_value(uint64_t* entry);

uint64_t* get_variable_entry(char* variable);
uint64_t  load_variable(char* variable);

void compile_assignment(char* variable);

uint64_t compile_expression(); // returns type
uint64_t compile_arithmetic(); // returns type
uint64_t compile_term();       // returns type
uint64_t compile_factor();     // returns type

void load_small_and_medium_integer(uint64_t reg, uint64_t value);
void load_big_integer(uint64_t value);
void load_integer(uint64_t value);
void load_address(uint64_t* entry);
void load_string();

uint64_t compile_literal(); // returns type

void compile_if();
void compile_while();

char*    bootstrap_non_0_boot_level_procedures(char* procedure);
uint64_t is_boot_level_0_only_procedure(char* procedure);

void procedure_prologue(uint64_t number_of_local_variable_bytes);
void procedure_epilogue(uint64_t number_of_local_variable_bytes, uint64_t number_of_parameter_bytes);

void compile_procedure(char* procedure, uint64_t type);

uint64_t save_temporaries();
void     restore_temporaries(uint64_t number_of_temporaries);

uint64_t macro_expand(uint64_t* entry);
uint64_t procedure_call(uint64_t* entry, char* procedure, uint64_t number_of_actual_parameters);

uint64_t compile_call(char* procedure); // returns type
void     compile_return();

// ------------------------ GLOBAL VARIABLES -----------------------

uint64_t allocated_temporaries = 0; // number of allocated temporaries

uint64_t* current_procedure = (uint64_t*) 0; // currently parsed procedure definition

uint64_t return_jumps = 0; // fixup chain for return statements

uint64_t return_type = 0; // return type of currently parsed procedure

uint64_t number_of_global_variables = 0;
uint64_t number_of_procedures       = 0;
uint64_t number_of_string_literals  = 0;

uint64_t number_of_assignments = 0;
uint64_t number_of_while       = 0;
uint64_t number_of_if          = 0;
uint64_t number_of_calls       = 0;
uint64_t number_of_return      = 0;

// ------------------------- INITIALIZATION ------------------------

void reset_parser() {
  number_of_global_variables = 0;
  number_of_procedures       = 0;
  number_of_string_literals  = 0;

  number_of_assignments = 0;
  number_of_while       = 0;
  number_of_if          = 0;
  number_of_calls       = 0;
  number_of_return      = 0;

  number_of_syntax_errors = 0;

  get_symbol();
}

// -----------------------------------------------------------------
// ---------------------------- MACROS -----------------------------
// -----------------------------------------------------------------

// these C macros are seen as procedures by the bootstrapping compiler ...
void  var_start(uint64_t* args);
char* var_arg(uint64_t* args);
void  var_end(uint64_t* args);

// ... and need a dummy definition using all arguments to avoid compiler warnings
void  var_start(uint64_t* args) { *args = *args; }
char* var_arg(uint64_t* args)   { *args = *args; return ""; }
void  var_end(uint64_t* args)   { *args = *args; }

// these procedures are the actual selfie implementation of the above C macros
void macro_var_start();
void macro_var_arg();
void macro_var_end();

// -----------------------------------------------------------------
// ---------------------- MACHINE CODE LIBRARY ---------------------
// -----------------------------------------------------------------

void emit_round_up(uint64_t reg, uint64_t m);
void emit_multiply_by(uint64_t reg, uint64_t m);

// bootstrapping binary

void emit_program_entry();
void emit_bootstrapping();

// ------------------------ GLOBAL CONSTANTS -----------------------

char* main_name = (char*) 0;
char* bump_name = (char*) 0;

// ------------------------- INITIALIZATION ------------------------

void init_bootstrapping() {
  // obtain unique hash for global symbol table
  main_name = string_copy("main");
  bump_name = string_copy("_bump");
}

// -----------------------------------------------------------------
// --------------------------- COMPILER ----------------------------
// -----------------------------------------------------------------

uint64_t open_read_only(char* name);

void selfie_compile();

// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~
// -----------------------------------------------------------------
// -------------------     I N T E R F A C E     -------------------
// -----------------------------------------------------------------
// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~

// -----------------------------------------------------------------
// ---------------------------- REGISTER ---------------------------
// -----------------------------------------------------------------

char* get_register_name(uint64_t reg);
void  print_register_name(uint64_t reg);

uint64_t is_stack_register(uint64_t reg);
uint64_t is_system_register(uint64_t reg);
uint64_t is_argument_register(uint64_t reg);
uint64_t is_temporary_register(uint64_t reg);

void read_register_check_wrap(uint64_t reg, uint64_t check_wrap);
void read_register(uint64_t reg);

void write_register_wrap(uint64_t reg, uint64_t wrap);
void write_register(uint64_t reg);

// ------------------------ GLOBAL CONSTANTS -----------------------

uint64_t NUMBEROFREGISTERS   = 32;
uint64_t NUMBEROFTEMPORARIES = 7;

uint64_t REG_ZR  = 0;
uint64_t REG_RA  = 1;
uint64_t REG_SP  = 2;
uint64_t REG_GP  = 3;
uint64_t REG_TP  = 4;
uint64_t REG_T0  = 5;
uint64_t REG_T1  = 6;
uint64_t REG_T2  = 7;
uint64_t REG_S0  = 8;
uint64_t REG_S1  = 9;
uint64_t REG_A0  = 10;
uint64_t REG_A1  = 11;
uint64_t REG_A2  = 12;
uint64_t REG_A3  = 13;
uint64_t REG_A4  = 14;
uint64_t REG_A5  = 15;
uint64_t REG_A6  = 16;
uint64_t REG_A7  = 17;
uint64_t REG_S2  = 18;
uint64_t REG_S3  = 19;
uint64_t REG_S4  = 20;
uint64_t REG_S5  = 21;
uint64_t REG_S6  = 22;
uint64_t REG_S7  = 23;
uint64_t REG_S8  = 24;
uint64_t REG_S9  = 25;
uint64_t REG_S10 = 26;
uint64_t REG_S11 = 27;
uint64_t REG_T3  = 28;
uint64_t REG_T4  = 29;
uint64_t REG_T5  = 30;
uint64_t REG_T6  = 31;

uint64_t* REGISTERS; // strings representing registers

// ------------------------- INITIALIZATION ------------------------

void init_register() {
  REGISTERS = smalloc(NUMBEROFREGISTERS * sizeof(uint64_t*));

  *(REGISTERS + REG_ZR)  = (uint64_t) "zero";
  *(REGISTERS + REG_RA)  = (uint64_t) "ra";
  *(REGISTERS + REG_SP)  = (uint64_t) "sp";
  *(REGISTERS + REG_GP)  = (uint64_t) "gp";
  *(REGISTERS + REG_TP)  = (uint64_t) "tp";
  *(REGISTERS + REG_T0)  = (uint64_t) "t0";
  *(REGISTERS + REG_T1)  = (uint64_t) "t1";
  *(REGISTERS + REG_T2)  = (uint64_t) "t2";
  *(REGISTERS + REG_S0)  = (uint64_t) "s0"; // used to be fp
  *(REGISTERS + REG_S1)  = (uint64_t) "s1";
  *(REGISTERS + REG_A0)  = (uint64_t) "a0";
  *(REGISTERS + REG_A1)  = (uint64_t) "a1";
  *(REGISTERS + REG_A2)  = (uint64_t) "a2";
  *(REGISTERS + REG_A3)  = (uint64_t) "a3";
  *(REGISTERS + REG_A4)  = (uint64_t) "a4";
  *(REGISTERS + REG_A5)  = (uint64_t) "a5";
  *(REGISTERS + REG_A6)  = (uint64_t) "a6";
  *(REGISTERS + REG_A7)  = (uint64_t) "a7";
  *(REGISTERS + REG_S2)  = (uint64_t) "s2";
  *(REGISTERS + REG_S3)  = (uint64_t) "s3";
  *(REGISTERS + REG_S4)  = (uint64_t) "s4";
  *(REGISTERS + REG_S5)  = (uint64_t) "s5";
  *(REGISTERS + REG_S6)  = (uint64_t) "s6";
  *(REGISTERS + REG_S7)  = (uint64_t) "s7";
  *(REGISTERS + REG_S8)  = (uint64_t) "s8";
  *(REGISTERS + REG_S9)  = (uint64_t) "s9";
  *(REGISTERS + REG_S10) = (uint64_t) "s10";
  *(REGISTERS + REG_S11) = (uint64_t) "s11";
  *(REGISTERS + REG_T3)  = (uint64_t) "t3";
  *(REGISTERS + REG_T4)  = (uint64_t) "t4";
  *(REGISTERS + REG_T5)  = (uint64_t) "t5";
  *(REGISTERS + REG_T6)  = (uint64_t) "t6";
}

// -----------------------------------------------------------------
// ------------------------ ENCODER/DECODER ------------------------
// -----------------------------------------------------------------

void check_immediate_range(uint64_t found, uint64_t bits);

uint64_t encode_r_format(uint64_t funct7, uint64_t rs2, uint64_t rs1, uint64_t funct3, uint64_t rd, uint64_t opcode);
uint64_t get_funct7(uint64_t instruction);
uint64_t get_rs2(uint64_t instruction);
uint64_t get_rs1(uint64_t instruction);
uint64_t get_funct3(uint64_t instruction);
uint64_t get_rd(uint64_t instruction);
uint64_t get_opcode(uint64_t instruction);
void     decode_r_format();

uint64_t encode_i_format(uint64_t immediate, uint64_t rs1, uint64_t funct3, uint64_t rd, uint64_t opcode);
uint64_t get_immediate_i_format(uint64_t instruction);
void     decode_i_format();

uint64_t encode_s_format(uint64_t immediate, uint64_t rs2, uint64_t rs1, uint64_t funct3, uint64_t opcode);
uint64_t get_immediate_s_format(uint64_t instruction);
void     decode_s_format();

uint64_t encode_b_format(uint64_t immediate, uint64_t rs2, uint64_t rs1, uint64_t funct3, uint64_t opcode);
uint64_t get_immediate_b_format(uint64_t instruction);
void     decode_b_format();

uint64_t encode_j_format(uint64_t immediate, uint64_t rd, uint64_t opcode);
uint64_t get_immediate_j_format(uint64_t instruction);
void     decode_j_format();

uint64_t encode_u_format(uint64_t immediate, uint64_t rd, uint64_t opcode);
uint64_t get_immediate_u_format(uint64_t instruction);
void     decode_u_format();

// ------------------------ GLOBAL CONSTANTS -----------------------

// opcodes
uint64_t OP_LOAD   = 3;   // 0000011, I format (LD,LW)
uint64_t OP_IMM    = 19;  // 0010011, I format (ADDI, NOP)
uint64_t OP_STORE  = 35;  // 0100011, S format (SD,SW)
uint64_t OP_OP     = 51;  // 0110011, R format (ADD, SUB, MUL, DIVU, REMU, SLTU)
uint64_t OP_LUI    = 55;  // 0110111, U format (LUI)
uint64_t OP_BRANCH = 99;  // 1100011, B format (BEQ)
uint64_t OP_JALR   = 103; // 1100111, I format (JALR)
uint64_t OP_JAL    = 111; // 1101111, J format (JAL)
uint64_t OP_SYSTEM = 115; // 1110011, I format (ECALL)

// f3-codes
uint64_t F3_NOP   = 0; // 000
uint64_t F3_ADDI  = 0; // 000
uint64_t F3_ADD   = 0; // 000
uint64_t F3_SUB   = 0; // 000
uint64_t F3_MUL   = 0; // 000
uint64_t F3_DIVU  = 5; // 101
uint64_t F3_REMU  = 7; // 111
uint64_t F3_SLTU  = 3; // 011
uint64_t F3_LD    = 3; // 011
uint64_t F3_SD    = 3; // 011
uint64_t F3_LW    = 2; // 010
uint64_t F3_SW    = 2; // 010
uint64_t F3_BEQ   = 0; // 000
uint64_t F3_JALR  = 0; // 000
uint64_t F3_ECALL = 0; // 000

// f7-codes
uint64_t F7_ADD  = 0;  // 0000000
uint64_t F7_MUL  = 1;  // 0000001
uint64_t F7_SUB  = 32; // 0100000
uint64_t F7_DIVU = 1;  // 0000001
uint64_t F7_REMU = 1;  // 0000001
uint64_t F7_SLTU = 0;  // 0000000

// f12-codes (immediates)
uint64_t F12_ECALL = 0; // 000000000000

// ------------------------ GLOBAL VARIABLES -----------------------

uint64_t opcode = 0;
uint64_t rs1    = 0;
uint64_t rs2    = 0;
uint64_t rd     = 0;
uint64_t imm    = 0;
uint64_t funct3 = 0;
uint64_t funct7 = 0;

// -----------------------------------------------------------------
// ---------------------------- BINARY -----------------------------
// -----------------------------------------------------------------

uint64_t get_total_number_of_instructions();
uint64_t get_total_number_of_nops();

void print_instruction_counter(uint64_t counter, uint64_t ins);
void print_instruction_counter_with_nops(uint64_t counter, uint64_t nops, uint64_t ins);

void print_instruction_counters();

uint64_t get_low_word(uint64_t word);
uint64_t get_high_word(uint64_t word);

uint64_t load_word(uint64_t* memory, uint64_t waddr, uint64_t may_be_double_word);
void     store_word(uint64_t* memory, uint64_t waddr, uint64_t may_be_double_word, uint64_t word);

uint64_t load_instruction(uint64_t caddr);
void     store_instruction(uint64_t caddr, uint64_t instruction);

uint64_t load_code(uint64_t caddr);

uint64_t load_data(uint64_t daddr);
void     store_data(uint64_t daddr, uint64_t data);

void emit_instruction(uint64_t instruction);

uint64_t encode_nop();
void     emit_nop();

void emit_lui(uint64_t rd, uint64_t immediate);
void emit_addi(uint64_t rd, uint64_t rs1, uint64_t immediate);

void emit_add(uint64_t rd, uint64_t rs1, uint64_t rs2);
void emit_sub(uint64_t rd, uint64_t rs1, uint64_t rs2);
void emit_mul(uint64_t rd, uint64_t rs1, uint64_t rs2);
void emit_divu(uint64_t rd, uint64_t rs1, uint64_t rs2);
void emit_remu(uint64_t rd, uint64_t rs1, uint64_t rs2);
void emit_sltu(uint64_t rd, uint64_t rs1, uint64_t rs2);

void emit_load(uint64_t rd, uint64_t rs1, uint64_t immediate);
void emit_store(uint64_t rs1, uint64_t immediate, uint64_t rs2);

void emit_beq(uint64_t rs1, uint64_t rs2, uint64_t immediate);

void emit_jal(uint64_t rd, uint64_t immediate);
void emit_jalr(uint64_t rd, uint64_t rs1, uint64_t immediate);

void emit_ecall();

void fixup_BFormat(uint64_t from_address);
void fixup_JFormat(uint64_t from_address, uint64_t to_address);
void fixlink_JFormat(uint64_t from_address, uint64_t to_address);

void fixup_IFormat(uint64_t from_address, uint64_t immediate);

void emit_data_word(uint64_t data, uint64_t offset, uint64_t source_line_number);
void emit_string_data(uint64_t* entry);

void finalize_data_segment();

uint64_t* touch(uint64_t* memory, uint64_t bytes);

uint64_t* allocate_elf_header(uint64_t size);

uint64_t get_elf_header_size();

uint64_t* encode_elf_header();
void      encode_elf_program_header(uint64_t* header, uint64_t ph_index);

uint64_t validate_elf_file_header_top(uint64_t* header);

uint64_t decode_elf_file_header(uint64_t* header);
uint64_t decode_elf_program_header(uint64_t* header);

uint64_t open_write_only(char* name, uint64_t mode);

void selfie_output(char* filename);
void selfie_load(char* filename);

// ------------------------ GLOBAL CONSTANTS -----------------------

uint64_t ELF_HEADER_SIZE = 4096; // for page-aligned code and data segments

uint64_t ELFCLASS64 = 2;
uint64_t ELFCLASS32 = 1;

uint64_t MAX_BINARY_SIZE = 1048576; // 1MB

uint64_t MAX_CODE_SIZE = 524288; // 512KB
uint64_t MAX_DATA_SIZE = 65536;  // 64KB

uint64_t PK_CODE_START = 65536; // start of code segment at 0x10000 (according to RISC-V pk)

uint64_t PT_LOAD = 1; // loadable segment
uint64_t PF_RX   = 5; // readable and executable segment
uint64_t PF_RW   = 6; // readable and writable segment

// ELF file header

uint64_t EI_MAG0 = 127; // magic number part 0 is 0x7F
uint64_t EI_MAG1 = 'E'; // magic number part 1
uint64_t EI_MAG2 = 'L'; // magic number part 2
uint64_t EI_MAG3 = 'F'; // magic number part 3

uint64_t MACHO_MAG0 = 207; // first byte of magic number of Mach-O executables

uint64_t EI_CLASS   = 2; // file class is 2 (ELFCLASS64) or 1 (ELFCLASS32)
uint64_t EI_DATA    = 1; // object file data structures endianness is 1 (ELFDATA2LSB)
uint64_t EI_VERSION = 1; // version of the object file format
uint64_t EI_OSABI   = 0; // target OS ABI is usually set to 0

uint64_t EI_ABIVERSION = 0; // ABI version
uint64_t EI_PAD        = 0; // start of padding bytes

uint64_t e_type    = 2;   // object file type is 0x02 (ET_EXEC)
uint64_t e_machine = 243; // target architecture is 0xF3 (RISC-V)
uint64_t e_version = 1;   // version of the object file format

uint64_t e_entry = 65536; // entry point address 0x10000 (according to RISC-V pk)

uint64_t e_phoff = 64; // program header offset 0x40 (ELFCLASS64) or 0x34 (ELFCLASS32)
uint64_t e_shoff = 0;  // section header offset

uint64_t e_flags     = 0;  // ignored
uint64_t e_ehsize    = 64; // elf header size 64 bytes (ELFCLASS64) or 52 bytes (ELFCLASS32)
uint64_t e_phentsize = 56; // size of program header entry 56 bytes (ELFCLASS64) or 32 bytes (ELFCLASS32)

uint64_t e_phnum = 2; // number of program header entries (code and data segment)

uint64_t e_shentsize = 0; // size of section header entry
uint64_t e_shnum     = 0; // number of section header entries
uint64_t e_shstrndx  = 0; // section header offset

// ELF program header

uint64_t p_type  = 1; // type of segment is PT_LOAD
uint64_t p_flags = 0; // segment attributes

uint64_t p_offset = 0; // segment offset in file (must be a multiple of p_align)

uint64_t p_vaddr = 0; // start of segment in virtual memory
uint64_t p_paddr = 0; // start of segment in physical memory (ignored)

uint64_t p_filesz = 0; // size of segment in file
uint64_t p_memsz  = 0; // size of segment in memory

uint64_t p_align = 4096; // alignment of segment: p_vaddr % p_align == p_offset % p_align

// ------------------------ GLOBAL VARIABLES -----------------------

// instruction counters

uint64_t ic_lui   = 0;
uint64_t ic_addi  = 0;
uint64_t ic_add   = 0;
uint64_t ic_sub   = 0;
uint64_t ic_mul   = 0;
uint64_t ic_divu  = 0;
uint64_t ic_remu  = 0;
uint64_t ic_sltu  = 0;
uint64_t ic_load  = 0;
uint64_t ic_store = 0;
uint64_t ic_beq   = 0;
uint64_t ic_jal   = 0;
uint64_t ic_jalr  = 0;
uint64_t ic_ecall = 0;

// data counters

uint64_t dc_global_variable = 0;
uint64_t dc_string          = 0;
uint64_t dc_string_bytes    = 0;
uint64_t dc_big_integer     = 0;

char* binary_name = (char*) 0; // file name of binary

uint64_t* code_binary = (uint64_t*) 0; // code binary
uint64_t  code_start  = 0;             // start of code segment in virtual memory
uint64_t  code_size   = 0;             // size of code binary in bytes

uint64_t* data_binary = (uint64_t*) 0; // data binary
uint64_t  data_start  = 0;             // start of data segment in virtual memory
uint64_t  data_size   = 0;             // size of data binary in bytes

uint64_t* code_line_number = (uint64_t*) 0; // code line number per emitted instruction
uint64_t* data_line_number = (uint64_t*) 0; // data line number per emitted data word

// ------------------------- INITIALIZATION ------------------------

void reset_binary() {
  code_binary = (uint64_t*) 0;
  code_start  = 0;
  code_size   = 0;

  data_binary = (uint64_t*) 0;
  data_start  = 0;
  data_size   = 0;

  code_line_number = (uint64_t*) 0;
  data_line_number = (uint64_t*) 0;
}

void reset_binary_counters() {
  ic_lui   = 0;
  ic_addi  = 0;
  ic_add   = 0;
  ic_sub   = 0;
  ic_mul   = 0;
  ic_divu  = 0;
  ic_remu  = 0;
  ic_sltu  = 0;
  ic_load  = 0;
  ic_store = 0;
  ic_beq   = 0;
  ic_jal   = 0;
  ic_jalr  = 0;
  ic_ecall = 0;

  dc_global_variable = 0;
  dc_string          = 0;
  dc_string_bytes    = 0;
  dc_big_integer     = 0;
}

// -----------------------------------------------------------------
// ----------------------- MIPSTER SYSCALLS ------------------------
// -----------------------------------------------------------------

void emit_exit();
void implement_exit(uint64_t* context);

void     emit_read();
uint64_t copy_buffer(uint64_t* context, uint64_t vbuffer, uint64_t* buffer, uint64_t size, uint64_t upload);
void     implement_read(uint64_t* context);

void emit_write();
void implement_write(uint64_t* context);

void     emit_open();
uint64_t down_load_string(uint64_t* context, uint64_t vstring, char* s);
void     implement_openat(uint64_t* context);

void     emit_malloc();
uint64_t try_brk(uint64_t* context, uint64_t new_program_break);
void     implement_brk(uint64_t* context);

uint64_t is_boot_level_zero();

// ------------------------ GLOBAL CONSTANTS -----------------------

uint64_t debug_read  = 0;
uint64_t debug_write = 0;
uint64_t debug_open  = 0;
uint64_t debug_brk   = 0;

uint64_t SYSCALL_EXIT   = 93;
uint64_t SYSCALL_READ   = 63;
uint64_t SYSCALL_WRITE  = 64;
uint64_t SYSCALL_OPENAT = 56;
uint64_t SYSCALL_BRK    = 214;

/* DIRFD_AT_FDCWD corresponds to AT_FDCWD in fcntl.h and
   is passed as first argument of the openat system call
   emulating the (in Linux) deprecated open system call. */

uint64_t DIRFD_AT_FDCWD = -100;

// ------------------------ GLOBAL VARIABLES -----------------------

uint64_t* IO_buffer      = (uint64_t*) 0;
uint64_t  IO_buffer_size = 0;

// -----------------------------------------------------------------
// ------------------------ HYPSTER SYSCALL ------------------------
// -----------------------------------------------------------------

void emit_switch();
void do_switch(uint64_t* to_context, uint64_t timeout);
void implement_switch();

uint64_t* mipster_switch(uint64_t* to_context, uint64_t timeout);
uint64_t* hypster_switch(uint64_t* to_context, uint64_t timeout);

// ------------------------ GLOBAL CONSTANTS -----------------------

// TODO: fix this syscall for spike
uint64_t SYSCALL_SWITCH = 401;

uint64_t debug_switch = 0;

// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~
// -----------------------------------------------------------------
// -----------------    A R C H I T E C T U R E    -----------------
// -----------------------------------------------------------------
// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~

// -----------------------------------------------------------------
// ----------------------------- CACHE -----------------------------
// -----------------------------------------------------------------

// cache
// +---+------------------+
// | 0 | cache memory     | pointer to actual cache consisting of pointers to cache blocks
// | 1 | cache size       | cache size in bytes
// | 2 | associativity    | cache associativity
// | 3 | cache-block size | cache-block size in bytes
// | 4 | cache hits       | counter for cache hits
// | 5 | cache misses     | counter for cache misses
// | 6 | cache timer      | counter for LRU replacement strategy
// +---+------------------+

uint64_t* allocate_cache() {
  return smalloc(1 * sizeof(uint64_t*) + 6 * sizeof(uint64_t));
}

uint64_t* get_cache_memory(uint64_t* cache)     { return (uint64_t*) *cache; }
uint64_t  get_cache_size(uint64_t* cache)       { return             *(cache + 1); }
uint64_t  get_associativity(uint64_t* cache)    { return             *(cache + 2); }
uint64_t  get_cache_block_size(uint64_t* cache) { return             *(cache + 3); }
uint64_t  get_cache_hits(uint64_t* cache)       { return             *(cache + 4); }
uint64_t  get_cache_misses(uint64_t* cache)     { return             *(cache + 5); }
uint64_t  get_cache_timer(uint64_t* cache)      { return             *(cache + 6); }

void set_cache_memory(uint64_t* cache, uint64_t* cache_memory)        { *cache       = (uint64_t) cache_memory; }
void set_cache_size(uint64_t* cache, uint64_t cache_size)             { *(cache + 1) = cache_size; }
void set_associativity(uint64_t* cache, uint64_t associativity)       { *(cache + 2) = associativity; }
void set_cache_block_size(uint64_t* cache, uint64_t cache_block_size) { *(cache + 3) = cache_block_size; }
void set_cache_hits(uint64_t* cache, uint64_t cache_hits)             { *(cache + 4) = cache_hits; }
void set_cache_misses(uint64_t* cache, uint64_t cache_misses)         { *(cache + 5) = cache_misses; }
void set_cache_timer(uint64_t* cache, uint64_t cache_timer)           { *(cache + 6) = cache_timer; }

// cache block
// +---+------------+
// | 0 | valid flag | valid block or not
// | 1 | tag        | unique identifier within a set
// | 2 | memory     | pointer to cache-block memory
// | 3 | timestamp  | timestamp for replacement strategy
// +---+------------+

uint64_t* allocate_cache_block() {
  return zmalloc(1 * sizeof(uint64_t*) + 3 * sizeof(uint64_t));
}

uint64_t  get_valid_flag(uint64_t* cache_block)   { return             *cache_block; }
uint64_t  get_tag(uint64_t* cache_block)          { return             *(cache_block + 1); }
uint64_t* get_block_memory(uint64_t* cache_block) { return (uint64_t*) *(cache_block + 2); }
uint64_t  get_timestamp(uint64_t* cache_block)    { return             *(cache_block + 3); }

void set_valid_flag(uint64_t* cache_block, uint64_t valid)     { *cache_block       = valid; }
void set_tag(uint64_t* cache_block, uint64_t tag)              { *(cache_block + 1) = tag; }
void set_block_memory(uint64_t* cache_block, uint64_t* memory) { *(cache_block + 2) = (uint64_t) memory; }
void set_timestamp(uint64_t* cache_block, uint64_t timestamp)  { *(cache_block + 3) = timestamp; }

void reset_cache_counters(uint64_t* cache);
void reset_all_cache_counters();

void init_cache_memory(uint64_t* cache);
void init_cache(uint64_t* cache, uint64_t cache_size, uint64_t associativity, uint64_t cache_block_size);
void init_all_caches();

void flush_cache(uint64_t* cache);
void flush_all_caches();

uint64_t cache_set_size(uint64_t* cache);

uint64_t cache_tag(uint64_t* cache, uint64_t address);
uint64_t cache_index(uint64_t* cache, uint64_t address);
uint64_t cache_block_address(uint64_t* cache, uint64_t address);
uint64_t cache_byte_offset(uint64_t* cache, uint64_t address);

uint64_t* cache_set(uint64_t* cache, uint64_t vaddr);

uint64_t  get_new_timestamp(uint64_t* cache);
uint64_t* cache_lookup(uint64_t* cache, uint64_t vaddr, uint64_t paddr, uint64_t is_access);

void      fill_cache_block(uint64_t* cache, uint64_t* cache_block, uint64_t paddr);
uint64_t* handle_cache_miss(uint64_t* cache, uint64_t* cache_block, uint64_t paddr, uint64_t is_access);
uint64_t* retrieve_cache_block(uint64_t* cache, uint64_t vaddr, uint64_t paddr, uint64_t is_access);

void     flush_cache_block(uint64_t* cache, uint64_t* cache_block, uint64_t paddr);
uint64_t load_from_cache(uint64_t* cache, uint64_t vaddr, uint64_t paddr);
void     store_in_cache(uint64_t* cache, uint64_t vaddr, uint64_t paddr, uint64_t data);

uint64_t load_instruction_from_cache(uint64_t vaddr, uint64_t paddr);
uint64_t load_data_from_cache(uint64_t vaddr, uint64_t paddr);
void     store_data_in_cache(uint64_t vaddr, uint64_t paddr, uint64_t data);

void print_cache_profile(uint64_t hits, uint64_t misses, char* cache_name);

// ------------------------ GLOBAL CONSTANTS -----------------------

// indicates whether the machine has a cache or not
uint64_t L1_CACHE_ENABLED = 0;

// machine-enforced coherency for self-modifying code (selfie also
// works if this is turned off since there is no code modification
// during runtime and stores in the code segment are illegal)
uint64_t L1_CACHE_COHERENCY = 0;

// example configurations:
// +-------------------+---------------+-----------------------------+------------+
// |              name |    cache size |               associativity | block size |
// +===================+===============+=============================+============+
// |       CORE-V CVA6 | dcache: 32 KB |                   dcache: 8 |       16 B |
// |                   | icache: 16 KB |                   icache: 4 |            |
// +-------------------+---------------+-----------------------------+------------+
// |     direct-mapped |         2^x B |                           1 |      2^y B |
// +-------------------+---------------+-----------------------------+------------+
// | fully associative |         2^x B | (cache size) / (block size) |      2^y B |
// +-------------------+---------------+-----------------------------+------------+

// L1-cache size in byte
// assert: cache sizes are powers of 2
uint64_t L1_DCACHE_SIZE = 32768; // 32 KB data cache
uint64_t L1_ICACHE_SIZE = 16384; // 16 KB instruction cache

// L1-cache associativity
// assert: associativities are powers of 2
// assert: L1_xCACHE_SIZE / L1_xCACHE_ASSOCIATIVITY <= PAGESIZE
// (this is necessary in order to prevent aliasing problems)
uint64_t L1_DCACHE_ASSOCIATIVITY = 8;
uint64_t L1_ICACHE_ASSOCIATIVITY = 4;

// L1 cache-block size
// assert: cache-block sizes are powers of 2
// assert: L1_xCACHE_BLOCK_SIZE >= WORDSIZE
// assert: L1_xCACHE_SIZE / L1_xCACHE_ASSOCIATIVITY >= L1_xCACHE_BLOCK_SIZE
uint64_t L1_DCACHE_BLOCK_SIZE = 16; // in bytes
uint64_t L1_ICACHE_BLOCK_SIZE = 16; // in bytes

// pointers to VIPT n-way set-associative write-through L1-caches
uint64_t* L1_ICACHE;
uint64_t* L1_DCACHE;

// ------------------------ GLOBAL VARIABLES -----------------------

uint64_t L1_icache_coherency_invalidations = 0;

// -----------------------------------------------------------------
// ---------------------------- MEMORY -----------------------------
// -----------------------------------------------------------------

uint64_t load_physical_memory(uint64_t* paddr);
void     store_physical_memory(uint64_t* paddr, uint64_t data);

uint64_t root_PDE_offset(uint64_t page);
uint64_t leaf_PTE_offset(uint64_t page);

uint64_t* get_PTE_address(uint64_t* parent_table, uint64_t* table, uint64_t page);

uint64_t get_page_frame(uint64_t* table, uint64_t page);
uint64_t is_page_mapped(uint64_t* table, uint64_t page);
void     set_page_frame(uint64_t* table, uint64_t page, uint64_t frame);

uint64_t page_of_virtual_address(uint64_t vaddr);
uint64_t virtual_address_of_page(uint64_t page);

uint64_t is_virtual_address_valid(uint64_t vaddr, uint64_t alignment);
uint64_t is_virtual_address_mapped(uint64_t* table, uint64_t vaddr);

uint64_t* translate_virtual_to_physical(uint64_t* table, uint64_t vaddr);

uint64_t load_virtual_memory(uint64_t* table, uint64_t vaddr);
void     store_virtual_memory(uint64_t* table, uint64_t vaddr, uint64_t data);

uint64_t load_cached_virtual_memory(uint64_t* table, uint64_t vaddr);
void     store_cached_virtual_memory(uint64_t* table, uint64_t vaddr, uint64_t data);

uint64_t load_cached_instruction_word(uint64_t* table, uint64_t vaddr);

// ------------------------ GLOBAL CONSTANTS -----------------------

uint64_t debug_translation = 0;

uint64_t KILOBYTE = 1024;       // 1KB (KiB: 2^10B)
uint64_t MEGABYTE = 1048576;    // 1MB (MiB: 2^20B)
uint64_t GIGABYTE = 1073741824; // 1GB (GiB: 2^30B)

uint64_t VIRTUALMEMORYSIZE     = 4; // 4GB of virtual memory (avoiding 32-bit overflow)
uint64_t HIGHESTVIRTUALADDRESS = 4294967295; // VIRTUALMEMORYSIZE * GIGABYTE - 1 (avoiding 32-bit overflow)

uint64_t PAGESIZE      = 4096; // 4KB-virtual pages
uint64_t PAGEFRAMESIZE = 4096; // 4KB-physical page frames: target-dependent, see init_memory()

uint64_t NUMBEROFPAGES    = 1048576; // VIRTUALMEMORYSIZE * GIGABYTE / PAGESIZE
uint64_t NUMBEROFLEAFPTES = 512;     // PAGESIZE / sizeof(uint64_t*): host-dependent, see init_memory()

uint64_t PAGETABLETREE = 1; // two-level page table is default

uint64_t PHYSICALMEMORYSIZE   = 0; // total amount of physical memory available for page frames
uint64_t PHYSICALMEMORYEXCESS = 2; // tolerate more allocation than physically available

// ------------------------- INITIALIZATION ------------------------

void init_memory(uint64_t megabytes) {
  // target-dependent: reinitialize in case sizeof(uint64_t) is not WORDSIZE
  PAGEFRAMESIZE = PAGESIZE * (sizeof(uint64_t) / WORDSIZE);

  // host-dependent: reinitialize in case sizeof(uint64_t*) is SINGLEWORDSIZE
  NUMBEROFLEAFPTES = PAGESIZE / sizeof(uint64_t*);

  if (megabytes < 1)
    megabytes = 1;
  else if (megabytes > 4096)
    megabytes = 4096;

  PHYSICALMEMORYSIZE   = megabytes * MEGABYTE;
  PHYSICALMEMORYEXCESS = PHYSICALMEMORYEXCESS * (PAGEFRAMESIZE / PAGESIZE);
}

// -----------------------------------------------------------------
// ------------------------- INSTRUCTIONS --------------------------
// -----------------------------------------------------------------

uint64_t print_code_line_number_for_instruction(uint64_t address, uint64_t offset);
uint64_t print_code_context_for_instruction(uint64_t address);

uint64_t print_lui();
void     print_lui_before();
void     print_lui_after();
void     record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
void     do_lui();
void     undo_lui_addi_add_sub_mul_divu_remu_sltu_load_jal_jalr();

uint64_t print_addi();
void     print_addi_before();
void     print_addi_add_sub_mul_divu_remu_sltu_after();
void     do_addi();

uint64_t print_add_sub_mul_divu_remu_sltu();
void     print_add_sub_mul_divu_remu_sltu_before();

void do_add();
void do_sub();
void do_mul();
void do_divu();
void do_remu();

void do_sltu();

uint64_t print_load();
void     print_load_before();
void     print_load_after(uint64_t vaddr);
void     record_load();
uint64_t do_load();

uint64_t print_store();
void     print_store_before();
void     print_store_after(uint64_t vaddr);
void     record_store();
uint64_t do_store();
void     undo_store();

uint64_t print_beq();
void     print_beq_before();
void     print_beq_after();
void     record_beq();
void     do_beq();

uint64_t print_jal();
void     print_jal_before();
void     print_jal_jalr_after();
void     do_jal();

uint64_t print_jalr();
void     print_jalr_before();
void     do_jalr();

uint64_t print_ecall();
void     record_ecall();
void     do_ecall();
void     undo_ecall();

uint64_t print_data_line_number();
uint64_t print_data_context();
uint64_t print_data();

// ------------------------ GLOBAL CONSTANTS -----------------------

// RISC-U instructions

uint64_t LUI   = 1; // 0 is reserved for unknown instructions
uint64_t ADDI  = 2;
uint64_t ADD   = 3;
uint64_t SUB   = 4;
uint64_t MUL   = 5;
uint64_t DIVU  = 6;
uint64_t REMU  = 7;
uint64_t SLTU  = 8;
uint64_t LOAD  = 9;
uint64_t STORE = 10;
uint64_t BEQ   = 11;
uint64_t JAL   = 12;
uint64_t JALR  = 13;
uint64_t ECALL = 14;

uint64_t* MNEMONICS; // assembly mnemonics of instructions

// -----------------------------------------------------------------
// -------------------------- DISASSEMBLER -------------------------
// -----------------------------------------------------------------

char* get_mnemonic(uint64_t ins);

uint64_t print_instruction();

void selfie_disassemble(uint64_t verbose);

// ------------------------ GLOBAL VARIABLES -----------------------

char*    assembly_name = (char*) 0; // name of assembly file
uint64_t assembly_fd   = 0;         // file descriptor of open assembly file

// ------------------------- INITIALIZATION ------------------------

void reset_disassembler() {
  if (IS64BITTARGET) {
    *(MNEMONICS + LOAD)  = (uint64_t) "ld";
    *(MNEMONICS + STORE) = (uint64_t) "sd";
  } else {
    *(MNEMONICS + LOAD)  = (uint64_t) "lw";
    *(MNEMONICS + STORE) = (uint64_t) "sw";
  }
}

void init_disassembler() {
  MNEMONICS = smalloc((ECALL + 1) * sizeof(uint64_t*));

  *(MNEMONICS + LUI)   = (uint64_t) "lui";
  *(MNEMONICS + ADDI)  = (uint64_t) "addi";
  *(MNEMONICS + ADD)   = (uint64_t) "add";
  *(MNEMONICS + SUB)   = (uint64_t) "sub";
  *(MNEMONICS + MUL)   = (uint64_t) "mul";
  *(MNEMONICS + DIVU)  = (uint64_t) "divu";
  *(MNEMONICS + REMU)  = (uint64_t) "remu";
  *(MNEMONICS + SLTU)  = (uint64_t) "sltu";

  reset_disassembler();

  *(MNEMONICS + BEQ)   = (uint64_t) "beq";
  *(MNEMONICS + JAL)   = (uint64_t) "jal";
  *(MNEMONICS + JALR)  = (uint64_t) "jalr";
  *(MNEMONICS + ECALL) = (uint64_t) "ecall";
}

// -----------------------------------------------------------------
// -------------------------- REPLAY ENGINE ------------------------
// -----------------------------------------------------------------

void record_state(uint64_t value);

void replay_trace();

// ------------------------ GLOBAL CONSTANTS -----------------------

uint64_t MAX_REPLAY_LENGTH = 100;

// trace

uint64_t tc = 0; // trace counter

uint64_t* pcs    = (uint64_t*) 0; // trace of program counter values
uint64_t* values = (uint64_t*) 0; // trace of values

// ------------------------- INITIALIZATION ------------------------

void init_replay_engine() {
  pcs    = zmalloc(MAX_REPLAY_LENGTH * sizeof(uint64_t));
  values = zmalloc(MAX_REPLAY_LENGTH * sizeof(uint64_t));
}

// -----------------------------------------------------------------
// -------------------------- INTERPRETER --------------------------
// -----------------------------------------------------------------

void print_register_hexadecimal(uint64_t reg);
void print_register_octal(uint64_t reg);
void print_register_value(uint64_t reg);

uint64_t is_valid_segment_read(uint64_t vaddr);
uint64_t is_valid_segment_write(uint64_t vaddr);

void print_exception(uint64_t exception, uint64_t fault);
void throw_exception(uint64_t exception, uint64_t fault);

void fetch();
void decode();
void execute();

void execute_record();
void execute_undo();
void execute_debug();

void interrupt();

void run_until_exception();

uint64_t instruction_with_max_counter(uint64_t* counters, uint64_t max);
uint64_t print_per_instruction_counter(uint64_t total, uint64_t* counters, uint64_t max);
void     print_per_instruction_profile(char* message, uint64_t total, uint64_t* counters);

void print_access_profile(char* message, char* padding, uint64_t reads, uint64_t writes);
void print_per_register_profile(uint64_t reg);

void print_register_memory_profile();

void down_load_profiles();
void print_instruction_versus_exception_profile(uint64_t* parent_context);

void print_profile();

void print_host_os();

// ------------------------ GLOBAL CONSTANTS -----------------------

uint64_t INSTRUCTIONSIZE       = 4;  // in bytes
uint64_t INSTRUCTIONSIZEINBITS = 32; // INSTRUCTIONSIZE * 8

// exceptions

uint64_t EXCEPTION_NOEXCEPTION           = 0;
uint64_t EXCEPTION_PAGEFAULT             = 1;
uint64_t EXCEPTION_SEGMENTATIONFAULT     = 2;
uint64_t EXCEPTION_SYSCALL               = 3;
uint64_t EXCEPTION_TIMER                 = 4;
uint64_t EXCEPTION_DIVISIONBYZERO        = 5;
uint64_t EXCEPTION_INVALIDADDRESS        = 6;
uint64_t EXCEPTION_UNKNOWNINSTRUCTION    = 7;
uint64_t EXCEPTION_UNINITIALIZEDREGISTER = 8;
uint64_t EXCEPTION_INTEGEROVERFLOW       = 9;

uint64_t* EXCEPTIONS; // textual representation of exceptions

uint64_t debug_exception = 0;

uint64_t run = 0; // flag for running code

uint64_t debug = 0; // // flag for recording and debugging code

uint64_t debug_syscalls = 0; // flag for debugging syscalls

uint64_t record = 0; // flag for recording code execution
uint64_t redo   = 0; // flag for redoing code execution

uint64_t disassemble_verbose = 0; // flag for disassembling code in more detail

uint64_t symbolic = 0; // flag for symbolically executing code
uint64_t model    = 0; // flag for modeling code

// number of instructions from context switch to timer interrupt
// CAUTION: interrupting kernel code may cause race conditions
// TODO: implement proper interrupt controller to turn interrupts on and off
uint64_t TIMESLICE = 100000;

uint64_t TIMEROFF = 0; // must be 0 to turn off timer interrupt

// ------------------------ GLOBAL VARIABLES -----------------------

// hardware thread state

uint64_t pc = 0; // program counter

uint64_t ir = 0; // instruction register
uint64_t is = 0; // instruction id

uint64_t* registers = (uint64_t*) 0; // general-purpose registers

uint64_t* pt = (uint64_t*) 0; // page table

// core state

uint64_t timer = 0; // counter for timer interrupt
uint64_t trap  = 0; // flag for creating a trap

// effective nop counters

uint64_t nopc_lui   = 0;
uint64_t nopc_addi  = 0;
uint64_t nopc_add   = 0;
uint64_t nopc_sub   = 0;
uint64_t nopc_mul   = 0;
uint64_t nopc_divu  = 0;
uint64_t nopc_remu  = 0;
uint64_t nopc_sltu  = 0;
uint64_t nopc_load  = 0;
uint64_t nopc_store = 0;
uint64_t nopc_beq   = 0;
uint64_t nopc_jal   = 0;
uint64_t nopc_jalr  = 0;

// source profile

uint64_t  calls               = 0;             // total number of executed procedure calls
uint64_t* calls_per_procedure = (uint64_t*) 0; // number of executed calls of each procedure

uint64_t  iterations          = 0;             // total number of executed loop iterations
uint64_t* iterations_per_loop = (uint64_t*) 0; // number of executed iterations of each loop

uint64_t* loads_per_instruction  = (uint64_t*) 0; // number of executed loads per load instruction
uint64_t* stores_per_instruction = (uint64_t*) 0; // number of executed stores per store instruction

// registers profile

uint64_t* reads_per_register  = (uint64_t*) 0;
uint64_t* writes_per_register = (uint64_t*) 0;

uint64_t stack_register_reads  = 0;
uint64_t stack_register_writes = 0;

uint64_t argument_register_reads  = 0;
uint64_t argument_register_writes = 0;

uint64_t temporary_register_reads  = 0;
uint64_t temporary_register_writes = 0;

// segments profile

uint64_t data_reads  = 0;
uint64_t data_writes = 0;

uint64_t stack_reads  = 0;
uint64_t stack_writes = 0;

uint64_t heap_reads  = 0;
uint64_t heap_writes = 0;

// ------------------------- INITIALIZATION ------------------------

void init_interpreter() {
  EXCEPTIONS = smalloc((EXCEPTION_INTEGEROVERFLOW + 1) * sizeof(uint64_t*));

  *(EXCEPTIONS + EXCEPTION_NOEXCEPTION)           = (uint64_t) "no exception";
  *(EXCEPTIONS + EXCEPTION_PAGEFAULT)             = (uint64_t) "page fault";
  *(EXCEPTIONS + EXCEPTION_SEGMENTATIONFAULT)     = (uint64_t) "segmentation fault";
  *(EXCEPTIONS + EXCEPTION_SYSCALL)               = (uint64_t) "syscall";
  *(EXCEPTIONS + EXCEPTION_TIMER)                 = (uint64_t) "timer interrupt";
  *(EXCEPTIONS + EXCEPTION_DIVISIONBYZERO)        = (uint64_t) "division by zero";
  *(EXCEPTIONS + EXCEPTION_INVALIDADDRESS)        = (uint64_t) "invalid address";
  *(EXCEPTIONS + EXCEPTION_UNKNOWNINSTRUCTION)    = (uint64_t) "unknown instruction";
  *(EXCEPTIONS + EXCEPTION_UNINITIALIZEDREGISTER) = (uint64_t) "uninitialized register";
  *(EXCEPTIONS + EXCEPTION_INTEGEROVERFLOW)       = (uint64_t) "integer overflow";
}

void reset_interpreter() {
  pc = 0;
  ir = 0;
  is = 0;

  registers = (uint64_t*) 0;

  pt = (uint64_t*) 0;

  trap = 0;

  timer = TIMEROFF;
}

void reset_nop_counters() {
  nopc_lui   = 0;
  nopc_addi  = 0;
  nopc_add   = 0;
  nopc_sub   = 0;
  nopc_mul   = 0;
  nopc_divu  = 0;
  nopc_remu  = 0;
  nopc_sltu  = 0;
  nopc_load  = 0;
  nopc_store = 0;
  nopc_beq   = 0;
  nopc_jal   = 0;
  nopc_jalr  = 0;
}

void reset_source_profile() {
  calls               = 0;
  calls_per_procedure = zmalloc(code_size / INSTRUCTIONSIZE * sizeof(uint64_t));

  iterations          = 0;
  iterations_per_loop = zmalloc(code_size / INSTRUCTIONSIZE * sizeof(uint64_t));

  loads_per_instruction  = zmalloc(code_size / INSTRUCTIONSIZE * sizeof(uint64_t));
  stores_per_instruction = zmalloc(code_size / INSTRUCTIONSIZE * sizeof(uint64_t));
}

void reset_registers_profile() {
  reads_per_register  = zmalloc(NUMBEROFREGISTERS * sizeof(uint64_t));
  writes_per_register = zmalloc(NUMBEROFREGISTERS * sizeof(uint64_t));

  // zero register is initialized by definition
  *(writes_per_register + REG_ZR) = 1;

  // stack and frame pointer registers are initialized by boot loader
  *(writes_per_register + REG_SP) = 1;
  *(writes_per_register + REG_S0) = 1;

  stack_register_reads      = 0;
  stack_register_writes     = 0;
  argument_register_reads   = 0;
  argument_register_writes  = 0;
  temporary_register_reads  = 0;
  temporary_register_writes = 0;
}

void reset_segments_profile() {
  data_reads   = 0;
  data_writes  = 0;
  stack_reads  = 0;
  stack_writes = 0;
  heap_reads   = 0;
  heap_writes  = 0;
}

void reset_profiler() {
  reset_binary_counters();
  reset_nop_counters();
  reset_source_profile();
  reset_registers_profile();
  reset_segments_profile();
  reset_all_cache_counters();
}

// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~
// -----------------------------------------------------------------
// ----------------------    R U N T I M E    ----------------------
// -----------------------------------------------------------------
// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~

// -----------------------------------------------------------------
// ------------------------ MACHINE CONTEXTS -----------------------
// -----------------------------------------------------------------

// machine context
// +----+-----------------+
// |  0 | next context    | pointer to next context
// |  1 | prev context    | pointer to previous context
// |  2 | program counter | program counter
// |  3 | registers       | pointer to general purpose registers
// |  4 | page table      | pointer to page table
// |  5 | lowest lo page  | lowest low uncached page (code, data, heap)
// |  6 | highest lo page | highest low uncached page (code, data, heap)
// |  7 | lowest hi page  | lowest high uncached page (stack)
// |  8 | highest hi page | highest high uncached page (stack)
// |  9 | code start      | start of code segment
// | 10 | code size       | size of code segment
// | 11 | data start      | start of data segment
// | 12 | data size       | size of data segment
// | 13 | heap start      | start of heap segment
// | 14 | program break   | current program break
// | 15 | exception       | exception ID
// | 16 | faulting page   | faulting page
// | 17 | exit code       | exit code
// | 18 | parent          | context that created this context
// | 19 | virtual context | virtual context address
// | 20 | name            | binary name loaded into context
// +----+-----------------+
// | 21 | instr. counter  | number of executed instructions
// | 22 | malloc counter  | number of executed mallocs
// | 23 | syscall counter | number of handled syscalls
// | 24 | page fault ctr  | number of handled page faults
// | 25 | timer interrupt | number of handled timer interrupts
// | 26 | peak stack size | peak stack size
// | 27 | mapped heap     | size of mapped heap memory
// +----+-----------------+
// | 28 | used-list head  | pointer to head of used list
// | 29 | free-list head  | pointer to head of free list
// | 30 | gcs counter     | number of gc runs in gc period
// | 31 | gc enabled      | flag indicating whether to use gc or not
// +----+-----------------+

// number of entries of a machine context:
// 14 uint64_t + 6 uint64_t* + 1 char* + 7 uint64_t + 2 uint64_t* + 2 uint64_t entries
// extended in the symbolic execution engine and the Boehm garbage collector
uint64_t CONTEXTENTRIES = 32;

uint64_t* allocate_context(); // declaration avoids warning in the Boehm garbage collector

uint64_t* allocate_context() {
  // assert: sizeof(uint64_t) == sizeof(uint64_t*)
  return smalloc(CONTEXTENTRIES * sizeof(uint64_t));
}

uint64_t next_context(uint64_t* context)    { return (uint64_t) context; }
uint64_t prev_context(uint64_t* context)    { return (uint64_t) (context + 1); }
uint64_t program_counter(uint64_t* context) { return (uint64_t) (context + 2); }
uint64_t regs(uint64_t* context)            { return (uint64_t) (context + 3); }
uint64_t page_table(uint64_t* context)      { return (uint64_t) (context + 4); }
uint64_t lowest_lo_page(uint64_t* context)  { return (uint64_t) (context + 5); }
uint64_t highest_lo_page(uint64_t* context) { return (uint64_t) (context + 6); }
uint64_t lowest_hi_page(uint64_t* context)  { return (uint64_t) (context + 7); }
uint64_t highest_hi_page(uint64_t* context) { return (uint64_t) (context + 8); }
uint64_t code_seg_start(uint64_t* context)  { return (uint64_t) (context + 9); }
uint64_t code_seg_size(uint64_t* context)   { return (uint64_t) (context + 10); }
uint64_t data_seg_start(uint64_t* context)  { return (uint64_t) (context + 11); }
uint64_t data_seg_size(uint64_t* context)   { return (uint64_t) (context + 12); }
uint64_t heap_seg_start(uint64_t* context)  { return (uint64_t) (context + 13); }
uint64_t program_break(uint64_t* context)   { return (uint64_t) (context + 14); }
uint64_t exception(uint64_t* context)       { return (uint64_t) (context + 15); }
uint64_t fault(uint64_t* context)           { return (uint64_t) (context + 16); }
uint64_t exit_code(uint64_t* context)       { return (uint64_t) (context + 17); }
uint64_t parent(uint64_t* context)          { return (uint64_t) (context + 18); }
uint64_t virtual_context(uint64_t* context) { return (uint64_t) (context + 19); }
uint64_t name(uint64_t* context)            { return (uint64_t) (context + 20); }

// only profile data of malloc, exceptions, and heap is cached

uint64_t lc_malloc(uint64_t* context)       { return (uint64_t) (context + 22); }
uint64_t ec_syscall(uint64_t* context)      { return (uint64_t) (context + 23); }
uint64_t ec_page_fault(uint64_t* context)   { return (uint64_t) (context + 24); }
uint64_t ec_timer(uint64_t* context)        { return (uint64_t) (context + 25); }
uint64_t mc_mapped_heap(uint64_t* context)  { return (uint64_t) (context + 27); }

uint64_t used_list_head(uint64_t* context) { return (uint64_t) (context + 28); }
uint64_t free_list_head(uint64_t* context) { return (uint64_t) (context + 29); }
uint64_t gcs_in_period(uint64_t* context)  { return (uint64_t) (context + 30); }
uint64_t use_gc_kernel(uint64_t* context)  { return (uint64_t) (context + 31); }

uint64_t* get_next_context(uint64_t* context)    { return (uint64_t*) *context; }
uint64_t* get_prev_context(uint64_t* context)    { return (uint64_t*) *(context + 1); }
uint64_t  get_pc(uint64_t* context)              { return             *(context + 2); }
uint64_t* get_regs(uint64_t* context)            { return (uint64_t*) *(context + 3); }
uint64_t* get_pt(uint64_t* context)              { return (uint64_t*) *(context + 4); }
uint64_t  get_lowest_lo_page(uint64_t* context)  { return             *(context + 5); }
uint64_t  get_highest_lo_page(uint64_t* context) { return             *(context + 6); }
uint64_t  get_lowest_hi_page(uint64_t* context)  { return             *(context + 7); }
uint64_t  get_highest_hi_page(uint64_t* context) { return             *(context + 8); }
uint64_t  get_code_seg_start(uint64_t* context)  { return             *(context + 9); }
uint64_t  get_code_seg_size(uint64_t* context)   { return             *(context + 10); }
uint64_t  get_data_seg_start(uint64_t* context)  { return             *(context + 11); }
uint64_t  get_data_seg_size(uint64_t* context)   { return             *(context + 12); }
uint64_t  get_heap_seg_start(uint64_t* context)  { return             *(context + 13); }
uint64_t  get_program_break(uint64_t* context)   { return             *(context + 14); }
uint64_t  get_exception(uint64_t* context)       { return             *(context + 15); }
uint64_t  get_fault(uint64_t* context)           { return             *(context + 16); }
uint64_t  get_exit_code(uint64_t* context)       { return             *(context + 17); }
uint64_t* get_parent(uint64_t* context)          { return (uint64_t*) *(context + 18); }
uint64_t* get_virtual_context(uint64_t* context) { return (uint64_t*) *(context + 19); }
char*     get_name(uint64_t* context)            { return (char*)     *(context + 20); }

uint64_t  get_ic_all(uint64_t* context)         { return *(context + 21); }
uint64_t  get_lc_malloc(uint64_t* context)      { return *(context + 22); }
uint64_t  get_ec_syscall(uint64_t* context)     { return *(context + 23); }
uint64_t  get_ec_page_fault(uint64_t* context)  { return *(context + 24); }
uint64_t  get_ec_timer(uint64_t* context)       { return *(context + 25); }
uint64_t  get_mc_stack_peak(uint64_t* context)  { return *(context + 26); }
uint64_t  get_mc_mapped_heap(uint64_t* context) { return *(context + 27); }

uint64_t* get_used_list_head(uint64_t* context) { return (uint64_t*) *(context + 28); }
uint64_t* get_free_list_head(uint64_t* context) { return (uint64_t*) *(context + 29); }
uint64_t  get_gcs_in_period(uint64_t* context)  { return             *(context + 30); }
uint64_t  get_use_gc_kernel(uint64_t* context)  { return             *(context + 31); }

void set_next_context(uint64_t* context, uint64_t* next)     { *context        = (uint64_t) next; }
void set_prev_context(uint64_t* context, uint64_t* prev)     { *(context + 1)  = (uint64_t) prev; }
void set_pc(uint64_t* context, uint64_t pc)                  { *(context + 2)  = pc; }
void set_regs(uint64_t* context, uint64_t* regs)             { *(context + 3)  = (uint64_t) regs; }
void set_pt(uint64_t* context, uint64_t* pt)                 { *(context + 4)  = (uint64_t) pt; }
void set_lowest_lo_page(uint64_t* context, uint64_t page)    { *(context + 5)  = page; }
void set_highest_lo_page(uint64_t* context, uint64_t page)   { *(context + 6)  = page; }
void set_lowest_hi_page(uint64_t* context, uint64_t page)    { *(context + 7)  = page; }
void set_highest_hi_page(uint64_t* context, uint64_t page)   { *(context + 8)  = page; }
void set_code_seg_start(uint64_t* context, uint64_t start)   { *(context + 9)  = start; }
void set_code_seg_size(uint64_t* context, uint64_t size)     { *(context + 10) = size; }
void set_data_seg_start(uint64_t* context, uint64_t start)   { *(context + 11) = start; }
void set_data_seg_size(uint64_t* context, uint64_t size)     { *(context + 12) = size; }
void set_heap_seg_start(uint64_t* context, uint64_t start)   { *(context + 13) = start; }
void set_program_break(uint64_t* context, uint64_t brk)      { *(context + 14) = brk; }
void set_exception(uint64_t* context, uint64_t exception)    { *(context + 15) = exception; }
void set_fault(uint64_t* context, uint64_t page)             { *(context + 16) = page; }
void set_exit_code(uint64_t* context, uint64_t code)         { *(context + 17) = code; }
void set_parent(uint64_t* context, uint64_t* parent)         { *(context + 18) = (uint64_t) parent; }
void set_virtual_context(uint64_t* context, uint64_t* vctxt) { *(context + 19) = (uint64_t) vctxt; }
void set_name(uint64_t* context, char* name)                 { *(context + 20) = (uint64_t) name; }

void set_ic_all(uint64_t* context, uint64_t ic)         { *(context + 21) = ic; }
void set_lc_malloc(uint64_t* context, uint64_t ic)      { *(context + 22) = ic; }
void set_ec_syscall(uint64_t* context, uint64_t ec)     { *(context + 23) = ec; }
void set_ec_page_fault(uint64_t* context, uint64_t ec)  { *(context + 24) = ec; }
void set_ec_timer(uint64_t* context, uint64_t ec)       { *(context + 25) = ec; }
void set_mc_stack_peak(uint64_t* context, uint64_t mc)  { *(context + 26) = mc; }
void set_mc_mapped_heap(uint64_t* context, uint64_t mc) { *(context + 27) = mc; }

void set_used_list_head(uint64_t* context, uint64_t* used_list_head) { *(context + 28) = (uint64_t) used_list_head; }
void set_free_list_head(uint64_t* context, uint64_t* free_list_head) { *(context + 29) = (uint64_t) free_list_head; }
void set_gcs_in_period(uint64_t* context, uint64_t gcs)              { *(context + 30) = gcs; }
void set_use_gc_kernel(uint64_t* context, uint64_t use)              { *(context + 31) = use; }

// -----------------------------------------------------------------
// ---------------------------- MEMORY -----------------------------
// -----------------------------------------------------------------

uint64_t is_code_address(uint64_t* context, uint64_t vaddr);
uint64_t is_data_address(uint64_t* context, uint64_t vaddr);
uint64_t is_stack_address(uint64_t* context, uint64_t vaddr);
uint64_t is_heap_address(uint64_t* context, uint64_t vaddr);

uint64_t is_address_between_stack_and_heap(uint64_t* context, uint64_t vaddr);
uint64_t is_data_stack_heap_address(uint64_t* context, uint64_t vaddr);

// -----------------------------------------------------------------
// ---------------------- GARBAGE COLLECTOR ------------------------
// -----------------------------------------------------------------

// bootstrapped to actual functions during compilation ...
uint64_t fetch_stack_pointer()     { return 0; } // indicate that gc is unavailable
uint64_t fetch_global_pointer()    { return 0; }
uint64_t fetch_data_segment_size() { return 0; }

// ... here, not available on boot level 0 - only for compilation
void emit_fetch_stack_pointer();
void emit_fetch_global_pointer();
void emit_fetch_data_segment_size_interface();
void emit_fetch_data_segment_size_implementation(uint64_t fetch_dss_code_location);

void implement_gc_brk(uint64_t* context);

uint64_t is_gc_library(uint64_t* context);

// gc metadata entry
// +----+---------+
// |  0 | next    | pointer to next entry
// |  1 | memory  | pointer to allocated memory
// |  2 | size    | size of allocated memory (in bytes!)
// |  3 | markbit | markbit indicating reachability of allocated memory
// +----+---------+

uint64_t* allocate_metadata(uint64_t* context);

uint64_t* get_metadata_next(uint64_t* entry)    { return (uint64_t*) *entry; }
uint64_t* get_metadata_memory(uint64_t* entry)  { return (uint64_t*) *(entry + 1); }
uint64_t  get_metadata_size(uint64_t* entry)    { return             *(entry + 2); }
uint64_t  get_metadata_markbit(uint64_t* entry) { return             *(entry + 3); }

void set_metadata_next(uint64_t* entry, uint64_t* next)      { *entry       = (uint64_t) next; }
void set_metadata_memory(uint64_t* entry, uint64_t* memory)  { *(entry + 1) = (uint64_t) memory; }
void set_metadata_size(uint64_t* entry, uint64_t size)       { *(entry + 2) = size; }
void set_metadata_markbit(uint64_t* entry, uint64_t markbit) { *(entry + 3) = markbit; }

// getters and setters with different access in library/kernel

uint64_t  get_stack_seg_start_gc(uint64_t* context);
uint64_t  get_data_seg_start_gc(uint64_t* context);
uint64_t  get_data_seg_end_gc(uint64_t* context);
uint64_t  get_heap_seg_start_gc(uint64_t* context);
uint64_t  get_heap_seg_end_gc(uint64_t* context);
uint64_t* get_used_list_head_gc(uint64_t* context);
uint64_t* get_free_list_head_gc(uint64_t* context);
uint64_t  get_gcs_in_period_gc(uint64_t* context);
uint64_t  get_gc_enabled_gc(uint64_t* context);

void set_data_and_heap_segments_gc(uint64_t* context);
void set_used_list_head_gc(uint64_t* context, uint64_t* used_list_head);
void set_free_list_head_gc(uint64_t* context, uint64_t* free_list_head);
void set_gcs_in_period_gc(uint64_t* context, uint64_t gcs);
void set_gc_enabled_gc(uint64_t* context);

uint64_t is_gc_enabled(uint64_t* context);

void gc_init_selfie(uint64_t* context);

// interface to initialize an external garbage collector (e.g. tools/boehm-gc.c)
void gc_init(uint64_t* context);

// this function performs first-fit retrieval of free memory in O(n) where n is memory size
// improvement: push O(n) down to O(1), e.g. using Boehm's chunk allocator, or even compact-fit
// see github.com/cksystemsgroup/compact-fit
uint64_t* retrieve_from_free_list(uint64_t* context, uint64_t size);

uint64_t gc_load_memory(uint64_t* context, uint64_t address);
void     gc_store_memory(uint64_t* context, uint64_t address, uint64_t value);

void zero_block(uint64_t* context, uint64_t* metadata);

uint64_t* allocate_new_memory(uint64_t* context, uint64_t size);
uint64_t* reuse_memory(uint64_t* context, uint64_t size);
uint64_t* allocate_memory_selfie(uint64_t* context, uint64_t size);
uint64_t* gc_malloc_implementation(uint64_t* context, uint64_t size);

// interface to allocate a memory block using an external collector (e.g. tools/boehm-gc.c)
uint64_t* allocate_memory(uint64_t* context, uint64_t size);

// this function performs an O(n) list search where n is memory size
// improvement: push O(n) down to O(1), e.g. using Boehm's chunk allocator
uint64_t* get_metadata_if_address_is_valid(uint64_t* context, uint64_t address);

// interface to marking a memory block using an external collector (e.g. tools/boehm-gc.c)
void mark_block(uint64_t* context, uint64_t address);

void mark_block_selfie(uint64_t* context, uint64_t gc_address);
void mark_segment(uint64_t* context, uint64_t segment_start, uint64_t segment_end);

// this function scans the heap from two roots (data segment and stack) in O(n^2)
// where n is memory size; checking if a value is a pointer takes O(n), see above
// improvement: push O(n^2) down to O(n)
void mark(uint64_t* context);

void free_block(uint64_t* context, uint64_t* metadata, uint64_t* prev_metadata);

// interface to sweep marked memory blocks using an external collector (e.g. tools/boehm-gc.c)
void sweep(uint64_t* context);

void sweep_selfie(uint64_t* context);

void gc_collect(uint64_t* context);

void print_gc_profile(uint64_t is_gc_kernel);

// ----------------------- LIBRARY FUNCTIONS -----------------------

uint64_t* gc_malloc(uint64_t size) {
  return gc_malloc_implementation((uint64_t*) 0, size);
}

// ------------------------ GLOBAL CONSTANTS -----------------------

uint64_t GC_DISABLED = 0;
uint64_t GC_ENABLED  = 1;

uint64_t GC_ON = 0; // turn on kernel variant of gc, generate gc library code

uint64_t USE_GC_LIBRARY = 0; // use library variant of gc or not

uint64_t GC_PERIOD = 1000; // gc every so often

uint64_t GC_REUSE = 1; // reuse memory with freelist by default

uint64_t GC_METADATA_SIZE = 32; // 2 * sizeof(uint64_t) + 2 * sizeof(uint64_t*)

uint64_t GC_WORDSIZE = 8; // sizeof(uint64_t) for library variant, otherwise WORDSIZE

uint64_t GC_MARKBIT_UNREACHABLE = 0; // indicating that a memory block is not reachable
uint64_t GC_MARKBIT_REACHABLE   = 1; // indicating that a memory block is reachable, by root or other reachable block

// ------------------------ GLOBAL VARIABLES -----------------------

uint64_t gc_data_seg_start = 0;
uint64_t gc_data_seg_end   = 0;
uint64_t gc_heap_seg_start = 0;
uint64_t gc_heap_seg_end   = 0;

uint64_t* gc_used_list = (uint64_t*) 0; // pointer to used-list head
uint64_t* gc_free_list = (uint64_t*) 0; // pointer to free-list head

uint64_t gc_num_gcs_in_period = 0;

uint64_t gc_num_mallocated     = 0;
uint64_t gc_num_gced_mallocs   = 0;
uint64_t gc_num_ungced_mallocs = 0;
uint64_t gc_num_reused_mallocs = 0;
uint64_t gc_num_collects       = 0;
uint64_t gc_mem_mallocated     = 0;
uint64_t gc_mem_blocks         = 0;
uint64_t gc_mem_metadata       = 0;
uint64_t gc_mem_reused         = 0;
uint64_t gc_mem_collected      = 0;

// ------------------------- INITIALIZATION ------------------------

void reset_gc_counters() {
  gc_num_mallocated     = 0;
  gc_num_gced_mallocs   = 0;
  gc_num_ungced_mallocs = 0;
  gc_num_reused_mallocs = 0;
  gc_num_collects       = 0;
  gc_mem_mallocated     = 0;
  gc_mem_blocks         = 0;
  gc_mem_metadata       = 0;
  gc_mem_reused         = 0;
  gc_mem_collected      = 0;
}

// -----------------------------------------------------------------
// -------------------------- MICROKERNEL --------------------------
// -----------------------------------------------------------------

uint64_t* new_context();
void      free_context(uint64_t* context);
uint64_t* delete_context(uint64_t* context, uint64_t* from);

void init_context(uint64_t* context, uint64_t* parent, uint64_t* vctxt);

uint64_t* create_context(uint64_t* parent, uint64_t* vctxt);
uint64_t* find_context(uint64_t* parent, uint64_t* vctxt);
uint64_t* cache_context(uint64_t* vctxt);

void save_context(uint64_t* context);

uint64_t lowest_page(uint64_t page, uint64_t lo);
uint64_t highest_page(uint64_t page, uint64_t hi);
void     map_page(uint64_t* context, uint64_t page, uint64_t frame);

void cache_page_table(uint64_t* context, uint64_t* table, uint64_t* parent_table, uint64_t lo, uint64_t hi);

void restore_context(uint64_t* context);

uint64_t pavailable();
uint64_t pused();

uint64_t* palloc();
void      pfree(uint64_t* frame);

void map_and_store(uint64_t* context, uint64_t vaddr, uint64_t data);

void map_unmapped_pages(uint64_t* context);

// ------------------------ GLOBAL CONSTANTS -----------------------

uint64_t debug_create = 0;
uint64_t debug_map    = 0;

// ------------------------ GLOBAL VARIABLES -----------------------

uint64_t* current_context = (uint64_t*) 0; // context currently running

uint64_t* used_contexts = (uint64_t*) 0; // doubly-linked list of used contexts
uint64_t* free_contexts = (uint64_t*) 0; // singly-linked list of free contexts

uint64_t allocated_page_frame_memory = 0;
uint64_t free_page_frame_memory      = 0;

uint64_t next_page_frame = 0;

// ------------------------- INITIALIZATION ------------------------

void reset_microkernel() {
  current_context = (uint64_t*) 0;

  while (used_contexts != (uint64_t*) 0)
    used_contexts = delete_context(used_contexts, used_contexts);
}

// -----------------------------------------------------------------
// ---------------------------- KERNEL -----------------------------
// -----------------------------------------------------------------

void up_load_binary(uint64_t* context);

uint64_t up_load_string(uint64_t* context, char* s, uint64_t SP);
void     up_load_arguments(uint64_t* context, uint64_t argc, uint64_t* argv);

char* replace_extension(char* filename, char* suffix, char* extension);

char* boot_level_prefix(char* s);
char* increment_boot_level_prefix(char* s, char* t);

void boot_loader(uint64_t* context);

uint64_t handle_system_call(uint64_t* context);
uint64_t handle_page_fault(uint64_t* context);
uint64_t handle_division_by_zero(uint64_t* context);
uint64_t handle_timer(uint64_t* context);
uint64_t handle_exception(uint64_t* context);

uint64_t mipster(uint64_t* to_context);
uint64_t hypster(uint64_t* to_context);

uint64_t mixter(uint64_t* to_context, uint64_t mix);

uint64_t minmob(uint64_t* to_context);
uint64_t minster(uint64_t* to_context);
uint64_t mobster(uint64_t* to_context);

uint64_t selfie_run(uint64_t machine);

// ------------------------ GLOBAL CONSTANTS -----------------------

uint64_t* MY_CONTEXT = (uint64_t*) 0;

uint64_t DONOTEXIT = 0;
uint64_t EXIT      = 1; // extended in symbolic execution engine

uint64_t EXITCODE_NOERROR                = 0;
uint64_t EXITCODE_NOARGUMENTS            = 11; // leaving 1-10 for apps
uint64_t EXITCODE_BADARGUMENTS           = 12;
uint64_t EXITCODE_MOREARGUMENTS          = 13;
uint64_t EXITCODE_SYSTEMERROR            = 14;
uint64_t EXITCODE_IOERROR                = 15;
uint64_t EXITCODE_SCANNERERROR           = 16;
uint64_t EXITCODE_PARSERERROR            = 17;
uint64_t EXITCODE_COMPILERERROR          = 18;
uint64_t EXITCODE_SYNTAXERROR            = 19;
uint64_t EXITCODE_OUTOFVIRTUALMEMORY     = 20;
uint64_t EXITCODE_OUTOFPHYSICALMEMORY    = 21;
uint64_t EXITCODE_DIVISIONBYZERO         = 22;
uint64_t EXITCODE_UNKNOWNINSTRUCTION     = 23;
uint64_t EXITCODE_UNKNOWNSYSCALL         = 24;
uint64_t EXITCODE_UNSUPPORTEDSYSCALL     = 25;
uint64_t EXITCODE_MULTIPLEEXCEPTIONERROR = 26;
uint64_t EXITCODE_UNCAUGHTEXCEPTION      = 27;

uint64_t SYSCALL_BITWIDTH = 32; // integer bit width for system calls

uint64_t MIPSTER = 0;
uint64_t HYPSTER = 1;
uint64_t MINSTER = 2;
uint64_t MOBSTER = 3;
uint64_t MIXTER  = 4;

uint64_t* MACHINES; // named machines

uint64_t DIPSTER = 5;
uint64_t RIPSTER = 6;
uint64_t CAPSTER = 7;

// ------------------------- INITIALIZATION ------------------------

void init_kernel () {
  MACHINES = smalloc((MIXTER + 1) * sizeof(uint64_t*));

  *(MACHINES + MIPSTER) = (uint64_t) "mipster";
  *(MACHINES + HYPSTER) = (uint64_t) "hypster";
  *(MACHINES + MINSTER) = (uint64_t) "minster";
  *(MACHINES + MOBSTER) = (uint64_t) "mobster";
  *(MACHINES + MIXTER) = (uint64_t) "mixter";
}

// -----------------------------------------------------------------
// ------------------- CONSOLE ARGUMENT SCANNER --------------------
// -----------------------------------------------------------------

uint64_t  number_of_remaining_arguments();
uint64_t* remaining_arguments();

char* peek_argument(uint64_t lookahead);

char* get_argument();
void  set_argument(char* argv);

uint64_t no_or_bad_or_more_arguments(uint64_t exit_code);

void print_synopsis(char* extras);

// ------------------------ GLOBAL VARIABLES -----------------------

uint64_t  selfie_argc = 0;
uint64_t* selfie_argv = (uint64_t*) 0;

char* argument = (char*) 0;

// -----------------------------------------------------------------
// ----------------------------- SELFIE ----------------------------
// -----------------------------------------------------------------

void experimental_features();

uint64_t exit_selfie(uint64_t exit_code, char* extras);

// ------------------------ GLOBAL CONSTANTS -----------------------

char* selfie_name = (char*) 0; // name of running selfie executable

// IDs for host operating systems

uint64_t SELFIE    = 0;
uint64_t LINUX     = 1;
uint64_t MACOS     = 2;
uint64_t WINDOWS   = 3;
uint64_t BAREMETAL = 4;

uint64_t OS = 0; // default host operating system is selfie

// ------------------------- INITIALIZATION ------------------------

void init_selfie(uint64_t argc, uint64_t* argv) {
  selfie_argc = argc;
  selfie_argv = argv;

  selfie_name = get_argument();
}

void init_system() {
  uint64_t selfie_fd;

  if (sizeof(uint64_t) != sizeof(uint64_t*))
    // selfie requires an LP64 or ILP32 data model
    // uint64_t and uint64_t* must be the same size
    exit(EXITCODE_SYSTEMERROR);

  if (SIZEOFUINT64INBITS != 64) {
    if (SIZEOFUINT64INBITS == 32) {
      IS64BITSYSTEM = 0;
      IS64BITTARGET = 0;
    } else
      // selfie only supports 64-bit and 32-bit systems
      exit(EXITCODE_SYSTEMERROR);
  }

  if (is_boot_level_zero()) {
    // try opening executable
    selfie_fd = open_read_only(selfie_name);

    if (signed_less_than(selfie_fd, 0))
      // failure likely indicates Windows
      OS = WINDOWS;
    else {
      // read first byte of magic number
      read(selfie_fd, binary_buffer, 1);

      if (*binary_buffer == EI_MAG0)
        OS = LINUX;
      else if (*binary_buffer == MACHO_MAG0)
        OS = MACOS;
      else
        OS = WINDOWS;
    }
  } else
    OS = SELFIE;

  if (OS == MACOS)
    O_CREAT_TRUNC_WRONLY = MAC_O_CREAT_TRUNC_WRONLY;
  else if (OS == WINDOWS)
    O_CREAT_TRUNC_WRONLY = WINDOWS_O_BINARY_CREAT_TRUNC_WRONLY;
  else
    // Linux file opening flags are the default for Linux, selfie, and bare-metal hosts
    O_CREAT_TRUNC_WRONLY = LINUX_O_CREAT_TRUNC_WRONLY;
}

void init_target() {
  if (IS64BITTARGET) {
    if (IS64BITSYSTEM) {
      SIZEOFUINT     = sizeof(uint64_t);
      SIZEOFUINTSTAR = sizeof(uint64_t*);
      UINT_MAX       = UINT64_MAX;
      INT_MIN        = INT64_MIN;
      WORDSIZE       = sizeof(uint64_t);
      WORDSIZEINBITS = WORDSIZE * 8;

      MAX_INTEGER_LENGTH = 20; // 2^64-1 requires 20 decimal digits

      // configuring ELF64 file header

      EI_CLASS = ELFCLASS64; // file class is 2 (ELFCLASS64)

      e_phoff = 64; // program header offset 0x40 (ELFCLASS64)

      e_ehsize    = 64; // elf header size 64 bytes (ELFCLASS64)
      e_phentsize = 56; // size of program header entry 56 bytes (ELFCLASS64)
    } else
      // selfie does not support 64-bit targets on 32-bit systems
      exit(EXITCODE_SYSTEMERROR);
  } else {
    if (IS64BITSYSTEM) {
      SIZEOFUINT     = SINGLEWORDSIZE;
      SIZEOFUINTSTAR = SINGLEWORDSIZE;
      UINT_MAX       = two_to_the_power_of(SINGLEWORDSIZEINBITS) - 1;
      INT_MIN        = two_to_the_power_of(SINGLEWORDSIZEINBITS - 1);
      WORDSIZE       = SINGLEWORDSIZE;
    } else {
      SIZEOFUINT     = sizeof(uint64_t);
      SIZEOFUINTSTAR = sizeof(uint64_t*);
      UINT_MAX       = UINT64_MAX;
      INT_MIN        = INT64_MIN;
      WORDSIZE       = sizeof(uint64_t);
    }

    WORDSIZEINBITS = WORDSIZE * 8;

    MAX_INTEGER_LENGTH = 10; // 2^32-1 requires 10 decimal digits

    // configuring ELF32 file header

    EI_CLASS = ELFCLASS32; // file class is 1 (ELFCLASS32)

    e_phoff = 52; // program header offset 0x34 (ELFCLASS32)

    e_ehsize    = 52; // elf header size 52 bytes (ELFCLASS32)
    e_phentsize = 32; // size of program header entry 32 bytes (ELFCLASS32)
  }
}

void turn_on_gc_library(uint64_t period, char* name) {
  if (fetch_stack_pointer() != 0) {
    gc_init((uint64_t*) 0);

    GC_PERIOD = period;

    selfie_name = name;
  } else
    USE_GC_LIBRARY = GC_DISABLED;
}

// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~
// -----------------------------------------------------------------
// ---------------------     L I B R A R Y     ---------------------
// -----------------------------------------------------------------
// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~

// -----------------------------------------------------------------
// ----------------------- LIBRARY PROCEDURES ----------------------
// -----------------------------------------------------------------

uint64_t is_true(uint64_t a) {
  return a > 0;
}

uint64_t not(uint64_t a) {
  return a == 0;
}

uint64_t and(uint64_t a, uint64_t b) {
  return is_true(is_true(a) * is_true(b));
}

uint64_t or(uint64_t a, uint64_t b) {
  return is_true(is_true(a) + is_true(b));
}

uint64_t xor(uint64_t a, uint64_t b) {
  return is_true(a) != is_true(b);
}

uint64_t two_to_the_power_of(uint64_t p) {
  if (p < SIZEOFUINT64INBITS)
    return *(power_of_two_table + p);

  printf("%s: 2^%lu overflow\n", selfie_name, p);

  exit(EXITCODE_SYSTEMERROR);
}

uint64_t log_two(uint64_t n) {
  // use recursion for simplicity and educational value
  // for n < 0x10000 performance is not relevant
  if (n < 2)
    return 0;
  else
    return log_two(n / 2) + 1;
}

uint64_t ten_to_the_power_of(uint64_t p) {
  // use recursion for simplicity and educational value
  // for p close to 0 performance is not relevant
  if (p == 0)
    return 1;
  else
    return ten_to_the_power_of(p - 1) * 10;
}

uint64_t log_ten(uint64_t n) {
  // use recursion for simplicity and educational value
  // for n < 1000000 performance is not relevant
  if (n < 10)
    return 0;
  else
    return log_ten(n / 10) + 1;
}

uint64_t left_shift(uint64_t n, uint64_t b) {
  // assert: 0 <= b < SIZEOFUINT64INBITS
  return n * two_to_the_power_of(b);
}

uint64_t right_shift(uint64_t n, uint64_t b) {
  // assert: 0 <= b < SIZEOFUINT64INBITS
  return n / two_to_the_power_of(b);
}

uint64_t get_bits(uint64_t n, uint64_t i, uint64_t b) {
  // assert: 0 <= i + b <= SIZEOFUINT64INBITS
  // assert: 0 < b
  if (b < SIZEOFUINT64INBITS)
    return right_shift(n, i) % two_to_the_power_of(b);
  else
    return right_shift(n, i);
}

uint64_t absolute(uint64_t n) {
  if (signed_less_than(n, 0))
    return -n;
  else
    return n;
}

uint64_t max(uint64_t a, uint64_t b) {
  if (a > b)
    return a;
  else
    return b;
}

uint64_t signed_less_than(uint64_t a, uint64_t b) {
  // INT64_MIN <= n <= INT64_MAX iff
  // INT64_MIN + INT64_MIN <= n + INT64_MIN <= INT64_MAX + INT64_MIN iff
  // -2^64 <= n + INT64_MIN <= 2^64 - 1 (sign-extended to 65 bits) iff
  // 0 <= n + INT64_MIN <= UINT64_MAX
  return a + INT64_MIN < b + INT64_MIN;
}

uint64_t signed_division(uint64_t a, uint64_t b) {
  if (b != 0) {
    if (a == INT64_MIN)
      if (b == INT64_MIN)
        return 1;
      else if (signed_less_than(b, 0)) {
        if (b != (uint64_t) -1)
          return INT64_MIN / absolute(b);
        // signed division overflow
      } else
        return -(INT64_MIN / b);
    else if (b == INT64_MIN)
      return 0;
    else if (signed_less_than(a, 0))
      if (signed_less_than(b, 0))
        return absolute(a) / absolute(b);
      else
        return -(absolute(a) / b);
    else if (signed_less_than(b, 0))
      return -(a / absolute(b));
    else
      return a / b;
  }

  exit(EXITCODE_DIVISIONBYZERO);
}

uint64_t is_signed_integer(uint64_t n, uint64_t b) {
  // assert: 0 < b <= SIZEOFUINT64INBITS
  if (n < two_to_the_power_of(b - 1))
    return 1;
  else if (n >= -two_to_the_power_of(b - 1))
    return 1;
  else
    return 0;
}

uint64_t sign_extend(uint64_t n, uint64_t b) {
  // assert: -2^(b - 1) <= n < 2^(b - 1)
  // assert: 0 < b <= SIZEOFUINT64INBITS
  if (n < two_to_the_power_of(b - 1))
    return n;
  else if (b < SIZEOFUINT64INBITS)
    return n - two_to_the_power_of(b);
  else
    return n;
}

uint64_t sign_shrink(uint64_t n, uint64_t b) {
  // assert: 0 < b <= SIZEOFUINT64INBITS
  return get_bits(n, 0, b);
}

char load_character(char* s, uint64_t i) {
  // assert: i >= 0
  uint64_t a;

  // a is the index of the word where the
  // to-be-loaded i-th character in s is
  a = i / sizeof(uint64_t);

  // CAUTION: at boot levels higher than 0, s is only accessible
  // in C* at word granularity, not individual characters

  // return i-th 8-bit character in s
  return get_bits(*((uint64_t*) s + a), (i % sizeof(uint64_t)) * 8, 8);
}

char* store_character(char* s, uint64_t i, char c) {
  // assert: i >= 0, 0 <= c < 2^8 (all characters are 8-bit)
  uint64_t a;

  // a is the index of the word where the with c
  // to-be-overwritten i-th character in s is
  a = i / sizeof(uint64_t);

  // CAUTION: at boot levels higher than 0, s is only accessible
  // in C* at word granularity, not individual characters

  // subtract the to-be-overwritten character to reset its bits in s
  // then add c to set its bits at the i-th position in s
  *((uint64_t*) s + a) = (*((uint64_t*) s + a)
    - left_shift(load_character(s, i), (i % sizeof(uint64_t)) * 8))
      + left_shift(c, (i % sizeof(uint64_t)) * 8);

  return s;
}

uint64_t is_letter(char c) {
  // ASCII codes for lower- and uppercase letters are in contiguous intervals
  if (c >= 'a')
    if (c <= 'z')
      return 1;
    else
      return 0;
  else if (c >= 'A')
    if (c <= 'Z')
      return 1;
    else
      return 0;
  else
    return 0;
}

uint64_t is_digit(char c) {
  // ASCII codes for digits are in a contiguous interval
  if (c >= '0')
    if (c <= '9')
      return 1;
    else
      return 0;
  else
    return 0;
}

char* string_alloc(uint64_t l) {
  // allocates zeroed memory for a string of l characters
  // plus a null terminator aligned to word size
  return (char*) zmalloc(l + 1);
}

uint64_t string_length(char* s) {
  uint64_t i;

  i = 0;

  while (load_character(s, i) != 0)
    i = i + 1;

  return i;
}

char* string_copy(char* s) {
  uint64_t l;
  char* t;
  uint64_t i;

  l = string_length(s);
  t = string_alloc(l);
  i = 0;

  while (i < l) {
    store_character(t, i, load_character(s, i));

    i = i + 1;
  }

  store_character(t, i, 0); // null-terminated string

  return t;
}

void string_reverse(char* s) {
  uint64_t i;
  uint64_t j;
  uint64_t tmp;

  i = 0;
  j = string_length(s) - 1;

  while (i < j) {
    tmp = load_character(s, i);

    store_character(s, i, load_character(s, j));
    store_character(s, j, tmp);

    i = i + 1;
    j = j - 1;
  }
}

uint64_t string_compare(char* s, char* t) {
  uint64_t i;

  i = 0;

  while (1)
    if (load_character(s, i) == 0)
      if (load_character(t, i) == 0)
        return 1;
      else
        return 0;
    else if (load_character(s, i) == load_character(t, i))
      i = i + 1;
    else
      return 0;
}

uint64_t atoi(char* s) {
  uint64_t i;
  uint64_t n;
  uint64_t c;
  uint64_t sign;

  // the conversion of the ASCII string in s to its
  // numerical value n begins with the leftmost digit in s
  i = 0;

  // and the numerical value 0 for n
  n = 0;

  // load character (one byte) at index i in s from memory requires
  // bit shifting since memory access can only be done at word granularity
  c = load_character(s, i);

  // only used by console argument scanner
  if (c == '-') {
    sign = 1;

    i = i + 1;

    c = load_character(s, i);
  } else
    sign = 0;

  // loop until s is terminated
  while (c != 0) {
    // the numerical value of ASCII-encoded decimal digits
    // is offset by the ASCII code of '0' (which is 48)
    c = c - '0';

    if (c > 9) {
      printf("%s: cannot convert non-decimal number %s\n", selfie_name, s);

      exit(EXITCODE_SCANNERERROR);
    }

    // assert: s contains a decimal number

    // use base 10 but detect wrap around
    if (n < UINT64_MAX / 10)
      n = n * 10 + c;
    else if (n == UINT64_MAX / 10)
      if (c <= UINT64_MAX % 10)
        n = n * 10 + c;
      else {
        // s contains a decimal number larger than UINT64_MAX
        printf("%s: cannot convert out-of-bound number %s\n", selfie_name, s);

        exit(EXITCODE_SCANNERERROR);
      }
    else {
      // s contains a decimal number larger than UINT64_MAX
      printf("%s: cannot convert out-of-bound number %s\n", selfie_name, s);

      exit(EXITCODE_SCANNERERROR);
    }

    // go to the next digit
    i = i + 1;

    // load character (one byte) at index i in s from memory requires
    // bit shifting since memory access can only be done at word granularity
    c = load_character(s, i);
  }

  if (sign)
    if (n <= INT64_MIN)
      return -n;
    else {
      // s contains a decimal number smaller than INT64_MIN
      printf("%s: cannot convert out-of-bound number %s\n", selfie_name, s);

      exit(EXITCODE_SCANNERERROR);
    }
  else
    return n;
}

char* itoa(uint64_t n, char* s, uint64_t b, uint64_t d, uint64_t a) {
  // assert: b in {2,4,8,10,16}

  uint64_t i;
  uint64_t sign;

  // conversion of the integer n to an ASCII string in s with base b,
  // sign d, and alignment a begins with the leftmost digit in s
  i = 0;

  // for now assuming n is positive
  sign = 0;

  if (n == 0) {
    store_character(s, 0, '0');

    i = 1;
  } else if (d)
    if (signed_less_than(n, 0))
      if (b == 10) {
        // n is represented as two's complement
        // convert n to a positive number but remember the sign
        n = -n;

        sign = 1;
      }

  while (n != 0) {
    if (n % b > 9)
      // the ASCII code of hexadecimal digits larger than 9
      // is offset by the ASCII code of 'A' (which is 65)
      store_character(s, i, n % b - 10 + 'A');
    else
      // the ASCII code of digits less than or equal to 9
      // is offset by the ASCII code of '0' (which is 48)
      store_character(s, i, n % b + '0');

    // convert n by dividing n with base b
    n = n / b;

    i = i + 1;
  }

  if (b == 10) {
    if (sign) {
      store_character(s, i, '-'); // negative decimal numbers start with -

      i = i + 1;
    }

    while (i < a) {
      store_character(s, i, ' '); // align with spaces

      i = i + 1;
    }
  } else {
    while (i < a) {
      store_character(s, i, '0'); // align with 0s

      i = i + 1;
    }

  }

  store_character(s, i, 0); // null-terminated string

  // our numeral system is positional hindu-arabic, that is,
  // the weight of digits increases right to left, which means
  // that we need to reverse the string we computed above
  string_reverse(s);

  return s;
}

uint64_t fixed_point_ratio(uint64_t a, uint64_t b, uint64_t f) {
  // compute fixed-point ratio with f fractional digits
  return a / b * ten_to_the_power_of(f) + a % b * ten_to_the_power_of(f) / b;
}

uint64_t fixed_point_percentage(uint64_t a, uint64_t b, uint64_t f) {
  if (b == 0)
    return 0;
  // 10^(f+2) (for at least 2 fractional digits), 10^4 (for 100.00%)
  else if (a > b)
    return ten_to_the_power_of(f + 2 + 4) / fixed_point_ratio(a, b, f + 2);
  else if (a != 0)
    return ten_to_the_power_of(4) * fixed_point_ratio(b, a, f + 2) / ten_to_the_power_of(f + 2);
  else
    return 0;
}

uint64_t fixed_point_integral(uint64_t a, uint64_t f) {
  return a / ten_to_the_power_of(f);
}

uint64_t fixed_point_fractional(uint64_t a, uint64_t f) {
 return a % ten_to_the_power_of(f);
}

uint64_t ratio_format_integral_2(uint64_t a, uint64_t b) {
  return fixed_point_integral(fixed_point_ratio(a, b, 2), 2);
}

uint64_t ratio_format_fractional_2(uint64_t a, uint64_t b) {
  return fixed_point_fractional(fixed_point_ratio(a, b, 2), 2);
}

uint64_t percentage_format_integral_2(uint64_t a, uint64_t b) {
  return fixed_point_integral(fixed_point_percentage(a, b, 2), 2);
}

uint64_t percentage_format_fractional_2(uint64_t a, uint64_t b) {
  return fixed_point_fractional(fixed_point_percentage(a, b, 2), 2);
}

uint64_t write_to_printf(uint64_t fd, uint64_t* buffer, uint64_t bytes_to_write) {
  if (fd == 1)
    // writing to console
    if (OS != SELFIE)
      // on boot level 0 use printf to write to console
      // keeping output synchronized with other printf output

      // assert: buffer is null-terminated
      return printf("%s", (char*) buffer);

  return sign_extend(write(fd, buffer, bytes_to_write), SYSCALL_BITWIDTH);
}

void put_character(char c) {
  if (output_buffer) {
    // buffering character instead of outputting
    store_character(output_buffer, output_cursor, c);

    output_cursor = output_cursor + 1;
  } else if (character_buffer) {
    *character_buffer = c;

    // assert: character_buffer is mapped

    if (write_to_printf(output_fd, (uint64_t*) character_buffer, 1) != 1) {
      // output failed
      if (output_fd != STDOUT_FD) {
        // not printing to standard output, we may thus still print error message
        output_fd = STDOUT_FD;

        printf("%s: could not write character into output file %s\n", selfie_name, output_name);
      }

      exit(EXITCODE_IOERROR);
    }
  } else
    // character_buffer has not been successfully allocated yet
    exit(EXITCODE_IOERROR);
}

void print(char* s) {
  uint64_t i;

  if (s == (char*) 0)
    print("NULL");
  else {
    i = 0;

    while (load_character(s, i) != 0) {
      put_character(load_character(s, i));

      i = i + 1;
    }
  }
}

void println() {
  // not used in printf implementation
  printf("\n");
}

void print_character(char c) {
  // not used in printf implementation
  if (c == CHAR_EOF)
    printf("'end of file'");
  else if (c == CHAR_TAB)
    printf("'tabulator'");
  else if (c == CHAR_LF)
    printf("'line feed'");
  else if (c == CHAR_CR)
    printf("'carriage return'");
  else
    printf("'%c'", c);
}

void print_string(char* s) {
  // not used in printf implementation
  printf("\"%s\"", s);
}

void print_unsigned_integer(uint64_t n) {
  // use print, not printf to avoid recursion
  print(itoa(n, integer_buffer, 10, 0, 0));
}

void print_integer(uint64_t n) {
  // use print, not printf to avoid recursion
  print(itoa(n, integer_buffer, 10, 1, 0));
}

void print_fractional(uint64_t n, uint64_t p) {
  uint64_t d;

  // number of digits of n
  d = log_ten(n) + 1;

  while (p > d) {
    put_character('0');

    p = p - 1;
  }

  // use print, not printf to avoid recursion
  print(itoa(n, integer_buffer, 10, 0, 0));
}

void unprint_integer(uint64_t n) {
  n = string_length(itoa(n, integer_buffer, 10, 1, 0));

  while (n > 0) {
    put_character(CHAR_BACKSPACE);

    n = n - 1;
  }
}

void print_hexadecimal_no_prefix(uint64_t n, uint64_t a) {
  // use print, not printf to avoid recursion
  print(itoa(n, integer_buffer, 16, 0, a));
}

void print_hexadecimal(uint64_t n, uint64_t a) {
  printf("0x");print_hexadecimal_no_prefix(n, a);
}

void print_octal_no_prefix(uint64_t n, uint64_t a) {
  // use print, not printf to avoid recursion
  print(itoa(n, integer_buffer, 8, 0, a));
}

void print_octal(uint64_t n, uint64_t a) {
  printf("0o");print_octal_no_prefix(n, a);
}

void print_binary_no_prefix(uint64_t n, uint64_t a) {
  // use print, not printf to avoid recursion
  print(itoa(n, integer_buffer, 2, 0, a));
}

void print_binary(uint64_t n, uint64_t a) {
  printf("0b");print_binary_no_prefix(n, a);
}

uint64_t print_format(char* s, uint64_t i, char* a) {
  // print argument a according to the encountered % formatting code
  // that starts at position i in string s

  uint64_t p;

  if (load_character(s, i) == 's') {
    print(a);

    return i + 1;
  } else if (load_character(s, i) == 'c') {
    put_character((uint64_t) a);

    return i + 1;
  } else if (load_character(s, i) == 'l') {
    if (load_character(s, i + 1) == 'u') {
      print_unsigned_integer((uint64_t) a);

      return i + 2;
    } else if (load_character(s, i + 1) == 'd') {
      print_integer((uint64_t) a);

      return i + 2;
    } else if (load_character(s, i + 1) == 'X') {
      print_hexadecimal_no_prefix((uint64_t) a, 0);

      return i + 2;
    } else if (load_character(s, i + 1) == 'o') {
      print_octal_no_prefix((uint64_t) a, 0);

      return i + 2;
    }
  } else if (load_character(s, i) == 'b') {
    print_binary_no_prefix((uint64_t) a, 0);

    return i + 1;
  } else if (load_character(s, i) == '.') {
    // for integer specifiers, precision specifies
    // the minimum number of digits to be written;
    // we only support single-digit precision
    p = load_character(s, i + 1);

    if (is_digit(p))
      if (load_character(s, i + 2) == 'l')
        if (load_character(s, i + 3) == 'u') {
          // precision support only for %lu
          print_fractional((uint64_t) a, p - '0');

          return i + 4;
        }
  } else if (load_character(s, i) == '0') {
    // we only support padding with 0s and single-digit width
    p = load_character(s, i + 1);

    if (is_digit(p))
      if (load_character(s, i + 2) == 'l')
        if (load_character(s, i + 3) == 'X') {
          // padding support only for %lX
          print_hexadecimal_no_prefix((uint64_t) a, p - '0');

          return i + 4;
        }
  }

  return i;
}

uint64_t vdsprintf(uint64_t fd, char* buffer, char* s, uint64_t* args) {
  uint64_t number_of_put_characters;
  uint64_t i;

  number_of_put_characters = 0;

  if (s != (char*) 0) {
    if (buffer)
      output_buffer = buffer;
    else
      output_buffer = console_buffer;

    output_cursor = 0;

    i = 0;

    while (load_character(s, i) != 0) {
      if (load_character(s, i) == '%') {
        i = i + 1;

        if (load_character(s, i) != 0) {
          if (load_character(s, i) != '%')
            i = print_format(s, i, var_arg(args));
          else {
            // for %% print just one %
            put_character('%');

            i = i + 1;
          }
        }
      } else {
        put_character(load_character(s, i));

        i = i + 1;
      }
    }

    // null-terminate output buffer
    store_character(output_buffer, output_cursor, 0);

    number_of_put_characters = output_cursor;
    output_cursor            = 0;

    if (buffer)
      output_buffer = (char*) 0;
    else {
      buffer        = output_buffer;
      output_buffer = (char*) 0;

      return write_to_printf(fd, (uint64_t*) buffer, number_of_put_characters);
    }
  }

  return number_of_put_characters;
}

uint64_t non_0_boot_level_printf(char* format, ...) {
  uint64_t* args;
  uint64_t written_bytes;

  args = (uint64_t*) 0;

  var_start(args);
  written_bytes = vdsprintf(1, (char*) 0, format, args);
  var_end(args);

  return written_bytes;
}

uint64_t non_0_boot_level_sprintf(char* buffer, char* format, ...) {
  uint64_t* args;
  uint64_t written_bytes;

  args = (uint64_t*) 0;

  var_start(args);
  written_bytes = vdsprintf(0, buffer, format, args);
  var_end(args);

  return written_bytes;
}

uint64_t non_0_boot_level_dprintf(uint64_t fd, char* format, ...) {
  uint64_t* args;
  uint64_t written_bytes;

  args = (uint64_t*) 0;

  var_start(args);
  written_bytes = vdsprintf(fd, (char*) 0, format, args);
  var_end(args);

  return written_bytes;
}

uint64_t printf_or_write(uint64_t length) {
  if (output_fd == STDOUT_FD)
    // use printf to write to console to stay in sync with other printf output
    return printf("%s", string_buffer);
  else
    // write appears to be in sync with dprintf but is more efficient
    return write(output_fd, (uint64_t*) string_buffer, length);
}

uint64_t round_up(uint64_t n, uint64_t m) {
  if (n % m == 0)
    return n;
  else
    return n - n % m + m;
}

uint64_t* smalloc(uint64_t size) {
  // use this procedure, instead of malloc, for heap allocation
  // to ensure a defined program exit if no memory can be allocated
  uint64_t* memory;

  if (USE_GC_LIBRARY)
    memory = gc_malloc(size);
  else
    memory = malloc(size);

  if (size == 0)
    // any address including 0
    return memory;
  else if (memory != (uint64_t*) 0)
    return memory;
  else {
    if (character_buffer)
      // can only print error message if character_buffer has been successfully allocated
      printf("%s: malloc out of memory\n", selfie_name);

    exit(EXITCODE_OUTOFVIRTUALMEMORY);
  }
}

uint64_t* smalloc_system(uint64_t size) {
  // internal use only!

  uint64_t gc;
  uint64_t* memory;

  gc = USE_GC_LIBRARY;

  USE_GC_LIBRARY = GC_DISABLED;

  memory = smalloc(size);

  USE_GC_LIBRARY = gc;

  return memory;
}

void zero_memory(uint64_t* memory, uint64_t size) {
  uint64_t i;

  size = round_up(size, sizeof(uint64_t)) / sizeof(uint64_t);

  i = 0;

  while (i < size) {
    // erase memory by setting it to 0
    *(memory + i) = 0;

    i = i + 1;
  }
}

uint64_t* zalloc(uint64_t size) {
  // internal use only!

  // this procedure is only executed at boot level 0;
  // zalloc allocates size bytes rounded up to word size
  // and then zeroes that memory, similar to calloc, but
  // called zalloc to avoid redeclaring calloc
  uint64_t* memory;

  size = round_up(size, sizeof(uint64_t));

  memory = smalloc(size);

  zero_memory(memory, size);

  return memory;
}

uint64_t* zmalloc(uint64_t size) {
  // use this procedure, instead of smalloc,
  // if allocated memory needs to be zeroed
  if (USE_GC_LIBRARY)
    // assert: on boot level 1 or above mallocated memory is zeroed
    return gc_malloc(size);
  else
    return zalloc(size);
}

void assert(uint64_t assert_expr, uint64_t lineno, const char* procedure_name) {
  if (assert_expr == 0) {
    if (procedure_name == (char*) 0)
      printf("assertion failed at lineno : %ld\n", lineno);
    else
      printf("assertion failed at lineno : %ld, in function : %s\n", lineno, procedure_name);

    exit(EXITCODE_UNCAUGHTEXCEPTION);
  }
}

// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~
// -----------------------------------------------------------------
// ---------------------    C O M P I L E R    ---------------------
// -----------------------------------------------------------------
// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~

// -----------------------------------------------------------------
// ---------------------------- SCANNER ----------------------------
// -----------------------------------------------------------------

void print_symbol(uint64_t symbol) {
  if (symbol == SYM_EOF)
    print_string("end of file");
  else
    print_string((char*) *(SYMBOLS + symbol));
}

void print_line_number(char* message, uint64_t line) {
  printf("%s: %s in %s in line %lu: ", selfie_name, message, source_name, line);
}

void syntax_error_message(char* message) {
  print_line_number("syntax error", line_number);
  printf("%s\n", message);

  number_of_syntax_errors = number_of_syntax_errors + 1;
}

void syntax_error_expected_character(char expected) {
  print_line_number("syntax error", line_number);
  print_character(expected);
  printf(" expected but ");
  print_character(character);
  printf(" found\n");

  number_of_syntax_errors = number_of_syntax_errors + 1;
}

void get_character() {
  uint64_t number_of_read_bytes;

  // assert: character_buffer is mapped

  // try to read 1 character into character_buffer
  // from file with source_fd file descriptor
  number_of_read_bytes = read(source_fd, (uint64_t*) character_buffer, 1);

  if (number_of_read_bytes == 1) {
    // store the read character in the global variable called character
    character = *character_buffer;

    number_of_read_characters = number_of_read_characters + 1;
  } else if (number_of_read_bytes == 0)
    // reached end of file
    character = CHAR_EOF;
  else {
    printf("%s: could not read character from input file %s\n", selfie_name, source_name);

    exit(EXITCODE_IOERROR);
  }
}

uint64_t is_character_new_line() {
  if (character == CHAR_LF)
    return 1;
  else if (character == CHAR_CR)
    return 1;
  else
    return 0;
}

uint64_t is_character_whitespace() {
  if (character == CHAR_SPACE)
    return 1;
  else if (character == CHAR_TAB)
    return 1;
  else
    return is_character_new_line();
}

char find_next_character() {
  uint64_t in_single_line_comment;
  uint64_t in_multi_line_comment;

  // assuming we are not in a comment
  in_single_line_comment = 0;
  in_multi_line_comment  = 0;

  // read and discard all whitespace and comments until a character is found
  // that is not whitespace and does not occur in a comment, or the file ends
  while (1) {
    if (in_single_line_comment) {
      if (is_character_new_line())
        // single-line comments end with new line
        in_single_line_comment = 0;
      else if (character == CHAR_EOF)
        // or end of file
        return character;
      else {
        // count the characters in comments as ignored characters
        number_of_ignored_characters = number_of_ignored_characters + 1;

        get_character();
      }

    } else if (in_multi_line_comment) {
      while (character == CHAR_ASTERISK) {
        // look for "*/" by looping over consecutive '*' counting them as ignored characters
        number_of_ignored_characters = number_of_ignored_characters + 1;

        get_character();

        if (character == CHAR_SLASH)
          // multi-line comments end with "*/"
          in_multi_line_comment = 0;
      }

      if (in_multi_line_comment) {
        // keep track of line numbers for error reporting and code annotation
        if (character == CHAR_LF)
          // only line feeds count towards line numbers, not carriage returns
          line_number = line_number + 1;
        else if (character == CHAR_EOF) {
          // multi-line comment is not terminated
          syntax_error_message("runaway multi-line comment");

          exit(EXITCODE_SCANNERERROR);
        }
      }

      // count the characters in comments as ignored characters including '/' in "*/"
      number_of_ignored_characters = number_of_ignored_characters + 1;

      get_character();

    } else if (is_character_whitespace()) {
      // keep track of line numbers for error reporting and code annotation
      if (character == CHAR_LF)
        // only line feeds count towards line numbers, not carriage returns
        line_number = line_number + 1;

      // also count line feed and carriage return as ignored characters
      number_of_ignored_characters = number_of_ignored_characters + 1;

      get_character();

    } else if (character == CHAR_SLASH) {
      get_character();

      if (character == CHAR_SLASH) {
        // "//" begins a comment
        in_single_line_comment = 1;

        // count both slashes as ignored characters
        number_of_ignored_characters = number_of_ignored_characters + 2;

        number_of_comments = number_of_comments + 1;

        get_character();

      } else if (character == CHAR_ASTERISK) {
        // "/*" begins a multi-line comment
        in_multi_line_comment = 1;

        // count both slash and asterisk as ignored characters
        number_of_ignored_characters = number_of_ignored_characters + 2;

        number_of_comments = number_of_comments + 1;

        get_character();

      } else {
        // while looking for "//" and "/*" we actually found '/'
        symbol = SYM_DIVISION;

        return character;
      }

    } else
      // character found that is not whitespace and not occurring in a comment
      return character;
  }
}

uint64_t is_character_letter_or_digit_or_underscore() {
  if (is_letter(character))
    return 1;
  else if (is_digit(character))
    return 1;
  else if (character == CHAR_UNDERSCORE)
    return 1;
  else
    return 0;
}

uint64_t is_character_not_double_quote_or_new_line_or_eof() {
  if (character == CHAR_DOUBLEQUOTE)
    return 0;
  else if (is_character_new_line())
    return 0;
  else if (character == CHAR_EOF)
    return 0;
  else
    return 1;
}

uint64_t identifier_string_match(uint64_t keyword) {
  return string_compare(identifier, (char*) *(SYMBOLS + keyword));
}

uint64_t identifier_or_keyword() {
  if (identifier_string_match(SYM_UINT64))
    return SYM_UINT64;
  else if (identifier_string_match(SYM_IF))
    return SYM_IF;
  else if (identifier_string_match(SYM_ELSE))
    return SYM_ELSE;
  else if (identifier_string_match(SYM_VOID))
    return SYM_VOID;
  else if (identifier_string_match(SYM_RETURN))
    return SYM_RETURN;
  else if (identifier_string_match(SYM_WHILE))
    return SYM_WHILE;
  else if (identifier_string_match(SYM_SIZEOF))
    return SYM_SIZEOF;
  else if (identifier_string_match(SYM_INT))
    // selfie bootstraps int to uint64_t!
    return SYM_UINT64;
  else if (identifier_string_match(SYM_CHAR))
    // selfie bootstraps char to uint64_t!
    return SYM_UINT64;
  else if (identifier_string_match(SYM_UNSIGNED))
    // selfie bootstraps unsigned to uint64_t!
    return SYM_UINT64;
  else if (identifier_string_match(SYM_CONST))
    // selfie bootstraps const to uint64_t!
    return SYM_UINT64;
  else
    return SYM_IDENTIFIER;
}

void get_symbol() {
  uint64_t i;

  // reset previously scanned symbol
  symbol      = SYM_EOF;
  macro_const = (char*) 0;
  identifier  = (char*) 0;
  integer     = (char*) 0;
  literal     = 0;
  string      = (char*) 0;

  if (find_next_character() != CHAR_EOF) {
    if (symbol != SYM_DIVISION) {
      // '/' may have already been recognized
      // while looking for whitespace and "//"

      // start state of finite state machine
      // for recognizing C* symbols is here
      if (character == CHAR_UNDERSCORE) {
        // something starting with an underscore is interpreted as macro constant
        macro_const = string_alloc(MAX_IDENTIFIER_LENGTH);

        i = 0;

        while (is_character_letter_or_digit_or_underscore()) {
          if (i >= MAX_IDENTIFIER_LENGTH) {
            syntax_error_message("macro constant identifier too long");

            exit(EXITCODE_SCANNERERROR);
          }

          store_character(macro_const, i, character);

          i = i + 1;

          get_character();
        }

        store_character(macro_const, i, 0); // null-terminated string

        if (string_compare("__LINE__", macro_const)) {
          literal = line_number;
          symbol = SYM_INTEGER;
        } else if (string_compare("__func__", macro_const)) {
          string = get_string(current_procedure);
          symbol = SYM_STRING;
        } else {
          syntax_error_message("unrecognized macro constant");
        }
      } else if (is_letter(character)) {
        // accommodate identifier and null for termination
        identifier = string_alloc(MAX_IDENTIFIER_LENGTH);

        i = 0;

        while (is_character_letter_or_digit_or_underscore()) {
          if (i >= MAX_IDENTIFIER_LENGTH) {
            syntax_error_message("identifier too long");

            exit(EXITCODE_SCANNERERROR);
          }

          store_character(identifier, i, character);

          i = i + 1;

          get_character();
        }

        store_character(identifier, i, 0); // null-terminated string

        symbol = identifier_or_keyword();
      } else if (is_digit(character)) {
        if (character == '0') {
          // 0 is 0, not 00, 000, etc.
          get_character();

          literal = 0;
        } else {
          // accommodate integer and null for termination
          integer = string_alloc(MAX_INTEGER_LENGTH);

          i = 0;

          while (is_digit(character)) {
            if (i >= MAX_INTEGER_LENGTH) {
              if (integer_is_signed)
                syntax_error_message("signed integer out of bound");
              else
                syntax_error_message("unsigned integer out of bound");

              exit(EXITCODE_SCANNERERROR);
            }

            store_character(integer, i, character);

            i = i + 1;

            get_character();
          }

          store_character(integer, i, 0); // null-terminated string

          literal = atoi(integer);

          if (integer_is_signed) {
            if (literal > INT_MIN) {
              syntax_error_message("signed integer out of target bound");

              exit(EXITCODE_SCANNERERROR);
            }
          } else if (literal > UINT_MAX) {
            syntax_error_message("unsigned integer out of target bound");

            exit(EXITCODE_SCANNERERROR);
          } else if (literal >= INT_MIN)
            literal = sign_extend(literal, WORDSIZEINBITS);
        }

        symbol = SYM_INTEGER;
      } else if (character == CHAR_SINGLEQUOTE) {
        get_character();

        if (character == CHAR_EOF) {
          syntax_error_message("reached end of file looking for a character literal");

          exit(EXITCODE_SCANNERERROR);
        } else
          literal = character;

        get_character();

        if (character == CHAR_SINGLEQUOTE)
          get_character();
        else if (character == CHAR_EOF) {
          syntax_error_expected_character(CHAR_SINGLEQUOTE);

          exit(EXITCODE_SCANNERERROR);
        } else
          syntax_error_expected_character(CHAR_SINGLEQUOTE);

        symbol = SYM_CHARACTER;
      } else if (character == CHAR_DOUBLEQUOTE) {
        get_character();

        // accommodate string and null for termination,
        // allocate zeroed memory since strings are emitted
        // in words but may end non-word-aligned
        string = string_alloc(MAX_STRING_LENGTH);

        i = 0;

        while (is_character_not_double_quote_or_new_line_or_eof()) {
          if (i >= MAX_STRING_LENGTH) {
            syntax_error_message("string too long");

            exit(EXITCODE_SCANNERERROR);
          }

          if (character == CHAR_BACKSLASH)
            handle_escape_sequence();

          store_character(string, i, character);

          i = i + 1;

          get_character();
        }

        if (character == CHAR_DOUBLEQUOTE)
          get_character();
        else {
          syntax_error_expected_character(CHAR_DOUBLEQUOTE);

          exit(EXITCODE_SCANNERERROR);
        }

        store_character(string, i, 0); // null-terminated string

        symbol = SYM_STRING;
      } else if (character == CHAR_COMMA) {
        get_character();

        symbol = SYM_COMMA;
      } else if (character == CHAR_SEMICOLON) {
        get_character();

        symbol = SYM_SEMICOLON;
      } else if (character == CHAR_LPARENTHESIS) {
        get_character();

        symbol = SYM_LPARENTHESIS;
      } else if (character == CHAR_RPARENTHESIS) {
        get_character();

        symbol = SYM_RPARENTHESIS;
      } else if (character == CHAR_LBRACE) {
        get_character();

        symbol = SYM_LBRACE;
      } else if (character == CHAR_RBRACE) {
        get_character();

        symbol = SYM_RBRACE;
      } else if (character == CHAR_PLUS) {
        get_character();

        symbol = SYM_PLUS;
      } else if (character == CHAR_DASH) {
        get_character();

        symbol = SYM_MINUS;
      } else if (character == CHAR_ASTERISK) {
        get_character();

        symbol = SYM_ASTERISK;
      } else if (character == CHAR_PERCENTAGE) {
        get_character();

        symbol = SYM_REMAINDER;
      } else if (character == CHAR_EQUAL) {
        get_character();

        if (character == CHAR_EQUAL) {
          get_character();

          symbol = SYM_EQUALITY;
        } else
          symbol = SYM_ASSIGN;
      } else if (character == CHAR_EXCLAMATION) {
        get_character();

        if (character == CHAR_EQUAL)
          get_character();
        else
          syntax_error_expected_character(CHAR_EQUAL);

        symbol = SYM_NOTEQ;
      } else if (character == CHAR_LT) {
        get_character();

        if (character == CHAR_EQUAL) {
          get_character();

          symbol = SYM_LEQ;
        } else
          symbol = SYM_LT;
      } else if (character == CHAR_GT) {
        get_character();

        if (character == CHAR_EQUAL) {
          get_character();

          symbol = SYM_GEQ;
        } else
          symbol = SYM_GT;
      } else if (character == CHAR_DOT) {
        get_character();

        if (character == CHAR_DOT) {
          get_character();

          if (character == CHAR_DOT)
            get_character();
          else
            syntax_error_expected_character(CHAR_DOT);
        } else
          syntax_error_expected_character(CHAR_DOT);

        symbol = SYM_ELLIPSIS;
      } else {
        print_line_number("syntax error", line_number);
        printf("found unknown character ");
        print_character(character);
        println();

        number_of_syntax_errors = number_of_syntax_errors + 1;

        exit(EXITCODE_SCANNERERROR);
      }
    }

    number_of_scanned_symbols = number_of_scanned_symbols + 1;
  }
}

void handle_escape_sequence() {
  // ignoring the backslash
  number_of_ignored_characters = number_of_ignored_characters + 1;

  get_character();

  if (character == 'n')
    character = CHAR_LF;
  else if (character == 't')
    character = CHAR_TAB;
  else if (character == 'b')
    character = CHAR_BACKSPACE;
  else if (character == CHAR_SINGLEQUOTE)
    character = CHAR_SINGLEQUOTE;
  else if (character == CHAR_DOUBLEQUOTE)
    character = CHAR_DOUBLEQUOTE;
  else if (character == CHAR_PERCENTAGE)
    character = CHAR_PERCENTAGE;
  else if (character == CHAR_BACKSLASH)
    character = CHAR_BACKSLASH;
  else {
    syntax_error_message("unknown escape sequence found");

    exit(EXITCODE_SCANNERERROR);
  }
}

// -----------------------------------------------------------------
// ------------------------- SYMBOL TABLE --------------------------
// -----------------------------------------------------------------

uint64_t hash(uint64_t* key) {
  // assert: key != (uint64_t*) 0
  return (*key + (*key + (*key + (*key + (*key + *key / HASH_TABLE_SIZE) / HASH_TABLE_SIZE) / HASH_TABLE_SIZE) / HASH_TABLE_SIZE) / HASH_TABLE_SIZE) % HASH_TABLE_SIZE;
}

uint64_t* create_symbol_table_entry(uint64_t table, char* string,
  uint64_t line, uint64_t class, uint64_t type, uint64_t value, uint64_t address) {
  uint64_t* new_entry;
  uint64_t* hashed_entry_address;

  new_entry = allocate_symbol_table_entry();

  set_string(new_entry, string);
  set_line_number(new_entry, line);
  set_class(new_entry, class);
  set_type(new_entry, type);
  set_value(new_entry, value);
  set_address(new_entry, address);

  // create entry at head of list of symbols
  if (table == GLOBAL_TABLE) {
    set_scope(new_entry, REG_GP);

    hashed_entry_address = global_symbol_table + hash((uint64_t*) string);

    set_next_entry(new_entry, (uint64_t*) *hashed_entry_address);
    *hashed_entry_address = (uint64_t) new_entry;
  } else {
    set_scope(new_entry, REG_S0);
    set_next_entry(new_entry, local_symbol_table);
    local_symbol_table = new_entry;
  }

  return new_entry;
}

uint64_t* search_symbol_table(uint64_t* entry, char* string, uint64_t class) {
  number_of_symbol_lookups = number_of_symbol_lookups + 1;

  while (entry != (uint64_t*) 0) {
    number_of_list_iterations = number_of_list_iterations + 1;

    if (class == get_class(entry))
      if (string_compare(string, get_string(entry)))
        return entry;

    // keep looking
    entry = get_next_entry(entry);
  }

  return (uint64_t*) 0;
}

uint64_t* search_global_symbol_table(char* string, uint64_t class) {
  return search_symbol_table((uint64_t*) *(global_symbol_table + hash((uint64_t*) string)), string, class);
}

uint64_t* search_local_symbol_table(char* string) {
  return search_symbol_table(local_symbol_table, string, VARIABLE);
}

uint64_t* get_scoped_symbol_table_entry(char* string) {
  uint64_t* entry;

  // local variables override global variables
  entry = search_local_symbol_table(string);

  if (entry == (uint64_t*) 0)
    entry = search_global_symbol_table(string, VARIABLE);

  return entry;
}

uint64_t is_undefined_procedure(uint64_t* entry) {
  if (get_class(entry) != PROCEDURE)
    return 0;

  if (get_address(entry) == 0)
    // procedure declared but not defined
    return 1;
  else if (get_opcode(load_instruction(get_address(entry))) == OP_JAL)
    // procedure called but not defined
    return 1;
  else
    return 0;
}

uint64_t report_undefined_procedures() {
  uint64_t undefined;
  uint64_t i;
  uint64_t* entry;

  undefined = 0;

  i = 0;

  while (i < HASH_TABLE_SIZE) {
    entry = (uint64_t*) *(global_symbol_table + i);

    while (entry != (uint64_t*) 0) {
      if (is_undefined_procedure(entry)) {
        undefined = 1;

        print_line_number("syntax error", get_line_number(entry));
        printf("procedure %s undefined\n", get_string(entry));

        number_of_syntax_errors = number_of_syntax_errors + 1;
      }

      // keep looking
      entry = get_next_entry(entry);
    }

    i = i + 1;
  }

  return undefined;
}

// -----------------------------------------------------------------
// ---------------------- REGISTER ALLOCATOR -----------------------
// -----------------------------------------------------------------

void talloc() {
  // we use registers REG_T0-REG_T6 for temporaries
  if (allocated_temporaries < NUMBEROFTEMPORARIES)
    allocated_temporaries = allocated_temporaries + 1;
  else {
    syntax_error_message("out of registers");

    exit(EXITCODE_COMPILERERROR);
  }
}

uint64_t current_temporary() {
  if (allocated_temporaries > 0)
    if (allocated_temporaries < 4)
      return REG_TP + allocated_temporaries;
    else
      return REG_S11 + allocated_temporaries - 3;
  else {
    syntax_error_message("illegal register access");

    exit(EXITCODE_COMPILERERROR);
  }
}

uint64_t previous_temporary() {
  if (allocated_temporaries > 1)
    if (allocated_temporaries == 4)
      return REG_T2;
    else
      return current_temporary() - 1;
  else {
    syntax_error_message("illegal register access");

    exit(EXITCODE_COMPILERERROR);
  }
}

uint64_t next_temporary() {
  if (allocated_temporaries < NUMBEROFTEMPORARIES)
    if (allocated_temporaries == 3)
      return REG_T3;
    else
      return current_temporary() + 1;
  else {
    syntax_error_message("out of registers");

    exit(EXITCODE_COMPILERERROR);
  }
}

void tfree(uint64_t number_of_temporaries) {
  if (allocated_temporaries >= number_of_temporaries)
    allocated_temporaries = allocated_temporaries - number_of_temporaries;
  else {
    syntax_error_message("illegal register deallocation");

    exit(EXITCODE_COMPILERERROR);
  }
}

// -----------------------------------------------------------------
// ---------------------------- PARSER -----------------------------
// -----------------------------------------------------------------

uint64_t is_type() {
  return symbol == SYM_UINT64;
}

uint64_t is_value() {
  if (symbol == SYM_INTEGER)
    return 1;
  else if (symbol == SYM_CHARACTER)
    return 1;
  else
    return 0;
}

uint64_t is_expression() {
  return is_factor();
}

uint64_t is_comparison() {
  if (symbol == SYM_EQUALITY)
    return 1;
  else if (symbol == SYM_NOTEQ)
    return 1;
  else if (symbol == SYM_LT)
    return 1;
  else if (symbol == SYM_GT)
    return 1;
  else if (symbol == SYM_LEQ)
    return 1;
  else if (symbol == SYM_GEQ)
    return 1;
  else
    return 0;
}

uint64_t is_plus_or_minus() {
  if (symbol == SYM_PLUS)
    return 1;
  else if (symbol == SYM_MINUS)
    return 1;
  else
    return 0;
}

uint64_t is_mult_or_div_or_rem() {
  if (symbol == SYM_ASTERISK)
    return 1;
  else if (symbol == SYM_DIVISION)
    return 1;
  else if (symbol == SYM_REMAINDER)
    return 1;
  else
    return 0;
}

uint64_t is_factor() {
  if (symbol == SYM_LPARENTHESIS)
    return 1;
  else if (symbol == SYM_MINUS)
    return 1;
  else if (symbol == SYM_ASTERISK)
    return 1;
  else if (symbol == SYM_SIZEOF)
    return 1;
  else if (is_literal())
    return 1;
  else if (symbol == SYM_IDENTIFIER)
    return 1;
  else
    return 0;
}

uint64_t is_literal() {
  if (is_value())
    return 1;
  else if (symbol == SYM_STRING)
    return 1;
  else
    return 0;
}

uint64_t is_neither_rbrace_nor_eof() {
  if (symbol == SYM_RBRACE)
    return 0;
  else if (symbol == SYM_EOF)
    return 0;
  else
    return 1;
}

uint64_t is_possibly_parameter(uint64_t is_already_variadic) {
  if (symbol == SYM_COMMA)
    if (not(is_already_variadic))
      return 1;

  return 0;
}

uint64_t is_neither_type_nor_void() {
  if (is_type())
    return 0;
  else if (symbol == SYM_VOID)
    return 0;
  else if (symbol == SYM_EOF)
    return 0;
  else
    return 1;
}

uint64_t is_not_statement() {
  if (symbol == SYM_ASTERISK)
    return 0;
  else if (symbol == SYM_IDENTIFIER)
    return 0;
  else if (symbol == SYM_IF)
    return 0;
  else if (symbol == SYM_WHILE)
    return 0;
  else if (symbol == SYM_RETURN)
    return 0;
  else if (symbol == SYM_EOF)
    return 0;
  else
    return 1;
}

uint64_t is_not_factor() {
  if (is_factor())
    return 0;
  else if (symbol == SYM_EOF)
    return 0;
  else
    return 1;
}

void syntax_error_expected_symbol(uint64_t expected) {
  print_line_number("syntax error", line_number);
  print_symbol(expected);
  printf(" expected but ");
  print_symbol(symbol);
  printf(" found\n");

  number_of_syntax_errors = number_of_syntax_errors + 1;
}

void syntax_error_unexpected_symbol() {
  print_line_number("syntax error", line_number);
  printf("unexpected symbol ");
  print_symbol(symbol);
  printf(" found\n");

  number_of_syntax_errors = number_of_syntax_errors + 1;
}

uint64_t get_expected_symbol(uint64_t expected_symbol) {
  if (symbol == expected_symbol) {
    get_symbol();

    return 1;
  }

  syntax_error_expected_symbol(expected_symbol);

  return 0;
}

void get_required_symbol(uint64_t required_symbol) {
  if (get_expected_symbol(required_symbol) == 0)
    exit(EXITCODE_PARSERERROR);
}

void check_variable_redefinition(uint64_t scope, char* variable) {
  uint64_t* entry;

  assert(and(variable != (char*) 0, or(scope == GLOBAL_TABLE, scope == LOCAL_TABLE)), __LINE__, __func__);

  if (scope == GLOBAL_TABLE)
    entry = search_global_symbol_table(variable, VARIABLE);
  else
    entry = search_local_symbol_table(variable);

  if (entry == (uint64_t*) 0)
    return;

  print_line_number("syntax error", line_number);
  printf("redefinition of variable %s, original definition in line %lu\n", get_string(entry), get_line_number(entry));

  number_of_syntax_errors = number_of_syntax_errors + 1;
}

void syntax_error_undeclared_identifier(char* name) {
  print_line_number("syntax error", line_number);
  printf("%s undeclared\n", name);

  number_of_syntax_errors = number_of_syntax_errors + 1;
}

void syntax_error_unexpected_identifier(char* expected) {
  print_line_number("syntax error", line_number);
  printf("%s expected but %s found\n", expected, identifier);

  number_of_syntax_errors = number_of_syntax_errors + 1;
}

void print_type(uint64_t type) {
  if (type == UINT64_T)
    printf("uint64_t");
  else if (type == UINT64STAR_T)
    printf("uint64_t*");
  else if (type == VOID_T)
    printf("void");
  else if (type == UNDECLARED_T)
    printf("undeclared");
  else
    printf("unknown");
}

void type_warning(uint64_t expected, uint64_t found) {
  print_line_number("warning", line_number);
  printf("type mismatch, ");
  print_type(expected);
  printf(" expected but ");
  print_type(found);
  printf(" found\n");
}

void compile_cstar() {
  uint64_t type;
  char* variable_or_procedure;
  uint64_t* entry;

  while (symbol != SYM_EOF) {
    // synchronizing on strong symbols in case of syntax errors
    while (is_neither_type_nor_void()) {
      syntax_error_unexpected_symbol();

      if (symbol == SYM_EOF)
        exit(EXITCODE_PARSERERROR);
      else
        get_symbol();
    }

    if (is_type()) {
      type = compile_type();

      if (symbol == SYM_IDENTIFIER) {
        variable_or_procedure = identifier;

        get_symbol();

        if (symbol != SYM_LPARENTHESIS) {
          // type identifier [ initialize ] ";"
          // global variable declaration or definition
          entry = compile_variable(variable_or_procedure, type, 0);

          set_value(entry, compile_initialize(type));

          emit_data_word(get_value(entry), get_address(entry), get_line_number(entry));

          dc_global_variable = dc_global_variable + 1;

          get_expected_symbol(SYM_SEMICOLON);
        } else
          // type identifier "(" ...
          // procedure declaration or definition
          compile_procedure(variable_or_procedure, type);
      } else
        syntax_error_expected_symbol(SYM_IDENTIFIER);
    } else if (symbol == SYM_VOID) {
      // not a type, must be void, followed by procedure
      get_symbol();

      if (symbol == SYM_ASTERISK) {
        // we tolerate void* return types for bootstrapping
        get_symbol();

        type = UINT64STAR_T;
      } else
        type = VOID_T;

      if (symbol == SYM_IDENTIFIER) {
        // void identifier "(" ...
        // procedure declaration or definition
        variable_or_procedure = identifier;

        get_symbol();

        compile_procedure(variable_or_procedure, type);
      } else
        syntax_error_expected_symbol(SYM_IDENTIFIER);
    } else
      syntax_error_unexpected_symbol();
  }
}

uint64_t* compile_variable(char* variable, uint64_t type, uint64_t offset) {
  uint64_t* entry;

  if (variable != (char*) 0) {
    // lookahead of 1: identifier already parsed into variable (type may be left-factored)

    // global variable
    check_variable_redefinition(GLOBAL_TABLE, variable);

    // allocate memory for global variable in data segment
    data_size = data_size + WORDSIZE;

    entry = create_symbol_table_entry(GLOBAL_TABLE, variable,
      line_number, VARIABLE, type, 0, -data_size);

    number_of_global_variables = number_of_global_variables + 1;
  } else {
    // local variable or formal parameter
    if (symbol == SYM_IDENTIFIER) {
      check_variable_redefinition(LOCAL_TABLE, identifier);

      entry = create_symbol_table_entry(LOCAL_TABLE, identifier,
        line_number, VARIABLE, type, 0, offset);

      get_symbol();
    } else {
      syntax_error_expected_symbol(SYM_IDENTIFIER);

      entry = create_symbol_table_entry(LOCAL_TABLE, "no_name",
        line_number, VARIABLE, type, 0, offset);
    }
  }

  return entry;
}

uint64_t compile_type() {
  uint64_t type;

  type = UINT64_T;

  if (is_type()) {
    get_symbol();

    while (is_type())
      // we tolerate multiple uint64_t aliases for bootstrapping
      get_symbol();

    while (symbol == SYM_ASTERISK) {
      // we tolerate pointer to pointers for bootstrapping
      type = UINT64STAR_T;

      get_symbol();
    }
  } else
    syntax_error_expected_symbol(SYM_UINT64);

  // type is grammar attribute
  return type;
}

uint64_t compile_initialize(uint64_t type) {
  uint64_t initial_value;

  if (symbol == SYM_ASSIGN) {
    // "=" [ cast ] [ "-" ] value
    // global variable definition
    get_symbol();

    // optional: [ cast ]
    if (symbol == SYM_LPARENTHESIS) {
      get_symbol();

      compile_cast(type);
    } else if (type != UINT64_T)
      type_warning(type, UINT64_T);

    // optional: [ "-" ]
    if (symbol == SYM_MINUS) {
      integer_is_signed = 1;

      get_symbol();
    }

    // value
    initial_value = compile_value();

    if (integer_is_signed) {
      integer_is_signed = 0;

      return -initial_value;
    } else
      return initial_value;
  } else
    // uninitialized global variables are initialized to 0
    return 0;

  // initial value is grammar attribute
}

uint64_t compile_cast(uint64_t type) {
  uint64_t cast;

  // lookahead of 1: "(" already parsed

  cast = compile_type();

  if (type != UNDECLARED_T)
    if (type != cast)
      type_warning(type, cast);

  get_expected_symbol(SYM_RPARENTHESIS);

  return cast;
}

uint64_t compile_value() {
  uint64_t value;

  if (is_value()) {
    value = literal;

    get_symbol();

    return value;
  } else {
    syntax_error_unexpected_symbol();

    return 0;
  }
}

void compile_statement() {
  char* variable_or_procedure;

  // assert: allocated_temporaries == 0

  // synchronizing on strong symbols in case of syntax errors
  while (is_not_statement()) {
    syntax_error_unexpected_symbol();

    if (symbol == SYM_EOF)
      exit(EXITCODE_PARSERERROR);
    else
      get_symbol();
  }

  if (symbol == SYM_ASTERISK) {
    // assignment: "*" ...
    compile_assignment((char*) 0);

    get_expected_symbol(SYM_SEMICOLON);
  } else if (symbol == SYM_IDENTIFIER) {
    variable_or_procedure = identifier;

    get_symbol();

    if (symbol != SYM_LPARENTHESIS)
      // assignment: identifier ...
      compile_assignment(variable_or_procedure);
    else
      // procedure call: identifier "(" ... ")"
      compile_call(variable_or_procedure);

    get_expected_symbol(SYM_SEMICOLON);
  } else if (symbol == SYM_IF)
    compile_if();
  else if (symbol == SYM_WHILE)
    compile_while();
  else if (symbol == SYM_RETURN) {
    compile_return();

    get_expected_symbol(SYM_SEMICOLON);
  }

  // assert: allocated_temporaries == 0
}

uint64_t load_upper_value(uint64_t reg, uint64_t value) {
  uint64_t lower;
  uint64_t upper;

  // assert: -2^31 <= value < 2^31

  lower = get_bits(value,  0, 12);
  upper = get_bits(value, 12, 20);

  if (lower >= two_to_the_power_of(11)) {
    // add 1 which is effectively 2^12 to cancel sign extension of lower
    upper = upper + 1;

    // assert: 0 < upper <= 2^(32-12)
    emit_lui(reg, sign_extend(upper, 20));

    if (upper == two_to_the_power_of(19))
      // for example, value == 2147481600, in binary:

      // ..00 0111 1111 1111 1111 1111 1000 0000 0000
      //      ======================== --------------
      //               upper               lower

      // ..00 1000 0000 0000 0000 0000 1000 0000 0000
      //      ======================== --------------
      //             upper + 1             lower

      // ..11 1000 0000 0000 0000 0000 0000 0000 0000 = (upper + 1) sign-extended << 12
      // ..11 1111 1111 1111 1111 1111 1000 0000 0000 = lower sign-extended
      // ++++++++++++++++++++++++++++++++++++++++++++
      // ..11 0111 1111 1111 1111 1111 1000 0000 0000 = incorrect with 64 bits

      // upper overflowed, cancel sign extension (redundant for 32-bit systems)
      emit_sub(reg, REG_ZR, reg);

      // ..11 1000 0000 0000 0000 0000 0000 0000 0000 = (upper + 1) sign-extended << 12
      // --------------------------------------------
      // ..00 0111 1111 1111 1111 1111 1111 1111 1111 = one'complement
      // ..00 0000 0000 0000 0000 0000 0000 0000 0001 = 1
      // ++++++++++++++++++++++++++++++++++++++++++++
      // ..00 1000 0000 0000 0000 0000 0000 0000 0000 = 0 - ((upper + 1) sign-extended << 12)
      // ..11 1111 1111 1111 1111 1111 1000 0000 0000 = lower sign-extended
      // ++++++++++++++++++++++++++++++++++++++++++++
      // ..00 0111 1111 1111 1111 1111 1000 0000 0000 = correct with 32-bit and 64-bit systems
  } else
    // assert: 0 < upper < 2^(32-12)
    emit_lui(reg, sign_extend(upper, 20));

  return sign_extend(lower, 12);
}

uint64_t load_upper_address(uint64_t* entry) {
  uint64_t offset;

  offset = get_address(entry);

  if (not(is_signed_integer(offset, 12))) {
    // offset does not fit 12-bit immediate value
    talloc();

    offset = load_upper_value(current_temporary(), offset);

    // calculate new base
    emit_add(current_temporary(), get_scope(entry), current_temporary());
  }

  // base changed to current temporary if offset != get_address(entry)
  return offset;
}

uint64_t load_value(uint64_t* entry) {
  uint64_t offset;

  // assert: n = allocated_temporaries

  offset = load_upper_address(entry);

  if (offset == get_address(entry)) {
    // offset fits 12-bit immediate value
    talloc();

    emit_load(current_temporary(), get_scope(entry), offset);
  } else
    // assert: current temporary is scope register + 20-MSB part of original offset
    // assert: offset is remaining 12-LSB part of original offset
    emit_load(current_temporary(), current_temporary(), offset);

  // assert: allocated_temporaries == n + 1

  return get_type(entry);
}

uint64_t* get_variable_entry(char* variable) {
  uint64_t* entry;

  entry = get_scoped_symbol_table_entry(variable);

  if (entry == (uint64_t*) 0) {
    syntax_error_undeclared_identifier(variable);

    // TODO: declare global variables to continue parsing

    exit(EXITCODE_PARSERERROR);
  }

  return entry;
}

uint64_t load_variable(char* variable) {
  return load_value(get_variable_entry(variable));
}

void compile_assignment(char* variable) {
  uint64_t dereference;
  uint64_t* entry;
  uint64_t base;
  uint64_t offset;
  uint64_t ltype;
  uint64_t rtype;

  // assert: identifier has already been parsed if variable != (char*) 0

  // assert: allocated_temporaries == 0

  if (variable != (char*) 0) {
    // variable is identifier
    dereference = 0;

    entry = get_variable_entry(variable);

    base = get_scope(entry);

    // load variable upper address, if needed
    offset = load_upper_address(entry);

    if (offset != get_address(entry))
      // assert: allocated_temporaries == 1
      base = current_temporary();

    ltype = get_type(entry);
  } else {
    // "*" identifier | "*" "(" expression ")"
    get_required_symbol(SYM_ASTERISK);

    dereference = 1;

    if (symbol == SYM_IDENTIFIER) {
      variable = identifier;

      get_symbol();

      // load variable value (as address)
      ltype = load_variable(variable);
    } else if (symbol == SYM_LPARENTHESIS) {
      get_symbol();

      // load expression value (as address)
      ltype = compile_arithmetic();

      get_expected_symbol(SYM_RPARENTHESIS);
    } else {
      syntax_error_unexpected_symbol();

      // we need allocated_temporaries == 1
      // to keep running the compiler
      talloc();

      // loading 0 to trigger store at address 0
      emit_addi(current_temporary(), REG_ZR, 0);

      // but no further warning below
      ltype = UINT64STAR_T;
    }

    // assert: allocated_temporaries == 1
    base   = current_temporary();
    offset = 0;
  }

  // assert: base + offset is address where to store

  if (symbol == SYM_ASSIGN) {
    get_symbol();

    if (dereference) {
      if (ltype != UINT64STAR_T)
        type_warning(UINT64STAR_T, ltype);
      else
        ltype = UINT64_T;
    }

    rtype = compile_expression();

    if (ltype != rtype)
      type_warning(ltype, rtype);

    // assign value of RHS in current temporary to LHS at base + offset
    emit_store(base, offset, current_temporary());

    tfree(1);

    number_of_assignments = number_of_assignments + 1;
  } else
    syntax_error_expected_symbol(SYM_ASSIGN);

  if (dereference)
    // assert: allocated_temporaries == 1
    tfree(1);
  else if (offset != get_address(entry))
    // assert: allocated_temporaries == 1
    tfree(1);

  // assert: allocated_temporaries == 0
}

uint64_t compile_expression() {
  uint64_t ltype;
  uint64_t operator_symbol;
  uint64_t rtype;

  // assert: n = allocated_temporaries

  ltype = compile_arithmetic();

  // assert: allocated_temporaries == n + 1

  //optional: ==, !=, <, >, <=, >= simple_expression
  if (is_comparison()) {
    operator_symbol = symbol;

    get_symbol();

    rtype = compile_arithmetic();

    // assert: allocated_temporaries == n + 2

    if (ltype != rtype)
      type_warning(ltype, rtype);

    // for lack of boolean type
    ltype = UINT64_T;

    if (operator_symbol == SYM_EQUALITY) {
      // a == b iff unsigned b - a < 1
      emit_sub(previous_temporary(), current_temporary(), previous_temporary());
      emit_addi(current_temporary(), REG_ZR, 1);
      emit_sltu(previous_temporary(), previous_temporary(), current_temporary());

      tfree(1);

    } else if (operator_symbol == SYM_NOTEQ) {
      // a != b iff unsigned 0 < b - a
      emit_sub(previous_temporary(), current_temporary(), previous_temporary());

      tfree(1);

      emit_sltu(current_temporary(), REG_ZR, current_temporary());

    } else if (operator_symbol == SYM_LT) {
      // a < b
      emit_sltu(previous_temporary(), previous_temporary(), current_temporary());

      tfree(1);

    } else if (operator_symbol == SYM_GT) {
      // a > b iff b < a
      emit_sltu(previous_temporary(), current_temporary(), previous_temporary());

      tfree(1);

    } else if (operator_symbol == SYM_LEQ) {
      // a <= b iff 1 - (b < a)
      emit_sltu(previous_temporary(), current_temporary(), previous_temporary());
      emit_addi(current_temporary(), REG_ZR, 1);
      emit_sub(previous_temporary(), current_temporary(), previous_temporary());

      tfree(1);

    } else if (operator_symbol == SYM_GEQ) {
      // a >= b iff 1 - (a < b)
      emit_sltu(previous_temporary(), previous_temporary(), current_temporary());
      emit_addi(current_temporary(), REG_ZR, 1);
      emit_sub(previous_temporary(), current_temporary(), previous_temporary());

      tfree(1);
    }
  }

  // assert: allocated_temporaries == n + 1

  // type of expression is grammar attribute
  return ltype;
}

uint64_t compile_arithmetic() {
  uint64_t ltype;
  uint64_t operator_symbol;
  uint64_t rtype;

  // assert: n = allocated_temporaries

  ltype = compile_term();

  // assert: allocated_temporaries == n + 1

  while (is_plus_or_minus()) {
    operator_symbol = symbol;

    get_symbol();

    rtype = compile_term();

    // assert: allocated_temporaries == n + 2

    if (operator_symbol == SYM_PLUS) {
      if (ltype == UINT64STAR_T) {
        if (rtype == UINT64_T)
          // UINT64STAR_T + UINT64_T
          // pointer arithmetic: left_term + right_term * SIZEOFUINT
          emit_multiply_by(current_temporary(), SIZEOFUINT);
        else
          // UINT64STAR_T + UINT64STAR_T
          syntax_error_message("(uint64_t*) + (uint64_t*) is undefined");
      } else if (rtype == UINT64STAR_T) {
        // UINT64_T + UINT64STAR_T
        // pointer arithmetic: left_term * SIZEOFUINT + right_term
        emit_multiply_by(previous_temporary(), SIZEOFUINT);

        ltype = UINT64STAR_T;
      }

      emit_add(previous_temporary(), previous_temporary(), current_temporary());

    } else if (operator_symbol == SYM_MINUS) {
      if (ltype == UINT64STAR_T) {
        if (rtype == UINT64_T) {
          // UINT64STAR_T - UINT64_T
          // pointer arithmetic: left_term - right_term * SIZEOFUINT
          emit_multiply_by(current_temporary(), SIZEOFUINT);
          emit_sub(previous_temporary(), previous_temporary(), current_temporary());
        } else {
          // UINT64STAR_T - UINT64STAR_T
          // pointer arithmetic: (left_term - right_term) / SIZEOFUINT
          emit_sub(previous_temporary(), previous_temporary(), current_temporary());
          emit_addi(current_temporary(), REG_ZR, SIZEOFUINT);
          emit_divu(previous_temporary(), previous_temporary(), current_temporary());

          ltype = UINT64_T;
        }
      } else if (rtype == UINT64STAR_T)
        // UINT64_T - UINT64STAR_T
        syntax_error_message("(uint64_t) - (uint64_t*) is undefined");
      else
        // UINT64_T - UINT64_T
        emit_sub(previous_temporary(), previous_temporary(), current_temporary());
    }

    tfree(1);
  }

  // assert: allocated_temporaries == n + 1

  // type of simple expression is grammar attribute
  return ltype;
}

uint64_t compile_term() {
  uint64_t ltype;
  uint64_t operator_symbol;
  uint64_t rtype;

  // assert: n = allocated_temporaries

  ltype = compile_factor();

  // assert: allocated_temporaries == n + 1

  while (is_mult_or_div_or_rem()) {
    operator_symbol = symbol;

    get_symbol();

    rtype = compile_factor();

    // assert: allocated_temporaries == n + 2

    if (ltype != rtype)
      type_warning(ltype, rtype);

    if (operator_symbol == SYM_ASTERISK)
      emit_mul(previous_temporary(), previous_temporary(), current_temporary());
    else if (operator_symbol == SYM_DIVISION)
      emit_divu(previous_temporary(), previous_temporary(), current_temporary());
    else if (operator_symbol == SYM_REMAINDER)
      emit_remu(previous_temporary(), previous_temporary(), current_temporary());

    tfree(1);
  }

  // assert: allocated_temporaries == n + 1

  // type of term is grammar attribute
  return ltype;
}

uint64_t compile_factor() {
  uint64_t cast;
  uint64_t type;
  uint64_t negative;
  uint64_t dereference;
  char* variable_or_procedure;

  // assert: n = allocated_temporaries

  // synchronizing on strong symbols in case of syntax errors
  while (is_not_factor()) {
    syntax_error_unexpected_symbol();

    if (symbol == SYM_EOF)
      exit(EXITCODE_PARSERERROR);
    else
      get_symbol();
  }

  // optional: cast
  cast = 0;
  if (symbol == SYM_LPARENTHESIS) {
    get_symbol();

    if (is_type())
      // cast: "(" "uint64_t" [ "*" ] ")"
      cast = compile_cast(UNDECLARED_T);
    else {
      // not a cast but: "(" expression ")"
      type = compile_expression();

      get_expected_symbol(SYM_RPARENTHESIS);

      // assert: allocated_temporaries == n + 1

      return type;
    }
  }
  // optional: "-"
  if (symbol == SYM_MINUS) {
    negative = 1;

    integer_is_signed = 1;

    get_symbol();

    integer_is_signed = 0;
  } else
    negative = 0;
  // optional: "*"
  if (symbol == SYM_ASTERISK) {
    dereference = 1;

    get_symbol();
  } else
    dereference = 0;

  if (symbol == SYM_SIZEOF) {
    // "sizeof" "(" type ")"
    get_symbol();

    get_expected_symbol(SYM_LPARENTHESIS);

    type = compile_type();

    get_expected_symbol(SYM_RPARENTHESIS);

    talloc();

    // bootstrap with target-dependent size, not sizeof
    if (type == UINT64_T)
      emit_addi(current_temporary(), REG_ZR, SIZEOFUINT);
    else
      emit_addi(current_temporary(), REG_ZR, SIZEOFUINTSTAR);

    type = UINT64_T;
  } else if (is_literal())
    // integer, character, or string literal
    type = compile_literal();
  else if (symbol == SYM_IDENTIFIER) {
    variable_or_procedure = identifier;

    get_symbol();

    if (symbol != SYM_LPARENTHESIS)
      // variable access: identifier ...
      type = load_variable(variable_or_procedure);
    else {
      // procedure call: identifier "(" ... ")"
      type = compile_call(variable_or_procedure);

      talloc();

      if (type != VOID_T)
        // retrieve return value
        emit_addi(current_temporary(), REG_A0, 0);
      else
        // TODO: check if used as actual parameter
        // for now just load deterministic value
        emit_addi(current_temporary(), REG_ZR, 0);
    }
  } else if (symbol == SYM_LPARENTHESIS) {
    // "(" expression ")"
    get_symbol();

    type = compile_expression();

    get_expected_symbol(SYM_RPARENTHESIS);
  } else {
    syntax_error_unexpected_symbol();

    // we must allocate an additional temporary
    load_integer(0);

    type = UINT64_T;
  }

  if (dereference) {
    if (type != UINT64STAR_T)
      type_warning(UINT64STAR_T, type);

    // dereference
    emit_load(current_temporary(), current_temporary(), 0);

    type = UINT64_T;
  }
  if (negative) {
    if (type != UINT64_T) {
      type_warning(UINT64_T, type);

      type = UINT64_T;
    }
    // subtract from 0
    emit_sub(current_temporary(), REG_ZR, current_temporary());
  }

  // assert: allocated_temporaries == n + 1

  if (cast != 0)
    // cast is grammar attribute
    return cast;
  else
    // type of factor is grammar attribute
    return type;
}

void load_small_and_medium_integer(uint64_t reg, uint64_t value) {
  // assert: -2^31 <= value < 2^31

  if (is_signed_integer(value, 12))
    // integers with -2^11 <= value < 2^11
    // are loaded with one addi into a register
    emit_addi(reg, REG_ZR, value);
  else {
    // integers with -2^31 <= value < -2^11 and 2^11 <= value < 2^31
    // are loaded with one lui and one addi into a register plus
    // an additional sub to cancel sign extension if necessary
    value = load_upper_value(reg, value);

    emit_addi(reg, reg, value);
  }
}

void load_big_integer(uint64_t value) {
  uint64_t* entry;

  // assert: n = allocated_temporaries

  // reuse big integers
  entry = search_global_symbol_table(integer, BIGINT);

  if (entry == (uint64_t*) 0) {
    // allocate memory for big integer in data segment
    data_size = data_size + WORDSIZE;

    entry = create_symbol_table_entry(GLOBAL_TABLE, integer,
      line_number, BIGINT, UINT64_T, value, -data_size);

    emit_data_word(value, -data_size, line_number);

    dc_big_integer = dc_big_integer + 1;
  }

  load_value(entry);

  // assert: allocated_temporaries == n + 1
}

void load_integer(uint64_t value) {
  // assert: n = allocated_temporaries

  if (is_signed_integer(value, 32)) {
    // integers with -2^31 <= value < 2^31 are loaded as immediate values
    talloc();

    load_small_and_medium_integer(current_temporary(), value);
  } else
    // integers with value < -2^31 or value >= 2^31 are stored in data segment
    load_big_integer(value);

  // assert: allocated_temporaries == n + 1
}

void load_address(uint64_t* entry) {
  // assert: n = allocated_temporaries

  load_integer(get_address(entry));

  emit_add(current_temporary(), get_scope(entry), current_temporary());

  // assert: allocated_temporaries == n + 1
}

void load_string() {
  uint64_t* entry;

  // assert: n = allocated_temporaries

  // reuse string literals
  entry = search_global_symbol_table(string, STRING);

  if (entry == (uint64_t*) 0) {
    // allocate memory for string in data segment
    data_size = data_size + round_up(string_length(string) + 1, WORDSIZE);

    entry = create_symbol_table_entry(GLOBAL_TABLE, string,
      line_number, STRING, UINT64STAR_T, 0, -data_size);

    emit_string_data(entry);
  }

  load_address(entry);

  // assert: allocated_temporaries == n + 1

  number_of_string_literals = number_of_string_literals + 1;
}

uint64_t compile_literal() {
  // assert: n = allocated_temporaries

  if (is_value()) {
    load_integer(literal);

    // assert: allocated_temporaries == n + 1

    compile_value();

    return UINT64_T;
  } else if (symbol == SYM_STRING) {
    load_string();

    // assert: allocated_temporaries == n + 1

    get_symbol();

    return UINT64STAR_T;
  } else {
    syntax_error_unexpected_symbol();

    // we must allocate an additional temporary
    load_integer(0);

    // assert: allocated_temporaries == n + 1

    return UINT64_T;
  }
}

void compile_if() {
  uint64_t branch_forward_to_else_or_end;
  uint64_t jump_forward_to_end;

  // assert: allocated_temporaries == 0

  if (symbol == SYM_IF) {
    get_symbol();

    if (symbol == SYM_LPARENTHESIS) {
      // "if" "(" expression ")"
      get_symbol();

      compile_expression();

      // if the "if" condition is false we skip the true case
      // by branching to "else" (if provided)
      branch_forward_to_else_or_end = code_size;

      // the target address is still unknown, using 0 for now
      emit_beq(current_temporary(), REG_ZR, 0);

      tfree(1);

      if (symbol == SYM_RPARENTHESIS) {
        get_symbol();

        if (symbol == SYM_LBRACE) {
          // zero or more statements: "{" { statement } "}"
          get_symbol();

          while (is_neither_rbrace_nor_eof())
            // assert: allocated_temporaries == 0
            compile_statement();

          get_required_symbol(SYM_RBRACE);
        } else
          // only one statement without {}
          compile_statement();

        if (symbol == SYM_ELSE) {
          // optional: "else"
          get_symbol();

          // if the "if" condition was true we skip "else"
          // by unconditionally jumping to the end
          jump_forward_to_end = code_size;

          // the target address is still unknown, using 0 for now
          emit_jal(REG_ZR, 0);

          // if the "if" condition was false we branch here
          fixup_BFormat(branch_forward_to_else_or_end);

          if (symbol == SYM_LBRACE) {
            // zero or more statements: "{" { statement } "}"
            get_symbol();

            while (is_neither_rbrace_nor_eof())
              // assert: allocated_temporaries == 0
              compile_statement();

            get_required_symbol(SYM_RBRACE);
          } else
            // only one statement without "{" "}"
            compile_statement();

          // if the "if" condition was true we unconditionally jump here
          fixup_JFormat(jump_forward_to_end, code_size);
        } else
          // if the "if" condition was false we branch here
          fixup_BFormat(branch_forward_to_else_or_end);
      } else
        syntax_error_expected_symbol(SYM_RPARENTHESIS);
    } else
      syntax_error_expected_symbol(SYM_LPARENTHESIS);
  } else
    syntax_error_expected_symbol(SYM_IF);

  // assert: allocated_temporaries == 0

  number_of_if = number_of_if + 1;
}

void compile_while() {
  uint64_t jump_back_to_while;
  uint64_t branch_forward_to_end;

  // assert: allocated_temporaries == 0

  jump_back_to_while = code_size;

  branch_forward_to_end = 0;

  if (symbol == SYM_WHILE) {
    // "while" "(" expression ")"
    get_symbol();

    if (symbol == SYM_LPARENTHESIS) {
      get_symbol();

      compile_expression();

      // if the "while" condition is false
      // we skip the "while" body by branching to the end
      branch_forward_to_end = code_size;

      // the target address is still unknown, using 0 for now
      emit_beq(current_temporary(), REG_ZR, 0);

      tfree(1);

      if (symbol == SYM_RPARENTHESIS) {
        get_symbol();

        if (symbol == SYM_LBRACE) {
          // zero or more statements: "{" { statement } "}"
          get_symbol();

          while (is_neither_rbrace_nor_eof())
            // assert: allocated_temporaries == 0
            compile_statement();

          get_required_symbol(SYM_RBRACE);
        } else
          // only one statement without "{" "}"
          compile_statement();
      } else
        syntax_error_expected_symbol(SYM_RPARENTHESIS);
    } else
      syntax_error_expected_symbol(SYM_LPARENTHESIS);
  } else
    syntax_error_expected_symbol(SYM_WHILE);

  // we use JAL for the unconditional jump back to the loop condition because:
  // 1. the RISC-V doc recommends to do so to not disturb branch prediction
  // 2. GCC also uses JAL for the unconditional back jump of a while loop
  emit_jal(REG_ZR, jump_back_to_while - code_size);

  if (branch_forward_to_end != 0)
    // first instruction after loop body will be generated here
    // now we have the address for the conditional branch from above
    fixup_BFormat(branch_forward_to_end);

  // assert: allocated_temporaries == 0

  number_of_while = number_of_while + 1;
}

char* bootstrap_non_0_boot_level_procedures(char* procedure) {
  // define non-0-boot-level procedures without prefix in name
  // copy names to obtain unique hash for global symbol table
  if (string_compare(procedure, "non_0_boot_level_printf"))
    return string_copy("printf");
  else if (string_compare(procedure, "non_0_boot_level_sprintf"))
    return string_copy("sprintf");
  else if (string_compare(procedure, "non_0_boot_level_dprintf"))
    return string_copy("dprintf");
  else
    return procedure;
}

uint64_t is_boot_level_0_only_procedure(char* procedure) {
  // the following procedures are only explicitly defined on boot level 0
  if (string_compare(procedure, "zalloc"))
    return 1;
  else if (string_compare(procedure, "hypster_switch"))
    return 1;
  else if (GC_ON) {
    if (string_compare(procedure, "fetch_stack_pointer"))
      return 1;
    else if (string_compare(procedure, "fetch_global_pointer"))
      return 1;
    else if (string_compare(procedure, "fetch_data_segment_size"))
      return 1;
  }

  return 0;
}

void procedure_prologue(uint64_t number_of_local_variable_bytes) {
  // allocate memory for return address
  emit_addi(REG_SP, REG_SP, -WORDSIZE);

  // save return address
  emit_store(REG_SP, 0, REG_RA);

  // allocate memory for caller's frame pointer
  emit_addi(REG_SP, REG_SP, -WORDSIZE);

  // save caller's frame pointer
  emit_store(REG_SP, 0, REG_S0);

  // set callee's frame pointer
  emit_addi(REG_S0, REG_SP, 0);

  // allocate memory for callee's local variables
  if (number_of_local_variable_bytes > 0) {
    if (is_signed_integer(-number_of_local_variable_bytes, 12))
      emit_addi(REG_SP, REG_SP, -number_of_local_variable_bytes);
    else {
      syntax_error_message("too many local variables");

      exit(EXITCODE_COMPILERERROR);
    }
  }
}

void procedure_epilogue(uint64_t number_of_local_variable_bytes, uint64_t number_of_parameter_bytes) {
  if (number_of_local_variable_bytes > 0)
    // deallocate memory for local variables
    emit_addi(REG_SP, REG_S0, 0);

  // restore caller's frame pointer
  emit_load(REG_S0, REG_SP, 0);

  // deallocate memory for caller's frame pointer
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  // restore return address
  emit_load(REG_RA, REG_SP, 0);

  if (is_signed_integer(WORDSIZE + number_of_parameter_bytes, 12))
    // deallocate memory for return address and (non-variadic) actual parameters
    emit_addi(REG_SP, REG_SP, WORDSIZE + number_of_parameter_bytes);
  else {
    syntax_error_message("too many formal parameters");

    exit(EXITCODE_COMPILERERROR);
  }
}

void compile_procedure(char* procedure, uint64_t type) {
  uint64_t is_variadic;
  uint64_t number_of_formal_parameters;
  uint64_t* entry;
  uint64_t number_of_local_variable_bytes;

  // lookahead of 1: identifier already parsed into procedure (type may be left-factored)

  local_symbol_table = (uint64_t*) 0;

  // assuming procedure is not variadic
  is_variadic = 0;

  number_of_formal_parameters = 0;

  // try parsing formal parameters

  if (symbol == SYM_LPARENTHESIS) {
    get_symbol();

    if (symbol != SYM_RPARENTHESIS) {
      // try parsing first formal parameter
      if (is_type()) {
        entry = compile_variable((char*) 0, compile_type(), 0);

        number_of_formal_parameters = 1;

        // 2 * WORDSIZE offset to skip frame pointer and link
        // additional offset (number_of_formal_parameters - 1) * WORDSIZE
        // since actual parameters are pushed onto stack in reverse
        set_address(entry, 2 * WORDSIZE + (number_of_formal_parameters - 1) * WORDSIZE);

        while (is_possibly_parameter(is_variadic)) {
          // try parsing next formal parameter
          get_symbol();

          if (symbol == SYM_ELLIPSIS) {
            get_symbol();

            is_variadic = 1;
          } else if (is_type()) {
            entry = compile_variable((char*) 0, compile_type(), 0);

            number_of_formal_parameters = number_of_formal_parameters + 1;

            set_address(entry, 2 * WORDSIZE + (number_of_formal_parameters - 1) * WORDSIZE);
          } else
            syntax_error_expected_symbol(SYM_UINT64);
        }

        get_expected_symbol(SYM_RPARENTHESIS);
      } else
        syntax_error_expected_symbol(SYM_UINT64);
    } else
      get_symbol();
  } else
    syntax_error_expected_symbol(SYM_LPARENTHESIS);

  if (is_variadic)
    // negative number of formal parameters indicates procedure is variadic
    number_of_formal_parameters = -number_of_formal_parameters;

  // try parsing rest of procedure declaration or definition

  procedure = bootstrap_non_0_boot_level_procedures(procedure);

  // look up procedure to see if it has been called, declared, or even defined
  entry = search_global_symbol_table(procedure, PROCEDURE);

  if (entry == (uint64_t*) 0)
    // procedure never called nor declared nor defined
    entry = create_symbol_table_entry(GLOBAL_TABLE, procedure,
      line_number, PROCEDURE, type, number_of_formal_parameters, 0);
  else if (get_type(entry) == UNDECLARED_T)
    // procedure already called but neither declared nor defined
    set_type(entry, type);

  if (symbol == SYM_SEMICOLON) {
    // this is a procedure declaration

    if (get_type(entry) != type)
      // procedure already declared or even defined before;
      // warn about mismatching return type but otherwise ignore
      type_warning(get_type(entry), type);

    get_symbol();
  } else if (symbol == SYM_LBRACE) {
    // this is a procedure definition

    if (is_undefined_procedure(entry)) {
      set_line_number(entry, line_number);

      if (get_type(entry) != type) {
        // procedure already declared but with different return type
        type_warning(get_type(entry), type);

        // return type of definition counts
        set_type(entry, type);
      }

      if (get_address(entry) != 0)
        // procedure already called but not defined
        fixlink_JFormat(get_address(entry), code_size);

      set_address(entry, code_size);

      if (string_compare(procedure, main_name))
        // first source containing main procedure provides binary name
        binary_name = source_name;

      // only accounting for procedures defined in source code
      number_of_procedures = number_of_procedures + 1;
    } else if (not(is_boot_level_0_only_procedure(procedure))) {
      // procedure already defined on all boot levels
      print_line_number("warning", line_number);
      printf("redefinition of procedure %s ignored\n", procedure);
    }

    get_symbol();

    // try parsing local variable declarations

    number_of_local_variable_bytes = 0;

    while (is_type()) {
      // try parsing next local variable declaration
      number_of_local_variable_bytes = number_of_local_variable_bytes + WORDSIZE;

      // offset of local variables relative to frame pointer is negative
      compile_variable((char*) 0, compile_type(), -number_of_local_variable_bytes);

      get_expected_symbol(SYM_SEMICOLON);
    }

    // try parsing statements in procedure body

    procedure_prologue(number_of_local_variable_bytes);

    // macros require access to current procedure
    current_procedure = entry;

    // create a fixup chain for return statements
    return_jumps = 0;

    return_type = type;

    while (is_neither_rbrace_nor_eof())
      // assert: allocated_temporaries == 0
      compile_statement();

    return_type = 0;

    if (type != VOID_T)
      // TODO: warn if reachable (missing return)
      // reset return register
      emit_addi(REG_A0, REG_ZR, 0);

    // all return statements jump here
    fixlink_JFormat(return_jumps, code_size);

    return_jumps = 0;

    if (is_variadic)
      procedure_epilogue(number_of_local_variable_bytes,
        -number_of_formal_parameters * WORDSIZE);
    else
      procedure_epilogue(number_of_local_variable_bytes,
        number_of_formal_parameters * WORDSIZE);

    // return
    emit_jalr(REG_ZR, REG_RA, 0);

    get_expected_symbol(SYM_RBRACE);
  } else
    syntax_error_unexpected_symbol();

  current_procedure = (uint64_t*) 0;

  local_symbol_table = (uint64_t*) 0;

  // assert: allocated_temporaries == 0
}

uint64_t save_temporaries() {
  uint64_t number_of_temporaries;

  number_of_temporaries = allocated_temporaries;

  while (allocated_temporaries > 0) {
    // push temporary onto stack
    emit_addi(REG_SP, REG_SP, -WORDSIZE);
    emit_store(REG_SP, 0, current_temporary());

    tfree(1);
  }

  return number_of_temporaries;
}

void restore_temporaries(uint64_t number_of_temporaries) {
  while (allocated_temporaries < number_of_temporaries) {
    talloc();

    // restore temporary from stack
    emit_load(current_temporary(), REG_SP, 0);
    emit_addi(REG_SP, REG_SP, WORDSIZE);
  }
}

uint64_t macro_expand(uint64_t* entry) {
  char* name;

  name = get_string(entry);

  if (string_compare(name, "var_start"))
    macro_var_start();
  else if (string_compare(name, "var_arg"))
    macro_var_arg();
  else if (string_compare(name, "var_end"))
    macro_var_end();

  // return type is grammar attribute
  return get_type(entry);
}

uint64_t procedure_call(uint64_t* entry, char* procedure, uint64_t number_of_actual_parameters) {
  uint64_t type;

  if (entry == (uint64_t*) 0) {
    // procedure never called nor declared nor defined
    syntax_error_undeclared_identifier(procedure);

    // return type will be determined by procedure declaration or definition
    type = UNDECLARED_T;

    create_symbol_table_entry(GLOBAL_TABLE, procedure,
      line_number, PROCEDURE, type, number_of_actual_parameters, code_size);

    emit_jal(REG_RA, 0);

  } else {
    type = get_type(entry);

    if (get_address(entry) == 0) {
      // procedure declared but never called nor defined
      set_address(entry, code_size);

      emit_jal(REG_RA, 0);
    } else if (get_opcode(load_instruction(get_address(entry))) == OP_JAL) {
      // procedure called and possibly declared but not defined

      // create fixup chain using absolute address
      emit_jal(REG_RA, get_address(entry));
      set_address(entry, code_size - INSTRUCTIONSIZE);
    } else
      // procedure defined, use relative address
      emit_jal(REG_RA, get_address(entry) - code_size);
  }

  // return type is grammar attribute
  return type;
}

uint64_t compile_call(char* procedure) {
  uint64_t* entry;
  uint64_t number_of_temporaries;
  uint64_t allocate_memory_on_stack;
  uint64_t number_of_actual_parameters;
  uint64_t number_of_formal_parameters;
  uint64_t type;

  // lookahead of 1: identifier already parsed into procedure

  get_symbol();

  entry = search_global_symbol_table(procedure, MACRO);

  if (entry != (uint64_t*) 0)
    // actually expanding a macro, not calling a procedure
    return macro_expand(entry);

  // assert: n = allocated_temporaries

  number_of_temporaries = save_temporaries();

  // assert: allocated_temporaries == 0

  if (is_expression()) {
    // try parsing first actual parameter
    compile_expression();

    // TODO: check if types of actual and formal parameters match

    // remember where to allocate memory on stack for actual parameters
    allocate_memory_on_stack = code_size;

    // we do not yet know how many bytes are needed, fixup later
    emit_addi(REG_SP, REG_SP, 0);

    // push first actual parameter onto top (!) of stack
    emit_store(REG_SP, 0, current_temporary());

    tfree(1);

    number_of_actual_parameters = 1;

    while (symbol == SYM_COMMA) {
      // try parsing next actual parameter
      get_symbol();

      compile_expression();

      // push next actual parameter onto stack in reverse (!) order
      emit_store(REG_SP, number_of_actual_parameters * WORDSIZE, current_temporary());

      tfree(1);

      number_of_actual_parameters = number_of_actual_parameters + 1;
    }

    // now we know the number of actual parameters
    fixup_IFormat(allocate_memory_on_stack, -(number_of_actual_parameters * WORDSIZE));
  } else
    number_of_actual_parameters = 0;

  if (symbol == SYM_RPARENTHESIS) {
    // ready to call procedure
    get_symbol();

    entry = search_global_symbol_table(procedure, PROCEDURE);

    type = procedure_call(entry, procedure, number_of_actual_parameters);

    if (entry != (uint64_t*) 0) {
      // procedure declared or defined before this call
      number_of_formal_parameters = get_value(entry);

      if (signed_less_than(number_of_formal_parameters, 0)) {
        // variadic procedure
        number_of_formal_parameters = -number_of_formal_parameters;

        if (number_of_actual_parameters > number_of_formal_parameters)
          // deallocate variadic actual parameters
          emit_addi(REG_SP, REG_SP,
            (number_of_actual_parameters - number_of_formal_parameters) * WORDSIZE);
        else if (number_of_actual_parameters < number_of_formal_parameters)
          syntax_error_message("fewer actual than formal parameters");
      } else if (number_of_actual_parameters != number_of_formal_parameters)
        syntax_error_message("different number of actual and formal parameters");
    }
  } else {
    syntax_error_expected_symbol(SYM_RPARENTHESIS);

    type = UINT64_T;
  }

  // assert: allocated_temporaries == 0

  restore_temporaries(number_of_temporaries);

  // assert: allocated_temporaries == n

  number_of_calls = number_of_calls + 1;

  // return type is grammar attribute
  return type;
}

void compile_return() {
  uint64_t type;

  // assert: allocated_temporaries == 0

  get_expected_symbol(SYM_RETURN);

  // optional: expression
  if (symbol != SYM_SEMICOLON) {
    type = compile_expression();

    if (type != return_type)
      type_warning(return_type, type);

    if (return_type != VOID_T)
      // save value of expression in return register
      emit_addi(REG_A0, current_temporary(), 0);

    tfree(1);
  } else if (return_type != VOID_T) {
    type_warning(return_type, VOID_T);

    // reset return register
    emit_addi(REG_A0, REG_ZR, 0);
  }

  // jump to procedure epilogue through fixup chain using absolute address
  emit_jal(REG_ZR, return_jumps);

  // new head of fixup chain
  return_jumps = code_size - INSTRUCTIONSIZE;

  // assert: allocated_temporaries == 0

  number_of_return = number_of_return + 1;
}

// -----------------------------------------------------------------
// ---------------------------- MACROS -----------------------------
// -----------------------------------------------------------------

void macro_var_start() {
  uint64_t* var_list_variable;

  var_list_variable = (uint64_t*) 0;

  // #non-variadic parameters == - get_value(current_procedure)
  if (not(signed_less_than(get_value(current_procedure), 0)))
    syntax_error_message("'var_start' used in non-variadic procedure");

  if (symbol == SYM_IDENTIFIER) {
    var_list_variable = search_local_symbol_table(identifier);

    if (var_list_variable != (uint64_t*) 0) {
      get_symbol();

      if (symbol == SYM_RPARENTHESIS) {
        get_symbol();

        // skip the return address and frame pointer as well as non-variadic parameters
        load_integer(2 * WORDSIZE - get_value(current_procedure) * WORDSIZE);

        // address of first variadic parameter is:
        // S0 + 2 * WORDSIZE + #non-variadic parameters * WORDSIZE
        emit_add(current_temporary(), REG_S0, current_temporary());

        // store address in variable passed as macro argument
        emit_store(REG_S0, get_address(var_list_variable), current_temporary());

        tfree(1);
      }
    } else {
      syntax_error_undeclared_identifier(identifier);

      get_symbol();
    }
  } else
    syntax_error_expected_symbol(SYM_IDENTIFIER);
}

void macro_var_arg() {
  uint64_t* var_list_variable;
  uint64_t  var_list_address;

  var_list_variable = (uint64_t*) 0;
  var_list_address  = 0;

  if (symbol == SYM_IDENTIFIER) {
    var_list_variable = search_local_symbol_table(identifier);

    if (var_list_variable != (uint64_t*) 0) {
      get_symbol();

      if (symbol == SYM_RPARENTHESIS) {
        get_symbol();

        var_list_address = get_address(var_list_variable);

        talloc();

        // load variadic parameter from memory address pointed to
        // by variable passed as macro argument (var_list_variable)
        emit_load(current_temporary(), REG_S0, var_list_address);

        // store variadic parameter as return value of macro
        emit_load(REG_A0, current_temporary(), 0);

        // increment var_list_variable pointer by one parameter size (WORDSIZE)
        emit_addi(current_temporary(), current_temporary(), WORDSIZE);

        // store incremented address in variable passed as macro argument
        emit_store(REG_S0, var_list_address, current_temporary());

        tfree(1);
      } else
        syntax_error_expected_symbol(SYM_RPARENTHESIS);
    } else {
      syntax_error_undeclared_identifier(identifier);

      get_symbol();
    }
  } else
    syntax_error_expected_symbol(SYM_IDENTIFIER);
}

// implementation of va_start, va_arg, and va_end is platform-specific;
// RISC-V va_end does nothing and is only implemented for parity with standard C
void macro_var_end() {
  if (not(signed_less_than(get_value(current_procedure), 0)))
    syntax_error_message("'var_end' used in non-variadic procedure");

  if (symbol == SYM_IDENTIFIER) {
    get_symbol();

    get_expected_symbol(SYM_RPARENTHESIS);
  } else
    syntax_error_expected_symbol(SYM_IDENTIFIER);
}

// -----------------------------------------------------------------
// ------------------------ MACHINE CODE LIBRARY -------------------
// -----------------------------------------------------------------

void emit_round_up(uint64_t reg, uint64_t m) {
  talloc();

  // computes value(reg) + m - 1 - (value(reg) + m - 1) % m
  emit_addi(reg, reg, m - 1);
  emit_addi(current_temporary(), REG_ZR, m);
  emit_remu(current_temporary(), reg, current_temporary());
  emit_sub(reg, reg, current_temporary());

  tfree(1);
}

void emit_multiply_by(uint64_t reg, uint64_t m) {
  // assert: there is a 0 <= b < 11 such that m == 2^b

  // load multiplier less than 2^11 to avoid sign extension
  emit_addi(next_temporary(), REG_ZR, m);
  emit_mul(reg, reg, next_temporary());
}

void emit_program_entry() {
  uint64_t i;

  i = 0;

  // allocate memory for machine initialization code

  // emit exactly 21 NOPs with source code line 1
  while (i < 21) {
    emit_nop();

    i = i + 1;
  }
}

void emit_bootstrapping() {
  /*
      1. initialize global pointer
      2. initialize malloc's _bump pointer
      3. push argv pointer onto stack
      4. call main procedure
      5. proceed to exit procedure
  */
  uint64_t gp_value;
  uint64_t saved_code_size;
  uint64_t* entry;

  // code segment starts at PK_CODE_START
  code_start = PK_CODE_START;

  // code size must be memory-word-aligned
  if (code_size % WORDSIZE != 0)
    emit_nop();

  // start of data segment is page-aligned for clarity
  data_start = round_up(code_start + code_size, PAGESIZE);

  // calculate global pointer value
  gp_value = data_start + data_size;

  // further allocations in the data segment are not allowed at this point,
  // because it would increase data_size and therefore lead to a false gp_value

  // set code emission to program entry
  saved_code_size = code_size;
  code_size       = 0;

  // assert: emitting no more than 21 instructions, see emit_program_entry

  if (report_undefined_procedures()) {
    // if there are undefined procedures just exit
    // by loading exit code 0 into return register
    emit_addi(REG_A0, REG_ZR, 0);
  } else {
    // avoid sign extension that would result in an additional sub instruction
    if (gp_value < two_to_the_power_of(31) - two_to_the_power_of(11))
      // assert: generates no more than two instructions and
      // no data segment allocations in load_integer for gp_value
      load_integer(gp_value);
    else {
      syntax_error_message("maximum program break exceeded");

      exit(EXITCODE_COMPILERERROR);
    }

    // initialize global pointer
    emit_addi(REG_GP, current_temporary(), 0);

    tfree(1);

    // retrieve current program break in return register
    emit_addi(REG_A0, REG_ZR, 0);
    emit_addi(REG_A7, REG_ZR, SYSCALL_BRK);
    emit_ecall();

    // memory-word-align current program break
    emit_round_up(REG_A0, WORDSIZE);

    // set program break to word-aligned program break
    emit_addi(REG_A7, REG_ZR, SYSCALL_BRK);
    emit_ecall();

    // look up global variable _bump for storing malloc's bump pointer
    // use bump_name string to obtain unique hash
    entry = search_global_symbol_table(bump_name, VARIABLE);

    // store word-aligned program break in _bump
    emit_store(get_scope(entry), get_address(entry), REG_A0);

    // assert: stack is set up with argv pointer still missing

    //   sp
    //    |
    //    V
    // | argc | argv[0] | argv[1] | ... | argv[n]

    // first push argc again (!) onto stack

    talloc();

    emit_load(current_temporary(), REG_SP, 0);
    emit_addi(REG_SP, REG_SP, -WORDSIZE);
    emit_store(REG_SP, 0, current_temporary());

    //   sp  sp+TRGTWRDSZ sp+2*WORDSIZE
    //    |      |        |
    //    V      V        V
    // | argc | argc | argv[0] | argv[1] | ... | argv[n]

    // then overwrite below-top argc with &argv

    emit_addi(current_temporary(), REG_SP, 2 * WORDSIZE);
    emit_store(REG_SP, WORDSIZE, current_temporary());

    tfree(1);

    //   sp
    //    |        _______
    //    V       |       V
    // | argc | &argv | argv[0] | argv[1] | ... | argv[n]

    // assert: global, _bump, and stack pointers are set up
    //         with all other non-temporary registers zeroed

    // use main_name string to obtain unique hash
    entry = search_global_symbol_table(main_name, PROCEDURE);

    procedure_call(entry, main_name, get_value(entry));
  }

  // we exit with exit code in return register pushed onto the stack
  emit_addi(REG_SP, REG_SP, -WORDSIZE);
  emit_store(REG_SP, 0, REG_A0);

  // discount NOPs in profile that were generated for program entry
  ic_addi = ic_addi - code_size / INSTRUCTIONSIZE;

  // wrapper code for exit must follow here

  // restore original code size
  code_size = saved_code_size;
}

// -----------------------------------------------------------------
// --------------------------- COMPILER ----------------------------
// -----------------------------------------------------------------

uint64_t open_read_only(char* name) {
  return sign_extend(open(name, O_RDONLY, 0), SYSCALL_BITWIDTH);
}

void selfie_compile() {
  uint64_t link;
  uint64_t number_of_source_files;
  uint64_t fetch_dss_code_location;

  fetch_dss_code_location = 0;

  // link until next console option
  link = 1;

  number_of_source_files = 0;

  source_name = "builtins";

  binary_name = source_name;

  reset_binary();

  // allocate zeroed memory for storing binary
  code_binary = zmalloc(MAX_CODE_SIZE);
  data_binary = zmalloc(MAX_DATA_SIZE);

  // allocate zeroed memory for storing source code line numbers
  code_line_number = zmalloc(MAX_CODE_SIZE / INSTRUCTIONSIZE * sizeof(uint64_t));
  data_line_number = zmalloc(MAX_DATA_SIZE / WORDSIZE * sizeof(uint64_t));

  reset_symbol_tables();
  reset_binary_counters();

  emit_program_entry();

  // emit system call wrappers
  // exit code must be first
  emit_exit();
  emit_read();
  emit_write();
  emit_open();

  emit_malloc();

  emit_switch();

  if (GC_ON) {
    emit_fetch_stack_pointer();
    emit_fetch_global_pointer();

    // save code location of eventual fetch_data_segment_size implementation
    fetch_dss_code_location = code_size;

    emit_fetch_data_segment_size_interface();
  }

  // declare macros in global symbol table
  // copy names to obtain unique hash
  create_symbol_table_entry(GLOBAL_TABLE, string_copy("var_start"), 0, MACRO, VOID_T, 1, 0);
  create_symbol_table_entry(GLOBAL_TABLE, string_copy("var_arg"), 0, MACRO, UINT64_T, 1, 0);
  create_symbol_table_entry(GLOBAL_TABLE, string_copy("var_end"), 0, MACRO, VOID_T, 1, 0);

  // declare main procedure in global symbol table
  // use main_name string to obtain unique hash
  create_symbol_table_entry(GLOBAL_TABLE, main_name, 0, PROCEDURE, UINT64_T, 0, 0);

  while (link) {
    if (number_of_remaining_arguments() == 0)
      link = 0;
    else if (load_character(peek_argument(0), 0) == '-')
      link = 0;
    else {
      source_name = get_argument();

      number_of_source_files = number_of_source_files + 1;

      if (number_of_source_files > 1)
        printf("%s: ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n", selfie_name);

      printf("%s: selfie compiling %s to %lu-bit RISC-U with %lu-bit starc\n", selfie_name,
        source_name, WORDSIZEINBITS, SIZEOFUINT64INBITS);

      // assert: source_name is mapped and not longer than MAX_FILENAME_LENGTH

      source_fd = open_read_only(source_name);

      if (signed_less_than(source_fd, 0)) {
        printf("%s: could not open input file %s\n", selfie_name, source_name);

        exit(EXITCODE_IOERROR);
      }

      reset_scanner();
      reset_parser();

      compile_cstar();

      printf("%s: --------------------------------------------------------------------------------\n", selfie_name);
      printf("%s: %lu characters read in %lu lines and %lu comments\n", selfie_name,
        number_of_read_characters,
        line_number,
        number_of_comments);

      printf("%s: with %lu(%lu.%.2lu%%) characters in %lu actual symbols\n", selfie_name,
        number_of_read_characters - number_of_ignored_characters,
        percentage_format_integral_2(number_of_read_characters, number_of_read_characters - number_of_ignored_characters),
        percentage_format_fractional_2(number_of_read_characters, number_of_read_characters - number_of_ignored_characters),
        number_of_scanned_symbols);

      printf("%s: %lu global variables, %lu procedures, %lu string literals\n", selfie_name,
        number_of_global_variables,
        number_of_procedures,
        number_of_string_literals);

      printf("%s: %lu assignments, %lu while, %lu if, %lu calls, %lu return\n", selfie_name,
        number_of_assignments,
        number_of_while,
        number_of_if,
        number_of_calls,
        number_of_return);

      if (number_of_syntax_errors != 0) {
        printf("%s: encountered %lu syntax errors while compiling %s - omitting output\n",
          selfie_name,
          number_of_syntax_errors,
          source_name);
        exit(EXITCODE_SYNTAXERROR);
      }
    }
  }

  if (number_of_source_files == 0)
    printf("%s: nothing to compile, only %s generated\n", selfie_name, source_name);

  finalize_data_segment();

  emit_bootstrapping();

  if (GC_ON)
    emit_fetch_data_segment_size_implementation(fetch_dss_code_location);

  if (number_of_symbol_lookups > 0) {
    printf("%s: --------------------------------------------------------------------------------\n", selfie_name);
    printf("%s: %lu symbol table lookups in %lu iterations on average\n", selfie_name,
      number_of_symbol_lookups,
      number_of_list_iterations / number_of_symbol_lookups);
  }

  printf("%s: vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n", selfie_name);
  printf("%s: %lu bytes generated with %lu instructions and %lu bytes of data\n", selfie_name,
    code_size + data_size,
    code_size / INSTRUCTIONSIZE,
    data_size);

  print_instruction_counters();

  printf("%s: --------------------------------------------------------------------------------\n", selfie_name);
  printf("%s: profile: data: total(bytes)\n", selfie_name);
  printf("%s: global variables:       %lu(%lu)\n", selfie_name,
    dc_global_variable,
    dc_global_variable * WORDSIZE);
  printf("%s: unique string literals: %lu(%lu)\n", selfie_name,
    dc_string,
    dc_string_bytes);
  printf("%s: unique big integers:    %lu(%lu)\n", selfie_name,
    dc_big_integer,
    dc_big_integer * WORDSIZE);
}

// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~
// -----------------------------------------------------------------
// -------------------     I N T E R F A C E     -------------------
// -----------------------------------------------------------------
// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~

// -----------------------------------------------------------------
// --------------------------- REGISTER ----------------------------
// -----------------------------------------------------------------

char* get_register_name(uint64_t reg) {
  return (char*) *(REGISTERS + reg);
}

void print_register_name(uint64_t reg) {
  printf("%s", get_register_name(reg));
}

uint64_t is_stack_register(uint64_t reg) {
  if (reg == REG_SP)
    return 1;
  else if (reg == REG_S0)
    return 1;
  else if (reg == REG_RA)
    return 1;
  else
    return 0;
}

uint64_t is_system_register(uint64_t reg) {
  if (reg == REG_GP)
    return 1;
  else
    return is_stack_register(reg);
}

uint64_t is_argument_register(uint64_t reg) {
  if (reg >= REG_A0)
    if (reg <= REG_A7)
      return 1;

  return 0;
}

uint64_t is_temporary_register(uint64_t reg) {
  if (reg >= REG_T0)
    if (reg <= REG_T2)
      return 1;
    else if (reg >= REG_T3)
      return 1;
    else
      return 0;
  else
    return 0;
}

void read_register_check_wrap(uint64_t reg, uint64_t check_wrap) {
  if (*(writes_per_register + reg) > 0) {
    // register has been written to before
    if (*(reads_per_register + reg) < UINT64_MAX)
      *(reads_per_register + reg) = *(reads_per_register + reg) + 1;

    if (check_wrap)
      if (*(registers + reg) > UINT_MAX) {
        print_instruction();
        printf(": reading unwrapped value from register ");
        print_register_name(reg);
        println();

        throw_exception(EXCEPTION_INTEGEROVERFLOW, reg);
      }
  } else {
    print_instruction();
    printf(": reading from uninitialized register ");
    print_register_name(reg);
    println();

    throw_exception(EXCEPTION_UNINITIALIZEDREGISTER, reg);
  }
}

void read_register(uint64_t reg) {
  read_register_check_wrap(reg, 1);
}

void write_register_wrap(uint64_t reg, uint64_t wrap) {
  if (wrap)
    *(registers + reg) = sign_shrink(*(registers + reg), WORDSIZEINBITS);

  if (*(writes_per_register + reg) < UINT64_MAX)
    *(writes_per_register + reg) = *(writes_per_register + reg) + 1;
}

void write_register(uint64_t reg) {
  write_register_wrap(reg, SIZEOFUINT64INBITS != WORDSIZEINBITS);
}

// -----------------------------------------------------------------
// ------------------------ ENCODER/DECODER ------------------------
// -----------------------------------------------------------------

void check_immediate_range(uint64_t immediate, uint64_t bits) {
  if (not(is_signed_integer(immediate, bits))) {
    print_line_number("encoding error", line_number);
    printf("%ld expected between %ld and %ld\n",
      immediate,
      -two_to_the_power_of(bits - 1),
      two_to_the_power_of(bits - 1) - 1);

    exit(EXITCODE_COMPILERERROR);
  }
}

// RISC-V R Format
// ----------------------------------------------------------------
// |        7         |  5  |  5  |  3   |        5        |  7   |
// +------------------+-----+-----+------+-----------------+------+
// |      funct7      | rs2 | rs1 |funct3|       rd        |opcode|
// +------------------+-----+-----+------+-----------------+------+
// |31              25|24 20|19 15|14  12|11              7|6    0|
// ----------------------------------------------------------------

uint64_t encode_r_format(uint64_t funct7, uint64_t rs2, uint64_t rs1, uint64_t funct3, uint64_t rd, uint64_t opcode) {
  // assert: 0 <= funct7 < 2^7
  // assert: 0 <= rs2 < 2^5
  // assert: 0 <= rs1 < 2^5
  // assert: 0 <= funct3 < 2^3
  // assert: 0 <= rd < 2^5
  // assert: 0 <= opcode < 2^7

  return left_shift(left_shift(left_shift(left_shift(left_shift(funct7, 5) + rs2, 5) + rs1, 3) + funct3, 5) + rd, 7) + opcode;
}

uint64_t get_funct7(uint64_t instruction) {
  return get_bits(instruction, 25, 7);
}

uint64_t get_rs2(uint64_t instruction) {
  return get_bits(instruction, 20, 5);
}

uint64_t get_rs1(uint64_t instruction) {
  return get_bits(instruction, 15, 5);
}

uint64_t get_funct3(uint64_t instruction) {
  return get_bits(instruction, 12, 3);
}

uint64_t get_rd(uint64_t instruction) {
  return get_bits(instruction, 7, 5);
}

uint64_t get_opcode(uint64_t instruction) {
  return get_bits(instruction, 0, 7);
}

void decode_r_format() {
  funct7 = get_funct7(ir);
  rs2    = get_rs2(ir);
  rs1    = get_rs1(ir);
  funct3 = get_funct3(ir);
  rd     = get_rd(ir);
  imm    = 0;
}

// RISC-V I Format
// ----------------------------------------------------------------
// |           12           |  5  |  3   |        5        |  7   |
// +------------------------+-----+------+-----------------+------+
// |    immediate[11:0]     | rs1 |funct3|       rd        |opcode|
// +------------------------+-----+------+-----------------+------+
// |31                    20|19 15|14  12|11              7|6    0|
// ----------------------------------------------------------------

uint64_t encode_i_format(uint64_t immediate, uint64_t rs1, uint64_t funct3, uint64_t rd, uint64_t opcode) {
  // assert: -2^11 <= immediate < 2^11
  // assert: 0 <= rs1 < 2^5
  // assert: 0 <= funct3 < 2^3
  // assert: 0 <= rd < 2^5
  // assert: 0 <= opcode < 2^7

  check_immediate_range(immediate, 12);

  immediate = sign_shrink(immediate, 12);

  return left_shift(left_shift(left_shift(left_shift(immediate, 5) + rs1, 3) + funct3, 5) + rd, 7) + opcode;
}

uint64_t get_immediate_i_format(uint64_t instruction) {
  return sign_extend(get_bits(instruction, 20, 12), 12);
}

void decode_i_format() {
  funct7 = 0;
  rs2    = REG_ZR;
  rs1    = get_rs1(ir);
  funct3 = get_funct3(ir);
  rd     = get_rd(ir);
  imm    = get_immediate_i_format(ir);
}

// RISC-V S Format
// ----------------------------------------------------------------
// |        7         |  5  |  5  |  3   |        5        |  7   |
// +------------------+-----+-----+------+-----------------+------+
// |    imm1[11:5]    | rs2 | rs1 |funct3|    imm2[4:0]    |opcode|
// +------------------+-----+-----+------+-----------------+------+
// |31              25|24 20|19 15|14  12|11              7|6    0|
// ----------------------------------------------------------------

uint64_t encode_s_format(uint64_t immediate, uint64_t rs2, uint64_t rs1, uint64_t funct3, uint64_t opcode) {
  // assert: -2^11 <= immediate < 2^11
  // assert: 0 <= rs2 < 2^5
  // assert: 0 <= rs1 < 2^5
  // assert: 0 <= funct3 < 2^3
  // assert: 0 <= opcode < 2^7
  uint64_t imm1;
  uint64_t imm2;

  check_immediate_range(immediate, 12);

  immediate = sign_shrink(immediate, 12);

  imm1 = get_bits(immediate, 5, 7);
  imm2 = get_bits(immediate, 0, 5);

  return left_shift(left_shift(left_shift(left_shift(left_shift(imm1, 5) + rs2, 5) + rs1, 3) + funct3, 5) + imm2, 7) + opcode;
}

uint64_t get_immediate_s_format(uint64_t instruction) {
  uint64_t imm1;
  uint64_t imm2;

  imm1 = get_bits(instruction, 25, 7);
  imm2 = get_bits(instruction,  7, 5);

  return sign_extend(left_shift(imm1, 5) + imm2, 12);
}

void decode_s_format() {
  funct7 = 0;
  rs2    = get_rs2(ir);
  rs1    = get_rs1(ir);
  funct3 = get_funct3(ir);
  rd     = REG_ZR;
  imm    = get_immediate_s_format(ir);
}

// RISC-V B Format
// ----------------------------------------------------------------
// |        7         |  5  |  5  |  3   |        5        |  7   |
// +------------------+-----+-----+------+-----------------+------+
// |imm1[12]imm2[10:5]| rs2 | rs1 |funct3|imm3[4:1]imm4[11]|opcode|
// +------------------+-----+-----+------+-----------------+------+
// |31              25|24 20|19 15|14  12|11              7|6    0|
// ----------------------------------------------------------------

uint64_t encode_b_format(uint64_t immediate, uint64_t rs2, uint64_t rs1, uint64_t funct3, uint64_t opcode) {
  // assert: -2^12 <= immediate < 2^12
  // assert: 0 <= rs2 < 2^5
  // assert: 0 <= rs1 < 2^5
  // assert: 0 <= funct3 < 2^3
  // assert: 0 <= opcode < 2^7
  uint64_t imm1;
  uint64_t imm2;
  uint64_t imm3;
  uint64_t imm4;

  check_immediate_range(immediate, 13);

  immediate = sign_shrink(immediate, 13);

  // LSB of immediate is lost
  imm1 = get_bits(immediate, 12, 1);
  imm2 = get_bits(immediate,  5, 6);
  imm3 = get_bits(immediate,  1, 4);
  imm4 = get_bits(immediate, 11, 1);

  return left_shift(left_shift(left_shift(left_shift(left_shift(left_shift(left_shift(imm1, 6) + imm2, 5) + rs2, 5) + rs1, 3) + funct3, 4) + imm3, 1) + imm4, 7) + opcode;
}

uint64_t get_immediate_b_format(uint64_t instruction) {
  uint64_t imm1;
  uint64_t imm2;
  uint64_t imm3;
  uint64_t imm4;

  imm1 = get_bits(instruction, 31, 1);
  imm2 = get_bits(instruction, 25, 6);
  imm3 = get_bits(instruction,  8, 4);
  imm4 = get_bits(instruction,  7, 1);

  // reassemble immediate and add trailing zero
  return sign_extend(left_shift(left_shift(left_shift(left_shift(imm1, 1) + imm4, 6) + imm2, 4) + imm3, 1), 13);
}

void decode_b_format() {
  funct7 = 0;
  rs2    = get_rs2(ir);
  rs1    = get_rs1(ir);
  funct3 = get_funct3(ir);
  rd     = REG_ZR;
  imm    = get_immediate_b_format(ir);
}

// RISC-V J Format
// ----------------------------------------------------------------
// |                  20                 |        5        |  7   |
// +-------------------------------------+-----------------+------+
// |imm1[20]imm2[10:1]imm3[11]imm4[19:12]|       rd        |opcode|
// +-------------------------------------+-----------------+------+
// |31                                 12|11              7|6    0|
// ----------------------------------------------------------------

uint64_t encode_j_format(uint64_t immediate, uint64_t rd, uint64_t opcode) {
  // assert: -2^20 <= immediate < 2^20
  // assert: 0 <= rd < 2^5
  // assert: 0 <= opcode < 2^7
  uint64_t imm1;
  uint64_t imm2;
  uint64_t imm3;
  uint64_t imm4;

  check_immediate_range(immediate, 21);

  immediate = sign_shrink(immediate, 21);

  // LSB of immediate is lost
  imm1 = get_bits(immediate, 20,  1);
  imm2 = get_bits(immediate,  1, 10);
  imm3 = get_bits(immediate, 11,  1);
  imm4 = get_bits(immediate, 12,  8);

  return left_shift(left_shift(left_shift(left_shift(left_shift(imm1, 10) + imm2, 1) + imm3, 8) + imm4, 5) + rd, 7) + opcode;
}

uint64_t get_immediate_j_format(uint64_t instruction) {
  uint64_t imm1;
  uint64_t imm2;
  uint64_t imm3;
  uint64_t imm4;

  imm1 = get_bits(instruction, 31,  1);
  imm2 = get_bits(instruction, 21, 10);
  imm3 = get_bits(instruction, 20,  1);
  imm4 = get_bits(instruction, 12,  8);

  // reassemble immediate and add trailing zero
  return sign_extend(left_shift(left_shift(left_shift(left_shift(imm1, 8) + imm4, 1) + imm3, 10) + imm2, 1), 21);
}

void decode_j_format() {
  funct7 = 0;
  rs2    = REG_ZR;
  rs1    = REG_ZR;
  funct3 = 0;
  rd     = get_rd(ir);
  imm    = get_immediate_j_format(ir);
}

// RISC-V U Format
// ----------------------------------------------------------------
// |                  20                 |        5        |  7   |
// +-------------------------------------+-----------------+------+
// |           immediate[19:0]           |       rd        |opcode|
// +-------------------------------------+-----------------+------+
// |31                                 12|11              7|6    0|
// ----------------------------------------------------------------

uint64_t encode_u_format(uint64_t immediate, uint64_t rd, uint64_t opcode) {
  // assert: -2^19 <= immediate < 2^19
  // assert: 0 <= rd < 2^5
  // assert: 0 <= opcode < 2^7

  check_immediate_range(immediate, 20);

  immediate = sign_shrink(immediate, 20);

  return left_shift(left_shift(immediate, 5) + rd, 7) + opcode;
}

uint64_t get_immediate_u_format(uint64_t instruction) {
  return sign_extend(get_bits(instruction, 12, 20), 20);
}

void decode_u_format() {
  funct7 = 0;
  rs2    = REG_ZR;
  rs1    = REG_ZR;
  funct3 = 0;
  rd     = get_rd(ir);
  imm    = get_immediate_u_format(ir);
}

// -----------------------------------------------------------------
// ---------------------------- BINARY -----------------------------
// -----------------------------------------------------------------

uint64_t get_total_number_of_instructions() {
  return ic_lui + ic_addi + ic_add + ic_sub + ic_mul + ic_divu + ic_remu + ic_sltu + ic_load + ic_store + ic_beq + ic_jal + ic_jalr + ic_ecall;
}

uint64_t get_total_number_of_nops() {
  return nopc_lui + nopc_addi + nopc_add + nopc_sub + nopc_mul + nopc_divu + nopc_remu + nopc_sltu + nopc_load + nopc_store + nopc_beq + nopc_jal + nopc_jalr;
}

void print_instruction_counter(uint64_t counter, uint64_t ins) {
  printf("%s: %lu(%lu.%.2lu%%)",
    get_mnemonic(ins),
    counter,
    percentage_format_integral_2(get_total_number_of_instructions(), counter),
    percentage_format_fractional_2(get_total_number_of_instructions(), counter));
}

void print_instruction_counter_with_nops(uint64_t counter, uint64_t nops, uint64_t ins) {
  print_instruction_counter(counter, ins);

  if (run)
    printf("[%lu.%.2lu%%]",
      percentage_format_integral_2(counter, nops),
      percentage_format_fractional_2(counter, nops));
}

void print_instruction_counters() {
  printf("%s: --------------------------------------------------------------------------------\n", selfie_name);
  printf("%s: profile: instruction: total(ratio%%)", selfie_name);
  if (run) printf("[nops%%]");
  println();

  printf("%s: init:    ", selfie_name);
  print_instruction_counter_with_nops(ic_lui, nopc_lui, LUI);
  printf(", ");
  print_instruction_counter_with_nops(ic_addi, nopc_addi, ADDI);
  println();

  printf("%s: memory:  ", selfie_name);
  print_instruction_counter_with_nops(ic_load, nopc_load, LOAD);
  printf(", ");
  print_instruction_counter_with_nops(ic_store, nopc_store, STORE);
  println();

  printf("%s: compute: ", selfie_name);
  print_instruction_counter_with_nops(ic_add, nopc_add, ADD);
  printf(", ");
  print_instruction_counter_with_nops(ic_sub, nopc_sub, SUB);
  printf(", ");
  print_instruction_counter_with_nops(ic_mul, nopc_mul, MUL);
  println();

  printf("%s: compute: ", selfie_name);
  print_instruction_counter_with_nops(ic_divu, nopc_divu, DIVU);
  printf(", ");
  print_instruction_counter_with_nops(ic_remu, nopc_remu, REMU);
  println();

  printf("%s: compare: ", selfie_name);
  print_instruction_counter_with_nops(ic_sltu, nopc_sltu, SLTU);
  println();

  printf("%s: control: ", selfie_name);
  print_instruction_counter_with_nops(ic_beq, nopc_beq, BEQ);
  printf(", ");
  print_instruction_counter_with_nops(ic_jal, nopc_jal, JAL);
  printf(", ");
  print_instruction_counter_with_nops(ic_jalr, nopc_jalr, JALR);
  println();

  printf("%s: system:  ", selfie_name);
  print_instruction_counter(ic_ecall, ECALL);
  println();
}

uint64_t get_low_word(uint64_t word) {
  return get_bits(word, 0, SINGLEWORDSIZEINBITS);
}

uint64_t get_high_word(uint64_t word) {
  return get_bits(word, SINGLEWORDSIZEINBITS, SINGLEWORDSIZEINBITS);
}

uint64_t load_word(uint64_t* memory, uint64_t waddr, uint64_t may_be_double_word) {
  if (IS64BITSYSTEM) {
    if (IS64BITTARGET)
      if (may_be_double_word)
        return *(memory + waddr / sizeof(uint64_t));

    if (waddr % sizeof(uint64_t) == 0)
      return get_low_word(*(memory + waddr / sizeof(uint64_t)));
    else
      return get_high_word(*(memory + waddr / sizeof(uint64_t)));
  } else
    return *(memory + waddr / sizeof(uint64_t));
}

void store_word(uint64_t* memory, uint64_t waddr, uint64_t may_be_double_word, uint64_t word) {
  if (IS64BITSYSTEM) {
    if (IS64BITTARGET)
      if (may_be_double_word) {
        *(memory + waddr / sizeof(uint64_t)) = word;

        return;
      }

    if (waddr % sizeof(uint64_t) == 0)
      // replace low word
      *(memory + waddr / sizeof(uint64_t)) =
        left_shift(load_word(memory, waddr + SINGLEWORDSIZE, may_be_double_word), SINGLEWORDSIZEINBITS)
          + sign_shrink(word, SINGLEWORDSIZEINBITS);
    else
      // replace high word
      *(memory + waddr / sizeof(uint64_t)) =
        left_shift(word, SINGLEWORDSIZEINBITS)
          + load_word(memory, waddr - SINGLEWORDSIZE, may_be_double_word);
  } else
    *(memory + waddr / sizeof(uint64_t)) = word;
}

uint64_t load_instruction(uint64_t caddr) {
  return load_word(code_binary, caddr, 0);
}

void store_instruction(uint64_t caddr, uint64_t instruction) {
  if (caddr >= MAX_CODE_SIZE) {
    syntax_error_message("maximum code size exceeded");

    exit(EXITCODE_COMPILERERROR);
  }

  store_word(code_binary, caddr, 0, instruction);
}

uint64_t load_code(uint64_t caddr) {
  return load_word(code_binary, caddr, 1);
}

uint64_t load_data(uint64_t daddr) {
  return load_word(data_binary, daddr, 1);
}

void store_data(uint64_t daddr, uint64_t data) {
  if (daddr >= MAX_DATA_SIZE) {
    syntax_error_message("maximum data size exceeded");

    exit(EXITCODE_COMPILERERROR);
  }

  store_word(data_binary, daddr, 1, data);
}

void emit_instruction(uint64_t instruction) {
  store_instruction(code_size, instruction);

  if (code_line_number != (uint64_t*) 0)
    if (*(code_line_number + code_size / INSTRUCTIONSIZE) == 0)
      *(code_line_number + code_size / INSTRUCTIONSIZE) = line_number;

  code_size = code_size + INSTRUCTIONSIZE;
}

uint64_t encode_nop() {
  return encode_i_format(0, REG_ZR, F3_NOP, REG_ZR, OP_IMM);
}

void emit_nop() {
  emit_instruction(encode_nop());

  ic_addi = ic_addi + 1;
}

void emit_lui(uint64_t rd, uint64_t immediate) {
  emit_instruction(encode_u_format(immediate, rd, OP_LUI));

  ic_lui = ic_lui + 1;
}

void emit_addi(uint64_t rd, uint64_t rs1, uint64_t immediate) {
  emit_instruction(encode_i_format(immediate, rs1, F3_ADDI, rd, OP_IMM));

  ic_addi = ic_addi + 1;
}

void emit_add(uint64_t rd, uint64_t rs1, uint64_t rs2) {
  emit_instruction(encode_r_format(F7_ADD, rs2, rs1, F3_ADD, rd, OP_OP));

  ic_add = ic_add + 1;
}

void emit_sub(uint64_t rd, uint64_t rs1, uint64_t rs2) {
  emit_instruction(encode_r_format(F7_SUB, rs2, rs1, F3_SUB, rd, OP_OP));

  ic_sub = ic_sub + 1;
}

void emit_mul(uint64_t rd, uint64_t rs1, uint64_t rs2) {
  emit_instruction(encode_r_format(F7_MUL, rs2, rs1, F3_MUL, rd, OP_OP));

  ic_mul = ic_mul + 1;
}

void emit_divu(uint64_t rd, uint64_t rs1, uint64_t rs2) {
  emit_instruction(encode_r_format(F7_DIVU, rs2, rs1, F3_DIVU, rd, OP_OP));

  ic_divu = ic_divu + 1;
}

void emit_remu(uint64_t rd, uint64_t rs1, uint64_t rs2) {
  emit_instruction(encode_r_format(F7_REMU, rs2, rs1, F3_REMU, rd, OP_OP));

  ic_remu = ic_remu + 1;
}

void emit_sltu(uint64_t rd, uint64_t rs1, uint64_t rs2) {
  emit_instruction(encode_r_format(F7_SLTU, rs2, rs1, F3_SLTU, rd, OP_OP));

  ic_sltu = ic_sltu + 1;
}

void emit_load(uint64_t rd, uint64_t rs1, uint64_t immediate) {
  if (IS64BITTARGET)
    emit_instruction(encode_i_format(immediate, rs1, F3_LD, rd, OP_LOAD));
  else
    emit_instruction(encode_i_format(immediate, rs1, F3_LW, rd, OP_LOAD));

  ic_load = ic_load + 1;
}

void emit_store(uint64_t rs1, uint64_t immediate, uint64_t rs2) {
  if (IS64BITTARGET)
    emit_instruction(encode_s_format(immediate, rs2, rs1, F3_SD, OP_STORE));
  else
    emit_instruction(encode_s_format(immediate, rs2, rs1, F3_SW, OP_STORE));

  ic_store = ic_store + 1;
}

void emit_beq(uint64_t rs1, uint64_t rs2, uint64_t immediate) {
  emit_instruction(encode_b_format(immediate, rs2, rs1, F3_BEQ, OP_BRANCH));

  ic_beq = ic_beq + 1;
}

void emit_jal(uint64_t rd, uint64_t immediate) {
  emit_instruction(encode_j_format(immediate, rd, OP_JAL));

  ic_jal = ic_jal + 1;
}

void emit_jalr(uint64_t rd, uint64_t rs1, uint64_t immediate) {
  emit_instruction(encode_i_format(immediate, rs1, F3_JALR, rd, OP_JALR));

  ic_jalr = ic_jalr + 1;
}

void emit_ecall() {
  emit_instruction(encode_i_format(F12_ECALL, REG_ZR, F3_ECALL, REG_ZR, OP_SYSTEM));

  ic_ecall = ic_ecall + 1;
}

void fixup_BFormat(uint64_t from_address) {
  uint64_t instruction;

  instruction = load_instruction(from_address);

  store_instruction(from_address,
    encode_b_format(code_size - from_address,
      get_rs2(instruction),
      get_rs1(instruction),
      get_funct3(instruction),
      get_opcode(instruction)));
}

void fixup_JFormat(uint64_t from_address, uint64_t to_address) {
  uint64_t instruction;

  instruction = load_instruction(from_address);

  store_instruction(from_address,
    encode_j_format(to_address - from_address,
      get_rd(instruction),
      get_opcode(instruction)));
}

void fixlink_JFormat(uint64_t from_address, uint64_t to_address) {
  uint64_t previous_address;

  while (from_address != 0) {
    previous_address = get_immediate_j_format(load_instruction(from_address));

    fixup_JFormat(from_address, to_address);

    from_address = previous_address;
  }
}

void fixup_IFormat(uint64_t from_address, uint64_t immediate) {
  uint64_t instruction;

  instruction = load_instruction(from_address);

  store_instruction(from_address,
    encode_i_format(immediate,
      get_rs1(instruction),
      get_funct3(instruction),
      get_rd(instruction),
      get_opcode(instruction)));
}

void emit_data_word(uint64_t data, uint64_t offset, uint64_t source_line_number) {
  // assert: offset < 0

  // the data segment is populated in reverse order from end to start
  store_data(MAX_DATA_SIZE + offset, data);

  if (data_line_number != (uint64_t*) 0)
    *(data_line_number + (MAX_DATA_SIZE + offset) / WORDSIZE) = source_line_number;
}

void emit_string_data(uint64_t* entry) {
  char* s;
  uint64_t i;
  uint64_t l;

  s = get_string(entry);

  i = 0;

  l = round_up(string_length(s) + 1, WORDSIZE);

  while (i < l) {
    emit_data_word(load_word((uint64_t*) s, i, 1), get_address(entry) + i, get_line_number(entry));

    i = i + WORDSIZE;
  }

  dc_string       = dc_string + 1;
  dc_string_bytes = dc_string_bytes + l;
}

void finalize_data_segment() {
  uint64_t daddr;

  // assert: data_size <= MAX_DATA_SIZE

  daddr = 0;

  // data binary is populated in reverse order
  // copy data to beginning of data binary
  while (daddr < data_size) {
    store_data(daddr, load_data(MAX_DATA_SIZE - data_size + daddr));

    if (data_line_number != (uint64_t*) 0)
      *(data_line_number + daddr / WORDSIZE) =
        *(data_line_number + (MAX_DATA_SIZE - data_size + daddr) / WORDSIZE);

    daddr = daddr + WORDSIZE;
  }
}

uint64_t* touch(uint64_t* memory, uint64_t bytes) {
  uint64_t* m;
  uint64_t n;

  m = memory;

  if (bytes > 0)
    // touch memory at beginning
    n = *m;

  while (bytes > PAGESIZE) {
    bytes = bytes - PAGESIZE;

    m = m + PAGESIZE / sizeof(uint64_t);

    // touch every following page
    n = *m;
  }

  if (bytes > 0) {
    m = m + (bytes - 1) / sizeof(uint64_t);

    // touch at end
    n = *m;
  }

  // avoids unused warning for n
  n = 0; n = n + 1;

  return memory;
}

uint64_t* allocate_elf_header(uint64_t size) {
  // allocate and map (on all boot levels) zeroed memory for ELF headers, preparing
  // read calls (memory must be mapped) and write calls (memory must be mapped and zeroed)
  return touch(zmalloc(size), size);
}

uint64_t get_elf_header_size() {
  return e_ehsize + e_phnum * e_phentsize;
}

uint64_t* encode_elf_header() {
  uint64_t* header;

  // entry point compatible with pk kernel
  e_entry = PK_CODE_START;

  // RISC-U binaries have two program header entries
  e_phnum = 2;

  // allocate page for alignment of code and data segments
  header = allocate_elf_header(ELF_HEADER_SIZE);

  // store all data necessary for creating a minimal and valid file and program header

  store_word(header, 0, 0, EI_MAG0
              + left_shift(EI_MAG1, 8)
              + left_shift(EI_MAG2, 16)
              + left_shift(EI_MAG3, 24));
  store_word(header, 4, 0, EI_CLASS
              + left_shift(EI_DATA, 8)
              + left_shift(EI_VERSION, 16)
              + left_shift(EI_OSABI, 24));
  store_word(header, 8, 0, EI_ABIVERSION); // ignoring 24 LSBs of EI_PAD
  store_word(header, 12, 0, EI_PAD);       // ignoring 24 MSBs of EI_PAD
  store_word(header, 16, 0, e_type + left_shift(e_machine, 16));
  store_word(header, 20, 0, e_version);

  if (EI_CLASS == ELFCLASS64) {
    // RISC-U ELF64 file header
    store_word(header, 24, 1, e_entry);
    store_word(header, 32, 1, e_phoff);
    store_word(header, 40, 1, e_shoff);
    store_word(header, 48, 1, e_flags
                + left_shift(e_ehsize, 32)
                + left_shift(e_phentsize, 48));
    store_word(header, 56, 1, e_phnum
                + left_shift(e_shentsize, 16)
                + left_shift(e_shnum, 32)
                + left_shift(e_shstrndx, 48));
  } else {
    // RISC-U ELF32 file header
    store_word(header, 24, 0, e_entry);
    store_word(header, 28, 0, e_phoff);
    store_word(header, 32, 0, e_shoff);
    store_word(header, 36, 0, e_flags);
    store_word(header, 40, 0, e_ehsize + left_shift(e_phentsize, 16));
    store_word(header, 44, 0, e_phnum + left_shift(e_shentsize, 16));
    store_word(header, 48, 0, e_shnum + left_shift(e_shstrndx, 16));
  }

  p_type = PT_LOAD;

  p_flags  = PF_RX;
  p_offset = ELF_HEADER_SIZE;
  p_vaddr  = code_start;
  p_paddr  = code_start;
  p_filesz = code_size;
  p_memsz  = code_size;

  encode_elf_program_header(header, 0);

  p_flags  = PF_RW;
  p_offset = ELF_HEADER_SIZE + round_up(code_size, PAGESIZE);
  p_vaddr  = data_start;
  p_paddr  = data_start;
  p_filesz = data_size;
  p_memsz  = data_size;

  encode_elf_program_header(header, 1);

  return header;
}

void encode_elf_program_header(uint64_t* header, uint64_t ph_index) {
  uint64_t ph_offset;

  ph_offset = e_ehsize + e_phentsize * ph_index;

  if (EI_CLASS == ELFCLASS64) {
    // RISC-U ELF64 program header
    store_word(header, ph_offset + 0, 1, p_type + left_shift(p_flags, 32));
    store_word(header, ph_offset + 8, 1, p_offset);
    store_word(header, ph_offset + 16, 1, p_vaddr);
    store_word(header, ph_offset + 24, 1, p_paddr);
    store_word(header, ph_offset + 32, 1, p_filesz);
    store_word(header, ph_offset + 40, 1, p_memsz);
    store_word(header, ph_offset + 48, 1, p_align);
  } else {
    // RISC-U ELF32 program header
    store_word(header, ph_offset + 0, 0, p_type);
    store_word(header, ph_offset + 4, 0, p_offset);
    store_word(header, ph_offset + 8, 0, p_vaddr);
    store_word(header, ph_offset + 12, 0, p_paddr);
    store_word(header, ph_offset + 16, 0, p_filesz);
    store_word(header, ph_offset + 20, 0, p_memsz);
    store_word(header, ph_offset + 24, 0, p_flags);
    store_word(header, ph_offset + 28, 0, p_align);
  }
}

uint64_t validate_elf_file_header_top(uint64_t* header) {
  uint64_t magic_number;
  uint64_t ei_class;

  magic_number = load_word(header, 0, 0);

  if (get_bits(magic_number, 0, 8) == EI_MAG0)
    if (get_bits(magic_number, 8, 8) == EI_MAG1)
      if (get_bits(magic_number, 16, 8) == EI_MAG2)
        if (get_bits(magic_number, 24, 8) == EI_MAG3) {
          ei_class = get_bits(load_word(header, 4, 0), 0, 8);

          if (ei_class == ELFCLASS64) {
            IS64BITTARGET = 1;

            return 1;
          } else if (ei_class == ELFCLASS32) {
            IS64BITTARGET = 0;

            return 1;
          }
        }

  return 0;
}

uint64_t decode_elf_file_header(uint64_t* header) {
  if (EI_CLASS == ELFCLASS64) {
    if (get_bits(load_word(header, 48, 1), 32, 16) == e_ehsize)
      if (get_bits(load_word(header, 48, 1), 48, 16) == e_phentsize) {
        e_entry = load_word(header, 24, 1);
        e_phnum = get_bits(load_word(header, 56, 1), 0, 16);

        ISRISCU = 0;

        if (e_entry == PK_CODE_START)
          if (e_phnum == 2)
            ISRISCU = 1;

        return 1;
      }
  } else if (EI_CLASS == ELFCLASS32) {
    if (get_bits(load_word(header, 40, 0), 0, 16) == e_ehsize)
      if (get_bits(load_word(header, 40, 0), 16, 16) == e_phentsize) {
        e_entry = load_word(header, 24, 0);
        e_phnum = get_bits(load_word(header, 44, 0), 0, 16);

        ISRISCU = 0;

        if (e_entry == PK_CODE_START)
          if (e_phnum == 2)
            ISRISCU = 1;

        return 1;
      }
  }

  return 0;
}

uint64_t decode_elf_program_header(uint64_t* header) {
  uint64_t vaddr;
  uint64_t filesz;
  uint64_t memsz;

  if (EI_CLASS == ELFCLASS64) {
    vaddr  = load_word(header, 16, 1);
    filesz = load_word(header, 32, 1);
    memsz  = load_word(header, 40, 1);

    if (vaddr == load_word(header, 24, 1))
      // p_vaddr == p_paddr
      if (filesz <= memsz) {
        p_type   = get_bits(load_word(header, 0, 1), 0, 32);
        p_flags  = get_bits(load_word(header, 0, 1), 32, 32);
        p_offset = load_word(header, 8, 1);
        p_vaddr  = vaddr;
        p_filesz = filesz;
        p_memsz  = memsz;

        return 1;
      }
  } else if (EI_CLASS == ELFCLASS32) {
    vaddr  = load_word(header, 8, 0);
    filesz = load_word(header, 16, 0);
    memsz  = load_word(header, 20, 0);

    if (vaddr == load_word(header, 12, 0))
      // p_vaddr == p_paddr
      if (filesz <= memsz) {
        p_type   = load_word(header, 0, 0);
        p_offset = load_word(header, 4, 0);
        p_vaddr  = vaddr;
        p_filesz = filesz;
        p_memsz  = memsz;
        p_flags  = load_word(header, 24, 0);

        return 1;
      }
  }

  return 0;
}

uint64_t open_write_only(char* name, uint64_t mode) {
  return sign_extend(open(name, O_CREAT_TRUNC_WRONLY, mode), SYSCALL_BITWIDTH);
}

void selfie_output(char* filename) {
  uint64_t fd;
  uint64_t* ELF_header;
  uint64_t code_size_with_padding;

  binary_name = filename;

  if (code_size + data_size == 0) {
    printf("%s: nothing to emit to output file %s\n", selfie_name, binary_name);

    return;
  }

  // assert: binary_name is mapped and not longer than MAX_FILENAME_LENGTH

  fd = open_write_only(binary_name, S_IRUSR_IWUSR_IXUSR_IRGRP_IXGRP_IROTH_IXOTH);

  if (signed_less_than(fd, 0)) {
    printf("%s: could not create binary output file %s\n", selfie_name, binary_name);

    exit(EXITCODE_IOERROR);
  }

  ELF_header = encode_elf_header();

  // assert: ELF_header is mapped

  // first write ELF header
  if (write(fd, ELF_header, ELF_HEADER_SIZE) != ELF_HEADER_SIZE) {
    printf("%s: could not write ELF header of binary output file %s\n", selfie_name, binary_name);

    exit(EXITCODE_IOERROR);
  }

  code_size_with_padding = round_up(code_size, PAGESIZE);

  touch(code_binary, code_size_with_padding);

  // assert: code_binary is mapped

  // then write code with padding bytes
  if (write(fd, code_binary, code_size_with_padding) != code_size_with_padding) {
    printf("%s: could not write code into binary output file %s\n", selfie_name, binary_name);

    exit(EXITCODE_IOERROR);
  }

  // assert: data_binary is mapped

  // finally write data
  if (write(fd, data_binary, data_size) != data_size) {
    printf("%s: could not write data into binary output file %s\n", selfie_name, binary_name);

    exit(EXITCODE_IOERROR);
  }

  printf("%s: %lu bytes with %lu %lu-bit RISC-U instructions and %lu bytes of data written into %s\n", selfie_name,
    ELF_HEADER_SIZE + code_size_with_padding + data_size,
    code_size / INSTRUCTIONSIZE,
    WORDSIZEINBITS,
    data_size,
    binary_name);
}

void selfie_load(char* filename) {
  uint64_t fd;
  uint64_t* ELF_file_header;
  uint64_t number_of_read_bytes;
  uint64_t code_file_offset;
  uint64_t code_file_size;
  uint64_t data_file_offset;
  uint64_t data_file_size;
  uint64_t* ELF_program_header;
  uint64_t i;
  uint64_t number_of_read_bytes_in_total;
  uint64_t to_be_read_bytes;

  binary_name = filename;

  // assert: binary_name is mapped and not longer than MAX_FILENAME_LENGTH

  fd = open_read_only(binary_name);

  if (signed_less_than(fd, 0)) {
    printf("%s: could not open input file %s\n", selfie_name, binary_name);

    exit(EXITCODE_IOERROR);
  }

  // no source line numbers in binaries
  reset_binary();

  // allocate and map (on all boot levels) memory for reading into it
  ELF_file_header = touch(smalloc(MAX_BINARY_SIZE), MAX_BINARY_SIZE);

  number_of_read_bytes = read(fd, ELF_file_header, 8);

  if (number_of_read_bytes == 8) {
    if (validate_elf_file_header_top(ELF_file_header)) {
      init_target();
      reset_disassembler();

      number_of_read_bytes = read(fd, (uint64_t*) ((uint64_t) ELF_file_header + 8), e_ehsize - 8);

      if (number_of_read_bytes == e_ehsize - 8) {
        if (decode_elf_file_header(ELF_file_header)) {
          code_file_offset = 0;
          code_file_size   = 0;
          data_file_offset = 0;
          data_file_size   = 0;

          ELF_program_header = (uint64_t*) ((uint64_t) ELF_file_header + e_ehsize);

          i = 0;

          while (i < e_phnum) {
            number_of_read_bytes = read(fd, ELF_program_header, e_phentsize);

            if (number_of_read_bytes == e_phentsize) {
              if (decode_elf_program_header(ELF_program_header)) {
                if (p_type == PT_LOAD) {
                  if (p_flags == PF_RX) {
                    // assert: code segment
                    code_file_offset = p_offset;
                    code_file_size   = p_filesz;

                    code_start = p_vaddr;
                    code_size  = p_memsz;
                  } else if (p_flags == PF_RW) {
                    // assert: data segment
                    data_file_offset = p_offset;
                    data_file_size   = p_filesz;

                    data_start = p_vaddr;
                    data_size  = p_memsz;
                  }
                }
              }
            }

            ELF_program_header = (uint64_t*) ((uint64_t) ELF_program_header + e_phentsize);

            i = i + 1;
          }

          if (code_file_size > 0)
            if (code_file_size == code_size)
              if (data_file_size > 0)
                if (code_file_offset + code_file_size <= data_file_offset) {
                  code_binary = (uint64_t*) ((uint64_t) ELF_file_header + code_file_offset);

                  number_of_read_bytes_in_total = get_elf_header_size();

                  to_be_read_bytes = code_file_offset + code_file_size - number_of_read_bytes_in_total;

                  if (to_be_read_bytes + number_of_read_bytes_in_total <= MAX_BINARY_SIZE) {
                    number_of_read_bytes = sign_extend(
                      read(fd,
                        (uint64_t*) ((uint64_t) ELF_file_header + number_of_read_bytes_in_total),
                        to_be_read_bytes),
                      SYSCALL_BITWIDTH);

                    if (number_of_read_bytes == to_be_read_bytes) {
                      number_of_read_bytes_in_total = number_of_read_bytes_in_total + number_of_read_bytes;

                      data_binary = (uint64_t*) ((uint64_t) ELF_file_header + data_file_offset);

                      to_be_read_bytes = data_file_offset + data_file_size - number_of_read_bytes_in_total;

                      if (to_be_read_bytes + number_of_read_bytes_in_total <= MAX_BINARY_SIZE) {
                        number_of_read_bytes = sign_extend(
                          read(fd,
                            (uint64_t*) ((uint64_t) ELF_file_header + number_of_read_bytes_in_total),
                            to_be_read_bytes),
                          SYSCALL_BITWIDTH);

                        if (number_of_read_bytes == to_be_read_bytes) {
                          number_of_read_bytes_in_total = number_of_read_bytes_in_total + number_of_read_bytes;

                          printf("%s: %lu bytes with ", selfie_name, number_of_read_bytes_in_total);
                          if (ISRISCU)
                            printf("%lu %lu-bit RISC-U instructions ", code_file_size / INSTRUCTIONSIZE, WORDSIZEINBITS);
                          else
                            printf("%lu bytes of %lu-bit RISC-V code ", code_file_size, WORDSIZEINBITS);
                          printf("and %lu bytes of data loaded from %s\n", data_file_size, binary_name);

                          return;
                        }
                      }
                    }
                  }
                }
        }
      }
    }
  }

  printf("%s: failed to load and decode binary from input file %s\n", selfie_name, binary_name);

  exit(EXITCODE_IOERROR);
}

// -----------------------------------------------------------------
// ----------------------- MIPSTER SYSCALLS ------------------------
// -----------------------------------------------------------------

void emit_exit() {
  create_symbol_table_entry(GLOBAL_TABLE, string_copy("exit"),
    0, PROCEDURE, VOID_T, 1, code_size);

  // load signed 32-bit integer exit code
  emit_load(REG_A0, REG_SP, 0);

  // remove the exit code from the stack
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  // load the correct syscall number and invoke syscall
  emit_addi(REG_A7, REG_ZR, SYSCALL_EXIT);

  emit_ecall();

  // never returns here
}

void implement_exit(uint64_t* context) {
  // parameter
  uint64_t signed_int_exit_code;

  if (debug_syscalls) {
    printf("(exit): ");
    print_register_hexadecimal(REG_A0);
    printf(" |- ->\n");
  }

  signed_int_exit_code = *(get_regs(context) + REG_A0);

  set_exit_code(context, sign_shrink(signed_int_exit_code, SYSCALL_BITWIDTH));
}

void emit_read() {
  create_symbol_table_entry(GLOBAL_TABLE, string_copy("read"),
    0, PROCEDURE, UINT64_T, 3, code_size);

  emit_load(REG_A0, REG_SP, 0); // fd
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_load(REG_A1, REG_SP, 0); // *buffer
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_load(REG_A2, REG_SP, 0); // size
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_addi(REG_A7, REG_ZR, SYSCALL_READ);

  emit_ecall();

  // jump back to caller, return value is in REG_A0
  emit_jalr(REG_ZR, REG_RA, 0);
}

uint64_t copy_buffer(uint64_t* context, uint64_t vbuffer, uint64_t* buffer, uint64_t size, uint64_t upload) {
  uint64_t is_string;
  uint64_t vaddr;
  uint64_t i;

  if (size == 0) {
    // include null terminator
    size = MAX_FILENAME_LENGTH + 1;

    is_string = 1;
  } else
    is_string = 0;

  vaddr = vbuffer;

  // avoid integer overflow with vbuffer + size
  while (vaddr - vbuffer < size) {
    if (is_virtual_address_valid(vaddr, WORDSIZE))
      if (is_data_stack_heap_address(context, vaddr)) {
        if (is_virtual_address_mapped(get_pt(context), vaddr)) {
          if (upload)
            store_virtual_memory(get_pt(context), vaddr, load_word(buffer, vaddr - vbuffer, 1));
          else
            store_word(buffer, vaddr - vbuffer, 1, load_virtual_memory(get_pt(context), vaddr));
        } else {
          printf("%s: virtual address 0x%08lX is unmapped\n", selfie_name, vaddr);

          return 0;
        }

        if (is_string) {
          i = 0;

          // check if string ends in the current word
          // WORDSIZE may be less than sizeof(uint64_t)
          while (i < WORDSIZE) {
            if (load_character((char*) buffer, vaddr - vbuffer + i) == 0)
              return 1;

            i = i + 1;
          }
        }

        // advance to the next word in virtual memory
        vaddr = vaddr + WORDSIZE;
      } else {
        printf("%s: virtual address 0x%08lX is in an invalid segment\n", selfie_name, vaddr);

        return 0;
      }
    else {
      printf("%s: virtual address 0x%08lX is invalid\n", selfie_name, vaddr);

      return 0;
    }
  }

  if (is_string) {
    printf("%s: string is too long at virtual address 0x%08lX\n", selfie_name, vaddr);

    return 0;
  }

  return 1;
}

void implement_read(uint64_t* context) {
  // parameters
  uint64_t fd;
  uint64_t vbuffer;
  uint64_t size;

  if (debug_syscalls) {
    printf("(read): ");
    print_register_value(REG_A0);
    printf(",");
    print_register_hexadecimal(REG_A1);
    printf(",");
    print_register_value(REG_A2);
    printf(" |- ");
    print_register_value(REG_A0);
  }

  fd      = *(get_regs(context) + REG_A0);
  vbuffer = *(get_regs(context) + REG_A1);
  size    = *(get_regs(context) + REG_A2);

  if (debug_read)
    printf("%s: trying to read %lu bytes from file with descriptor %lu into buffer at virtual address 0x%08lX\n", selfie_name,
      size, fd, vbuffer);

  if (round_up(size, sizeof(uint64_t)) > IO_buffer_size) {
    // accommodate integer-aligned buffer
    IO_buffer_size = round_up(size, sizeof(uint64_t));

    // buffer must be fully mapped for subsequent read syscalls with greater sizes
    IO_buffer = touch(smalloc(IO_buffer_size), IO_buffer_size);
  }

  // read syscall may read less than size bytes
  // zero buffer up to integer-aligned size
  zero_memory(IO_buffer, round_up(size, sizeof(uint64_t)));

  *(get_regs(context) + REG_A0) = read(fd, IO_buffer, size);

  size = sign_extend(*(get_regs(context) + REG_A0), SYSCALL_BITWIDTH);

  if (signed_less_than(0, size))
    if (not(copy_buffer(context, vbuffer, IO_buffer, size, 1)))
      *(get_regs(context) + REG_A0) = sign_shrink(-1, SYSCALL_BITWIDTH);

  set_pc(context, get_pc(context) + INSTRUCTIONSIZE);

  if (debug_read)
    printf("%s: actually read %lu bytes from file with descriptor %lu\n", selfie_name,
      size, fd);

  if (debug_syscalls) {
    printf(" -> ");
    print_register_value(REG_A0);
    println();
  }
}

void emit_write() {
  create_symbol_table_entry(GLOBAL_TABLE, string_copy("write"),
    0, PROCEDURE, UINT64_T, 3, code_size);

  emit_load(REG_A0, REG_SP, 0); // fd
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_load(REG_A1, REG_SP, 0); // *buffer
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_load(REG_A2, REG_SP, 0); // size
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_addi(REG_A7, REG_ZR, SYSCALL_WRITE);

  emit_ecall();

  emit_jalr(REG_ZR, REG_RA, 0);
}

void implement_write(uint64_t* context) {
  // parameters
  uint64_t fd;
  uint64_t vbuffer;
  uint64_t size;

  if (debug_syscalls) {
    printf("(write): ");
    print_register_value(REG_A0);
    printf(",");
    print_register_hexadecimal(REG_A1);
    printf(",");
    print_register_value(REG_A2);
    printf(" |- ");
    print_register_value(REG_A0);
  }

  fd      = *(get_regs(context) + REG_A0);
  vbuffer = *(get_regs(context) + REG_A1);
  size    = *(get_regs(context) + REG_A2);

  if (debug_write)
    printf("%s: trying to write %lu bytes from buffer at virtual address 0x%08lX into file with descriptor %lu\n", selfie_name,
      size, vbuffer, fd);

  if (size >= IO_buffer_size) {
    // integer-align and accommodate potentially needed null terminator
    IO_buffer_size = round_up(size + 1, sizeof(uint64_t));

    // buffer must be fully mapped for subsequent read syscalls with greater sizes
    IO_buffer = touch(smalloc(IO_buffer_size), IO_buffer_size);
  }

  // null-terminate buffer
  *(IO_buffer + size / sizeof(uint64_t)) = 0;

  if (copy_buffer(context, vbuffer, IO_buffer, size, 0))
    *(get_regs(context) + REG_A0) = sign_shrink(write_to_printf(fd, IO_buffer, size), SYSCALL_BITWIDTH);
  else
    *(get_regs(context) + REG_A0) = sign_shrink(-1, SYSCALL_BITWIDTH);

  set_pc(context, get_pc(context) + INSTRUCTIONSIZE);

  if (debug_write)
    printf("%s: actually wrote %lu bytes into file with descriptor %lu\n", selfie_name,
      sign_extend(*(get_regs(context) + REG_A0), SYSCALL_BITWIDTH), fd);

  if (debug_syscalls) {
    printf(" -> ");
    print_register_value(REG_A0);
    println();
  }
}

void emit_open() {
  create_symbol_table_entry(GLOBAL_TABLE, string_copy("open"),
    0, PROCEDURE, UINT64_T, 3, code_size);

  emit_load(REG_A1, REG_SP, 0); // filename
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_load(REG_A2, REG_SP, 0); // flags
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_load(REG_A3, REG_SP, 0); // mode
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  // DIRFD_AT_FDCWD makes sure that openat behaves like open
  emit_addi(REG_A0, REG_ZR, DIRFD_AT_FDCWD);

  emit_addi(REG_A7, REG_ZR, SYSCALL_OPENAT);

  emit_ecall();

  emit_jalr(REG_ZR, REG_RA, 0);
}

uint64_t down_load_string(uint64_t* context, uint64_t vstring, char* s) {
  return copy_buffer(context, vstring, (uint64_t*) s, 0, 0);
}

void implement_openat(uint64_t* context) {
  // parameters
  uint64_t vfilename;
  uint64_t flags;
  uint64_t mode;

  if (debug_syscalls) {
    printf("(openat): ");
    print_register_hexadecimal(REG_A0);
    printf(",");
    print_register_hexadecimal(REG_A1);
    printf(",");
    print_register_hexadecimal(REG_A2);
    printf(",");
    print_register_octal(REG_A3);
    printf(" |- ");
    print_register_value(REG_A1);
  }

  /* We actually emulate the part of the openat system call here that is
     implemented by the open system call which is deprecated in Linux.
     In particular, the first parameter (REG_A0) is ignored here while
     set to DIRFD_AT_FDCWD in the wrapper for the open library call to
     make sure openat behaves like open when bootstrapping selfie. */

  vfilename = *(get_regs(context) + REG_A1);
  flags     = *(get_regs(context) + REG_A2);
  mode      = *(get_regs(context) + REG_A3);

  if (down_load_string(context, vfilename, filename_buffer)) {
    if (flags == LINUX_O_CREAT_TRUNC_WRONLY)
      // use correct flags for host operating system
      flags = O_CREAT_TRUNC_WRONLY;

    *(get_regs(context) + REG_A0) = open(filename_buffer, flags, mode);
  } else
    *(get_regs(context) + REG_A0) = sign_shrink(-1, SYSCALL_BITWIDTH);

  set_pc(context, get_pc(context) + INSTRUCTIONSIZE);

  if (debug_open)
    printf("%s: opened file %s with flags 0x%lX and mode 0o%lo returning file descriptor %lu\n", selfie_name,
      filename_buffer, flags, mode, sign_extend(*(get_regs(context) + REG_A0), SYSCALL_BITWIDTH));

  if (debug_syscalls) {
    printf(" -> ");
    print_register_value(REG_A0);
    println();
  }
}

void emit_malloc() {
  uint64_t* entry;

  create_symbol_table_entry(GLOBAL_TABLE, string_copy("malloc"),
    0, PROCEDURE, UINT64STAR_T, 1, code_size);

  // on boot levels higher than 0, zalloc falls back to malloc
  // assuming that page frames are zeroed on boot level 0
  create_symbol_table_entry(GLOBAL_TABLE, string_copy("zalloc"),
    0, PROCEDURE, UINT64STAR_T, 1, code_size);

  // allocate memory in data segment for recording state of
  // malloc (bump pointer) in compiler-declared global variable
  data_size = data_size + WORDSIZE;

  // define global variable _bump for storing malloc's bump pointer
  // use bump_name string to obtain unique hash
  entry = create_symbol_table_entry(GLOBAL_TABLE, bump_name,
    1, VARIABLE, UINT64_T, 0, -data_size);

  emit_data_word(get_value(entry), get_address(entry), get_line_number(entry));

  dc_global_variable = dc_global_variable + 1;

  // allocate register for size parameter
  talloc();

  emit_load(current_temporary(), REG_SP, 0); // size
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  // round up to target-dependent memory word size
  emit_round_up(current_temporary(), WORDSIZE);

  // allocate register to compute new bump pointer
  talloc();

  // assert: current temporary is $t1 register to enable propagation of
  // lower and upper bounds on addresses in model generation of brk syscall

  // get current _bump which will be returned upon success
  emit_load(current_temporary(), get_scope(entry), get_address(entry));

  // call brk syscall to set new program break to _bump + size
  emit_add(REG_A0, current_temporary(), previous_temporary());
  emit_addi(REG_A7, REG_ZR, SYSCALL_BRK);
  emit_ecall();

  // caution: if mipster runs with garbage collection enabled,
  // the brk syscall is redirected to the gc_brk syscall which
  // skips the next seven instructions, see implement_gc_brk

  // return 0 if memory allocation failed, that is,
  // if new program break is still _bump and size != 0
  emit_beq(REG_A0, current_temporary(), 2 * INSTRUCTIONSIZE);
  emit_beq(REG_ZR, REG_ZR, 4 * INSTRUCTIONSIZE);
  emit_beq(REG_ZR, previous_temporary(), 3 * INSTRUCTIONSIZE);
  emit_addi(REG_A0, REG_ZR, 0);
  emit_beq(REG_ZR, REG_ZR, 3 * INSTRUCTIONSIZE);

  // if memory was successfully allocated
  // set _bump to new program break
  // and then return original _bump
  emit_store(get_scope(entry), get_address(entry), REG_A0);
  emit_addi(REG_A0, current_temporary(), 0);

  // gc_brk syscall skips up to this point, see implement_gc_brk

  tfree(2);

  emit_jalr(REG_ZR, REG_RA, 0);
}

uint64_t try_brk(uint64_t* context, uint64_t new_program_break) {
  uint64_t current_program_break;

  current_program_break = get_program_break(context);

  if (is_virtual_address_valid(new_program_break, WORDSIZE))
    if (is_address_between_stack_and_heap(context, new_program_break)) {
      if (debug_brk)
        printf("%s: setting program break to 0x%08lX\n", selfie_name, (uint64_t) new_program_break);

      set_program_break(context, new_program_break);

      return new_program_break;
    }

  // setting new program break failed, return current program break

  if (debug_brk)
    printf("%s: retrieving current program break 0x%08lX\n", selfie_name, (uint64_t) current_program_break);

  return current_program_break;
}

void implement_brk(uint64_t* context) {
  // parameter
  uint64_t new_program_break;

  // local variable
  uint64_t previous_program_break;

  new_program_break = *(get_regs(context) + REG_A0);

  previous_program_break = get_program_break(context);

  // attempt to update program break

  new_program_break = try_brk(context, new_program_break);

  if (debug_syscalls) {
    printf("(brk): ");
    print_register_hexadecimal(REG_A0);
    printf(" |- ");
    print_register_hexadecimal(REG_A0);
  }

  if (new_program_break == *(get_regs(context) + REG_A0)) {
    // attempt to update program break succeeded
    if (*(get_regs(context) + REG_A0) != previous_program_break)
      // assert: brk syscall invoked by malloc
      // account for malloc if program break actually changed
      set_lc_malloc(context, get_lc_malloc(context) + 1);
  } else
    // attempt to update program break failed
    *(get_regs(context) + REG_A0) = previous_program_break;

  set_pc(context, get_pc(context) + INSTRUCTIONSIZE);

  if (debug_syscalls) {
    printf(" -> ");
    print_register_hexadecimal(REG_A0);
    println();
  }
}

uint64_t is_boot_level_zero() {
  // C99 malloc(0) returns either a null pointer or a unique pointer,
  // see pubs.opengroup.org/onlinepubs/9699919799
  // in contrast, selfie's malloc(0) returns the same not null address,
  // if malloc(0) is called consecutively.
  uint64_t first_malloc;
  uint64_t second_malloc;

  first_malloc  = (uint64_t) malloc(0);
  second_malloc = (uint64_t) malloc(0);

  if (first_malloc == 0)
    return 1;
  if (first_malloc != second_malloc)
    return 1;

  // selfie's malloc, cannot be boot level 0!
  return 0;
}

// -----------------------------------------------------------------
// ------------------------ HYPSTER SYSCALL ------------------------
// -----------------------------------------------------------------

void emit_switch() {
  create_symbol_table_entry(GLOBAL_TABLE, string_copy("hypster_switch"),
    0, PROCEDURE, UINT64STAR_T, 2, code_size);

  emit_load(REG_A0, REG_SP, 0); // context to which we switch
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_load(REG_A1, REG_SP, 0); // number of instructions to execute
  emit_addi(REG_SP, REG_SP, WORDSIZE);

  emit_addi(REG_A7, REG_ZR, SYSCALL_SWITCH);

  emit_ecall();

  // find context from which we are switching back in REG_A6 and save in return register
  emit_addi(REG_A0, REG_A6, 0);

  emit_jalr(REG_ZR, REG_RA, 0);
}

void do_switch(uint64_t* to_context, uint64_t timeout) {
  // provide context from which we are switching in REG_A6 to avoid overwriting REG_A0
  if (get_parent(current_context) != MY_CONTEXT)
    *(registers + REG_A6) = (uint64_t) get_virtual_context(current_context);
  else
    // TODO: support global machine-context identifiers
    *(registers + REG_A6) = (uint64_t) current_context;

  write_register(REG_A6);

  if (debug_switch) {
    printf("%s: switching from context %s to context %s", selfie_name,
      get_name(current_context), get_name(to_context));
    if (timeout != TIMEROFF)
      printf(" to execute %lu instructions", timeout);
    println();
  }

  current_context = to_context;

  timer = timeout;
}

void implement_switch() {
  // parameters
  uint64_t* to_context;
  uint64_t timeout;

  if (debug_syscalls) {
    printf("(switch): ");
    print_register_hexadecimal(REG_A0);
    printf(",");
    print_register_value(REG_A1);
    printf(" |- ");
    print_register_value(REG_A6);
  }

  read_register(REG_A0);
  read_register(REG_A1);

  to_context = (uint64_t*) *(registers + REG_A0);
  timeout    =             *(registers + REG_A1);

  save_context(current_context);

  // cache context on my boot level before switching
  to_context = cache_context(to_context);

  restore_context(to_context);

  do_switch(to_context, timeout);

  if (debug_syscalls) {
    printf(" -> ");
    print_register_hexadecimal(REG_A6);
    println();
  }
}

uint64_t* mipster_switch(uint64_t* to_context, uint64_t timeout) {
  restore_context(to_context);

  do_switch(to_context, timeout);

  run_until_exception();

  save_context(current_context);

  return current_context;
}

uint64_t* hypster_switch(uint64_t* to_context, uint64_t timeout) {
  // this procedure is only executed at boot level 0
  return mipster_switch(to_context, timeout);
}

// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~
// -----------------------------------------------------------------
// -----------------    A R C H I T E C T U R E    -----------------
// -----------------------------------------------------------------
// *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~ *~*~

// -----------------------------------------------------------------
// ----------------------------- CACHE -----------------------------
// -----------------------------------------------------------------

void reset_cache_counters(uint64_t* cache) {
  set_cache_hits(cache, 0);
  set_cache_misses(cache, 0);
}

void reset_all_cache_counters() {
  if (L1_CACHE_ENABLED) {
    reset_cache_counters(L1_DCACHE);
    reset_cache_counters(L1_ICACHE);
  }
}

void init_cache_memory(uint64_t* cache) {
  uint64_t number_of_cache_blocks;
  uint64_t* cache_memory;
  uint64_t i;
  uint64_t* cache_block;

  number_of_cache_blocks = get_cache_size(cache) / get_cache_block_size(cache);

  cache_memory = smalloc(number_of_cache_blocks * sizeof(uint64_t*));

  set_cache_memory(cache, cache_memory);

  i = 0;

  while (i < number_of_cache_blocks) {
    cache_block = allocate_cache_block();

    // valid bit and timestamp are already initialized to 0

    *(cache_memory + i) = (uint64_t) cache_block;

    set_block_memory(cache_block, smalloc(get_cache_block_size(cache)));

    i = i + 1;
  }
}

void init_cache(uint64_t* cache, uint64_t cache_size, uint64_t associativity, uint64_t cache_block_size) {
  set_cache_size(cache, cache_size);
  set_associativity(cache, associativity);
  set_cache_block_size(cache, cache_block_size);

  init_cache_memory(cache);

  reset_cache_counters(cache);
}

void init_all_caches() {
  L1_DCACHE = allocate_cache();

  init_cache(L1_DCACHE, L1_DCACHE_SIZE, L1_DCACHE_ASSOCIATIVITY, L1_DCACHE_BLOCK_SIZE);

  L1_ICACHE = allocate_cache();

  init_cache(L1_ICACHE, L1_ICACHE_SIZE, L1_ICACHE_ASSOCIATIVITY, L1_ICACHE_BLOCK_SIZE);
}

void flush_cache(uint64_t* cache) {
  uint64_t number_of_cache_blocks;
  uint64_t* cache_memory;
  uint64_t i;
  uint64_t* cache_block;

  number_of_cache_blocks = get_cache_size(cache) / get_cache_block_size(cache);

  cache_memory = get_cache_memory(cache);

  i = 0;

  while (i < number_of_cache_blocks) {
    cache_block = (uint64_t*) *(cache_memory + i);

    set_valid_flag(cache_block, 0);
    set_timestamp(cache_block, 0);

    i = i + 1;
  }

  set_cache_timer(cache, 0);
}

void flush_all_caches() {
  if (L1_CACHE_ENABLED) {
    flush_cache(L1_DCACHE);
    flush_cache(L1_ICACHE);
  }
}

uint64_t cache_set_size(uint64_t* cache) {
  return get_cache_size(cache) / get_associativity(cache);
}

// cache addressing:
//
// vaddr
// +-----+-------+-------------+
// |     | index | byte offset |
// +-----+-------+-------------+
// 31    ^       ^             0
//       |       |
//       |  log(cache_block_size)
//       |
// log(cache_size / associativity)
//       |
// paddr v
// +-----+---------------------+
// | tag |                     |
// +-----+---------------------+

uint64_t cache_tag(uint64_t* cache, uint64_t address) {
  return address / cache_set_size(cache);
}

uint64_t cache_index(uint64_t* cache, uint64_t address) {
  return (address - cache_tag(cache, address) * cache_set_size(cache)) / get_cache_block_size(cache);
}

uint64_t cache_block_address(uint64_t* cache, uint64_t address) {
  return address / get_cache_block_size(cache) * get_cache_block_size(cache);
}

uint64_t cache_byte_offset(uint64_t* cache, uint64_t address) {
  return address - cache_block_address(cache, address);
}

uint64_t* cache_set(uint64_t* cache, uint64_t vaddr) {
  return get_cache_memory(cache) + cache_index(cache, vaddr) * get_associativity(cache);
}

uint64_t get_new_timestamp(uint64_t* cache) {
  uint64_t timestamp;

  timestamp = get_cache_timer(cache);

  set_cache_timer(cache, timestamp + 1);

  return timestamp;
}

uint64_t* cache_lookup(uint64_t* cache, uint64_t vaddr, uint64_t paddr, uint64_t is_access) {
  uint64_t tag;
  uint64_t* set;
  uint64_t i;
  uint64_t* lru_block;
  uint64_t* cache_block;

  tag = cache_tag(cache, paddr);
  set = cache_set(cache, vaddr);

  i = 0;

  lru_block = (uint64_t*) *set;

  while (i < get_associativity(cache)) {
    cache_block = (uint64_t*) *(set + i);

    if (get_timestamp(cache_block) < get_timestamp(lru_block))
      lru_block = cache_block;

    if (get_valid_flag(cache_block))
      if (get_tag(cache_block) == tag) {
        // cache hit

        if (is_access) {
          set_cache_hits(cache, get_cache_hits(cache) + 1);

          set_timestamp(cache_block, get_new_timestamp(cache));
        }

        return cache_block;
      }

    i = i + 1;
  }

  // cache miss

  set_valid_flag(lru_block, 0);

  return lru_block;
}

void fill_cache_block(uint64_t* cache, uint64_t* cache_block, uint64_t paddr) {
  uint64_t number_of_words_in_cache_block;
  uint64_t* block_memory;
  uint64_t i;

  // cache block size / sizeof(uint64_t) (not WORDSIZE)
  number_of_words_in_cache_block = get_cache_block_size(cache) / sizeof(uint64_t);

  block_memory = get_block_memory(cache_block);

  // align paddr to cache block
  paddr = cache_block_address(cache, paddr);

  i = 0;

  while (i < number_of_words_in_cache_block) {
    *(block_memory + i) = load_physical_memory((uint64_t*) paddr + i);

    i = i + 1;
  }
}

uint64_t* handle_cache_miss(uint64_t* cache, uint64_t* cache_block, uint64_t paddr, uint64_t is_access) {
  if (is_access) {
    set_cache_misses(cache, get_cache_misses(cache) + 1);

    // make sure the entire cache block contains valid data
    fill_cache_block(cache, cache_block, paddr);

    set_tag(cache_block, cache_tag(cache, paddr));

    set_timestamp(cache_block, get_new_timestamp(cache));

    set_valid_flag(cache_block, 1);

    return cache_block;
  } else
    return (uint64_t*) 0;
}

uint64_t* retrieve_cache_block(uint64_t* cache, uint64_t vaddr, uint64_t paddr, uint64_t is_access) {
  uint64_t* cache_block;

  cache_block = cache_lookup(cache, vaddr, paddr, is_access);

  if (get_valid_flag(cache_block))
    // cache hit
    return cache_block;
  else
    return handle_cache_miss(cache, cache_block, paddr, is_access);
}

void flush_cache_block(uint64_t* cache, uint64_t* cache_block, uint64_t paddr) {
  uint64_t number_of_words_in_cache_block;
  uint64_t* block_memory;
  uint64_t i;

  // cache block size / sizeof(uint64_t) (not WORDSIZE)
  number_of_words_in_cache_block = get_cache_block_size(cache) / sizeof(uint64_t);

  block_memory = get_block_memory(cache_block);

  // align paddr to cache block
  paddr = cache_block_address(cache, paddr);

  i = 0;

  while (i < number_of_words_in_cache_block) {
    store_physical_memory((uint64_t*) paddr + i, *(block_memory + i));

    i = i + 1;
  }
}

uint64_t load_from_cache(uint64_t* cache, uint64_t vaddr, uint64_t paddr) {
  uint64_t* cache_block;
  uint64_t* block_memory;

  cache_block = retrieve_cache_block(cache, vaddr, paddr, 1);

  block_memory = get_block_memory(cache_block);

  return *(block_memory + cache_byte_offset(cache, vaddr) / sizeof(uint64_t));
}

void store_in_cache(uint64_t* cache, uint64_t vaddr, uint64_t paddr, uint64_t data) {
  uint64_t* cache_block;
  uint64_t* block_memory;

  cache_block = retrieve_cache_block(cache, vaddr, paddr, 1);

  block_memory = get_block_memory(cache_block);

  *(block_memory + cache_byte_offset(cache, vaddr) / sizeof(uint64_t)) = data;

  flush_cache_block(cache, cache_block, paddr);
}

uint64_t load_instruction_from_cache(uint64_t vaddr, uint64_t paddr) {
  // assert: is_valid_virtual_address(vaddr) == 1

  return load_from_cache(L1_ICACHE, vaddr, paddr);
}

uint64_t load_data_from_cache(uint64_t vaddr, uint64_t paddr) {
  // assert: is_valid_virtual_address(vaddr) == 1

  return load_from_cache(L1_DCACHE, vaddr, paddr);
}

void store_data_in_cache(uint64_t vaddr, uint64_t paddr, uint64_t data) {
  uint64_t* cache_block;

  // assert: is_valid_virtual_address(vaddr) == 1

  store_in_cache(L1_DCACHE, vaddr, paddr, data);

  if (L1_CACHE_COHERENCY) {
    cache_block = retrieve_cache_block(L1_ICACHE, vaddr, paddr, 0);

    // mimicking x86 behavior (see Intel 64 and IA-32 Architectures Software Developer's
    // Manual Volume 3, Chapter 11.6 Self-Modifying Code: "A write to a memory location in
    // a code segment that is currently cached in the processor causes the associated
    // cache line (or lines) to be invalidated")
    if (cache_block != (uint64_t*) 0) {
      set_valid_flag(cache_block, 0);
      set_timestamp(cache_block, 0);

      L1_icache_coherency_invalidations = L1_icache_coherency_invalidations + 1;
    }
  }
}

void print_cache_profile(uint64_t hits, uint64_t misses, char* cache_name) {
  uint64_t accesses;

  accesses = hits + misses;

  printf("%s: %s%lu,", selfie_name, cache_name, accesses);
  printf("%lu(%lu.%.2lu%%),%lu(%lu.%.2lu%%)",
    hits,
    percentage_format_integral_2(accesses, hits),
    percentage_format_fractional_2(accesses, hits),
    misses,
    percentage_format_integral_2(accesses, misses),
    percentage_format_fractional_2(accesses, misses));
}

// -----------------------------------------------------------------
// ---------------------------- MEMORY -----------------------------
// -----------------------------------------------------------------

uint64_t load_physical_memory(uint64_t* paddr) {
  return *paddr;
}

void store_physical_memory(uint64_t* paddr, uint64_t data) {
  *paddr = data;
}

uint64_t root_PDE_offset(uint64_t page) {
  // with 4GB (2^32B) virtual memory there are 2^(32-12) 4KB (2^12B) pages;
  // in a two-level page table with 4KB (2^12B) pages as leaf nodes and
  // 64-bit pointers (2^3B), each leaf node accommodates 2^(12-3) PTEs;
  // thus bits 9 through 19 encode the root PDE (page directory entry) offset
  return page / NUMBEROFLEAFPTES; // right shift by 9 bits
}

uint64_t leaf_PTE_offset(uint64_t page) {
  // bits 0 through 8 encode the leaf PTE (page table entry) offset
  return page - root_PDE_offset(page) * NUMBEROFLEAFPTES; // extract the 9 LSBs
}

uint64_t* get_PTE_address(uint64_t* parent_table, uint64_t* table, uint64_t page) {
  uint64_t* leaf_pt;

  // assert: 0 <= page < NUMBEROFPAGES

  if (not(PAGETABLETREE))
    // just pointer arithmetic, no access!
    return table + page;
  else {
    // to get leaf page table, root page directory access is required!
    if (parent_table == (uint64_t*) 0)
      leaf_pt = (uint64_t*) *(table + root_PDE_offset(page));
    else
      // table is in address space of parent_table
      leaf_pt = (uint64_t*) load_virtual_memory(parent_table, (uint64_t) (table + root_PDE_offset(page)));

    if (leaf_pt == (uint64_t*) 0)
      return (uint64_t*) 0;
    else
      // again, just pointer arithmetic, no access!
      return leaf_pt + leaf_PTE_offset(page);
  }
}

uint64_t get_page_frame(uint64_t* table, uint64_t page) {
  uint64_t* PTE_address;

  // TODO: translation lookaside buffer (TLB)

  PTE_address = get_PTE_address(0, table, page);

  if (PTE_address == (uint64_t*) 0)
    return 0;
  else
    return (uint64_t) *PTE_address;
}

uint64_t is_page_mapped(uint64_t* table, uint64_t page) {
  if (get_page_frame(table, page) != 0)
    return 1;
  else
    return 0;
}

void set_page_frame(uint64_t* table, uint64_t page, uint64_t frame) {
  uint64_t* leaf_pt;

  // assert: 0 <= page < NUMBEROFPAGES

  if (not(PAGETABLETREE))
    *(table + page) = frame;
  else {
    leaf_pt = (uint64_t*) *(table + root_PDE_offset(page));

    if (leaf_pt == (uint64_t*) 0) {
      leaf_pt = palloc(); // 4KB leaf page table

      *(table + root_PDE_offset(page)) = (uint64_t) leaf_pt;
    }

    *(leaf_pt + leaf_PTE_offset(page)) = frame;
  }
}

uint64_t page_of_virtual_address(uint64_t vaddr) {
  return vaddr / PAGESIZE;
}

uint64_t virtual_address_of_page(uint64_t page) {
  return page * PAGESIZE;
}

uint64_t is_virtual_address_valid(uint64_t vaddr, uint64_t alignment) {
  // is address in range?
  if (vaddr <= HIGHESTVIRTUALADDRESS)
    // is address aligned?
    if (vaddr % alignment == 0)
      return 1;

  return 0;
}

uint64_t is_virtual_address_mapped(uint64_t* table, uint64_t vaddr) {
  // assert: is_virtual_address_valid(vaddr, WORDSIZE) == 1

  return is_page_mapped(table, page_of_virtual_address(vaddr));
}

uint64_t* translate_virtual_to_physical(uint64_t* table, uint64_t vaddr) {
  uint64_t page;
  uint64_t frame;
  uint64_t paddr;

  // assert: is_virtual_address_valid(vaddr, WORDSIZE) == 1
  // assert: is_virtual_address_mapped(table, vaddr) == 1

  // assert: page is PAGESIZE-aligned in virtual memory
  page = page_of_virtual_address(vaddr);

  // assert: frame is PAGEFRAMESIZE-aligned in physical memory
  frame = get_page_frame(table, page);

  // translate virtual address to physical address
  paddr = (vaddr - page * PAGESIZE) * (PAGEFRAMESIZE / PAGESIZE) + frame;

  if (debug_translation)
    printf("%s: translating:\n vaddr: 0x%08lX\n page: 0x%04lX\n frame: 0x%08lX\n paddr: 0x%08lX\n", selfie_name,
      vaddr,
      page,
      frame,
      paddr);

  return (uint64_t*) paddr;
}

uint64_t load_virtual_memory(uint64_t* table, uint64_t vaddr) {
  // assert: is_virtual_address_valid(vaddr, WORDSIZE) == 1
  // assert: is_virtual_address_mapped(table, vaddr) == 1

  return load_physical_memory(translate_virtual_to_physical(table, vaddr));
}

void store_virtual_memory(uint64_t* table, uint64_t vaddr, uint64_t data) {
  // assert: is_virtual_address_valid(vaddr, WORDSIZE) == 1
  // assert: is_virtual_address_mapped(table, vaddr) == 1

  store_physical_memory(translate_virtual_to_physical(table, vaddr), data);
}

uint64_t load_cached_virtual_memory(uint64_t* table, uint64_t vaddr) {
  if (L1_CACHE_ENABLED)
    // assert: is_virtual_address_valid(vaddr, WORDSIZE) == 1
    // assert: is_virtual_address_mapped(table, vaddr) == 1
    return load_data_from_cache(vaddr, (uint64_t) translate_virtual_to_physical(table, vaddr));
  else
    return load_virtual_memory(table, vaddr);
}

void store_cached_virtual_memory(uint64_t* table, uint64_t vaddr, uint64_t data) {
  if (L1_CACHE_ENABLED)
    // assert: is_virtual_address_valid(vaddr, WORDSIZE) == 1
    // assert: is_virtual_address_mapped(table, vaddr) == 1
    store_data_in_cache(vaddr, (uint64_t) translate_virtual_to_physical(table, vaddr), data);
  else
    store_virtual_memory(table, vaddr, data);
}

uint64_t load_cached_instruction_word(uint64_t* table, uint64_t vaddr) {
  if (L1_CACHE_ENABLED)
    // assert: is_virtual_address_valid(vaddr, WORDSIZE) == 1
    // assert: is_virtual_address_mapped(table, vaddr) == 1
    return load_instruction_from_cache(vaddr, (uint64_t) translate_virtual_to_physical(table, vaddr));
  else
    return load_virtual_memory(table, vaddr);
}

// -----------------------------------------------------------------
// ------------------------- INSTRUCTIONS --------------------------
// -----------------------------------------------------------------

uint64_t print_code_line_number_for_instruction(uint64_t address, uint64_t offset) {
  if (code_line_number != (uint64_t*) 0)
    return printf_or_write(sprintf(string_buffer, "(~%lu)",
      *(code_line_number + (address - offset) / INSTRUCTIONSIZE)));
  else
    return 0;
}

uint64_t print_code_context_for_instruction(uint64_t address) {
  uint64_t w;

  if (run) {
    w = printf_or_write(sprintf(string_buffer, "%s: pc==0x%lX", binary_name, address))
      + print_code_line_number_for_instruction(address, code_start);
    if (symbolic)
      // skip further output
      return w;
    else
      return w + printf_or_write(sprintf(string_buffer, ": "));
  } else if (model)
    return dprintf(output_fd, "0x%lX", address)
      + print_code_line_number_for_instruction(address, code_start)
      + dprintf(output_fd, ": ");
  else if (disassemble_verbose)
    return dprintf(output_fd, "0x%lX", address)
      + print_code_line_number_for_instruction(address, 0)
      + dprintf(output_fd, ": 0x%08lX: ", (uint64_t) ir);
  else
    return 0;
}

uint64_t print_lui() {
  return print_code_context_for_instruction(pc) +
    printf_or_write(sprintf(string_buffer, "%s %s,0x%lX",
      get_mnemonic(is), get_register_name(rd), sign_shrink(imm, 20)));
}

void print_lui_before() {
  printf(": |- ");
  print_register_hexadecimal(rd);
}

void print_lui_after() {
  printf(" -> ");
  print_register_hexadecimal(rd);
}

void record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr() {
  record_state(*(registers + rd));
}

void do_lui() {
  // load upper immediate

  uint64_t next_rd_value;

  if (rd != REG_ZR) {
    // semantics of lui
    next_rd_value = left_shift(imm, 12);

    if (*(registers + rd) != next_rd_value)
      *(registers + rd) = next_rd_value;
    else
      nopc_lui = nopc_lui + 1;
  } else
    nopc_lui = nopc_lui + 1;

  write_register(rd);

  pc = pc + INSTRUCTIONSIZE;

  ic_lui = ic_lui + 1;
}

void undo_lui_addi_add_sub_mul_divu_remu_sltu_load_jal_jalr() {
  *(registers + rd) = *(values + (tc % MAX_REPLAY_LENGTH));
}

uint64_t print_addi() {
  uint64_t w;

  w = print_code_context_for_instruction(pc);

  if (rd == REG_ZR)
    if (rs1 == REG_ZR)
      if (imm == 0)
        return w + printf_or_write(sprintf(string_buffer, "nop"));

  return w + printf_or_write(sprintf(string_buffer, "%s %s,%s,%ld",
    get_mnemonic(is), get_register_name(rd), get_register_name(rs1), imm));
}

void print_addi_before() {
  printf(": ");
  print_register_value(rs1);
  printf(" |- ");
  print_register_value(rd);
}

void print_addi_add_sub_mul_divu_remu_sltu_after() {
  printf(" -> ");
  print_register_value(rd);
}

void do_addi() {
  // add immediate

  uint64_t next_rd_value;

  // tolerate unwrapped values in register-to-register transfers
  read_register_check_wrap(rs1, imm);

  if (rd != REG_ZR) {
    // semantics of addi
    next_rd_value = *(registers + rs1) + imm;

    if (*(registers + rd) != next_rd_value)
      *(registers + rd) = next_rd_value;
    else
      nopc_addi = nopc_addi + 1;
  } else
    nopc_addi = nopc_addi + 1;

  write_register(rd);

  pc = pc + INSTRUCTIONSIZE;

  ic_addi = ic_addi + 1;
}

uint64_t print_add_sub_mul_divu_remu_sltu() {
  return print_code_context_for_instruction(pc)
    + printf_or_write(sprintf(string_buffer, "%s %s,%s,%s",
        get_mnemonic(is), get_register_name(rd),
          get_register_name(rs1), get_register_name(rs2)));
}

void print_add_sub_mul_divu_remu_sltu_before() {
  printf(": ");
  print_register_value(rs1);
  printf(",");
  print_register_value(rs2);
  printf(" |- ");
  print_register_value(rd);
}

void do_add() {
  uint64_t next_rd_value;

  read_register(rs1);
  read_register(rs2);

  if (rd != REG_ZR) {
    // semantics of add
    next_rd_value = *(registers + rs1) + *(registers + rs2);

    if (*(registers + rd) != next_rd_value)
      *(registers + rd) = next_rd_value;
    else
      nopc_add = nopc_add + 1;
  } else
    nopc_add = nopc_add + 1;

  write_register(rd);

  pc = pc + INSTRUCTIONSIZE;

  ic_add = ic_add + 1;
}

void do_sub() {
  uint64_t next_rd_value;

  read_register(rs1);
  read_register(rs2);

  if (rd != REG_ZR) {
    // semantics of sub
    next_rd_value = *(registers + rs1) - *(registers + rs2);

    if (*(registers + rd) != next_rd_value)
      *(registers + rd) = next_rd_value;
    else
      nopc_sub = nopc_sub + 1;
  } else
    nopc_sub = nopc_sub + 1;

  write_register(rd);

  pc = pc + INSTRUCTIONSIZE;

  ic_sub = ic_sub + 1;
}

void do_mul() {
  uint64_t next_rd_value;

  read_register(rs1);
  read_register(rs2);

  if (rd != REG_ZR) {
    // semantics of mul
    next_rd_value = *(registers + rs1) * *(registers + rs2);

    // TODO: support of 128-bit resolution

    if (*(registers + rd) != next_rd_value)
      *(registers + rd) = next_rd_value;
    else
      nopc_mul = nopc_mul + 1;
  } else
    nopc_mul = nopc_mul + 1;

  write_register(rd);

  pc = pc + INSTRUCTIONSIZE;

  ic_mul = ic_mul + 1;
}

void do_divu() {
  // division unsigned

  uint64_t next_rd_value;

  if (*(registers + rs2) != 0) {
    read_register(rs1);
    read_register(rs2);

    if (rd != REG_ZR) {
      // semantics of divu
      next_rd_value = *(registers + rs1) / *(registers + rs2);

      if (*(registers + rd) != next_rd_value)
        *(registers + rd) = next_rd_value;
      else
        nopc_divu = nopc_divu + 1;
    } else
      nopc_divu = nopc_divu + 1;

    write_register(rd);

    pc = pc + INSTRUCTIONSIZE;
  } else
    throw_exception(EXCEPTION_DIVISIONBYZERO, pc);

  ic_divu = ic_divu + 1;
}

void do_remu() {
  // remainder unsigned

  uint64_t next_rd_value;

  if (*(registers + rs2) != 0) {
    read_register(rs1);
    read_register(rs2);

    if (rd != REG_ZR) {
      // semantics of remu
      next_rd_value = *(registers + rs1) % *(registers + rs2);

      if (*(registers + rd) != next_rd_value)
        *(registers + rd) = next_rd_value;
      else
        nopc_remu = nopc_remu + 1;
    } else
      nopc_remu = nopc_remu + 1;

    write_register(rd);

    pc = pc + INSTRUCTIONSIZE;
  } else
    throw_exception(EXCEPTION_DIVISIONBYZERO, pc);

  ic_remu = ic_remu + 1;
}

void do_sltu() {
  // set on less than unsigned

  uint64_t next_rd_value;

  read_register(rs1);
  read_register(rs2);

  if (rd != REG_ZR) {
    // semantics of sltu
    if (*(registers + rs1) < *(registers + rs2))
      next_rd_value = 1;
    else
      next_rd_value = 0;

    if (*(registers + rd) != next_rd_value)
      *(registers + rd) = next_rd_value;
    else
      nopc_sltu = nopc_sltu + 1;
  } else
    nopc_sltu = nopc_sltu + 1;

  write_register(rd);

  pc = pc + INSTRUCTIONSIZE;

  ic_sltu = ic_sltu + 1;
}

uint64_t print_load() {
  return print_code_context_for_instruction(pc)
    + printf_or_write(sprintf(string_buffer, "%s %s,%ld(%s)",
        get_mnemonic(is), get_register_name(rd), imm, get_register_name(rs1)));
}

void print_load_before() {
  uint64_t vaddr;

  vaddr = *(registers + rs1) + imm;

  printf(": ");
  print_register_hexadecimal(rs1);
  printf(",");

  if (is_virtual_address_valid(vaddr, WORDSIZE))
    if (is_valid_segment_read(vaddr))
      if (is_virtual_address_mapped(pt, vaddr)) {
        if (is_system_register(rd))
          printf("mem[0x%lX]==0x%lX |- ", vaddr, load_virtual_memory(pt, vaddr));
        else
          printf("mem[0x%lX]==%ld |- ", vaddr, load_virtual_memory(pt, vaddr));
        print_register_value(rd);
      } else
        printf("0x%lX is unmapped (page fault)", vaddr);
    else
      printf("0x%lX is ill-segmented (segmentation fault)", vaddr);
  else
    printf("0x%lX is invalid", vaddr);
}

void print_load_after(uint64_t vaddr) {
  if (is_virtual_address_valid(vaddr, WORDSIZE))
    if (is_valid_segment_read(vaddr))
      if (is_virtual_address_mapped(pt, vaddr)) {
        printf(" -> ");
        print_register_value(rd);
        printf("==mem[0x%lX]", vaddr);
      }
}

void record_load() {
  uint64_t vaddr;

  vaddr = *(registers + rs1) + imm;

  if (is_virtual_address_valid(vaddr, WORDSIZE))
    if (is_valid_segment_read(vaddr))
      if (is_virtual_address_mapped(pt, vaddr))
        record_state(*(registers + rd));
}

uint64_t do_load() {
  uint64_t vaddr;
  uint64_t next_rd_value;
  uint64_t a;

  // load (double) word

  read_register(rs1);

  vaddr = *(registers + rs1) + imm;

  if (is_virtual_address_valid(vaddr, WORDSIZE)) {
    if (is_valid_segment_read(vaddr)) {
      if (is_virtual_address_mapped(pt, vaddr)) {
        if (rd != REG_ZR) {
          // semantics of load (double) word
          next_rd_value = load_cached_virtual_memory(pt, vaddr);

          if (*(registers + rd) != next_rd_value)
            *(registers + rd) = next_rd_value;
          else
            nopc_load = nopc_load + 1;
        } else
          nopc_load = nopc_load + 1;

        // load values unwrapped
        write_register_wrap(rd, 0);

        // keep track of instruction address for profiling loads
        a = (pc - code_start) / INSTRUCTIONSIZE;

        pc = pc + INSTRUCTIONSIZE;

        // keep track of number of loads in total
        ic_load = ic_load + 1;

        // and individually
        *(loads_per_instruction + a) = *(loads_per_instruction + a) + 1;
      } else
        throw_exception(EXCEPTION_PAGEFAULT, page_of_virtual_address(vaddr));
    } else
      throw_exception(EXCEPTION_SEGMENTATIONFAULT, vaddr);
  } else
    throw_exception(EXCEPTION_INVALIDADDRESS, vaddr);

  return vaddr;
}

uint64_t print_store() {
  return print_code_context_for_instruction(pc)
    + printf_or_write(sprintf(string_buffer, "%s %s,%ld(%s)",
        get_mnemonic(is), get_register_name(rs2), imm, get_register_name(rs1)));
}

void print_store_before() {
  uint64_t vaddr;

  vaddr = *(registers + rs1) + imm;

  printf(": ");
  print_register_hexadecimal(rs1);
  printf(",");
  print_register_value(rs2);
  printf(" |- ");

  if (is_virtual_address_valid(vaddr, WORDSIZE))
    if (is_valid_segment_write(vaddr))
      if (is_virtual_address_mapped(pt, vaddr))
        if (is_system_register(rs2))
          printf("mem[0x%lX]==0x%lX", vaddr, load_virtual_memory(pt, vaddr));
        else
          printf("mem[0x%lX]==%ld", vaddr, load_virtual_memory(pt, vaddr));
      else
        printf("0x%lX is unmapped (page fault)", vaddr);
    else
      printf("0x%lX is ill-segmented (segmentation fault)", vaddr);
  else
    printf("0x%lX is invalid", vaddr);
}

void print_store_after(uint64_t vaddr) {
  if (is_virtual_address_valid(vaddr, WORDSIZE))
    if (is_valid_segment_write(vaddr))
      if (is_virtual_address_mapped(pt, vaddr)) {
        printf(" -> mem[0x%lX]==", vaddr);
        print_register_value(rs2);
      }
}

void record_store() {
  uint64_t vaddr;

  vaddr = *(registers + rs1) + imm;

  if (is_virtual_address_valid(vaddr, WORDSIZE))
    if (is_valid_segment_write(vaddr))
      if (is_virtual_address_mapped(pt, vaddr))
        record_state(load_virtual_memory(pt, vaddr));
}

uint64_t do_store() {
  uint64_t vaddr;
  uint64_t a;

  // store (double) word

  read_register(rs1);

  vaddr = *(registers + rs1) + imm;

  if (is_virtual_address_valid(vaddr, WORDSIZE)) {
    if (is_valid_segment_write(vaddr)) {
      if (is_virtual_address_mapped(pt, vaddr)) {
        // tolerate storing unwrapped values
        read_register_check_wrap(rs2, 0);

        // semantics of store (double) word
        if (load_virtual_memory(pt, vaddr) != *(registers + rs2))
          store_cached_virtual_memory(pt, vaddr, *(registers + rs2));
        else {
          nopc_store = nopc_store + 1;

          if (L1_CACHE_ENABLED)
            // effective nop still changes the cache state
            store_cached_virtual_memory(pt, vaddr, *(registers + rs2));
        }

        // keep track of instruction address for profiling stores
        a = (pc - code_start) / INSTRUCTIONSIZE;

        pc = pc + INSTRUCTIONSIZE;

        // keep track of number of stores in total
        ic_store = ic_store + 1;

        // and individually
        *(stores_per_instruction + a) = *(stores_per_instruction + a) + 1;
      } else
        throw_exception(EXCEPTION_PAGEFAULT, page_of_virtual_address(vaddr));
    } else
      throw_exception(EXCEPTION_SEGMENTATIONFAULT, vaddr);
  } else
    throw_exception(EXCEPTION_INVALIDADDRESS, vaddr);

  return vaddr;
}

void undo_store() {
  uint64_t vaddr;

  vaddr = *(registers + rs1) + imm;

  store_virtual_memory(pt, vaddr, *(values + (tc % MAX_REPLAY_LENGTH)));
}

uint64_t print_beq() {
  uint64_t w;

  w = print_code_context_for_instruction(pc)
    + printf_or_write(sprintf(string_buffer, "%s %s,%s,%ld",
        get_mnemonic(is), get_register_name(rs1), get_register_name(rs2),
        signed_division(imm, INSTRUCTIONSIZE)));
  if (disassemble_verbose)
    return w + dprintf(output_fd, "[0x%lX]", pc + imm);
  else
    return w;
}

void print_beq_before() {
  printf(": ");
  print_register_value(rs1);
  printf(",");
  print_register_value(rs2);
  printf(" |- pc==0x%lX", pc);
}

void print_beq_after() {
  printf(" -> pc==0x%lX", pc);
}

void record_beq() {
  record_state(0);
}

void do_beq() {
  // branch on equal

  read_register(rs1);
  read_register(rs2);

  // semantics of beq
  if (*(registers + rs1) == *(registers + rs2))
    pc = pc + imm;
  else {
    pc = pc + INSTRUCTIONSIZE;

    nopc_beq = nopc_beq + 1;
  }

  ic_beq = ic_beq + 1;
}

uint64_t print_jal() {
  uint64_t w;

  w = print_code_context_for_instruction(pc)
    + printf_or_write(sprintf(string_buffer, "%s %s,%ld",
        get_mnemonic(is), get_register_name(rd), signed_division(imm, INSTRUCTIONSIZE)));
  if (disassemble_verbose)
    return w + dprintf(output_fd, "[0x%lX]", pc + imm);
  else
    return w;
}

void print_jal_before() {
  printf(": |- ");
  if (rd != REG_ZR) {
    print_register_hexadecimal(rd);
    printf(",");
  }
  printf("pc==0x%lX", pc);
}

void print_jal_jalr_after() {
  print_beq_after();
  if (rd != REG_ZR) {
    printf(",");
    print_register_hexadecimal(rd);
  }
}

void do_jal() {
  uint64_t a;

  // jump and link

  if (rd != REG_ZR) {
    // first link
    *(registers + rd) = pc + INSTRUCTIONSIZE;

    // then jump (for procedure call)
    pc = pc + imm;

    // prologue address for profiling procedure calls
    a = (pc - code_start) / INSTRUCTIONSIZE;

    // keep track of number of procedure calls in total
    calls = calls + 1;

    // and individually
    *(calls_per_procedure + a) = *(calls_per_procedure + a) + 1;
  } else if (signed_less_than(imm, 0)) {
    // just jump backward (to check loop condition again)
    pc = pc + imm;

    // first loop instruction address for profiling loop iterations
    a = (pc - code_start) / INSTRUCTIONSIZE;

    // keep track of number of loop iterations in total
    iterations = iterations + 1;

    // and individually
    *(iterations_per_loop + a) = *(iterations_per_loop + a) + 1;
  } else {
    // just jump forward
    pc = pc + imm;

    if (imm == INSTRUCTIONSIZE)
      nopc_jal = nopc_jal + 1;
  }

  write_register(rd);

  ic_jal = ic_jal + 1;
}

uint64_t print_jalr() {
  return print_code_context_for_instruction(pc)
    + printf_or_write(sprintf(string_buffer, "%s %s,%ld(%s)",
        get_mnemonic(is), get_register_name(rd),
          signed_division(imm, INSTRUCTIONSIZE), get_register_name(rs1)));
}

void print_jalr_before() {
  printf(": ");
  print_register_hexadecimal(rs1);
  printf(" |- ");
  if (rd != REG_ZR) {
    print_register_hexadecimal(rd);
    printf(",");
  }
  printf("pc==0x%lX", pc);
}

void do_jalr() {
  uint64_t next_pc;

  // jump and link register

  read_register(rs1);

  // prepare jump rs1-relative with LSB reset
  next_pc = left_shift(right_shift(*(registers + rs1) + imm, 1), 1);

  if (rd == REG_ZR) {
    // just jump
    if (next_pc == pc + INSTRUCTIONSIZE)
      nopc_jalr = nopc_jalr + 1;

    pc = next_pc;
  } else {
    // first link, then jump

    // link to next instruction (works even if rd == rs1)
    *(registers + rd) = pc + INSTRUCTIONSIZE;

    // jump
    pc = next_pc;
  }

  write_register(rd);

  ic_jalr = ic_jalr + 1;
}

uint64_t print_ecall() {
  return print_code_context_for_instruction(pc)
    + printf_or_write(sprintf(string_buffer, "%s", get_mnemonic(is)));
}

void record_ecall() {
  // TODO: record all side effects
  record_state(*(registers + REG_A0));
}

void do_ecall() {
  uint64_t a7;

  read_register(REG_A7);

  ic_ecall = ic_ecall + 1;

  a7 = *(registers + REG_A7);

  if (redo) {
    // TODO: redo all side effects
    *(registers + REG_A0) = *(values + (tc % MAX_REPLAY_LENGTH));

    // TODO: print ecall details
    println();

    pc = pc + INSTRUCTIONSIZE;
  } else if (a7 == SYSCALL_SWITCH)
    if (record) {
      printf("%s: context switching during recording is unsupported\n", selfie_name);

      exit(EXITCODE_UNSUPPORTEDSYSCALL);
    } else if (symbolic) {
      printf("%s: context switching during symbolic execution is unsupported\n", selfie_name);

      exit(EXITCODE_UNSUPPORTEDSYSCALL);
    } else {
      pc = pc + INSTRUCTIONSIZE;

      implement_switch();
    }
  else {
    read_register(REG_A0);

    if (a7 != SYSCALL_EXIT) {
      if (a7 != SYSCALL_BRK) {
        read_register(REG_A1);
        read_register(REG_A2);

        if (a7 == SYSCALL_OPENAT)
          read_register(REG_A3);
      }

      write_register(REG_A0);
    }

    // all system calls other than switch are handled by exception
    throw_exception(EXCEPTION_SYSCALL, a7);
  }
}

void undo_ecall() {
  uint64_t a0;

  a0 = *(registers + REG_A0);

  // TODO: undo all side effects
  *(registers + REG_A0) = *(values + (tc % MAX_REPLAY_LENGTH));

  // save register a0 for redoing system call
  *(values + (tc % MAX_REPLAY_LENGTH)) = a0;
}

uint64_t print_data_line_number() {
  if (data_line_number != (uint64_t*) 0)
    return dprintf(output_fd, "(~%lu)", *(data_line_number + (pc - code_size) / WORDSIZE));
  else
    return 0;
}

uint64_t print_data_context() {
  return dprintf(output_fd, "0x%lX", pc)
    + print_data_line_number()
    + dprintf(output_fd, ": ");
}

uint64_t print_data() {
  uint64_t w;

  if (disassemble_verbose)
    w = print_data_context();
  else
    w = 0;
  if (IS64BITTARGET)
    w = w + dprintf(output_fd, ".8byte ");
  else
    w = w + dprintf(output_fd, ".4byte ");

  return w + dprintf(output_fd, "0x%lX", load_data(pc - code_size));
}

// -----------------------------------------------------------------
// -------------------------- DISASSEMBLER -------------------------
// -----------------------------------------------------------------

char* get_mnemonic(uint64_t ins) {
  return (char*) *(MNEMONICS + ins);
}

uint64_t print_instruction() {
  // assert: 1 <= is <= number of RISC-U instructions
  if (is == ADDI)
    return print_addi();
  else if (is == LOAD)
    return print_load();
  else if (is == STORE)
    return print_store();
  else if (is == ADD)
    return print_add_sub_mul_divu_remu_sltu();
  else if (is == SUB)
    return print_add_sub_mul_divu_remu_sltu();
  else if (is == MUL)
    return print_add_sub_mul_divu_remu_sltu();
  else if (is == DIVU)
    return print_add_sub_mul_divu_remu_sltu();
  else if (is == REMU)
    return print_add_sub_mul_divu_remu_sltu();
  else if (is == SLTU)
    return print_add_sub_mul_divu_remu_sltu();
  else if (is == BEQ)
    return print_beq();
  else if (is == JAL)
    return print_jal();
  else if (is == JALR)
    return print_jalr();
  else if (is == LUI)
    return print_lui();
  else if (is == ECALL)
    return print_ecall();
  else
    return 0;
}

void selfie_disassemble(uint64_t verbose) {
  uint64_t number_of_written_characters;

  assembly_name = get_argument();

  if (code_size + data_size == 0) {
    printf("%s: nothing to disassemble to output file %s\n", selfie_name, assembly_name);

    return;
  }

  // assert: assembly_name is mapped and not longer than MAX_FILENAME_LENGTH

  assembly_fd = open_write_only(assembly_name, S_IRUSR_IWUSR_IRGRP_IROTH);

  if (signed_less_than(assembly_fd, 0)) {
    printf("%s: could not create assembly output file %s\n", selfie_name, assembly_name);

    exit(EXITCODE_IOERROR);
  }

  output_name = assembly_name;
  output_fd   = assembly_fd;

  reset_library();
  reset_interpreter();

  run = 0;

  disassemble_verbose = verbose;

  number_of_written_characters = 0;

  while (pc < code_size) {
    ir = load_instruction(pc);

    decode();

    number_of_written_characters = number_of_written_characters
      + print_instruction()
      + dprintf(output_fd, "\n");

    pc = pc + INSTRUCTIONSIZE;
  }

  while (pc - code_size < data_size) {
    number_of_written_characters = number_of_written_characters
      + print_data()
      + dprintf(output_fd, "\n");

    pc = pc + WORDSIZE;
  }

  disassemble_verbose = 0;

  output_name = (char*) 0;
  output_fd   = STDOUT_FD;

  printf("%s: %lu characters of assembly with %lu %lu-bit RISC-U instructions and %lu bytes of data written into %s\n", selfie_name,
    number_of_written_characters,
    code_size / INSTRUCTIONSIZE,
    WORDSIZEINBITS,
    data_size,
    assembly_name);
}

// -----------------------------------------------------------------
// -------------------------- REPLAY ENGINE ------------------------
// -----------------------------------------------------------------

void record_state(uint64_t value) {
  *(pcs + (tc % MAX_REPLAY_LENGTH))    = pc;
  *(values + (tc % MAX_REPLAY_LENGTH)) = value;

  tc = tc + 1;
}

void replay_trace() {
  uint64_t trace_length;
  uint64_t tl;

  if (tc < MAX_REPLAY_LENGTH)
    trace_length = tc;
  else
    trace_length = MAX_REPLAY_LENGTH;

  record = 0;

  tl = trace_length;

  // undo trace_length number of instructions
  while (tl > 0) {
    tc = tc - 1;

    pc = *(pcs + (tc % MAX_REPLAY_LENGTH));

    fetch();
    decode();
    execute_undo();

    tl = tl - 1;
  }

  redo = 1;

  tl = trace_length;

  // redo trace_length number of instructions
  while (tl > 0) {
    // assert: pc == *(pcs + (tc % MAX_REPLAY_LENGTH))

    fetch();
    decode();
    execute_debug();

    tc = tc + 1;
    tl = tl - 1;
  }

  redo   = 0;
  record = 1;
}

// -----------------------------------------------------------------
// -------------------------- INTERPRETER --------------------------
// -----------------------------------------------------------------

void print_register_hexadecimal(uint64_t reg) {
  printf("%s==0x%lX", get_register_name(reg), *(registers + reg));
}

void print_register_octal(uint64_t reg) {
  printf("%s==0o%lo", get_register_name(reg), *(registers + reg));
}

void print_register_value(uint64_t reg) {
  if (is_system_register(reg))
    print_register_hexadecimal(reg);
  else
    printf("%s==%ld(0x%lX)", get_register_name(reg), *(registers + reg), *(registers + reg));
}

uint64_t is_valid_segment_read(uint64_t vaddr) {
  if (is_data_address(current_context, vaddr)) {
    data_reads = data_reads + 1;

    return 1;
  } else if (is_stack_address(current_context, vaddr)) {
    stack_reads = stack_reads + 1;

    return 1;
  } else if (is_heap_address(current_context, vaddr)) {
    heap_reads = heap_reads + 1;

    return 1;
  } else
    return 0;
}

uint64_t is_valid_segment_write(uint64_t vaddr) {
  if (is_data_address(current_context, vaddr)) {
    data_writes = data_writes + 1;

    return 1;
  } else if (is_stack_address(current_context, vaddr)) {
    stack_writes = stack_writes + 1;

    return 1;
  } else if (is_heap_address(current_context, vaddr)) {
    heap_writes = heap_writes + 1;

    return 1;
  } else
    return 0;
}

void print_exception(uint64_t exception, uint64_t fault) {
  printf("%s", (char*) *(EXCEPTIONS + exception));

  if (exception == EXCEPTION_PAGEFAULT)
    printf(" at page 0x%04lX", (uint64_t) fault);
  else if (exception == EXCEPTION_SEGMENTATIONFAULT)
    printf(" at address 0x%08lX", (uint64_t) fault);
  else if (exception == EXCEPTION_SYSCALL)
    printf(" ID %lu", fault);
  else if (exception == EXCEPTION_DIVISIONBYZERO)
    printf(" at address 0x%08lX", (uint64_t) fault);
  else if (exception == EXCEPTION_INVALIDADDRESS)
    printf(" 0x%08lX", (uint64_t) fault);
  else if (exception == EXCEPTION_UNKNOWNINSTRUCTION)
    printf(" at address 0x%08lX", (uint64_t) fault);
  else if (exception == EXCEPTION_UNINITIALIZEDREGISTER) {
    printf(" ");print_register_name(fault);
  }
}

void throw_exception(uint64_t exception, uint64_t fault) {
  if (get_exception(current_context) != EXCEPTION_NOEXCEPTION)
    if (get_exception(current_context) != exception) {
      printf("%s: context %s throws exception: ", selfie_name, get_name(current_context));
      print_exception(exception, fault);
      printf(" in presence of existing exception: ");
      print_exception(get_exception(current_context), get_fault(current_context));
      println();

      exit(EXITCODE_MULTIPLEEXCEPTIONERROR);
    }

  set_exception(current_context, exception);
  set_fault(current_context, fault);

  trap = 1;

  if (debug_exception) {
    printf("%s: context %s throws exception: ", selfie_name, get_name(current_context));
    print_exception(exception, fault);
    printf("\n%s: exception triggered by instruction in binary ", selfie_name);
    print_instruction();
    println();
  }
}

void fetch() {
  if (is_virtual_address_valid(pc, INSTRUCTIONSIZE)) {
    if (is_code_address(current_context, pc)) {
      // assert: is_virtual_address_mapped(pt, pc) == 1

      if (pc % WORDSIZE == 0)
        ir = get_low_word(load_cached_instruction_word(pt, pc));
      else
        ir = get_high_word(load_cached_instruction_word(pt, pc - INSTRUCTIONSIZE));

      return;
    } else
      throw_exception(EXCEPTION_SEGMENTATIONFAULT, pc);
  } else
    throw_exception(EXCEPTION_INVALIDADDRESS, pc);

  // reset instruction register
  ir = encode_nop();
}

void decode() {
  opcode = get_opcode(ir);

  is = 0;

  if (opcode == OP_IMM) {
    decode_i_format();

    if (funct3 == F3_ADDI)
      is = ADDI;
  } else if (opcode == OP_LOAD) {
    decode_i_format();

    if (IS64BITTARGET) {
      if (funct3 == F3_LD)
        is = LOAD;
    } else if (funct3 == F3_LW)
      is = LOAD;
  } else if (opcode == OP_STORE) {
    decode_s_format();

    if (IS64BITTARGET) {
      if (funct3 == F3_SD)
        is = STORE;
    } else if (funct3 == F3_SW)
      is = STORE;
  } else if (opcode == OP_OP) { // could be ADD, SUB, MUL, DIVU, REMU, SLTU
    decode_r_format();

    if (funct3 == F3_ADD) { // = F3_SUB = F3_MUL
      if (funct7 == F7_ADD)
        is = ADD;
      else if (funct7 == F7_SUB)
        is = SUB;
      else if (funct7 == F7_MUL)
        is = MUL;
    } else if (funct3 == F3_DIVU) {
      if (funct7 == F7_DIVU)
        is = DIVU;
    } else if (funct3 == F3_REMU) {
      if (funct7 == F7_REMU)
        is = REMU;
    } else if (funct3 == F3_SLTU) {
      if (funct7 == F7_SLTU)
        is = SLTU;
    }
  } else if (opcode == OP_BRANCH) {
    decode_b_format();

    if (funct3 == F3_BEQ)
      is = BEQ;
  } else if (opcode == OP_JAL) {
    decode_j_format();

    is = JAL;
  } else if (opcode == OP_JALR) {
    decode_i_format();

    if (funct3 == F3_JALR)
      is = JALR;
  } else if (opcode == OP_LUI) {
    decode_u_format();

    is = LUI;
  } else if (opcode == OP_SYSTEM) {
    decode_i_format();

    if (funct3 == F3_ECALL)
      is = ECALL;
  }

  if (is == 0) {
    if (run)
      throw_exception(EXCEPTION_UNKNOWNINSTRUCTION, pc);
    else {
      //report the error on the console
      output_fd = STDOUT_FD;

      printf("%s: at address 0x%08lX unknown instruction 0x%lX with opcode 0x%lX detected\n", selfie_name,
        pc, ir, opcode);

      exit(EXITCODE_UNKNOWNINSTRUCTION);
    }
  }
}

void execute() {
  if (debug) {
    if (record)
      execute_record();
    else
      execute_debug();

    return;
  }

  // assert: 1 <= is <= number of RISC-U instructions
  if (is == ADDI)
    do_addi();
  else if (is == LOAD)
    do_load();
  else if (is == STORE)
    do_store();
  else if (is == ADD)
    do_add();
  else if (is == SUB)
    do_sub();
  else if (is == MUL)
    do_mul();
  else if (is == DIVU)
    do_divu();
  else if (is == REMU)
    do_remu();
  else if (is == SLTU)
    do_sltu();
  else if (is == BEQ)
    do_beq();
  else if (is == JAL)
    do_jal();
  else if (is == JALR)
    do_jalr();
  else if (is == LUI)
    do_lui();
  else if (is == ECALL)
    do_ecall();
}

void execute_record() {
  // assert: 1 <= is <= number of RISC-U instructions
  if (is == ADDI) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_addi();
  } else if (is == LOAD) {
    record_load();
    do_load();
  } else if (is == STORE) {
    record_store();
    do_store();
  } else if (is == ADD) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_add();
  } else if (is == SUB) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_sub();
  } else if (is == MUL) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_mul();
  } else if (is == DIVU) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_divu();
  } else if (is == REMU) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_remu();
  } else if (is == SLTU) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_sltu();
  } else if (is == BEQ) {
    record_beq();
    do_beq();
  } else if (is == JAL) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_jal();
  } else if (is == JALR) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_jalr();
  } else if (is == LUI) {
    record_lui_addi_add_sub_mul_divu_remu_sltu_jal_jalr();
    do_lui();
  } else if (is == ECALL) {
    record_ecall();
    do_ecall();
  }
}

void execute_undo() {
  // assert: 1 <= is <= number of RISC-U instructions
  if (is == STORE)
    undo_store();
  else if (is == BEQ)
    // beq does not require any undo
    return;
  else if (is == ECALL)
    undo_ecall();
  else
    undo_lui_addi_add_sub_mul_divu_remu_sltu_load_jal_jalr();
}

void execute_debug() {
  print_instruction();

  // assert: 1 <= is <= number of RISC-U instructions
  if (is == ADDI) {
    print_addi_before();
    do_addi();
    print_addi_add_sub_mul_divu_remu_sltu_after();
  } else if (is == LOAD) {
    print_load_before();
    print_load_after(do_load());
  } else if (is == STORE) {
    print_store_before();
    print_store_after(do_store());
  } else if (is == ADD) {
    print_add_sub_mul_divu_remu_sltu_before();
    do_add();
    print_addi_add_sub_mul_divu_remu_sltu_after();
  } else if (is == SUB) {
    print_add_sub_mul_divu_remu_sltu_before();
    do_sub();
    print_addi_add_sub_mul_divu_remu_sltu_after();
  } else if (is == MUL) {
    print_add_sub_mul_divu_remu_sltu_before();
    do_mul();
    print_addi_add_sub_mul_divu_remu_sltu_after();
  } else if (is == DIVU) {
    print_add_sub_mul_divu_remu_sltu_before();
    do_divu();
    print_addi_add_sub_mul_divu_remu_sltu_after();
  } else if (is == REMU) {
    print_add_sub_mul_divu_remu_sltu_before();