From ca118555214a176728b9aab87849391344306d6d Mon Sep 17 00:00:00 2001 From: Paul Oliver Date: Thu, 29 Feb 2024 02:29:13 +0100 Subject: Initial commit. --- .gitignore | 8 + Makefile | 29 + README.md | 52 ++ bin/common/.keep | 0 bin/genomes/.keep | 0 bin/genomes/86.anc | 1 + bin/handler.py | 403 ++++++++++++++ bin/lib/.keep | 0 bin/printer.py | 833 ++++++++++++++++++++++++++++ bin/salis.py | 346 ++++++++++++ bin/sims/.keep | 0 bin/sims/auto/.keep | 0 bin/world.py | 277 ++++++++++ build/.keep | 0 include/common.h | 19 + include/evolver.h | 38 ++ include/getter.h | 20 + include/instset.h | 71 +++ include/memory.h | 134 +++++ include/process.h | 97 ++++ include/salis.h | 67 +++ include/types.h | 45 ++ src/common.c | 72 +++ src/evolver.c | 132 +++++ src/instset.c | 39 ++ src/memory.c | 325 +++++++++++ src/process.c | 1488 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/salis.c | 109 ++++ 28 files changed, 4605 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 bin/common/.keep create mode 100644 bin/genomes/.keep create mode 100644 bin/genomes/86.anc create mode 100644 bin/handler.py create mode 100644 bin/lib/.keep create mode 100644 bin/printer.py create mode 100755 bin/salis.py create mode 100644 bin/sims/.keep create mode 100644 bin/sims/auto/.keep create mode 100644 bin/world.py create mode 100644 build/.keep create mode 100644 include/common.h create mode 100644 include/evolver.h create mode 100644 include/getter.h create mode 100644 include/instset.h create mode 100644 include/memory.h create mode 100644 include/process.h create mode 100644 include/salis.h create mode 100644 include/types.h create mode 100644 src/common.c create mode 100644 src/evolver.c create mode 100644 src/instset.c create mode 100644 src/memory.c create mode 100644 src/process.c create mode 100644 src/salis.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee01acd --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +bin/__pycache__/* +bin/common/pipe +bin/error.log +bin/lib/libsalis.so +bin/sims/*.sim +bin/sims/auto/*.auto +build/*.d +build/*.o diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..156a068 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +CC := gcc +LIB := bin/lib/libsalis.so +SOURCES := $(wildcard src/*.c) +OBJECTS := $(patsubst src/%.c,build/%.o,$(SOURCES)) +DEPS := $(patsubst %.o,%.d,$(OBJECTS)) +LFLAGS := -shared + +# uncomment for debug +# OFLAGS := -ggdb + +# uncomment for release +OFLAGS := -O3 -DNDEBUG -Wno-unused-function -Wno-unused-result \ + -Wno-unused-variable + +CFLAGS := -Iinclude -c $(OFLAGS) -MMD -Wall -Wextra -std=c89 -fPIC -fopenmp \ + -DSALIS_API="" -DSALIS_INST="" -DSALIS_PROC_ELEMENT="" -pedantic-errors \ + -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition + +all: $(OBJECTS) + $(CC) $(LFLAGS) -fopenmp -o $(LIB) $(OBJECTS) + +-include $(DEPS) + +$(OBJECTS): $(patsubst build/%.o,src/%.c,$@) + $(CC) $(CFLAGS) $(patsubst build/%.o,src/%.c,$@) -o $@ + +clean: + -rm build/* + -rm $(LIB) diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccb4b64 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +## SALIS 2.0 - WIP + +### Main differences from Salis 1.0 +1. Tierran templates will be used instead of keys/locks +2. The instruction set is thus shorter +3. Organisms can send/receive instructions to/from a common pipe +4. Organisms can "eat" information +5. Organisms are rewarded for eating +6. Organisms are punished on faults +7. A better naming convention will be used + +### Python integration +1. Salis controller/viewer will be written in python/curses +2. Salis header files will be parsed for easier DLL loading +3. We can now show organisms' IPs on WORLD view +4. Console can make use of readline via curses.textbox +5. Compilation/loading/saving will be done via python +6. Salis may be run as a daemon process + +### New instruction set (32 instructions in total) ++ NOOP0 ++ NOOP1 ++ MOD0 ++ MOD1 ++ MOD2 ++ MOD3 ++ IF ++ NOT ++ JUMPB ++ JUMPF ++ ADDRB ++ ADDRF ++ MALLB ++ MALLF ++ BSWAP ++ SPLIT ++ INC ++ DEC ++ ZERO ++ ONE ++ ADD ++ SUB ++ MUL ++ DIV ++ LOAD ++ WRITE ++ SEND ++ RECEIVE ++ PUSH ++ POP ++ EATB ++ EATF diff --git a/bin/common/.keep b/bin/common/.keep new file mode 100644 index 0000000..e69de29 diff --git a/bin/genomes/.keep b/bin/genomes/.keep new file mode 100644 index 0000000..e69de29 diff --git a/bin/genomes/86.anc b/bin/genomes/86.anc new file mode 100644 index 0000000..30e76f3 --- /dev/null +++ b/bin/genomes/86.anc @@ -0,0 +1 @@ +:::[a...]b...^b^b^b-bba::.!d#d#b?d).:.{bc).::a:.:}bc:..LadWcd^a^cvb?b(.::$~b~d(..:a::: diff --git a/bin/handler.py b/bin/handler.py new file mode 100644 index 0000000..ad3b14e --- /dev/null +++ b/bin/handler.py @@ -0,0 +1,403 @@ +""" SALIS: Viewer/controller for the SALIS simulator. + +file: handler.py +Author: Paul Oliver +Email: paul.t.oliver.design@gmail.com + +This module should be considered the 'controller' part of the Salis simulator. +It receives and parses all user input via keyboard and console commands. It +also takes care of genome compilation (via genome files located on the +'genomes' directory). + +An user may open the Salis console by pressing the 'c' key while in a running +session. A nice quirk is the possibility to run python commands from within the +Salis console. As an example, to get the memory size, an user could type: + +>>> exec output = self._sim.lib.sal_mem_get_size() + +Note that 'output' denotes a storage variable that will get printed on the +console response. This ability gives an user a whole lot of power, and should +be used with care. +""" + +import os +import curses + + +class Handler: + ESCAPE_KEY = 27 + + def __init__(self, sim): + """ Handler constructor. Simply link this class to the main simulation + class and printer class and create symbol dictionary. + """ + self._sim = sim + self._printer = sim.printer + self._inst_dict = self._get_inst_dict() + self._console_history = [] + + def process_cmd(self, cmd): + """ Process incoming commands from curses. Commands are received via + ncurses' getch() function, thus, they must be transformed into their + character representations with 'ord()'. + """ + if cmd == self.ESCAPE_KEY: + self._sim.lib.sal_main_save( + self._sim.save_file_path.encode("utf-8") + ) + self._sim.exit() + elif cmd == ord(" "): + self._sim.toggle_state() + elif cmd == curses.KEY_LEFT: + self._printer.flip_page(-1) + elif cmd == curses.KEY_RIGHT: + self._printer.flip_page(1) + elif cmd == curses.KEY_DOWN: + self._printer.scroll_main(-1) + elif cmd == curses.KEY_UP: + self._printer.scroll_main(1) + elif cmd == curses.KEY_RESIZE: + self._printer.on_resize() + elif cmd == ord("X"): + self._printer.toggle_hex() + elif cmd == ord("x"): + self._printer.world.zoom_out() + elif cmd == ord("z"): + self._printer.world.zoom_in() + elif cmd == ord("a"): + self._printer.world.pan_left() + self._printer.proc_scroll_left() + elif cmd == ord("d"): + self._printer.world.pan_right() + self._printer.proc_scroll_right() + elif cmd == ord("s"): + self._printer.world.pan_down() + self._printer.proc_scroll_down() + elif cmd == ord("w"): + self._printer.world.pan_up() + self._printer.proc_scroll_up() + elif cmd == ord("S"): + self._printer.world.pan_reset() + self._printer.proc_scroll_vertical_reset() + elif cmd == ord("A"): + self._printer.world.pan_reset() + self._printer.proc_scroll_horizontal_reset() + elif cmd == ord("o"): + self._printer.proc_select_prev() + elif cmd == ord("p"): + self._printer.proc_select_next() + elif cmd == ord("f"): + self._printer.proc_select_first() + elif cmd == ord("l"): + self._printer.proc_select_last() + elif cmd == ord("k"): + self._printer.proc_scroll_to_selected() + elif cmd == ord("g"): + self._printer.proc_toggle_gene_view() + elif cmd == ord("\n"): + self._printer.run_cursor() + elif cmd == ord("c"): + self._printer.run_console() + else: + # Check for numeric input. Number keys [1 to 0] cycle the + # simulation [2 ** ((n - 1) % 10] times. + try: + if chr(cmd).isdigit(): + factor = int(chr(cmd)) + factor = int(2 ** ((factor - 1) % 10)) + self._cycle_sim(factor) + except ValueError: + pass + + def handle_console(self, command_raw): + """ Process console commands. We parse and check for input errors. Any + python exception messages are redirected to the console-response + window. + """ + if command_raw: + command = command_raw.split() + + try: + # Handle both python and self-thrown exceptions. + if command[0] in ["q", "quit"]: + self._on_quit(command, save=True) + elif command[0] in ["q!", "quit!"]: + self._on_quit(command, save=False) + elif command[0] in ["i", "input"]: + self._on_input(command) + elif command[0] in ["c", "compile"]: + self._on_compile(command) + elif command[0] in ["n", "new"]: + self._on_new(command) + elif command[0] in ["k", "kill"]: + self._on_kill(command) + elif command[0] in ["e", "exec"]: + self._on_exec(command) + elif command[0] in ["s", "scroll"]: + self._on_scroll(command) + elif command[0] in ["p", "process"]: + self._on_proc_select(command) + elif command[0] in ["r", "rename"]: + self._on_rename(command) + elif command[0] in ["save"]: + self._on_save(command) + elif command[0] in ["a", "auto"]: + self._on_set_autosave(command) + else: + # Raise if a non-existing command has been given. + self._raise("Invalid command: '{}'".format(command[0])) + except BaseException as exep: + # We parse and redirect python exceptions to the error + # console-window. + message = str(exep).strip() + message = message[0].upper() + message[1:] + self._printer.show_console_error(message) + finally: + # Store command on console history. + self._console_history.append(command_raw.strip()) + + @property + def console_history(self): + return self._console_history + + def _raise(self, message): + """ Generic exception thrower. Throws a 'RuntimeError' initialized with + the given message. + """ + raise RuntimeError("ERROR: {}".format(message)) + + def _respond(self, message): + """ Generic console responder. Throws a 'RuntimeError' initialized with + the given message. + """ + raise RuntimeError(message) + + def _cycle_sim(self, factor): + """ Simply cycle Salis 'factor' number of times. + """ + for _ in range(factor): + self._sim.lib.sal_main_cycle() + self._sim.check_autosave() + + def _get_inst_dict(self): + """ Transform the instruction list of the printer module into a + dictionary that's more useful for genome compilation. Instruction + symbols are keys, values are the actual byte representation. + """ + inst_dict = {} + + for i, inst in enumerate(self._printer.inst_list): + inst_dict[inst[1]] = i + + return inst_dict + + def _on_quit(self, command, save): + """ Exit simulation. We can choose whether to save the simulation into a + save file or not. + """ + if len(command) > 1: + self._raise("Invalid parameters for '{}'".format(command[0])) + + if save: + self._sim.lib.sal_main_save( + self._sim.save_file_path.encode("utf-8") + ) + + self._sim.exit() + + def _write_genome(self, genome, address_list): + """ Write genome stream into a given list of memory addresses. All + addresses must be valid or an exception is thrown. + """ + # All addresses we will write to must be valid. + for base_addr in address_list: + address = int(base_addr, 0) + + for _ in range(len(genome)): + if not self._sim.lib.sal_mem_is_address_valid(address): + self._raise("Address '{}' is invalid".format(address)) + + address += 1 + + # All looks well! Let's compile the genome into memory. + for base_addr in address_list: + address = int(base_addr, 0) + + for symbol in genome: + self._sim.lib.sal_mem_set_inst( + address, self._inst_dict[symbol] + ) + address += 1 + + def _on_input(self, command): + """ Compile organism from user typed input. Compilation can only occur + on valid memory addresses. An exception will be thrown when trying to + write into non-valid address or when input stream is invalid. + """ + if len(command) < 3: + self._raise("Invalid parameters for '{}'".format(command[0])) + + # All characters in file must be actual instruction symbols. + for character in command[1]: + if character not in self._inst_dict: + self._raise("Invalid symbol '{}' found on stream".format( + character + )) + + # All looks well, Let's write the genome into memory. + self._write_genome(command[1], command[2:]) + + def _on_compile(self, command): + """ Compile organism from source genome file. Genomes must be placed on + the './genomes' directory. Compilation can only occur on valid memory + addresses. An exception will be thrown when trying to write into + non-valid address or when genome file is invalid. + """ + if len(command) < 3: + self._raise("Invalid parameters for '{}'".format(command[0])) + + # Open genome file for compilation. + gen_file = os.path.join(self._sim.path, "genomes", command[1]) + + with open(gen_file, "r") as f: + genome = f.read().strip() + + # Entire genome must be written on a single line. + if "\n" in genome: + self._raise("Newline detected on '{}'".format(gen_file)) + + # All characters in file must be actual instruction symbols. + for character in genome: + if character not in self._inst_dict: + self._raise("Invalid symbol '{}' found on '{}'".format( + character, gen_file + )) + + # All looks well, Let's write the genome into memory. + self._write_genome(genome, command[2:]) + + def _on_new(self, command): + """ Instantiate new organism of given size on given address. These + memory areas must be free and valid or an exception is thrown. + """ + if len(command) < 3: + self._raise("Invalid parameters for '{}'".format(command[0])) + + # Check that all addresses we will allocate are free and valid. + for base_addr in command[2:]: + address = int(base_addr, 0) + + for _ in range(int(command[1])): + if not self._sim.lib.sal_mem_is_address_valid(address): + self._raise("Address '{}' is invalid".format(address)) + elif self._sim.lib.sal_mem_is_allocated(address): + self._raise("Address '{}' is allocated".format(address)) + + address += 1 + + # All looks well! Let's instantiate our new organism. + for base_addr in command[2:]: + address = int(base_addr, 0) + size = int(command[1], 0) + self._sim.lib.sal_proc_create(address, size) + + def _on_kill(self, command): + """ Kill organism on bottom of reaper queue. + """ + if len(command) > 1: + self._raise("Invalid parameters for '{}'".format(command[0])) + + # Call proc kill function only if there's any organisms to kill. + if not self._sim.lib.sal_proc_get_count(): + self._raise("No organisms currently alive") + else: + self._sim.lib.sal_proc_kill() + + def _on_exec(self, command): + """ Allow a user to execute a python command from within the console. + Using this is very hack-ish, and not recommended unless you're certain + of what you're doing! + """ + if len(command) < 2: + self._raise("'{}' must be followed by an executable string".format( + command[0]) + ) + + # User may query any simulation variable or status and the console will + # respond. For example, to query memory size or order, type one of the + # following: + # + # >>> exec output = self._sim.lib.sal_mem_get_size() + # >>> exec output = self._sim.lib.sal_mem_get_order() + # + output = {} + exec(" ".join(command[1:]), locals(), output) + + if output: + self._respond("EXEC RESPONDS: {}".format(str(output))) + + def _on_scroll(self, command): + """ We can scroll to a specific process (on PROCESS view) or to a + specific world address (on WORLD view) via the console. + """ + if len(command) != 2: + self._raise("Invalid parameters for '{}'".format(command[0])) + + target = int(command[1], 0) + + # If on PROCESS page, scroll to given process. + if self._printer.current_page == "PROCESS": + if target < self._sim.lib.sal_proc_get_capacity(): + self._printer.proc_scroll_to(target) + else: + self._raise("No process with ID '{}' found".format(target)) + elif self._printer.current_page == "WORLD": + if self._sim.lib.sal_mem_is_address_valid(target): + self._printer.world.scroll_to(target) + else: + self._raise("Address '{}' is invalid".format(address)) + else: + self._raise("'{}' must be called on PROCESS or WORLD page".format( + command[0]) + ) + + def _on_proc_select(self, command): + """ Select a specific process (on PROCESS or WORLD page). + """ + if len(command) != 2: + self._raise("Invalid parameters for '{}'".format(command[0])) + + target = int(command[1], 0) + + # If on PROCESS page, scroll to given process. + if target < self._sim.lib.sal_proc_get_capacity(): + self._printer.proc_select_by_id(target) + else: + self._raise("No process with ID '{}' found".format(target)) + + def _on_rename(self, command): + """ Set a new simulation name. Future auto-saved files will use this + name as prefix. + """ + if len(command) != 2: + self._raise("Invalid parameters for '{}'".format(command[0])) + + self._sim.rename(command[1]) + + def _on_save(self, command): + """ Save simulation on its current state. + """ + if len(command) != 1: + self._raise("Invalid parameters for '{}'".format(command[0])) + + self._sim.lib.sal_main_save(self._sim.save_file_path.encode("utf-8")) + + def _on_set_autosave(self, command): + """ Set the simulation's auto save interval. Provide any integer + between 0 and (2**32 - 1). If zero is provided, auto saving will be + disabled. + """ + if len(command) != 2: + self._raise("Invalid parameters for '{}'".format(command[0])) + + self._sim.set_autosave(int(command[1], 0)) diff --git a/bin/lib/.keep b/bin/lib/.keep new file mode 100644 index 0000000..e69de29 diff --git a/bin/printer.py b/bin/printer.py new file mode 100644 index 0000000..135e220 --- /dev/null +++ b/bin/printer.py @@ -0,0 +1,833 @@ +""" SALIS: Viewer/controller for the SALIS simulator. + +File: printer.py +Author: Paul Oliver +Email: paul.t.oliver.design@gmail.com + +This module should be considered the 'view' part of the Salis simulator. It +takes care of displaying the simulator's state in a nicely formatted, intuitive +format. It makes use of the curses library for terminal handling. +""" + +import curses +import curses.textpad +import os +import time +from collections import OrderedDict +from ctypes import c_uint8, c_uint32, cast, POINTER +from handler import Handler +from world import World + + +class Printer: + def __init__(self, sim): + """ Printer constructor. It takes care of starting up curses, defining + the data pages and setting the printer on its initial state. + """ + self._sim = sim + self._color_pair_count = 0 + self._screen = self._get_screen() + self._inst_list = self._get_inst_list() + self._proc_elements = self._get_proc_elements() + self._main = self._get_main() + self._pages = self._get_pages() + self._size = self._screen.getmaxyx() + self._current_page = "MEMORY" + self._main_scroll = 0 + self._selected_proc = 0 + self._selected_proc_data = (c_uint32 * len(self._proc_elements))() + self._proc_list_scroll = 0 + self._proc_element_scroll = 0 + self._proc_gene_scroll = 0 + self._proc_gene_view = False + self._curs_y = 0 + self._curs_x = 0 + self._print_hex = False + self._world = World(self, self._sim) + + def __del__(self): + """ Printer destructor exits curses. + """ + curses.endwin() + + def get_color_pair(self, fg, bg=-1): + """ We use this method to set new color pairs, keeping track of the + number of pairs already set. We return the new color pair ID. + """ + self._color_pair_count += 1 + curses.init_pair(self._color_pair_count, fg, bg) + return self._color_pair_count + + def get_cmd(self): + """ This returns the pressed key from the curses handler. It's called + during the simulation's main loop. Flushing input is important when in + non-blocking mode. + """ + ch = self._screen.getch() + curses.flushinp() + return ch + + def set_nodelay(self, nodelay): + """ Toggles between blocking and non-blocking mode on curses. + """ + self._screen.nodelay(nodelay) + + def toggle_hex(self): + """ Toggle between decimal or hexadecimal printing of all simulation + state elements. + """ + self._print_hex = not self._print_hex + + def on_resize(self): + """ Called whenever the terminal window gets resized. + """ + self._size = self._screen.getmaxyx() + self.scroll_main() + self._world.zoom_reset() + + def flip_page(self, offset): + """ Change data page by given offset (i.e. '1' for next page or '-1' + for previous one). + """ + pidx = list(self._pages.keys()).index(self._current_page) + pidx = (pidx + offset) % len(self._pages) + self._current_page = list(self._pages.keys())[pidx] + self.scroll_main() + + def scroll_main(self, offset=0): + """ Scrolling is allowed whenever the current page does not fit inside + the terminal window. This method gets called, with no offset, under + certain situations, like changing pages, just to make sure the screen + gets cleared and at least some of the data is always scrolled into + view. + """ + self._screen.clear() + len_main = len(self._main) + len_page = len(self._pages[self._current_page]) + max_scroll = (len_main + len_page + 5) - self._size[0] + self._main_scroll += offset + self._main_scroll = max(0, min(self._main_scroll, max_scroll)) + + def proc_scroll_left(self): + """ Scroll process data elements or genomes (on PROCESS view) to the + left. + """ + if self._current_page == "PROCESS": + if self._proc_gene_view: + self._proc_gene_scroll -= 1 + self._proc_gene_scroll = max(0, self._proc_gene_scroll) + else: + self._proc_element_scroll -= 1 + self._proc_element_scroll = max(0, self._proc_element_scroll) + + def proc_scroll_right(self): + """ Scroll process data elements or genomes (on PROCESS view) to the + right. + """ + if self._current_page == "PROCESS": + if self._proc_gene_view: + self._proc_gene_scroll += 1 + else: + self._proc_element_scroll += 1 + max_scroll = len(self._proc_elements) - 1 + self._proc_element_scroll = min( + max_scroll, self._proc_element_scroll + ) + + def proc_scroll_down(self): + """ Scroll process data table (on PROCESS view) up. + """ + if self._current_page == "PROCESS": + self._proc_list_scroll = max(0, self._proc_list_scroll - 1) + + def proc_scroll_up(self): + """ Scroll process data table (on PROCESS view) down. + """ + if self._current_page == "PROCESS": + self._proc_list_scroll = min( + self._sim.lib.sal_proc_get_capacity() - 1, + self._proc_list_scroll + 1 + ) + + def proc_scroll_to(self, proc_id): + """ Scroll process data table (on PROCESS view) to a specific position. + """ + if self._current_page == "PROCESS": + if proc_id < self._sim.lib.sal_proc_get_capacity(): + self._proc_list_scroll = proc_id + else: + raise RuntimeError("Error: scrolling to invalid process") + + def proc_scroll_vertical_reset(self): + """ Scroll process data table (on PROCESS view) back to top. + """ + if self._current_page == "PROCESS": + self._proc_list_scroll = 0 + + def proc_scroll_horizontal_reset(self): + """ Scroll process data or genome table (on PROCESS view) back to the + left. + """ + if self._current_page == "PROCESS": + if self._proc_gene_view: + self._proc_gene_scroll = 0 + else: + self._proc_element_scroll = 0 + + def proc_select_prev(self): + """ Select previous process. + """ + if self._current_page in ["PROCESS", "WORLD"]: + self._selected_proc -= 1 + self._selected_proc %= self._sim.lib.sal_proc_get_capacity() + + def proc_select_next(self): + """ Select next process. + """ + if self._current_page in ["PROCESS", "WORLD"]: + self._selected_proc += 1 + self._selected_proc %= self._sim.lib.sal_proc_get_capacity() + + def proc_select_first(self): + """ Select first process on reaper queue. + """ + if self._current_page in ["PROCESS", "WORLD"]: + if self._sim.lib.sal_proc_get_count(): + self._selected_proc = self._sim.lib.sal_proc_get_first() + + def proc_select_last(self): + """ Select last process on reaper queue. + """ + if self._current_page in ["PROCESS", "WORLD"]: + if self._sim.lib.sal_proc_get_count(): + self._selected_proc = self._sim.lib.sal_proc_get_last() + + def proc_select_by_id(self, proc_id): + """ Select process from given ID. + """ + if proc_id < self._sim.lib.sal_proc_get_capacity(): + self._selected_proc = proc_id + else: + raise RuntimeError("Error: attempting to select non-existing proc") + + def proc_scroll_to_selected(self): + """ Scroll WORLD or PROCESS page so that selected process becomes + visible. + """ + if self._current_page == "PROCESS": + self._proc_list_scroll = self._selected_proc + elif self._current_page == "WORLD": + if not self._sim.lib.sal_proc_is_free(self._selected_proc): + index = self._proc_elements.index("mb1a") + address = self._selected_proc_data[index] + self._world.scroll_to(address) + + def proc_toggle_gene_view(self): + """ Toggle between data element or genome view on PROCESS page. + """ + if self._current_page == "PROCESS": + self._proc_gene_view = not self._proc_gene_view + + def run_cursor(self): + """ We can toggle a visible cursor on WORLD view to aid us in selecting + processes. + """ + if self._current_page == "WORLD" and self._size[1] > World.PADDING: + curses.curs_set(True) + + while True: + self._curs_y = max(0, min(self._curs_y, self._size[0] - 1)) + self._curs_x = max(World.PADDING, min( + self._curs_x, self._size[1] - 1 + )) + self._screen.move(self._curs_y, self._curs_x) + cmd = self._screen.getch() + + if cmd in [ord("c"), curses.KEY_RESIZE, Handler.ESCAPE_KEY]: + self.on_resize() + break + elif cmd == curses.KEY_LEFT: + self._curs_x -= 1 + elif cmd == curses.KEY_RIGHT: + self._curs_x += 1 + elif cmd == curses.KEY_DOWN: + self._curs_y += 1 + elif cmd == curses.KEY_UP: + self._curs_y -= 1 + elif cmd == ord("\n"): + self._proc_select_by_cursor() + break + + curses.curs_set(False) + + def run_console(self): + """ Run the Salis console. You can use the console to control all main + aspects of the simulation, like compiling genomes into memory, creating + or killing organisms, setting auto-save interval, among other stuff. + """ + # Print a pythonic prompt. + self._print_line(self._size[0] - 1, ">>> ", scroll=False) + self._screen.refresh() + + # Create the console child window. We turn it into a Textbox object in + # order to allow line-editing and extract output easily. + console = curses.newwin(1, self._size[1] - 5, self._size[0] - 1, 5) + textbox = curses.textpad.Textbox(console, insert_mode=True) + textbox.stripspaces = True + + # Grab a copy of the console history and instantiate a pointer to the + # last element. + history = self._sim.handler.console_history + [""] + pointer = len(history) - 1 + + # Nested method reinserts recorded commands from history into console. + def access_history(cmd): + nonlocal pointer + + if pointer == len(history) - 1: + history[-1] = console.instr().strip() + + if cmd == "up" and pointer != 0: + pointer -= 1 + elif cmd == "down" and pointer < len(history) - 1: + pointer += 1 + + console.clear() + console.addstr(0, 0, history[pointer]) + console.refresh() + + # Declare custom validator to control special commands. + def validator(cmd): + EXIT = 7 + + if cmd in [curses.KEY_RESIZE, Handler.ESCAPE_KEY]: + console.clear() + return EXIT + elif cmd == curses.KEY_UP: + access_history("up") + elif cmd == curses.KEY_DOWN: + access_history("down") + else: + return cmd + + # Run the Textbox object with our custom validator. + curses.curs_set(True) + output = textbox.edit(validator) + curses.curs_set(False) + + # Finally, extract data from console and send to handler. + self._sim.handler.handle_console(output) + self._screen.clear() + + def show_console_error(self, message): + """ Shows Salis console error messages, if any. These messages might + contain actual python exception output. + """ + self._print_line(self._size[0] - 1, ">>>", curses.color_pair( + self._pair_error + ) | curses.A_BOLD) + self._screen.refresh() + + # We also use a Textbox object, just so that execution gets halted + # until a key gets pressed (even on non-blocking mode). + console = curses.newwin(1, self._size[1] - 5, self._size[0] - 1, 5) + textbox = curses.textpad.Textbox(console) + + # Curses may raise an exception if printing on the edge of the screen; + # we can just ignore it. + try: + console.addstr(0, 0, message, curses.color_pair( + self._pair_error + ) | curses.A_BOLD) + except curses.error: + pass + + # Custom validator simply exits on any key. + def validator(cmd): + EXIT = 7 + return EXIT + + textbox.edit(validator) + self._screen.clear() + + def print_page(self): + """ Print current page to screen. We use the previously generated + '_pages' dictionary to easily associate a label to a Salis function. + """ + # Update selected proc data if in WORLD view. + if self._current_page == "WORLD": + self._sim.lib.sal_proc_get_proc_data(self._selected_proc, cast( + self._selected_proc_data, POINTER(c_uint32) + )) + + # Print MAIN simulation data. + self._print_line( + 1, "SALIS[{}]".format(self._sim.args.file), curses.color_pair( + self._pair_header + ) | curses.A_BOLD + ) + self._print_widget(2, self._main) + + # Print data of currently selected page. + main_lines = len(self._main) + 3 + self._print_header(main_lines, self._current_page) + self._print_widget(main_lines + 1, self._pages[self._current_page]) + + # Print special widgets (WORLD view and PROCESS list). + if self._current_page == "WORLD": + self._world.render() + elif self._current_page == "PROCESS": + self._print_proc_list() + + @property + def screen(self): + return self._screen + + @property + def inst_list(self): + return self._inst_list + + @property + def proc_elements(self): + return self._proc_elements + + @property + def size(self): + return self._size + + @property + def current_page(self): + return self._current_page + + @property + def selected_proc(self): + return self._selected_proc + + @property + def selected_proc_data(self): + return self._selected_proc_data + + @property + def proc_list_scroll(self): + return self._proc_list_scroll + + @property + def world(self): + return self._world + + def _set_colors(self): + """ Define the color pairs for the data printer. + """ + curses.start_color() + curses.use_default_colors() + self._pair_header = self.get_color_pair(curses.COLOR_BLUE) + self._pair_selected = self.get_color_pair(curses.COLOR_YELLOW) + self._pair_error = self.get_color_pair(curses.COLOR_RED) + + def _get_screen(self): + """ Prepare and return the main curses window. We also set a shorter + delay when responding to a pressed escape key. + """ + # Set a shorter delay to the ESCAPE key, so that we may use it to exit + # Salis. + os.environ.setdefault("ESCDELAY", "25") + + # Prepare curses screen. + screen = curses.initscr() + curses.noecho() + curses.cbreak() + screen.keypad(True) + curses.curs_set(False) + + # We need color support in order to run the printer module. + if curses.has_colors(): + self._set_colors() + else: + raise RuntimeError("Error: no color support.") + + return screen + + def _get_inst_list(self): + """ Parse instruction set from C header file named 'instset.h'. We're + using the keyword 'SALIS_INST' to identify an instruction definition, + so be careful not to use this keyword anywhere else on the headers. + """ + inst_list = [] + inst_file = os.path.join(self._sim.path, "../include/instset.h") + + with open(inst_file, "r") as f: + lines = f.read().splitlines() + + for line in lines: + if line and line.split()[0] == "SALIS_INST": + inst_name = line.split()[1][:4] + inst_symb = line.split()[3] + inst_list.append((inst_name, inst_symb)) + + return inst_list + + def _get_proc_elements(self): + """ Parse process structure member variables from C header file named + 'process.h'. We're using the keyword 'SALIS_PROC_ELEMENT' to identify + element declarations, so be careful not to use this keyword anywhere + else on the headers. + """ + proc_elem_list = [] + proc_elem_file = os.path.join(self._sim.path, "../include/process.h") + + with open(proc_elem_file, "r") as f: + lines = f.read().splitlines() + + for line in lines: + if line and line.split()[0] == "SALIS_PROC_ELEMENT": + proc_elem_name = line.split()[2].split(";")[0] + + if proc_elem_name == "stack[8]": + # The stack is a special member variable, an array. We + # translate it by returning a list of stack identifiers. + proc_elem_list += ["stack[{}]".format(i) for i in range(8)] + else: + # We can assume all other struct elements are single + # variables. + proc_elem_list.append(proc_elem_name) + + return proc_elem_list + + def _get_main(self): + """ Generate main set of data fields to be printed. We associate, on a + list object, a label to each Salis function to be called. The following + elements get printed on all pages. + """ + return [ + ("e", "cycle", self._sim.lib.sal_main_get_cycle), + ("e", "epoch", self._sim.lib.sal_main_get_epoch), + ("e", "state", lambda: self._sim.state), + ("e", "autosave", lambda: self._sim.autosave), + ] + + def _get_pages(self): + """ Generate data fields to be printed on each page. We associate, on a + list object, a label to each Salis function to be called. Each list + represents a PAGE. We initialize all pages inside an ordered dictionary + object. + """ + # The following widgets help up print special sets of data elements. + # The use of nested lambdas is needed to receive updated values. + # Instruction counter widget: + inst_widget = [("e", inst[0], (lambda j: ( + lambda: self._sim.lib.sal_mem_get_inst_count(j) + ))(i)) for i, inst in enumerate(self._inst_list)] + + # Evolver module state widget: + state_widget = [("e", "state[{}]".format(i), (lambda j: ( + lambda: self._sim.lib.sal_evo_get_state(j) + ))(i)) for i in range(4)] + + # Selected process state widget: + selected_widget = [("p", element, (lambda j: ( + lambda: self._selected_proc_data[j] + ))(i)) for i, element in enumerate(self._proc_elements)] + + # With the help of the widgets above, we can declare the PAGES + # dictionary object. + return OrderedDict([ + ("MEMORY", [ + ("e", "order", self._sim.lib.sal_mem_get_order), + ("e", "size", self._sim.lib.sal_mem_get_size), + ("e", "blocks", self._sim.lib.sal_mem_get_block_start_count), + ("e", "allocated", self._sim.lib.sal_mem_get_allocated_count), + ("e", "ips", self._sim.lib.sal_mem_get_ip_count), + ("s", ""), + ("h", "INSTRUCTIONS"), + ] + inst_widget), + ("EVOLVER", [ + ("e", "last", self._sim.lib.sal_evo_get_last_changed_address), + ("e", "calls", self._sim.lib.sal_evo_get_calls_on_last_cycle), + ] + state_widget), + ("PROCESS", [ + ("e", "count", self._sim.lib.sal_proc_get_count), + ("e", "capacity", self._sim.lib.sal_proc_get_capacity), + ("e", "first", self._sim.lib.sal_proc_get_first), + ("e", "last", self._sim.lib.sal_proc_get_last), + ("e", "exec", + self._sim.lib.sal_proc_get_instructions_executed + ), + ]), + ("WORLD", [ + ("e", "position", lambda: self._world.pos), + ("e", "zoom", lambda: self._world.zoom), + ("e", "selected", lambda: self._selected_proc), + ("s", ""), + ("h", "SELECTED PROC"), + ] + selected_widget), + ]) + + def _print_line(self, ypos, line, attrs=curses.A_NORMAL, scroll=True): + """ Print a single line on screen only when it's visible. + """ + if scroll: + ypos -= self._main_scroll + + if 0 <= ypos < self._size[0]: + # Curses raises an exception each time we print on the screen's + # edge. We can just catch and ignore it. + try: + line = line[:self._size[1] - 1] + self._screen.addstr(ypos, 1, line, attrs) + except curses.error: + pass + + def _print_header(self, ypos, line): + """ Print a bold header. + """ + header_attr = curses.A_BOLD | curses.color_pair(self._pair_header) + self._print_line(ypos, line, header_attr) + + def _print_value(self, ypos, element, value, attr=curses.A_NORMAL): + """ Print a label:value pair. + """ + if type(value) == int: + if value == ((2 ** 32) - 1): + # In Salis, UINT32_MAX is used to represent NULL. We print NULL + # as three dashes. + value = "---" + elif self._print_hex: + value = hex(value) + + line = "{:<10} : {:>10}".format(element, value) + self._print_line(ypos, line, attr) + + def _print_proc_element(self, ypos, element, value): + """ Print elements of currently selected process. We highlight in + YELLOW if the selected process is running. + """ + if self._sim.lib.sal_proc_is_free(self._selected_proc): + attr = curses.A_NORMAL + else: + attr = curses.color_pair(self._pair_selected) + + self._print_value(ypos, element, value, attr) + + def _print_widget(self, ypos, widget): + """ Print a widget (data PAGE) on screen. + """ + for i, element in enumerate(widget): + if element[0] == "s": + continue + elif element[0] == "h": + self._print_header(i + ypos, element[1]) + elif element[0] == "e": + self._print_value(i + ypos, element[1], element[2]()) + elif element[0] == "p": + self._print_proc_element(i + ypos, element[1], element[2]()) + + def _clear_line(self, ypos): + """ Clear the specified line. + """ + if 0 <= ypos < self._size[0]: + self._screen.move(ypos, 0) + self._screen.clrtoeol() + + def _print_proc_data_list(self): + """ Print list of process data elements in PROCESS page. We can toggle + between printing the data elements or the genomes by pressing the 'g' + key. + """ + # First, print the table header, by extracting element names from the + # previously generated proc element list. + ypos = len(self._main) + len(self._pages["PROCESS"]) + 5 + header = " | ".join(["{:<10}".format("pidx")] + [ + "{:>10}".format(element) + for element in self._proc_elements[self._proc_element_scroll:] + ]) + self._clear_line(ypos) + self._print_header(ypos, header) + ypos += 1 + proc_id = self._proc_list_scroll + + # Print all proc elements in decimal or hexadecimal format, depending + # on hex-flag being set. + if self._print_hex: + data_format = lambda x: hex(x) + else: + data_format = lambda x: x + + # Lastly, iterate all lines and print as much process data as it fits. + # We can scroll the process data table using the 'wasd' keys. + while ypos < self._size[0]: + self._clear_line(ypos) + + if proc_id < self._sim.lib.sal_proc_get_capacity(): + if proc_id == self._selected_proc: + # Always highlight the selected process. + attr = curses.color_pair(self._pair_selected) + else: + attr = curses.A_NORMAL + + # Retrieve a copy of the selected process state and store it in + # a list object. + proc_data = (c_uint32 * len(self._proc_elements))() + self._sim.lib.sal_proc_get_proc_data(proc_id, cast( + proc_data, POINTER(c_uint32)) + ) + + # Lastly, assemble and print the next table row. + row = " | ".join(["{:<10}".format(proc_id)] + [ + "{:>10}".format(data_format(element)) + for element in proc_data[self._proc_element_scroll:] + ]) + self._print_line(ypos, row, attr) + + proc_id += 1 + ypos += 1 + + def _print_proc_gene_block(self, ypos, gidx, xpos, mbs, mba, ip, sp, pair): + """ Print a sub-set of a process genome. Namely, on of its two memory + blocks. + """ + while gidx < mbs and xpos < curses.COLS: + gaddr = mba + gidx + + if gaddr == ip: + attr = curses.color_pair(self._world.pair_sel_ip) + elif gaddr == sp: + attr = curses.color_pair(self._world.pair_sel_sp) + else: + attr = curses.color_pair(pair) + + # Retrieve instruction from memory and transform it to correct + # symbol. + inst = self._sim.lib.sal_mem_get_inst(gaddr) + symb = self._inst_list[inst][1] + + # Curses raises an exception each time we print on the screen's + # edge. We can just catch and ignore it. + try: + self._screen.addch(ypos, xpos, symb, attr) + except curses.error: + pass + + gidx += 1 + xpos += 1 + + return xpos + + def _print_proc_gene(self, ypos, proc_id): + """ Print a single process genome on the genome table. We use the same + colors to represent memory blocks, IP and SP of each process, as those + used to represent the selected process on WORLD view. + """ + # There's nothing to print if process is free. + if self._sim.lib.sal_proc_is_free(proc_id): + return + + # Process is alive. Retrieve a copy of the current process state and + # store it in a list object. + proc_data = (c_uint32 * len(self._proc_elements))() + self._sim.lib.sal_proc_get_proc_data(proc_id, cast( + proc_data, POINTER(c_uint32)) + ) + + # Let's extract all data of interest. + mb1a = proc_data[self._proc_elements.index("mb1a")] + mb1s = proc_data[self._proc_elements.index("mb1s")] + mb2a = proc_data[self._proc_elements.index("mb2a")] + mb2s = proc_data[self._proc_elements.index("mb2s")] + ip = proc_data[self._proc_elements.index("ip")] + sp = proc_data[self._proc_elements.index("sp")] + + # Always print MAIN memory block (mb1) first (on the left side). That + # way we can keep most of our attention on the parent. + xpos = self._print_proc_gene_block( + ypos, self._proc_gene_scroll, 14, mb1s, mb1a, ip, sp, + self._world.pair_sel_mb1 + ) + + # Reset gene counter and print child memory block, if it exists. + if mb1s < self._proc_gene_scroll: + gidx = self._proc_gene_scroll - mb1s + else: + gidx = 0 + + self._print_proc_gene_block( + ypos, gidx, xpos, mb2s, mb2a, ip, sp, self._world.pair_sel_mb2 + ) + + def _print_proc_gene_list(self): + """ Print list of process genomes in PROCESS page. We can toggle + between printing the genomes or the data elements by pressing the 'g' + key. + """ + # First, print the table header. We print the current gene-scroll + # position for easy reference. Return back to zero scroll with the 'A' + # key. + ypos = len(self._main) + len(self._pages["PROCESS"]) + 5 + header = "{:<10} | genes {} -->".format( + "pidx", self._proc_gene_scroll + ) + self._clear_line(ypos) + self._print_header(ypos, header) + ypos += 1 + proc_id = self._proc_list_scroll + + # Iterate all lines and print as much genetic data as it fits. We can + # scroll the gene data table using the 'wasd' keys. + while ypos < self._size[0]: + self._clear_line(ypos) + + if proc_id < self._sim.lib.sal_proc_get_capacity(): + if proc_id == self._selected_proc: + # Always highlight the selected process. + attr = curses.color_pair(self._pair_selected) + else: + attr = curses.A_NORMAL + + # Assemble and print the next table row. + row = "{:<10} |".format(proc_id) + self._print_line(ypos, row, attr) + self._print_proc_gene(ypos, proc_id) + + proc_id += 1 + ypos += 1 + + def _print_proc_list(self): + """ Print list of process genomes or process data elements in PROCESS + page. We can toggle between printing the genomes or the data elements + by pressing the 'g' key. + """ + if self._proc_gene_view: + self._print_proc_gene_list() + else: + self._print_proc_data_list() + + def _proc_select_by_cursor(self): + """ Select process located on address under cursor, if any exists. + """ + # First, calculate address under cursor. + ypos = self._curs_y + xpos = self._curs_x - World.PADDING + line_size = self._size[1] - World.PADDING + address = self._world.pos + ( + ((ypos * line_size) + xpos) * self._world.zoom + ) + + # Now, iterate all living processes and try to find one that owns the + # calculated address. + if self._sim.lib.sal_mem_is_address_valid(address): + for proc_id in range(self._sim.lib.sal_proc_get_count()): + if not self._sim.lib.sal_proc_is_free(proc_id): + proc_data = (c_uint32 * len(self._proc_elements))() + self._sim.lib.sal_proc_get_proc_data(proc_id, cast( + proc_data, POINTER(c_uint32)) + ) + mb1a = proc_data[self._proc_elements.index("mb1a")] + mb1s = proc_data[self._proc_elements.index("mb1s")] + mb2a = proc_data[self._proc_elements.index("mb2a")] + mb2s = proc_data[self._proc_elements.index("mb2s")] + + if ( + mb1a <= address < (mb1a + mb1s) or + mb2a <= address < (mb2a + mb2s) + ): + self._selected_proc = proc_id + break diff --git a/bin/salis.py b/bin/salis.py new file mode 100755 index 0000000..6dd82b0 --- /dev/null +++ b/bin/salis.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 + +""" SALIS: Viewer/controller for the SALIS simulator. + +File: salis.py +Author: Paul Oliver +Email: paul.t.oliver.design@gmail.com + +Main handler for the Salis simulator. The Salis class takes care of +initializing, running and shutting down the simulator and other sub-modules. It +also takes care of parsing the command-line arguments and linking to the Salis +library with the help of ctypes. + +To execute this script, make sure to have python3 installed and in your path, +as well as the cython package. Also, make sure it has correct execute +permissions (chmod). +""" + +import os +import re +import sys +import time +import traceback +from argparse import ArgumentParser, HelpFormatter +from ctypes import CDLL, c_bool, c_uint8, c_uint32, c_char_p, POINTER +from handler import Handler +from printer import Printer + + +__version__ = "2.0" + + +class Salis: + def __init__(self): + """ Salis constructor. Arguments are passed through the command line + and parsed with the 'argparse' module. Library is loaded with 'CDLL' + and C headers are parsed to detect function argument and return types. + """ + self._path = self._get_path() + self._args = self._parse_args() + self._log = self._open_log_file() + self._save_file_path = self._get_save_file_path() + self._common_pipe = self._get_common_pipe() + self._lib = self._parse_lib() + self._printer = Printer(self) + self._handler = Handler(self) + self._state = "paused" + self._autosave = "---" + self._exit = False + + # Based on CLI arguments, initialize a new Salis simulation or load + # existing one from file. + if self._args.action == "new": + self._lib.sal_main_init( + self._args.order, self._common_pipe.encode("utf-8") + ) + elif self._args.action == "load": + self._lib.sal_main_load( + self._save_file_path.encode("utf-8"), + self._common_pipe.encode("utf-8") + ) + + def __del__(self): + """ Salis destructor. + """ + # In case an error occurred early during initialization, checks whether + # Salis has been initialized correctly before attempting to shut it + # down. + if hasattr(self, "_lib") and hasattr(self._lib, "sal_main_quit"): + if self._lib.sal_main_is_init(): + self._lib.sal_main_quit() + + # If simulation ended correctly, 'error.log' should be empty. Delete + # file it exists and its empty. + if ( + hasattr(self, "_log") and + os.path.isfile(self._log) and + os.stat(self._log).st_size == 0 + ): + os.remove(self._log) + + def run(self): + """ Runs main simulation loop. Curses may be placed on non-blocking + mode, which allows simulation to run freely while still listening to + user input. + """ + while not self._exit: + self._printer.print_page() + self._handler.process_cmd(self._printer.get_cmd()) + + # If in non-blocking mode, re-print data once every 15 + # milliseconds. + if self._state == "running": + end = time.time() + 0.015 + + while time.time() < end: + self._lib.sal_main_cycle() + self.check_autosave() + + def toggle_state(self): + """ Toggle between 'paused' and 'running' states. On 'running' curses + gets placed in non-blocking mode. + """ + if self._state == "paused": + self._state = "running" + self._printer.set_nodelay(True) + else: + self._state = "paused" + self._printer.set_nodelay(False) + + def rename(self, new_name): + """ Give the simulation a new name. + """ + self._args.file = new_name + self._save_file_path = self._get_save_file_path() + + def set_autosave(self, interval): + """ Set the simulation's auto-save interval. When set to zero, auto + saving is disabled, + """ + if not interval: + self._autosave = "---" + else: + self._autosave = interval + + def check_autosave(self): + """ Save simulation to './sims/auto/*' whenever the autosave interval + is reached. We use the following naming convention for auto-saved files: + + >>> ./sims/auto/...auto + """ + if self._autosave != "---": + if not self._lib.sal_main_get_cycle() % self._autosave: + auto_path = os.path.join(self._path, "sims/auto", ".".join([ + self._args.file, + "{:08x}".format(self._lib.sal_main_get_epoch()), + "{:08x}".format(self._lib.sal_main_get_cycle()), + "auto" + ])) + self._lib.sal_main_save(auto_path.encode("utf-8")) + + def exit(self): + """ Signal we want to exit the simulator. + """ + self._exit = True + + @property + def path(self): + return self._path + + @property + def save_file_path(self): + return self._save_file_path + + @property + def common_pipe(self): + return self._common_pipe + + @property + def args(self): + return self._args + + @property + def lib(self): + return self._lib + + @property + def printer(self): + return self._printer + + @property + def handler(self): + return self._handler + + @property + def state(self): + return self._state + + @property + def autosave(self): + return self._autosave + + def _get_path(self): + """ Retrieve the absolute path of this script. We need to do this in + order to detect the './lib', './sims' and './genomes' subdirectories. + """ + return os.path.dirname(__file__) + + def _get_save_file_path(self): + """ Retrieve the absolute path of the file to which we will save this + simulation when we exit Salis. + """ + return os.path.join(self._path, "sims", self._args.file) + + def _get_common_pipe(self): + """ Get absolute path of the common pipe. This FIFO object may be used + by concurrent Salis simulations to share data between themselves. + """ + return os.path.join(self._path, "common/pipe") + + def _parse_args(self): + """ Parse command-line arguments with the 'argparse' module. To learn + more about each command, invoke the simulator in one of the following + ways: + + (venv) $ python tsalis.py --help + (venv) $ python tsalis.py new --help + (venv) $ python tsalis.py load --help + + """ + # Custom formatter helps keep all help data aligned. + formatter = lambda prog: HelpFormatter(prog, max_help_position=30) + + # Initialize the main parser with our custom formatter. + parser = ArgumentParser( + description="Viewer/controller for the Salis simulator.", + formatter_class=formatter + ) + parser.add_argument( + "-v", "--version", action="version", + version="Salis: A-Life Simulator (" + __version__ + ")" + ) + + # Initialize the 'new/load' action subparsers. + subparsers = parser.add_subparsers( + dest="action", help="Possible actions..." + ) + subparsers.required = True + + # Set up subparser for the create 'new' action. + new_parser = subparsers.add_parser("new", formatter_class=formatter) + new_parser.add_argument( + "-o", "--order", required=True, type=lambda x: int(x, 0), + metavar="[1-31]", help="Create new simulation of given ORDER" + ) + new_parser.add_argument( + "-f", "--file", required=True, type=str, metavar="FILE", + help="Name of FILE to save simulation to on exit" + ) + + # Set up subparser for the 'load' existing action. + load_parser = subparsers.add_parser("load", formatter_class=formatter) + load_parser.add_argument( + "-f", "--file", required=True, type=str, metavar="FILE", + help="Load previously saved simulation from FILE" + ) + + # Finally, parse all arguments. + args = parser.parse_args() + + # Revise that parsed CL arguments are valid. + if args.action == "new": + if args.order not in range(1, 32): + parser.error("Order must be an integer between 1 and 31") + else: + savefile = os.path.join(self._path, "sims", args.file) + + # No save-file with given name has been detected. + if not os.path.isfile(savefile): + parser.error( + "Save file provided '{}' does not exist".format(savefile) + ) + + return args + + def _open_log_file(self): + """ Create a log file to store errors on. It will get deleted if no + errors are detected. + """ + log_file = os.path.join(self._path, "error.log") + sys.stderr = open(log_file, "w") + return log_file + + def _parse_lib(self): + """ Dynamically parse the Salis library C header files. We do this in + order to more easily set the correct input/output types of all loaded + functions. C functions to be parsed must be declared in a '.h' file + located on the '../include' directory, using the following syntax: + + SALIS_API restype func_name(arg1_type arg1, arg2_type arg2); + + Note to developers: the 'SALIS_API' keyword should *NOT* be used + anywhere else in the header files (not even in comments)! + """ + lib = CDLL(os.path.join(self._path, "lib/libsalis.so")) + include_dir = os.path.join(self._path, "../include") + c_includes = [ + os.path.join(include_dir, f) + for f in os.listdir(include_dir) + # Only parse '.h' header files. + if os.path.isfile(os.path.join(include_dir, f)) and f[-2:] == ".h" + ] + funcs_to_set = [] + + for include in c_includes: + with open(include, "r") as f: + text = f.read() + + # Regexp to detect C functions to parse. This is a *very lazy* + # parser. So, if you want to expand/tweak Salis, be careful when + # declaring new functions! + funcs = re.findall(r"SALIS_API([\s\S]+?);", text, re.MULTILINE) + + for func in funcs: + func = func.replace("\n", "") + func = func.replace("\t", "") + func = func.strip() + restype = func.split()[0] + name = func.split()[1].split("(")[0] + args = [ + arg.split()[0] + for arg in func.split("(")[1].split(")")[0].split(",") + ] + funcs_to_set.append({ + "name": name, + "restype": restype, + "args": args + }) + + # All Salis typedefs must be included here, associated to their CTYPES + # equivalents. + type_convert = { + "void": None, + "boolean": c_bool, + "uint8": c_uint8, + "uint8_p": POINTER(c_uint8), + "uint32": c_uint32, + "uint32_p": POINTER(c_uint32), + "string": c_char_p, + "Process": None, + } + + # Finally, set correct arguments and return types of all Salis + # functions. + for func in funcs_to_set: + func["restype"] = type_convert[func["restype"]] + func["args"] = [type_convert[arg] for arg in func["args"]] + getattr(lib, func["name"]).restype = func["restype"] + getattr(lib, func["name"]).argtype = func["args"] + + return lib + +if __name__ == "__main__": + """ Entry point... + """ + Salis().run() diff --git a/bin/sims/.keep b/bin/sims/.keep new file mode 100644 index 0000000..e69de29 diff --git a/bin/sims/auto/.keep b/bin/sims/auto/.keep new file mode 100644 index 0000000..e69de29 diff --git a/bin/world.py b/bin/world.py new file mode 100644 index 0000000..60fd427 --- /dev/null +++ b/bin/world.py @@ -0,0 +1,277 @@ +""" SALIS: Viewer/controller for the SALIS simulator. + +File: world.py +Author: Paul Oliver +Email: paul.t.oliver.design@gmail.com + +This module should be considered an extension of the 'printer' module. It takes +care of getting a pre-redered image from Salis and post-processing it in order +to print it into the curses screen. It also keeps track of user cntrollable +rendering parameters (position and zoom). +""" + +import curses +from ctypes import c_uint8, cast, POINTER + + +class World: + PADDING = 25 + + def __init__(self, printer, sim): + """ World constructor. We link to the printer and main simulation + classes. We also setup the colors for rendering the world. + """ + self._printer = printer + self._sim = sim + self._pos = 0 + self._zoom = 1 + self._set_world_colors() + + def render(self): + """ Function for rendering the world. We get a pre-rendered buffer from + Salis' memory module (its way faster to pre-render in C) and use that + to assemble the world image in Python. + """ + # Window is so narrow that world is not visible. + if self._printer.size[1] <= self.PADDING: + return + + # Get pre-rendered image from Salis' memory module. + line_width = self._printer.size[1] - self.PADDING + print_area = self._printer.size[0] * line_width + c_buffer = (c_uint8 * print_area)() + self._sim.lib.sal_mem_render_image( + self._pos, self._zoom, print_area, cast(c_buffer, POINTER(c_uint8)) + ) + + # Get data elements of selected process, if it's running, and store + # them into a convenient dict object. + if self._sim.lib.sal_proc_is_free(self._printer.selected_proc): + sel_data = None + else: + out_data = self._printer.selected_proc_data + out_elem = self._printer.proc_elements + sel_data = { + "ip": out_data[out_elem.index("ip")], + "sp": out_data[out_elem.index("sp")], + "mb1a": out_data[out_elem.index("mb1a")], + "mb1s": out_data[out_elem.index("mb1s")], + "mb2a": out_data[out_elem.index("mb2a")], + "mb2s": out_data[out_elem.index("mb2s")], + } + + # Iterate all cells on printable area and print the post-rendered + # cells. Rendered cells contain info about bit flags and instructions + # currently written into memory. + bidx = 0 + + for y in range(self._printer.size[0]): + for x in range(line_width): + xpad = x + self.PADDING + addr = self._pos + (self._zoom * bidx) + symb, attr = self._render_cell(c_buffer[bidx], addr, sel_data) + + # Curses raises an exception when printing on the edge of the + # screen; we can just ignore it. + try: + self._printer.screen.addch(y, xpad, symb, attr) + except curses.error: + pass + + bidx += 1 + + def zoom_out(self): + """ Zoom out by a factor of 2 (zoom *= 2). + """ + if self._is_world_editable(): + self._zoom = min(self._zoom * 2, self._get_max_zoom()) + + def zoom_in(self): + """ Zoom in by a factor of 2 (zoom //= 2). + """ + if self._is_world_editable(): + self._zoom = max(self._zoom // 2, 1) + + def zoom_reset(self): + """ Reset zoom to a valid value on certain events (i.e. during terminal + resizing). + """ + self._zoom = min(self._zoom, self._get_max_zoom()) + + def pan_left(self): + """ Pan world to the left (pos -= zoom). + """ + if self._is_world_editable(): + self._pos = max(self._pos - self._zoom, 0) + + def pan_right(self): + """ Pan world to the right (pos += zoom). + """ + if self._is_world_editable(): + max_pos = self._sim.lib.sal_mem_get_size() - 1 + self._pos = min(self._pos + self._zoom, max_pos) + + def pan_down(self): + """ Pan world downward (pos += zoom * columns). + """ + if self._is_world_editable(): + self._pos = max(self._pos - self._get_line_area(), 0) + + def pan_up(self): + """ Pan world upward (pos -= zoom * columns). + """ + if self._is_world_editable(): + max_pos = self._sim.lib.sal_mem_get_size() - 1 + self._pos = min(self._pos + self._get_line_area(), max_pos) + + def pan_reset(self): + """ Set world position to zero. + """ + if self._is_world_editable(): + self._pos = 0 + + def scroll_to(self, pos): + """ Move world pos to a specified position. + """ + if self._is_world_editable(): + if self._sim.lib.sal_mem_is_address_valid(pos): + self._pos = pos + else: + raise RuntimeError("Error: scrolling to an invalid address") + + @property + def pos(self): + return self._pos + + @property + def zoom(self): + return self._zoom + + @property + def pair_sel_mb2(self): + return self._pair_sel_mb2 + + @property + def pair_sel_mb1(self): + return self._pair_sel_mb1 + + @property + def pair_sel_sp(self): + return self._pair_sel_sp + + @property + def pair_sel_ip(self): + return self._pair_sel_ip + + def _set_world_colors(self): + """ Define color pairs for rendering the world. Each color has a + special meaning, referring to the selected process IP, SP and memory + blocks, or to bit flags currently set on rendered cells. + """ + self._pair_free = self._printer.get_color_pair( + curses.COLOR_BLUE + ) + self._pair_alloc = self._printer.get_color_pair( + curses.COLOR_BLACK, curses.COLOR_BLUE + ) + self._pair_mbstart = self._printer.get_color_pair( + curses.COLOR_BLACK, curses.COLOR_CYAN + ) + self._pair_ip = self._printer.get_color_pair( + curses.COLOR_BLACK, curses.COLOR_WHITE + ) + self._pair_sel_mb2 = self._printer.get_color_pair( + curses.COLOR_BLACK, curses.COLOR_GREEN + ) + self._pair_sel_mb1 = self._printer.get_color_pair( + curses.COLOR_BLACK, curses.COLOR_YELLOW + ) + self._pair_sel_sp = self._printer.get_color_pair( + curses.COLOR_BLACK, curses.COLOR_MAGENTA + ) + self._pair_sel_ip = self._printer.get_color_pair( + curses.COLOR_BLACK, curses.COLOR_RED + ) + + def _render_cell(self, byte, addr, sel_data=None): + """ Render a single cell on the WORLD view. All cells are rendered by + interpreting the values coming in from the buffer. We overlay special + colors for representing the selected organism's state, on top of the + more common colors used to represent memory state. + """ + # Paint black all cells that are out of memory bounds. + if not self._sim.lib.sal_mem_is_address_valid(addr): + return " ", curses.A_NORMAL + + # Check if cell contains part of the currently selected process. + if sel_data: + top_addr = addr + self._zoom + top_mb1a = sel_data["mb1a"] + sel_data["mb1s"] + top_mb2a = sel_data["mb2a"] + sel_data["mb2s"] + + if addr <= sel_data["ip"] < top_addr: + pair = self._pair_sel_ip + elif addr <= sel_data["sp"] < top_addr: + pair = self._pair_sel_sp + elif top_addr > sel_data["mb1a"] and top_mb1a > addr: + pair = self._pair_sel_mb1 + elif top_addr > sel_data["mb2a"] and top_mb2a > addr: + pair = self._pair_sel_mb2 + + # No pair has been selected yet; select pair based on bit-flags. + if not "pair" in locals(): + if byte >= 0x80: + pair = self._pair_ip + elif byte >= 0x40: + pair = self._pair_mbstart + elif byte >= 0x20: + pair = self._pair_alloc + else: + pair = self._pair_free + + # Select symbol to represent instructions currently on cell. + inst = byte % 32 + + if self._zoom == 1: + symb = self._printer.inst_list[inst][1] + elif inst > 16: + symb = ":" + else: + symb = "." + + # Return tuple containing our post-redered cell. + return symb, curses.color_pair(pair) + + def _get_max_zoom(self): + """ Calculate maximum needed zoom so that the entire world fits on the + terminal window. + """ + max_zoom = 1 + line_size = self._printer.size[1] - self.PADDING + coverage = self._printer.size[0] * line_size + + # We fix a maximum zoom level; otherwise, program may halt on extreme + # zoom levels. + while ( + (coverage * max_zoom) < self._sim.lib.sal_mem_get_size() and + max_zoom < 2 ** 16 + ): + max_zoom *= 2 + + return max_zoom + + def _is_world_editable(self): + """ For this to return True, printer's current page must be WORLD page. + Additionally, the WORLD panel must be visible on the terminal window + (i.e. curses.COLS > data_margin). + """ + correct_page = self._printer.current_page == "WORLD" + correct_size = self._printer.size[1] > self.PADDING + return correct_page and correct_size + + def _get_line_area(self): + """ Return amount of bytes contained in a printed WORLD line. + """ + line_size = self._printer.size[1] - self.PADDING + line_area = self._zoom * line_size + return line_area diff --git a/build/.keep b/build/.keep new file mode 100644 index 0000000..e69de29 diff --git a/include/common.h b/include/common.h new file mode 100644 index 0000000..7386a34 --- /dev/null +++ b/include/common.h @@ -0,0 +1,19 @@ +/** +* @file common.h +* @author Paul Oliver +* +* This module controls the 'common pipe', which is the FIFO file through which +* communication between different simulations can occur. By calling SEND, +* processes may output local instructions through the pipe. These instructions +* may then be read by processes running on a different simulation instance. +*/ + +#ifndef SALIS_COMMON_H +#define SALIS_COMMON_H + +void _sal_comm_init(string pipe); +void _sal_comm_quit(void); +void _sal_comm_send(uint8 inst); +uint8 _sal_comm_receive(void); + +#endif diff --git a/include/evolver.h b/include/evolver.h new file mode 100644 index 0000000..b2ead10 --- /dev/null +++ b/include/evolver.h @@ -0,0 +1,38 @@ +/** +* @file evolver.h +* @author Paul Oliver +* +* This module controls all random events in Salis. At its heart lies a +* XOR-Shift pseudo-random number generator with 128 bits of state. It controls +* cosmic rays and rises simulation entropy whenever organisms 'eat' +* information. +*/ + +#ifndef SALIS_EVOLVER_H +#define SALIS_EVOLVER_H + +void _sal_evo_init(void); +void _sal_evo_quit(void); +void _sal_evo_load_from(FILE *file); +void _sal_evo_save_into(FILE *file); + +/** Get address where the last cosmic ray landed. +* @return Last address changed by a cosmic ray +*/ +SALIS_API uint32 sal_evo_get_last_changed_address(void); + +/** Get amount of random numbers generated during the last simulation cycle. +* @return Number of calls to the random number generator during the last cycle +*/ +SALIS_API uint32 sal_evo_get_calls_on_last_cycle(void); + +/** Access the internal state of the XOR-Shift random number generator. +* @param state_index Index of one of the 32 bit state-blocks [0-4] +* @return State of the 32 bit block +*/ +SALIS_API uint32 sal_evo_get_state(uint8 state_index); + +void _sal_evo_randomize_at(uint32 address); +void _sal_evo_cycle(void); + +#endif diff --git a/include/getter.h b/include/getter.h new file mode 100644 index 0000000..83f0664 --- /dev/null +++ b/include/getter.h @@ -0,0 +1,20 @@ +/** +* @file getter.h +* @author Paul Oliver +* +* We declare a helper macro for easy 'getting' of module state variables. Other +* similar, more specific macros are defined inside the module sources. Don't +* repeat yourself! :-) +*/ + +#ifndef SALIS_GETTER_H +#define SALIS_GETTER_H + +#define UINT32_GETTER(mod, name) \ +uint32 sal_##mod##_get_##name(void) \ +{ \ + assert(g_is_init); \ + return g_##name; \ +} + +#endif diff --git a/include/instset.h b/include/instset.h new file mode 100644 index 0000000..ab7bab0 --- /dev/null +++ b/include/instset.h @@ -0,0 +1,71 @@ +/** +* @file instset.h +* @author Paul Oliver +* +* Here we declare the complete instruction set of the Salis virtual machine. +* Additionally, some helper functions are declared for determining instruction +* type and validity. +*/ + +#ifndef SALIS_INSTSET_H +#define SALIS_INSTSET_H + +#define INST_COUNT 32 + +/** Salis instruction set. The 'SALIS_INST' macro and inline doc-comments help +* python parse this file. Don't edit these unless you know what you're doing! +*/ +enum { + SALIS_INST NOP0, /**< . Template constructor */ + SALIS_INST NOP1, /**< : Template constructor */ + SALIS_INST MODA, /**< a Register modifier */ + SALIS_INST MODB, /**< b Register modifier */ + SALIS_INST MODC, /**< c Register modifier */ + SALIS_INST MODD, /**< d Register modifier */ + SALIS_INST JMPB, /**< ( Jump back to template complement */ + SALIS_INST JMPF, /**< ) Jump forward to template complement */ + SALIS_INST ADRB, /**< [ Search back for template complement */ + SALIS_INST ADRF, /**< ] Search forward for template complement */ + SALIS_INST MALB, /**< { Allocate backwards */ + SALIS_INST MALF, /**< } Allocate forward */ + SALIS_INST SWAP, /**< % Swap memory blocks */ + SALIS_INST SPLT, /**< $ Split child memory block */ + SALIS_INST INCN, /**< ^ Increment register */ + SALIS_INST DECN, /**< v Decrement register */ + SALIS_INST ZERO, /**< 0 Zero out register */ + SALIS_INST UNIT, /**< 1 Place 1 on register */ + SALIS_INST NOTN, /**< ! Negation operator */ + SALIS_INST IFNZ, /**< ? Conditional operator */ + SALIS_INST SUMN, /**< + Add two registers */ + SALIS_INST SUBN, /**< - Subtract two registers */ + SALIS_INST MULN, /**< * Multiply two registers */ + SALIS_INST DIVN, /**< / Divide two registers */ + SALIS_INST LOAD, /**< L Load instruction from memory */ + SALIS_INST WRTE, /**< W Write instruction into memory */ + SALIS_INST SEND, /**< S Send instruction to common pipe */ + SALIS_INST RECV, /**< R Receive instruction from common pipe */ + SALIS_INST PSHN, /**< # Push value to stack */ + SALIS_INST POPN, /**< ~ Pop value from stack */ + SALIS_INST EATB, /**< < Eat backwards */ + SALIS_INST EATF /**< > Eat forward */ +}; + +/** Determine if an unsigned integer contains a valid instruction. +* @param byte Any unsigned integer up to 32 bits +* @return Whether or nor integer contains a valid instruction +*/ +SALIS_API boolean sal_is_inst(uint32 word); + +/** Determine if instruction is a template constructor [NOP0-NOP1]. +* @param inst Must contain a valid instruction +* @return Whether or not instruction is a template constructor +*/ +SALIS_API boolean sal_is_template(uint32 inst); + +/** Determine if instruction a register modifier [MOD0-MOD3]. +* @param inst Must contain a valid instruction +* @return Whether or not instruction is a register modifier +*/ +SALIS_API boolean sal_is_mod(uint32 inst); + +#endif diff --git a/include/memory.h b/include/memory.h new file mode 100644 index 0000000..e7b97ee --- /dev/null +++ b/include/memory.h @@ -0,0 +1,134 @@ +/** +* @file memory.h +* @author Paul Oliver +* +* This module gives access to Salis memory. You can check the state of each +* byte (instruction and flags) at any time and also perform manual memory +* manipulations. +*/ + +#ifndef SALIS_MEMORY_H +#define SALIS_MEMORY_H + +#define IP_FLAG 0x80 +#define BLOCK_START_FLAG 0x40 +#define ALLOCATED_FLAG 0x20 +#define INSTRUCTION_MASK 0x1f + +void _sal_mem_init(uint32 order); +void _sal_mem_quit(void); +void _sal_mem_load_from(FILE *file); +void _sal_mem_save_into(FILE *file); + +/** Get memory order. +* @return Order of memory (memory_size == 1 << order) +*/ +SALIS_API uint32 sal_mem_get_order(void); + +/** Get memory size. +* @return Size of memory (memory_size == 1 << order) +*/ +SALIS_API uint32 sal_mem_get_size(void); + +/** Get amount of addresses with the IP flag set on them. +* @return Amount of addresses with the IP flag set +*/ +SALIS_API uint32 sal_mem_get_ip_count(void); + +/** Get amount of addresses with the memory-block-start flag set on them. +* @return Amount of addresses with the memory-block-start flag set +*/ +SALIS_API uint32 sal_mem_get_block_start_count(void); + +/** Get amount of addresses with the allocated flag set on them. +* @return Amount of addresses with the allocated flag set +*/ +SALIS_API uint32 sal_mem_get_allocated_count(void); + +/** Get memory capacity. +* @return Memory capacity (capacity == size / 2) +*/ +SALIS_API uint32 sal_mem_get_capacity(void); + +/** Get amount of addresses with a given instruction written on them. +* @param inst Instruction whose amount we want to count +* @return Amount of addresses with given instruction +*/ +SALIS_API uint32 sal_mem_get_inst_count(uint8 inst); + +/** Determine if memory is above its capacity. +* @return Memory is above capacity +*/ +SALIS_API boolean sal_mem_is_over_capacity(void); + +/** Check validity of address. +* @param address Address being queried +* @return Validity of address (validity == address < size) +*/ +SALIS_API boolean sal_mem_is_address_valid(uint32 address); + +/** Check if given address has the IP flag set. +* @param address Address being queried +* @return IP flag is set on this address +*/ +SALIS_API boolean sal_mem_is_ip(uint32 address); + +/** Check if given address has the memory-block-start flag set. +* @param address Address being queried +* @return Memory-block-start flag is set on this address +*/ +SALIS_API boolean sal_mem_is_block_start(uint32 address); + +/** Check if given address has the allocated flag set. +* @param address Address being queried +* @return Allocated flag is set on this address +*/ +SALIS_API boolean sal_mem_is_allocated(uint32 address); + +void _sal_mem_set_ip(uint32 address); +void _sal_mem_set_block_start(uint32 address); +void _sal_mem_set_allocated(uint32 address); +void _sal_mem_unset_ip(uint32 address); +void _sal_mem_unset_block_start(uint32 address); +void _sal_mem_unset_allocated(uint32 address); + +/** Get currently set flags at given address. +* @param address Address being queried +* @return Byte containing set flag bits +*/ +SALIS_API uint8 sal_mem_get_flags(uint32 address); + +/** Get current instruction at address. +* @param address Address being queried +* @return Instruction currently written at address +*/ +SALIS_API uint8 sal_mem_get_inst(uint32 address); + +/** Write instruction into address. +* @param address Address being set +* @param inst Instruction to write at given address +*/ +SALIS_API void sal_mem_set_inst(uint32 address, uint8 inst); + +/** Get current byte at address. +* @param address Address being queried +* @return Byte currently written at address (includes bit flags & instruction) +*/ +SALIS_API uint8 sal_mem_get_byte(uint32 address); + +/** Render a 1D image of a given block of memory. This is useful, as rendering +* directly in python would be too slow. We use openmp for multi-threaded image +* generation. +* +* @param origin Low bound of rendered image +* @param cell_size Amount of bytes per rendered pixel (cell) +* @param buff_size Amount of pixels (cells) to be generated +* @param buffer Pre-allocated buffer to store the rendered pixels into +*/ +SALIS_API void sal_mem_render_image( + uint32 origin, uint32 cell_size, uint32 buff_size, uint8_p buffer +); + +void _sal_mem_cycle(void); + +#endif diff --git a/include/process.h b/include/process.h new file mode 100644 index 0000000..3723ad6 --- /dev/null +++ b/include/process.h @@ -0,0 +1,97 @@ +/** +* @file process.h +* @author Paul Oliver +* +* This module allows access to Salis processes, or procs. Procs are the actual +* organisms in the simulation. They consist of a virtual CPU with 4 registers +* and a stack of 8. The instruction pointer (IP) and seeker pointer (SP) +* coordinate the execution of all instructions. Organisms get rewarded or +* punished, depending on certain conditions. +*/ + +#ifndef SALIS_PROCESS_H +#define SALIS_PROCESS_H + +/** The Process data-structure. The 'SALIS_PROC_ELEMENT' macro helps python +* parse the struct, so don't change it! +*/ +struct Process +{ + SALIS_PROC_ELEMENT uint32 mb1a; + SALIS_PROC_ELEMENT uint32 mb1s; + SALIS_PROC_ELEMENT uint32 mb2a; + SALIS_PROC_ELEMENT uint32 mb2s; + SALIS_PROC_ELEMENT uint32 reward; + SALIS_PROC_ELEMENT uint32 punish; + SALIS_PROC_ELEMENT uint32 ip; + SALIS_PROC_ELEMENT uint32 sp; + SALIS_PROC_ELEMENT uint32 rax; + SALIS_PROC_ELEMENT uint32 rbx; + SALIS_PROC_ELEMENT uint32 rcx; + SALIS_PROC_ELEMENT uint32 rdx; + SALIS_PROC_ELEMENT uint32 stack[8]; +}; + +typedef struct Process Process; + +void _sal_proc_init(void); +void _sal_proc_quit(void); +void _sal_proc_load_from(FILE *file); +void _sal_proc_save_into(FILE *file); + +/** Get process count. +* @return Amount of running (living) processes +*/ +SALIS_API uint32 sal_proc_get_count(void); + +/** Get reaper queue capacity. +* @return Currently allocated size of reaper queue +*/ +SALIS_API uint32 sal_proc_get_capacity(void); + +/** Get first process. +* @return Process currently on top of reaper queue +*/ +SALIS_API uint32 sal_proc_get_first(void); + +/** Get last process. +* @return Process currently on bottom of reaper queue (closest to death) +*/ +SALIS_API uint32 sal_proc_get_last(void); + +/** Get instructions executed on last cycle. +* @return Amount of executed instructions during the last cycle +*/ +SALIS_API uint32 sal_proc_get_instructions_executed(void); + +/** Check if process is currently free. +* @param proc_id ID of process whose status we want to check +* @return Status (either free or running) of the process with the given ID +*/ +SALIS_API boolean sal_proc_is_free(uint32 proc_id); + +/** Get process. +* @param proc_id ID of Process being queried +* @return A copy of the process with the given ID +*/ +SALIS_API Process sal_proc_get_proc(uint32 proc_id); + +/** Get process data. +* @param proc_id ID of Process being queried +* @param buffer Pre-allocated buffer to store data on [ > sizeof(Process)] +*/ +SALIS_API void sal_proc_get_proc_data(uint32 proc_id, uint32_p buffer); + +/** Create new process. +* @param address Address we want to allocate our process into +* @param mb1_size Size of the memory block we want to allocate for our process +*/ +SALIS_API void sal_proc_create(uint32 address, uint32 mb1_size); + +/** Kill process on bottom of reaper queue. +*/ +SALIS_API void sal_proc_kill(void); + +void _sal_proc_cycle(void); + +#endif diff --git a/include/salis.h b/include/salis.h new file mode 100644 index 0000000..8b261b1 --- /dev/null +++ b/include/salis.h @@ -0,0 +1,67 @@ +/** +* @file salis.h +* @author Paul Oliver +* +* Main header file for the Salis library. Loading this header imports all API +* modules and functions. It may be loaded from C or C++. +*/ + +#ifndef SALIS_H +#define SALIS_H + +#ifdef __cplusplus + extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +/** Initialize Salis simulation. +* @param order Order of memory (memory_size == 1 << order) +* @param pipe Desired path and file name of common pipe +*/ +SALIS_API void sal_main_init(uint32 order, string pipe); + +/** Free resources and quit Salis. +*/ +SALIS_API void sal_main_quit(void); + +/** Load existing Salis simulation from saved file. +* @param file_name Path of the save file to be loaded +* @param pipe Desired path and file name of common pipe +*/ +SALIS_API void sal_main_load(string file_name, string pipe); + +/** Save Salis simulation to a file. +* @param file_name Path of the save file to be created +*/ +SALIS_API void sal_main_save(string file_name); + +/** Check if Salis simulation has been correctly initialized. +* @return Salis has been correctly initialized +*/ +SALIS_API boolean sal_main_is_init(void); + +/** Get current simulation cycle. +* @return Current simulation cycle +*/ +SALIS_API uint32 sal_main_get_cycle(void); + +/** Get current simulation epoch. +* @return Current simulation epoch (1 epoch == 2^32 cycles) +*/ +SALIS_API uint32 sal_main_get_epoch(void); + +/** Update simulation once. This will cycle all Salis modules and processes. +*/ +SALIS_API void sal_main_cycle(void); + +#ifdef __cplusplus + } +#endif + +#endif diff --git a/include/types.h b/include/types.h new file mode 100644 index 0000000..3ae418f --- /dev/null +++ b/include/types.h @@ -0,0 +1,45 @@ +/** +* @file types.h +* @author Paul Oliver +* +* Declare main typedefs for the Salis library. Salis depends on fixed-width +* unsigned types being available. We use the limits header to define these in +* a C89 compliant way. Also, we typedef respective pointer types and a string +* type to aid in header parsing. +*/ + +#ifndef SALIS_TYPES_H +#define SALIS_TYPES_H + +#include + +#define UINT8_MAX 0xff +#define UINT32_MAX 0xffffffff + +#if UCHAR_MAX == UINT8_MAX + typedef unsigned char uint8; + typedef unsigned char *uint8_p; +#else + #error "Cannot define uint8/uint8_p types!" +#endif + +#if ULONG_MAX == UINT32_MAX + typedef unsigned long int uint32; + typedef unsigned long int *uint32_p; +#elif UINT_MAX == UINT32_MAX + typedef unsigned int uint32; + typedef unsigned int *uint32_p; +#elif USHRT_MAX == UINT32_MAX + typedef unsigned short int uint32; + typedef unsigned short int *uint32_p; +#else + #error "Cannot define uint32/uint32_p types!" +#endif + +typedef int boolean; +typedef const char *string; + +#define TRUE 1 +#define FALSE 0 + +#endif diff --git a/src/common.c b/src/common.c new file mode 100644 index 0000000..3653252 --- /dev/null +++ b/src/common.c @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include "types.h" +#include "instset.h" +#include "common.h" + +static boolean g_is_init; +static int g_file_desc; + +void _sal_comm_init(string pipe) +{ + /* Initialize the common pipe. This module is the only one on Salis that + makes use of Linux specific headers and types. If you want, feel free to + port this code into other platforms (should be easy). If you do so, let me + know and we can incorporate it into the Salis repository. + */ + assert(!g_is_init); + mkfifo(pipe, 0666); + g_is_init = TRUE; + + /* It's important to open the FIFO file in non-blocking mode, or else the + simulators might halt if the pipe becomes empty. + */ + g_file_desc = open(pipe, O_RDWR | O_NONBLOCK); + assert(g_file_desc != -1); +} + +void _sal_comm_quit(void) +{ + /* Close the common pipe FIFO file from within this instance. An empty pipe + file will remain unless it gets manually deleted. + */ + assert(g_is_init); + close(g_file_desc); + g_is_init = FALSE; + g_file_desc = 0; +} + +void _sal_comm_send(uint8 inst) +{ + /* Send a single byte (instruction) to the common pipe. This function is + called by processes that execute the SEND instruction. Hopefully, some of + them 'learn' to use this as an advantage. + + In the future, I want to make the common pipe able to communicate across + local networks (LANs) and over the Internet. + */ + assert(g_is_init); + assert(sal_is_inst(inst)); + write(g_file_desc, &inst, 1); +} + +uint8 _sal_comm_receive(void) +{ + /* Receive a single byte (instruction) from the common pipe. This function + is called by processes that execute the RCVE instruction. If the pipe is + empty, this function returns the NOP0 instruction. + */ + uint8 inst; + ssize_t res; + assert(g_is_init); + res = read(g_file_desc, &inst, 1); + + if (res) { + assert(sal_is_inst(inst)); + return inst; + } else { + return NOP0; + } +} diff --git a/src/evolver.c b/src/evolver.c new file mode 100644 index 0000000..e3b6ef7 --- /dev/null +++ b/src/evolver.c @@ -0,0 +1,132 @@ +#include +#include +#include +#include +#include +#include "types.h" +#include "getter.h" +#include "instset.h" +#include "memory.h" +#include "evolver.h" + +static boolean g_is_init; +static uint32 g_last_changed_address; +static uint32 g_calls_on_last_cycle; +static uint32 g_state[4]; + +void _sal_evo_init(void) +{ + /* Start up the evolver module. We simply set the 128 bits into a random + state by calling 'rand()'. + */ + assert(!g_is_init); + srand((uint32)time(NULL)); + g_state[0] = rand(); + g_state[1] = rand(); + g_state[2] = rand(); + g_state[3] = rand(); + g_is_init = TRUE; +} + +void _sal_evo_quit(void) +{ + /* Quit the evolver module. Reset everything back to zero. + */ + assert(g_is_init); + g_is_init = FALSE; + g_last_changed_address = 0; + g_calls_on_last_cycle = 0; + memset(g_state, 0, sizeof(uint32) * 4); +} + +void _sal_evo_load_from(FILE *file) +{ + /* Load evolver state from a binary file. + */ + assert(!g_is_init); + assert(file); + fread(&g_is_init, sizeof(boolean), 1, file); + fread(&g_last_changed_address, sizeof(uint32), 1, file); + fread(&g_calls_on_last_cycle, sizeof(uint32), 1, file); + fread(&g_state, sizeof(uint32), 4, file); +} + +void _sal_evo_save_into(FILE *file) +{ + /* Save evolver state into a binary file. + */ + assert(g_is_init); + assert(file); + fwrite(&g_is_init, sizeof(boolean), 1, file); + fwrite(&g_last_changed_address, sizeof(uint32), 1, file); + fwrite(&g_calls_on_last_cycle, sizeof(uint32), 1, file); + fwrite(&g_state, sizeof(uint32), 4, file); +} + +/* Getter methods for the evolver module. +*/ +UINT32_GETTER(evo, last_changed_address) +UINT32_GETTER(evo, calls_on_last_cycle) + +uint32 sal_evo_get_state(uint8 state_index) +{ + /* Get part of the evolver's internal state (32 bits of 128 total bits) as + an unsigned int. + */ + assert(g_is_init); + assert(state_index < 4); + return g_state[state_index]; +} + +static uint32 generate_random_number(void) +{ + /* Generate a single 32 bit random number. This module makes use of the + XOR-Shift pseudo-rng. We use XOR-Shift because it's extremely lightweight + and fast, while providing quite good results. Find more info about it here: + >>> https://en.wikipedia.org/wiki/Xorshift + */ + uint32 tmp1; + uint32 tmp2; + assert(g_is_init); + tmp2 = g_state[3]; + tmp2 ^= tmp2 << 11; + tmp2 ^= tmp2 >> 8; + g_state[3] = g_state[2]; + g_state[2] = g_state[1]; + g_state[1] = tmp1 = g_state[0]; + tmp2 ^= tmp1; + tmp2 ^= tmp1 >> 19; + g_state[0] = tmp2; + g_calls_on_last_cycle++; + return tmp2; +} + +void _sal_evo_randomize_at(uint32 address) +{ + /* Place a random instruction into a given address. + */ + uint8 inst; + assert(g_is_init); + assert(sal_mem_is_address_valid(address)); + inst = generate_random_number() % INST_COUNT; + g_last_changed_address = address; + sal_mem_set_inst(address, inst); +} + +void _sal_evo_cycle(void) +{ + /* During each simulation cycle, a random 32 bit integer is generated. If + this integer represents a 'valid' address in memory + (i.e. new_rand < memory_size), this address becomes hit by a cosmic ray + (randomized). This simple mutation scheme is enough to drive evolution in + Salis. + */ + uint32 address; + assert(g_is_init); + g_calls_on_last_cycle = 0; + address = generate_random_number(); + + if (sal_mem_is_address_valid(address)) { + _sal_evo_randomize_at(address); + } +} diff --git a/src/instset.c b/src/instset.c new file mode 100644 index 0000000..2ab127a --- /dev/null +++ b/src/instset.c @@ -0,0 +1,39 @@ +#include +#include "types.h" +#include "instset.h" + +boolean sal_is_inst(uint32 word) +{ + /* Test if a given 32 bit integer contains a valid Salis instruction. + */ + return word < INST_COUNT; +} + +static boolean is_in_between(uint32 inst, uint32 low, uint32 hi) +{ + /* Test whether a Salis instruction lies within a given range. This is + useful for identifying template instructions and/or register modifiers. + */ + assert(sal_is_inst(inst)); + assert(sal_is_inst(low)); + assert(sal_is_inst(hi)); + return (inst >= low) && (inst <= hi); +} + +boolean sal_is_template(uint32 inst) +{ + /* Test whether a given instruction is a template element + (i.e. NOP0 or NOP1). + */ + assert(sal_is_inst(inst)); + return is_in_between(inst, NOP0, NOP1); +} + +boolean sal_is_mod(uint32 inst) +{ + /* Test whether a given instruction is a register modifier + (i.e. MODA, MODB, MODC or MODD). + */ + assert(sal_is_inst(inst)); + return is_in_between(inst, MODA, MODD); +} diff --git a/src/memory.c b/src/memory.c new file mode 100644 index 0000000..c586eb0 --- /dev/null +++ b/src/memory.c @@ -0,0 +1,325 @@ +#include +#include +#include +#include +#include "types.h" +#include "getter.h" +#include "instset.h" +#include "memory.h" + +#define MAX_ZOOM 0x10000 + +static boolean g_is_init; +static uint32 g_order; +static uint32 g_size; +static uint32 g_ip_count; +static uint32 g_block_start_count; +static uint32 g_allocated_count; +static uint32 g_capacity; +static uint32 g_inst_counter[INST_COUNT]; +static uint8_p g_memory; + +void _sal_mem_init(uint32 order) +{ + /* Set memory module to its initial state. We calculate memory size based + on its order (size = 1 << order) and allocate an array of such size. We + also initialize the array completely to zero. + */ + assert(!g_is_init); + assert(order < 32); + g_is_init = TRUE; + g_order = order; + g_size = 1 << g_order; + g_capacity = g_size / 2; + g_inst_counter[0] = g_size; + g_memory = calloc(g_size, 1); + assert(g_memory); +} + +void _sal_mem_quit(void) +{ + /* Reset memory module entirely back to zero. That way, we can load several + simulations without restarting the application entirely. + */ + assert(g_is_init); + free(g_memory); + g_is_init = FALSE; + g_order = 0; + g_size = 0; + g_ip_count = 0; + g_block_start_count = 0; + g_allocated_count = 0; + g_capacity = 0; + memset(g_inst_counter, 0, sizeof(uint32) * INST_COUNT); + g_memory = NULL; +} + +void _sal_mem_load_from(FILE *file) +{ + /* Load memory state from a binary file. + */ + assert(!g_is_init); + assert(file); + fread(&g_is_init, sizeof(boolean), 1, file); + fread(&g_order, sizeof(uint32), 1, file); + fread(&g_size, sizeof(uint32), 1, file); + fread(&g_ip_count, sizeof(uint32), 1, file); + fread(&g_block_start_count, sizeof(uint32), 1, file); + fread(&g_allocated_count, sizeof(uint32), 1, file); + fread(&g_capacity, sizeof(uint32), 1, file); + fread(g_inst_counter, sizeof(uint32), INST_COUNT, file); + g_memory = calloc(g_size, sizeof(uint8)); + assert(g_memory); + fread(g_memory, sizeof(uint8), g_size, file); +} + +void _sal_mem_save_into(FILE *file) +{ + /* Save memory state to a binary file. + */ + assert(g_is_init); + assert(file); + fwrite(&g_is_init, sizeof(boolean), 1, file); + fwrite(&g_order, sizeof(uint32), 1, file); + fwrite(&g_size, sizeof(uint32), 1, file); + fwrite(&g_ip_count, sizeof(uint32), 1, file); + fwrite(&g_block_start_count, sizeof(uint32), 1, file); + fwrite(&g_allocated_count, sizeof(uint32), 1, file); + fwrite(&g_capacity, sizeof(uint32), 1, file); + fwrite(g_inst_counter, sizeof(uint32), INST_COUNT, file); + fwrite(g_memory, sizeof(uint8), g_size, file); +} + +/* Getter methods for the memory module. +*/ +UINT32_GETTER(mem, order) +UINT32_GETTER(mem, size) +UINT32_GETTER(mem, ip_count) +UINT32_GETTER(mem, block_start_count) +UINT32_GETTER(mem, allocated_count) +UINT32_GETTER(mem, capacity) + +uint32 sal_mem_get_inst_count(uint8 inst) +{ + /* Return number of times a certain instruction appears in memory. The + instruction counter gets updated dynamically during each cycle. + */ + assert(g_is_init); + assert(sal_is_inst(inst)); + return g_inst_counter[inst]; +} + +boolean sal_mem_is_over_capacity(void) +{ + /* Check if memory is filled above 50%. If so, old organisms will be popped + out of the reaper queue! + */ + assert(g_is_init); + return g_allocated_count > g_capacity; +} + +boolean sal_mem_is_address_valid(uint32 address) +{ + /* Check if given address is valid. + */ + assert(g_is_init); + return address < g_size; +} + +/* We declare a standard macro to test whether a specific FLAG is set on a given +byte. Remember, a Salis byte contains a 5 bit instruction (of 32 possible) plus +3 flags: IP, BLOCK_START and ALLOCATED. These flags help organisms identify +where there is free memory space to reproduce on, and tell the python printer +module how to color each byte. +*/ +#define FLAG_TEST(name, flag) \ +boolean sal_mem_is_##name(uint32 address) \ +{ \ + assert(g_is_init); \ + assert(sal_mem_is_address_valid(address)); \ + return !!(g_memory[address] & flag); \ +} + +FLAG_TEST(ip, IP_FLAG) +FLAG_TEST(block_start, BLOCK_START_FLAG) +FLAG_TEST(allocated, ALLOCATED_FLAG) + +/* We define a standard macro for 'setting' one of the 3 FLAGS into a given +memory address. +*/ +#define FLAG_SETTER(name, flag) \ +void _sal_mem_set_##name(uint32 address) \ +{ \ + assert(g_is_init); \ + assert(sal_mem_is_address_valid(address)); \ +\ + if (!sal_mem_is_##name(address)) { \ + g_memory[address] ^= flag; \ + g_##name##_count++; \ + } \ +} + +FLAG_SETTER(ip, IP_FLAG) +FLAG_SETTER(block_start, BLOCK_START_FLAG) +FLAG_SETTER(allocated, ALLOCATED_FLAG) + +/* We define a standard macro for 'unsetting' one of the 3 FLAGS into a given +memory address. +*/ +#define FLAG_UNSETTER(name, flag) \ +void _sal_mem_unset_##name(uint32 address) \ +{ \ + assert(g_is_init); \ + assert(sal_mem_is_address_valid(address)); \ +\ + if (sal_mem_is_##name(address)) { \ + g_memory[address] ^= flag; \ + g_##name##_count--; \ + } \ +} + +FLAG_UNSETTER(ip, IP_FLAG) +FLAG_UNSETTER(block_start, BLOCK_START_FLAG) +FLAG_UNSETTER(allocated, ALLOCATED_FLAG) + +uint8 sal_mem_get_flags(uint32 address) +{ + /* Get FLAG bits currently set on a specified address (byte). These may be + queried by using a bitwise 'and' operator against the returned byte. + */ + assert(g_is_init); + assert(sal_mem_is_address_valid(address)); + return g_memory[address] & ~INSTRUCTION_MASK; +} + +uint8 sal_mem_get_inst(uint32 address) +{ + /* Get instruction currently set on a specified address (byte), with the + FLAG bits turned off. + */ + assert(g_is_init); + assert(sal_mem_is_address_valid(address)); + return g_memory[address] & INSTRUCTION_MASK; +} + +void sal_mem_set_inst(uint32 address, uint8 inst) +{ + /* Set instruction at given address. This is useful when performing manual + memory manipulations (like compiling organism genomes). + */ + assert(g_is_init); + assert(sal_mem_is_address_valid(address)); + assert(sal_is_inst(inst)); + g_inst_counter[sal_mem_get_inst(address)]--; + g_memory[address] &= ~INSTRUCTION_MASK; + g_memory[address] |= inst; + g_inst_counter[inst]++; +} + +uint8 sal_mem_get_byte(uint32 address) +{ + /* Get unadulterated byte at given address. This could be used, for + example, to render nice images of the memory state. + */ + assert(g_is_init); + assert(sal_mem_is_address_valid(address)); + return g_memory[address]; +} + +void sal_mem_render_image( + uint32 origin, uint32 cell_size, uint32 buff_size, uint8_p buffer +) { + /* Render a 1D image of a given section of memory, at a given resolution + (zoom) and store it in a pre-allocated 'buffer'. + + On the Salis python handler we draw memory as a 1D 'image' on the WORLD + page. If we were to render this image directly on python, it would be + excruciatingly slow, as we have to iterate over large areas of memory! + Therefore, this memory module comes with a built-in, super fast renderer. + */ + uint32 i; + assert(g_is_init); + assert(sal_mem_is_address_valid(origin)); + assert(cell_size); + assert(cell_size <= MAX_ZOOM); + assert(buff_size); + assert(buffer); + + /* We make use of openmp for multi-threaded looping. This allows even + faster render times, wherever openmp is supported. + */ + #pragma omp parallel for + for (i = 0; i < buff_size; i++) { + uint32 j; + uint32 flag_sum = 0; + uint32 inst_sum = 0; + uint32 cell_addr = origin + (i * cell_size); + + for (j = 0; j < cell_size; j++) { + uint32 address = j + cell_addr; + + if (sal_mem_is_address_valid(address)) { + flag_sum |= sal_mem_get_flags(address); + inst_sum += sal_mem_get_inst(address); + } + } + + buffer[i] = (uint8)(inst_sum / cell_size); + buffer[i] |= (uint8)(flag_sum); + } +} + +static boolean inst_count_is_correct(void) +{ + /* Check that the instruction counter is in a valid state + (i.e. SUM inst_counter[0..(INST_COUNT - 1)] == memory_size). + */ + uint32 i; + uint32 sum = 0; + assert(g_is_init); + + for (i = 0; i < INST_COUNT; i++) { + assert(g_inst_counter[i] <= sal_mem_get_size()); + sum += g_inst_counter[i]; + } + + return sum == g_size; +} + +static boolean module_is_valid(void) +{ + /* Check for validity of memory module. This function only gets called when + Salis is running in debug mode. It makes Salis **very** slow in comparison + to when running optimized, but it is also **very** useful for debugging! + */ + uint32 bidx; + uint32 ip_count = 0; + uint32 block_start_count = 0; + uint32 allocated_count = 0; + assert(g_is_init); + assert(g_capacity <= g_size / 2); + assert(inst_count_is_correct()); + + /* Iterate through all memory, counting the flags set on each address. We + then compare the sum to the flag counters to assert module validity. + */ + for (bidx = 0; bidx < g_size; bidx++) { + if (sal_mem_is_ip(bidx)) ip_count++; + if (sal_mem_is_block_start(bidx)) block_start_count++; + if (sal_mem_is_allocated(bidx)) allocated_count++; + } + + assert(ip_count == g_ip_count); + assert(block_start_count == g_block_start_count); + assert(allocated_count == g_allocated_count); + return TRUE; +} + +void _sal_mem_cycle(void) +{ + /* Cycle memory module. Simply assert validity when running in debug mode. + When running optimized, this function does nothing. + */ + assert(g_is_init); + assert(module_is_valid()); +} diff --git a/src/process.c b/src/process.c new file mode 100644 index 0000000..8d500ae --- /dev/null +++ b/src/process.c @@ -0,0 +1,1488 @@ +#include +#include +#include +#include +#include "types.h" +#include "getter.h" +#include "instset.h" +#include "memory.h" +#include "evolver.h" +#include "common.h" +#include "process.h" + +static boolean g_is_init; +static uint32 g_count; +static uint32 g_capacity; +static uint32 g_first; +static uint32 g_last; +static uint32 g_instructions_executed; +static Process *g_procs; + +void _sal_proc_init(void) +{ + /* Initialize process module to its initial state. We initialize the reaper + queue with a capacity of 1. 'First' and 'last' organism pointers are + initialized to (uint32)-1 (to indicate they point to no organism, as no + organism exists yet). + */ + assert(!g_is_init); + g_is_init = TRUE; + g_capacity = 1; + g_first = UINT32_MAX; + g_last = UINT32_MAX; + g_procs = calloc(g_capacity, sizeof(Process)); + assert(g_procs); +} + +void _sal_proc_quit(void) +{ + /* Reset process module back to zero; free up the process queue. + */ + assert(g_is_init); + free(g_procs); + g_is_init = FALSE; + g_count = 0; + g_capacity = 0; + g_first = 0; + g_last = 0; + g_instructions_executed = 0; + g_procs = NULL; +} + +void _sal_proc_load_from(FILE *file) +{ + /* Load process module state from a binary file. + */ + assert(!g_is_init); + assert(file); + fread(&g_is_init, sizeof(boolean), 1, file); + fread(&g_count, sizeof(uint32), 1, file); + fread(&g_capacity, sizeof(uint32), 1, file); + fread(&g_first, sizeof(uint32), 1, file); + fread(&g_last, sizeof(uint32), 1, file); + fread(&g_instructions_executed, sizeof(uint32), 1, file); + g_procs = calloc(g_capacity, sizeof(Process)); + assert(g_procs); + fread(g_procs, sizeof(Process), g_capacity, file); +} + +void _sal_proc_save_into(FILE *file) +{ + /* Save process module state to a binary file. + */ + assert(g_is_init); + assert(file); + fwrite(&g_is_init, sizeof(boolean), 1, file); + fwrite(&g_count, sizeof(uint32), 1, file); + fwrite(&g_capacity, sizeof(uint32), 1, file); + fwrite(&g_first, sizeof(uint32), 1, file); + fwrite(&g_last, sizeof(uint32), 1, file); + fwrite(&g_instructions_executed, sizeof(uint32), 1, file); + fwrite(g_procs, sizeof(Process), g_capacity, file); +} + +/* Getter methods for the process module. +*/ +UINT32_GETTER(proc, count) +UINT32_GETTER(proc, capacity) +UINT32_GETTER(proc, first) +UINT32_GETTER(proc, last) +UINT32_GETTER(proc, instructions_executed) + +boolean sal_proc_is_free(uint32 proc_id) +{ + /* In Salis, the reaper queue is implemented as a circular queue. Thus, at + any given time, a process ID (which actually denotes a process 'address' + or, more correctly, a process 'container address') might contain a living + process or be empty. This function checks for the 'living' state of a given + process ID. + */ + assert(g_is_init); + assert(proc_id < g_capacity); + + if (!g_procs[proc_id].mb1s) { + /* When running in debug mode, we make sure that non-living processes + are completely set to zero, as this is the expected state. + */ + #ifndef NDEBUG + Process dummy_proc; + memset(&dummy_proc, 0, sizeof(Process)); + assert(!memcmp(&dummy_proc, &g_procs[proc_id], sizeof(Process))); + #endif + + return TRUE; + } + + return FALSE; +} + +Process sal_proc_get_proc(uint32 proc_id) +{ + /* Get a **copy** (not a reference) of the process with the given ID. Note, + this might be a non-living process. + */ + assert(g_is_init); + assert(proc_id < g_capacity); + return g_procs[proc_id]; +} + +void sal_proc_get_proc_data(uint32 proc_id, uint32_p buffer) +{ + /* Get a **copy** (not a reference) of the process with the given ID + (represented as a string of 32 bit integers) written into the given buffer. + The buffer must be pre-allocated to a large enough size + (i.e. malloc(sizeof(Process))). Note, copied process might be in a + non-living state. + */ + assert(g_is_init); + assert(proc_id < g_capacity); + assert(buffer); + memcpy(buffer, &g_procs[proc_id], sizeof(Process)); +} + +static boolean block_is_free_and_valid(uint32 address, uint32 size) +{ + /* Iterate all addresses in the given memory block and check that they lie + within memory bounds and have the ALLOCATED flag unset. + */ + uint32 offset; + + for (offset = 0; offset < size; offset++) { + uint32 off_addr = offset + address; + if (!sal_mem_is_address_valid(off_addr)) return FALSE; + if (sal_mem_is_allocated(off_addr)) return FALSE; + + /* Deallocated addresses must have the BLOCK_START flag unset as well. + */ + assert(!sal_mem_is_block_start(off_addr)); + } + + return TRUE; +} + +static void realloc_queue(uint32 queue_lock) +{ + /* Reallocate reaper queue into a new circular queue with double the + capacity. This function gets called whenever the reaper queue fills up + with new organisms. + + A queue_lock parameter may be provided, which 'centers' the reallocation on + a given process ID. This means that, after reallocating the queue, the + process with that ID will keep still have the same ID on the new queue. + */ + uint32 new_capacity; + Process *new_queue; + uint32 fwrd_idx; + uint32 back_idx; + assert(g_is_init); + assert(g_count == g_capacity); + assert(queue_lock < g_capacity); + new_capacity = g_capacity * 2; + new_queue = calloc(new_capacity, sizeof(Process)); + assert(new_queue); + fwrd_idx = queue_lock; + back_idx = (queue_lock - 1) % new_capacity; + + /* Copy all organisms that lie forward from queue lock. + */ + while (TRUE) { + uint32 old_idx = fwrd_idx % g_capacity; + memcpy(&new_queue[fwrd_idx], &g_procs[old_idx], sizeof(Process)); + + if (old_idx == g_last) { + g_last = fwrd_idx; + break; + } else { + fwrd_idx++; + } + } + + /* Copy all organisms that lie backwards from queue lock, making sure to + loop around the queue (with modulo '%') whenever the process index goes + below zero. + */ + if (queue_lock != g_first) { + while (TRUE) { + uint32 old_idx = back_idx % g_capacity; + memcpy(&new_queue[back_idx], &g_procs[old_idx], sizeof(Process)); + + if (old_idx == g_first) { + g_first = back_idx; + break; + } else { + back_idx--; + back_idx %= new_capacity; + } + } + } + + /* Free old reaper queue and re-link global pointer to new queue. + */ + free(g_procs); + g_capacity = new_capacity; + g_procs = new_queue; +} + +static uint32 get_new_proc_from_queue(uint32 queue_lock) +{ + /* Retrieve an unoccupied process ID from the reaper queue. This function + gets called whenever a new organism is generated (born). + */ + assert(g_is_init); + + /* If reaper queue is full, reallocate to double its current size. + */ + if (g_count == g_capacity) { + realloc_queue(queue_lock); + } + + g_count++; + + if (g_count == 1) { + g_first = 0; + g_last = 0; + return 0; + } else { + g_last++; + g_last %= g_capacity; + return g_last; + } +} + +static void proc_create( + uint32 address, uint32 size, uint32 queue_lock, + boolean set_ip, boolean allocate +) { + /* Give birth to a new process! We must specify the address and size of the + new organism. + */ + uint32 pidx; + assert(g_is_init); + assert(sal_mem_is_address_valid(address)); + assert(sal_mem_is_address_valid(address + size - 1)); + + /* When organisms are generated manually (by an user), we must set the IP + flag on the first byte of its owned memory. When organisms replicate by + themselves, we don't set the flag, as it gets set at the end of the module + cycle. Take a look at the '_sal_proc_cycle()' function for more info. + */ + if (set_ip) { + _sal_mem_set_ip(address); + } + + /* When organisms are generated manually (by an user), we must explicitly + allocate its entire memory block. When organisms replicate by themselves, + we assume they have already allocated the child's memory, so we don't need + to do it here. + */ + if (allocate) { + uint32 offset; + assert(block_is_free_and_valid(address, size)); + _sal_mem_set_block_start(address); + + for (offset = 0; offset < size; offset++) { + uint32 off_addr = offset + address; + _sal_mem_set_allocated(off_addr); + } + } + + /* Get a new process ID for the child process. Also, set initial state of + the child process data structure. + */ + pidx = get_new_proc_from_queue(queue_lock); + g_procs[pidx].mb1a = address; + g_procs[pidx].mb1s = size; + g_procs[pidx].ip = address; + g_procs[pidx].sp = address; +} + +void sal_proc_create(uint32 address, uint32 mb1s) +{ + /* API function to create a new process. Memory address and size of new + process must be provided. + */ + assert(g_is_init); + assert(block_is_free_and_valid(address, mb1s)); + proc_create(address, mb1s, 0, TRUE, TRUE); +} + +static void free_memory_block(uint32 address, uint32 size) +{ + /* Deallocate a memory block. This includes unsetting the BLOCK_START flag + on the first byte. + */ + uint32 offset; + assert(sal_mem_is_address_valid(address)); + assert(sal_mem_is_address_valid(address + size - 1)); + assert(sal_mem_is_block_start(address)); + assert(size); + _sal_mem_unset_block_start(address); + + for (offset = 0; offset < size; offset++) { + /* Iterate all addresses in block and unset the ALLOCATED flag in them. + */ + uint32 off_addr = offset + address; + assert(sal_mem_is_allocated(off_addr)); + assert(!sal_mem_is_block_start(off_addr)); + _sal_mem_unset_allocated(off_addr); + } +} + +static void free_memory_owned_by(uint32 pidx) +{ + /* Free memory specifically owned by the process with the given ID. + */ + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + free_memory_block(g_procs[pidx].mb1a, g_procs[pidx].mb1s); + + if (g_procs[pidx].mb2s) { + /* If process owns a child memory block, free it as well. + */ + free_memory_block(g_procs[pidx].mb2a, g_procs[pidx].mb2s); + } +} + +static void proc_kill(boolean reset_ips) +{ + /* Kill process on bottom of reaper queue (the oldest process). + */ + assert(g_is_init); + assert(g_count); + assert(g_first != UINT32_MAX); + assert(g_last != UINT32_MAX); + assert(!sal_proc_is_free(g_first)); + + /* When called manually by an user, we must clear and reset the IP flags of + all processes in order to preserve module validity. + */ + if (reset_ips) { + _sal_mem_unset_ip(g_procs[g_first].ip); + } + + /* Free up owned memory and reset process data structure back to zero. + */ + free_memory_owned_by(g_first); + memset(&g_procs[g_first], 0, sizeof(Process)); + g_count--; + + if (g_first == g_last) { + g_first = UINT32_MAX; + g_last = UINT32_MAX; + } else { + g_first++; + g_first %= g_capacity; + } + + /* Reset IP flags of all living processes. We use openmp to do this faster. + */ + if (reset_ips) { + uint32 pidx; + + #pragma omp parallel for + for (pidx = 0; pidx < g_capacity; pidx++) { + if (!sal_proc_is_free(pidx)) { + _sal_mem_set_ip(g_procs[pidx].ip); + } + } + } +} + +void sal_proc_kill(void) +{ + /* API function to kill a process. Make sure that at least one process is + alive, or 'assert()' will fail. + */ + assert(g_is_init); + assert(g_count); + assert(g_first != UINT32_MAX); + assert(g_last != UINT32_MAX); + assert(!sal_proc_is_free(g_first)); + proc_kill(TRUE); +} + +static boolean block_is_allocated(uint32 address, uint32 size) +{ + /* Assert that a given memory block is fully allocated. + */ + uint32 offset; + assert(g_is_init); + + for (offset = 0; offset < size; offset++) { + uint32 off_addr = offset + address; + assert(sal_mem_is_address_valid(off_addr)); + assert(sal_mem_is_allocated(off_addr)); + } + + return TRUE; +} + +static boolean proc_is_valid(uint32 pidx) +{ + /* Assert that the process with the given ID is in a valid state. This + means that all of its owned memory must be allocated and that the proper + flags are set in place. IP and SP must be located in valid addresses. + */ + assert(g_is_init); + assert(pidx < g_capacity); + + if (!sal_proc_is_free(pidx)) { + assert(sal_mem_is_address_valid(g_procs[pidx].ip)); + assert(sal_mem_is_address_valid(g_procs[pidx].sp)); + assert(sal_mem_is_block_start(g_procs[pidx].mb1a)); + assert(sal_mem_is_ip(g_procs[pidx].ip)); + assert(block_is_allocated(g_procs[pidx].mb1a, g_procs[pidx].mb1s)); + + if (g_procs[pidx].mb2s) { + assert(sal_mem_is_block_start(g_procs[pidx].mb2a)); + assert(block_is_allocated(g_procs[pidx].mb2a, g_procs[pidx].mb2s)); + } + } + + return TRUE; +} + +static boolean module_is_valid(void) +{ + /* Check for validity of process module. This function only gets called + when Salis is running in debug mode. It makes Salis **very** slow in + comparison to when running optimized, but it is also **very** useful for + debugging! + */ + uint32 pidx; + uint32 alloc_count = 0; + uint32 block_count = 0; + assert(g_is_init); + assert(g_count >= sal_mem_get_ip_count()); + + /* Check that each individual process is in a valid state. We can do this + in a multi-threaded way. + */ + #pragma omp parallel for + for (pidx = 0; pidx < g_capacity; pidx++) { + assert(proc_is_valid(pidx)); + } + + /* Iterate all processes, counting their memory blocks and adding up their + memory block sizes. At the end, we compare the sums to the flag counters of + the memory module. + */ + for (pidx = 0; pidx < g_capacity; pidx++) { + if (!sal_proc_is_free(pidx)) { + alloc_count += g_procs[pidx].mb1s; + block_count++; + + if (g_procs[pidx].mb2s) { + assert(g_procs[pidx].mb1a != g_procs[pidx].mb2a); + alloc_count += g_procs[pidx].mb2s; + block_count++; + } + } + } + + assert(block_count == sal_mem_get_block_start_count()); + assert(alloc_count == sal_mem_get_allocated_count()); + return TRUE; +} + +static void toggle_ip_flag(void (*toggler)(uint32 address)) +{ + /* At the start of each process module cycle, all memory addresses with the + IP flag set get their IP flag turned off. Once all processes finish + executing, the IP flags are turned on again on all addresses pointed by + 'g_procs[pidx].ip'. I've found this is the easiest way to preserve + correctness, given that more than one process can have their IPs pointed to + the same address. + + This function simply iterates through all processes, setting the IP flag on + or off on the address pointed to by their IP. + */ + uint32 pidx; + assert(g_is_init); + + for (pidx = 0; pidx < g_capacity; pidx++) { + if (!sal_proc_is_free(pidx)) { + toggler(g_procs[pidx].ip); + } + } +} + +static void on_fault(uint32 pidx) +{ + /* Organisms get punished whenever they execute an invalid instruction + (commit a 'fault') by having the halt one simulation cycle. + */ + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + g_procs[pidx].punish = 1; +} + +static void increment_ip(uint32 pidx) +{ + /* After executing each instruction, increment the given organism's IP to + the next valid address. + */ + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if (sal_mem_is_address_valid(g_procs[pidx].ip + 1)) { + g_procs[pidx].ip++; + } + + /* Wherever IP goes, SP follows. :P + */ + g_procs[pidx].sp = g_procs[pidx].ip; +} + +static boolean are_templates_complements(uint32 source, uint32 complement) +{ + /* Check whether 2 templates are complements. Templates are introduced in + Salis-2.0 and they function in the same way as templates in the original + Tierra. They consist of string of NOP0 and NOP1 instructions. + + We say that templates are complements whenever one is a 'negation' of + another (i.e. they are reverse copies of each other). So, on the following + example, the top template would be the complement of the bottom template. + + >>> NOP0 - NOP1 - NOP1 + >>> NOP1 - NOP0 - NOP0 + + This function looks into 2 given addresses in memory and checks whether + there are complementing templates on those addresses. + */ + assert(g_is_init); + assert(sal_mem_is_address_valid(source)); + assert(sal_mem_is_address_valid(complement)); + assert(sal_is_template(sal_mem_get_inst(source))); + + while ( + sal_mem_is_address_valid(source) && + sal_is_template(sal_mem_get_inst(source)) + ) { + /* Iterate address by address, checking complementarity on each + consecutive byte pair. + */ + uint8 inst_src; + uint8 inst_comp; + + /* If complement head moves to an invalid address, complementarity + fails. + */ + if (!sal_mem_is_address_valid(complement)) { + return FALSE; + } + + inst_src = sal_mem_get_inst(source); + inst_comp = sal_mem_get_inst(complement); + assert(inst_src == NOP0 || inst_src == NOP1); + + if (inst_src == NOP0 && inst_comp != NOP1) { + return FALSE; + } + + if (inst_src == NOP1 && inst_comp != NOP0) { + return FALSE; + } + + source++; + complement++; + } + + /* If we get to the end of a template in the source head, and target has + been complementary all the way through, we consider these blocks of memory + 'complements'. + */ + return TRUE; +} + +static void increment_sp(uint32 pidx, boolean forward) +{ + /* Increment or decrement SP to the next valid address. This function gets + called by organisms during jumps, searches, etc. (i.e. whenever the seeker + pointer gets sent on a 'mission'). + */ + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if (forward && sal_mem_is_address_valid(g_procs[pidx].sp + 1)) { + g_procs[pidx].sp++; + } + + if (!forward && sal_mem_is_address_valid(g_procs[pidx].sp - 1)) { + g_procs[pidx].sp--; + } +} + +static boolean jump_seek(uint32 pidx, boolean forward) +{ + /* Search (via the seeker pointer) for template to jump into. This gets + called by organisms each cycle during a JMP instruction. Only when a valid + template is found, will this function return TRUE. Otherwise it will return + FALSE, signaling the calling process that a template has not yet been + found. + */ + uint32 next_addr; + uint8 next_inst; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + next_addr = g_procs[pidx].ip + 1; + + /* This function causes a 'fault' when there is no template right in front + of the caller organism's instruction pointer. + */ + if (!sal_mem_is_address_valid(next_addr)) { + on_fault(pidx); + increment_ip(pidx); + return FALSE; + } + + next_inst = sal_mem_get_inst(next_addr); + + if (!sal_is_template(next_inst)) { + on_fault(pidx); + increment_ip(pidx); + return FALSE; + } + + /* Check for complementarity. Increment seeker pointer if template has not + been found yet. + */ + if (are_templates_complements(next_addr, g_procs[pidx].sp)) { + return TRUE; + } + + increment_sp(pidx, forward); + return FALSE; +} + +static void jump(uint32 pidx) +{ + /* This gets called when an organism has finally found a template to jump + into (see function above). Only when in debug mode, we make sure that the + entire jump operation has been performed in a valid way. + */ + #ifndef NDEBUG + uint32 next_addr; + uint8 next_inst; + uint8 sp_inst; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + next_addr = g_procs[pidx].ip + 1; + assert(sal_mem_is_address_valid(next_addr)); + next_inst = sal_mem_get_inst(next_addr); + sp_inst = sal_mem_get_inst(g_procs[pidx].sp); + assert(sal_is_template(next_inst)); + assert(sal_is_template(sp_inst)); + assert(are_templates_complements(next_addr, g_procs[pidx].sp)); + #endif + + g_procs[pidx].ip = g_procs[pidx].sp; +} + +static boolean addr_seek(uint32 pidx, boolean forward) +{ + /* Search (via the seeker pointer) for template address in memory. This + gets called by organisms each cycle during a ADR instruction. Only when a + valid template is found, will this function return TRUE. Otherwise it will + return FALSE, signaling the calling process that a template has not yet + been found. */ + uint32 next1_addr; + uint32 next2_addr; + uint8 next1_inst; + uint8 next2_inst; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + next1_addr = g_procs[pidx].ip + 1; + next2_addr = g_procs[pidx].ip + 2; + + /* This function causes a 'fault' when there is no register modifier right + in front of the caller organism's instruction pointer, and a template just + after that. + */ + if ( + !sal_mem_is_address_valid(next1_addr) || + !sal_mem_is_address_valid(next2_addr) + ) { + on_fault(pidx); + increment_ip(pidx); + return FALSE; + } + + next1_inst = sal_mem_get_inst(next1_addr); + next2_inst = sal_mem_get_inst(next2_addr); + + if ( + !sal_is_mod(next1_inst) || + !sal_is_template(next2_inst) + ) { + on_fault(pidx); + increment_ip(pidx); + return FALSE; + } + + /* Check for complementarity. Increment seeker pointer if template has not + been found yet. + */ + if (are_templates_complements(next2_addr, g_procs[pidx].sp)) { + return TRUE; + } + + increment_sp(pidx, forward); + return FALSE; +} + +static boolean get_register_pointers( + uint32 pidx, uint32_p *regs, uint32 reg_count +) { + /* This function is used to get pointers to a calling organism registers. + Specifically, registers returned are those that will be used when executing + the caller organism's current instruction. + */ + uint32 ridx; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + assert(regs); + assert(reg_count); + assert(reg_count < 4); + + /* Iterate 'reg_count' number of instructions forward from the IP, noting + down all found register modifiers. If less than 'reg_count' modifiers are + found, this function returns FALSE (triggering a 'fault'). + */ + for (ridx = 0; ridx < reg_count; ridx++) { + uint32 mod_addr = g_procs[pidx].ip + 1 + ridx; + + if ( + !sal_mem_is_address_valid(mod_addr) || + !sal_is_mod(sal_mem_get_inst(mod_addr)) + ) { + return FALSE; + } + + switch (sal_mem_get_inst(mod_addr)) { + case MODA: + regs[ridx] = &g_procs[pidx].rax; + break; + case MODB: + regs[ridx] = &g_procs[pidx].rbx; + break; + case MODC: + regs[ridx] = &g_procs[pidx].rcx; + break; + case MODD: + regs[ridx] = &g_procs[pidx].rdx; + break; + } + } + + return TRUE; +} + +static void addr(uint32 pidx) +{ + /* This gets called when an organism has finally found a template and is + ready to store its address. Only when in debug mode, we make sure that the + entire search operation has been performed in a valid way. + */ + uint32_p reg; + + #ifndef NDEBUG + uint32 next2_addr; + uint8 next2_inst; + uint8 sp_inst; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + next2_addr = g_procs[pidx].ip + 2; + assert(sal_mem_is_address_valid(next2_addr)); + next2_inst = sal_mem_get_inst(next2_addr); + sp_inst = sal_mem_get_inst(g_procs[pidx].sp); + assert(sal_is_template(next2_inst)); + assert(sal_is_template(sp_inst)); + assert(are_templates_complements(next2_addr, g_procs[pidx].sp)); + #endif + + /* Store address of complement into the given register. + */ + if (!get_register_pointers(pidx, ®, 1)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + *reg = g_procs[pidx].sp; + increment_ip(pidx); +} + +static void free_child_block_of(uint32 pidx) +{ + /* Free only the 'child' memory block (mb2) of the caller organism. + */ + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + assert(g_procs[pidx].mb2s); + free_memory_block(g_procs[pidx].mb2a, g_procs[pidx].mb2s); + g_procs[pidx].mb2a = 0; + g_procs[pidx].mb2s = 0; +} + +static void alloc(uint32 pidx, boolean forward) +{ + /* Allocate a 'child' memory block of size stored in the first given + register, and save its address into the second given register. This + function is the basis of Salisian reproduction. It's a fairly complicated + function (as the seeker pointer must function in a procedural way), so it's + divided into a series of steps, documented below. + */ + uint32_p regs[2]; + uint32 block_size; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + /* For this function to work, we need at least two register modifiers. + Then, we check for all possible error conditions. If any error conditions + are found, the instruction faults and returns. + */ + if (!get_register_pointers(pidx, regs, 2)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + block_size = *regs[0]; + + /* ERROR 1: requested child block is of size zero. + */ + if (!block_size) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + /* ERROR 2: seeker pointer not adjacent to existing child block. + */ + if (g_procs[pidx].mb2s) { + uint32 exp_addr; + + if (forward) { + exp_addr = g_procs[pidx].mb2a + g_procs[pidx].mb2s; + } else { + exp_addr = g_procs[pidx].mb2a - 1; + } + + if (g_procs[pidx].sp != exp_addr) { + on_fault(pidx); + increment_ip(pidx); + return; + } + } + + /* No errors were detected. We thus handle all correct conditions. + * CONDITION 1: allocation was successful. + */ + if (g_procs[pidx].mb2s == block_size) { + increment_ip(pidx); + *regs[1] = g_procs[pidx].mb2a; + return; + } + + /* CONDITION 2: seeker pointer has collided with allocated space. We free + child memory block and just continue searching. + */ + if (sal_mem_is_allocated(g_procs[pidx].sp)) { + if (g_procs[pidx].mb2s) { + free_child_block_of(pidx); + } + + increment_sp(pidx, forward); + return; + } + + /* CONDITION 3: no collision detected; enlarge child memory block and + increment seeker pointer. Also, correct position of BLOCK_START bit flag. + */ + _sal_mem_set_allocated(g_procs[pidx].sp); + + if (!g_procs[pidx].mb2s) { + g_procs[pidx].mb2a = g_procs[pidx].sp; + _sal_mem_set_block_start(g_procs[pidx].sp); + } else if (!forward) { + _sal_mem_unset_block_start(g_procs[pidx].mb2a); + g_procs[pidx].mb2a = g_procs[pidx].sp; + _sal_mem_set_block_start(g_procs[pidx].mb2a); + } + + g_procs[pidx].mb2s++; + increment_sp(pidx, forward); +} + +static void swap(uint32 pidx) +{ + /* Swap parent and child memory blocks. This function is the basis of + Salisian metabolism. + */ + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if (g_procs[pidx].mb2s) { + uint32 addr_temp = g_procs[pidx].mb1a; + uint32 size_temp = g_procs[pidx].mb1s; + g_procs[pidx].mb1a = g_procs[pidx].mb2a; + g_procs[pidx].mb1s = g_procs[pidx].mb2s; + g_procs[pidx].mb2a = addr_temp; + g_procs[pidx].mb2s = size_temp; + } else { + on_fault(pidx); + } + + increment_ip(pidx); +} + +static void split(uint32 pidx) +{ + /* Split child memory block into a new organism. A new baby is born. :-) + */ + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if (g_procs[pidx].mb2s) { + proc_create( + g_procs[pidx].mb2a, g_procs[pidx].mb2s, pidx, FALSE, FALSE + ); + g_procs[pidx].mb2a = 0; + g_procs[pidx].mb2s = 0; + } else { + on_fault(pidx); + } + + increment_ip(pidx); +} + +static void one_reg_op(uint32 pidx, uint8 inst) +{ + /* Here we group all 1-register operations. These include incrementing, + decrementing, placing zero or one on a register, and the negation + operation. + */ + uint32_p reg; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + assert(sal_is_inst(inst)); + + if (!get_register_pointers(pidx, ®, 1)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + switch (inst) { + case INCN: + (*reg)++; + break; + case DECN: + (*reg)--; + break; + case ZERO: + (*reg) = 0; + break; + case UNIT: + (*reg) = 1; + break; + case NOTN: + (*reg) = !(*reg); + break; + default: + assert(FALSE); + } + + increment_ip(pidx); +} + +static void if_not_zero(uint32 pidx) +{ + /* Conditional operator. Like in most programming languages, this + instruction is needed to allow organism execution to branch into different + execution streams. + */ + uint32_p reg; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if (!get_register_pointers(pidx, ®, 1)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + if (!(*reg)) { + increment_ip(pidx); + } + + increment_ip(pidx); + increment_ip(pidx); +} + +static void three_reg_op(uint32 pidx, uint8 inst) +{ + /* Here we group all 3-register arithmetic operations. These include + addition, subtraction, multiplication and division. + */ + uint32_p regs[3]; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + assert(sal_is_inst(inst)); + + if (!get_register_pointers(pidx, regs, 3)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + switch (inst) { + case SUMN: + *regs[0] = *regs[1] + *regs[2]; + break; + case SUBN: + *regs[0] = *regs[1] - *regs[2]; + break; + case MULN: + *regs[0] = *regs[1] * *regs[2]; + break; + case DIVN: + /* Division by 0 is not allowed and causes a fault. */ + if (!(*regs[2])) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + *regs[0] = *regs[1] / *regs[2]; + break; + default: + assert(FALSE); + } + + increment_ip(pidx); +} + +static void load(uint32 pidx) +{ + /* Load an instruction from a given address into a specified register. This + is used by organisms during their reproduction cycle. + */ + uint32_p regs[2]; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if ( + !get_register_pointers(pidx, regs, 2) || + !sal_mem_is_address_valid(*regs[0]) + ) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + if (g_procs[pidx].sp < *regs[0]) { + increment_sp(pidx, TRUE); + } else if (g_procs[pidx].sp > *regs[0]) { + increment_sp(pidx, FALSE); + } else { + *regs[1] = sal_mem_get_inst(*regs[0]); + increment_ip(pidx); + } +} + +static boolean is_writeable_by(uint32 pidx, uint32 address) +{ + /* Check whether an organisms has writing rights on a specified address. + Any organism may write to any valid address that is either self owned or + not allocated. + */ + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + assert(sal_mem_is_address_valid(address)); + + if (!sal_mem_is_allocated(address)) { + return TRUE; + } else { + uint32 lo1 = g_procs[pidx].mb1a; + uint32 lo2 = g_procs[pidx].mb2a; + uint32 hi1 = lo1 + g_procs[pidx].mb1s; + uint32 hi2 = lo2 + g_procs[pidx].mb2s; + return ( + (address >= lo1 && address < hi1) || + (address >= lo2 && address < hi2) + ); + } +} + +static void write(uint32 pidx) +{ + /* Write instruction on a given register into a specified address. This is + used by organisms during their reproduction cycle. + */ + uint32_p regs[2]; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if ( + !get_register_pointers(pidx, regs, 2) || + !sal_mem_is_address_valid(*regs[0]) || + !sal_is_inst(*regs[1]) + ) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + if (g_procs[pidx].sp < *regs[0]) { + increment_sp(pidx, TRUE); + } else if (g_procs[pidx].sp > *regs[0]) { + increment_sp(pidx, FALSE); + } else if (is_writeable_by(pidx, *regs[0])) { + sal_mem_set_inst(*regs[0], *regs[1]); + increment_ip(pidx); + } else { + on_fault(pidx); + increment_ip(pidx); + } +} + +static void send(uint32 pidx) +{ + /* Send instruction on given register into the common pipe. + */ + uint32_p reg; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if (!get_register_pointers(pidx, ®, 1)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + if (!sal_is_inst(*reg)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + _sal_comm_send((uint8)(*reg)); + increment_ip(pidx); +} + +static void receive(uint32 pidx) +{ + /* Receive a single instruction from the common pipe and store it into a + specified register. In case the common pipe is empty, it will return the + NOP0 instruction. + */ + uint32_p reg; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if (!get_register_pointers(pidx, ®, 1)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + *reg = _sal_comm_receive(); + assert(sal_is_inst(*reg)); + increment_ip(pidx); +} + +static void push(uint32 pidx) +{ + /* Push value on register into the stack. This is useful as a secondary + memory resource. + */ + uint32_p reg; + uint32 sidx; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if (!get_register_pointers(pidx, ®, 1)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + for (sidx = 7; sidx; sidx--) { + g_procs[pidx].stack[sidx] = g_procs[pidx].stack[sidx - 1]; + } + + g_procs[pidx].stack[0] = *reg; + increment_ip(pidx); +} + +static void +pop(uint32 pidx) +{ + /* Pop value from the stack into a given register. + */ + uint32_p reg; + uint32 sidx; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + if (!get_register_pointers(pidx, ®, 1)) { + on_fault(pidx); + increment_ip(pidx); + return; + } + + *reg = g_procs[pidx].stack[0]; + + for (sidx = 1; sidx < 8; sidx++) { + g_procs[pidx].stack[sidx - 1] = g_procs[pidx].stack[sidx]; + } + + g_procs[pidx].stack[7] = 0; + increment_ip(pidx); +} + +static boolean eat_seek(uint32 pidx, boolean forward) +{ + /* Search (via the seeker pointer) for an identical copy of the memory + stream right in front of the calling organism's IP. This function gets + called by organisms each cycle during an EAT instruction. Only when a valid + copy is found, this function will return TRUE. */ + uint32 next_addr; + uint8 next_inst; + uint8 sp_inst; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + next_addr = g_procs[pidx].ip + 1; + + if (!sal_mem_is_address_valid(next_addr)) { + on_fault(pidx); + increment_ip(pidx); + return FALSE; + } + + if (g_procs[pidx].sp == next_addr) { + increment_sp(pidx, forward); + return FALSE; + } + + next_inst = sal_mem_get_inst(next_addr); + sp_inst = sal_mem_get_inst(g_procs[pidx].sp); + + if (next_inst == sp_inst) { + return TRUE; + } + + increment_sp(pidx, forward); + return FALSE; +} + +static void eat(uint32 pidx) +{ + /* Salisian organisms may 'eat' information. They eat by searching for + 'copies' of the code in front of their IPs during the EAT instruction. When + a valid copy is found, an organism gets rewarded by setting their 'reward' + field to the length of the measured copy. Each cycle, organisms execute + 'reward' number of instructions plus one, thus, eating a larger stream + produces a larger advantage for an organism. + + However, whenever an organism eats, the detected copy of the source code + gets destroyed (randomized). The main idea of the EAT instruction is to + turn 'information' into a valuable resource in Salis. + */ + uint32 source; + uint32 target; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + source = g_procs[pidx].ip + 1; + target = g_procs[pidx].sp; + assert(sal_mem_is_address_valid(source)); + assert(sal_mem_get_inst(source) == sal_mem_get_inst(target)); + g_procs[pidx].reward = 0; + + while ( + sal_mem_is_address_valid(source) && + sal_mem_is_address_valid(target) && + sal_mem_get_inst(source) == sal_mem_get_inst(target) + ) { + g_procs[pidx].reward++; + _sal_evo_randomize_at(target); + source++; + target++; + } + + increment_ip(pidx); +} + +static void proc_cycle(uint32 pidx) +{ + /* Cycle a process once. During each process cycle, several things may + happen. For example, if a process is being punished (for committing a + fault), it will have to wait until the next simulation cycle to be able to + execute. + + Non-punished organisms execute at least one instruction per simulation + cycle. If they are being rewarded, they execute one, plus the number on + their 'reward' field, number of instructions each cycle. + */ + uint32 cycles; + assert(g_is_init); + assert(pidx < g_capacity); + assert(!sal_proc_is_free(pidx)); + + /* Organism is being punished. Clear its 'punish' field and return without + executing. + */ + if (g_procs[pidx].punish) { + g_procs[pidx].punish = 0; + return; + } + + /* Execute one instruction per number of 'reward' points awarded to this + organism. Switch case associates each instruction to its corresponding + instruction handler. Process module keeps track of the total number of + instructions executed (by all organisms) per simulation cycle. + */ + for (cycles = 0; cycles < g_procs[pidx].reward + 1; cycles++) { + uint8 inst = sal_mem_get_inst(g_procs[pidx].ip); + g_instructions_executed++; + + switch (inst) { + case JMPB: + if (jump_seek(pidx, FALSE)) jump(pidx); + break; + case JMPF: + if (jump_seek(pidx, TRUE)) jump(pidx); + break; + case ADRB: + if (addr_seek(pidx, FALSE)) addr(pidx); + break; + case ADRF: + if (addr_seek(pidx, TRUE)) addr(pidx); + break; + case MALB: + alloc(pidx, FALSE); + break; + case MALF: + alloc(pidx, TRUE); + break; + case SWAP: + swap(pidx); + break; + case SPLT: + split(pidx); + break; + case INCN: + case DECN: + case ZERO: + case UNIT: + case NOTN: + one_reg_op(pidx, inst); + break; + case IFNZ: + if_not_zero(pidx); + break; + case SUMN: + case SUBN: + case MULN: + case DIVN: + three_reg_op(pidx, inst); + break; + case LOAD: + load(pidx); + break; + case WRTE: + write(pidx); + break; + case SEND: + send(pidx); + break; + case RECV: + receive(pidx); + break; + case PSHN: + push(pidx); + break; + case POPN: + pop(pidx); + break; + case EATB: + if (eat_seek(pidx, FALSE)) eat(pidx); + break; + case EATF: + if (eat_seek(pidx, TRUE)) eat(pidx); + break; + default: + increment_ip(pidx); + } + } +} + +void _sal_proc_cycle(void) +{ + /* The process module cycle consists of a series of steps, which are needed + to preserve overall correctness. + */ + assert(g_is_init); + assert(module_is_valid()); + g_instructions_executed = 0; + + /* Iterate through all organisms in the reaper queue. First organism to + execute is the one pointed to by 'g_last' (the one on top of the queue). + Last one to execute is 'g_first'. We go around the circular queue, making + sure to modulo (%) around when iterator goes below zero. + */ + if (g_count) { + uint32 pidx = g_last; + + /* Turn off all IP flags in memory and cycle 'g_last'. Then, continue + with all other organisms until we reach 'g_first'. + */ + toggle_ip_flag(_sal_mem_unset_ip); + assert(!sal_mem_get_ip_count()); + proc_cycle(pidx); + + while (pidx != g_first) { + pidx--; + pidx %= g_capacity; + proc_cycle(pidx); + } + + /* Kill oldest processes whenever memory gets filled over capacity. + */ + while (sal_mem_get_allocated_count() > sal_mem_get_capacity()) { + proc_kill(FALSE); + } + + /* Finally, turn IP flags back on. Keep in mind that IP flags exist + for visualization purposes only. They are actually not really needed. + */ + toggle_ip_flag(_sal_mem_set_ip); + } +} diff --git a/src/salis.c b/src/salis.c new file mode 100644 index 0000000..1aae1fa --- /dev/null +++ b/src/salis.c @@ -0,0 +1,109 @@ +#include +#include +#include "getter.h" +#include "salis.h" + +static boolean g_is_init; +static uint32 g_cycle; +static uint32 g_epoch; + +void sal_main_init(uint32 order, string pipe) +{ + /* Initialize all Salis modules to their initial states. We pass along any + arguments to their respective modules. + */ + assert(!g_is_init); + _sal_mem_init(order); + _sal_comm_init(pipe); + _sal_evo_init(); + _sal_proc_init(); + g_is_init = TRUE; +} + +void sal_main_quit(void) +{ + /* Reset Salis and all of its modules back to zero. We may, thus, shutdown + Salis and re-initialize it with different parameters without having to + reload the library (useful, for example, when running data gathering + scripts that must iterate through many save files). + */ + assert(g_is_init); + _sal_proc_quit(); + _sal_evo_quit(); + _sal_comm_quit(); + _sal_mem_quit(); + g_is_init = FALSE; + g_cycle = 0; + g_epoch = 0; +} + +void sal_main_load(string file_name, string pipe) +{ + /* Load simulation state from file. This file must have been created by + 'sal_main_save()'. File name of common pipe must also be provided. + */ + FILE *file; + assert(!g_is_init); + assert(file_name); + file = fopen(file_name, "rb"); + assert(file); + fread(&g_is_init, sizeof(boolean), 1, file); + fread(&g_cycle, sizeof(uint32), 1, file); + fread(&g_epoch, sizeof(uint32), 1, file); + _sal_mem_load_from(file); + _sal_evo_load_from(file); + _sal_proc_load_from(file); + fclose(file); + _sal_comm_init(pipe); +} + +void sal_main_save(string file_name) +{ + /* Save simulation state to a file. This file may later be re-loaded with + 'sal_main_load()'. We save in binary format (to save space), which means + save files might not be entirely portable. + */ + FILE *file; + assert(g_is_init); + assert(file_name); + file = fopen(file_name, "wb"); + assert(file); + fwrite(&g_is_init, sizeof(boolean), 1, file); + fwrite(&g_cycle, sizeof(uint32), 1, file); + fwrite(&g_epoch, sizeof(uint32), 1, file); + _sal_mem_save_into(file); + _sal_evo_save_into(file); + _sal_proc_save_into(file); + fclose(file); +} + +boolean sal_main_is_init(void) +{ + /* Check if Salis is currently initialized/running. + */ + return g_is_init; +} + +/* Getter methods for the Salis main module. +*/ +UINT32_GETTER(main, cycle) +UINT32_GETTER(main, epoch) + +void sal_main_cycle(void) +{ + /* Cycle the Salis simulator once. The combination of a cycle * epoch + counter allows us to track simulations for an insane period of time + (2^64 cycles). + */ + g_cycle++; + + if (!g_cycle) { + g_epoch++; + } + + /* Cycle all of the Salis modules. + */ + _sal_mem_cycle(); + _sal_evo_cycle(); + _sal_proc_cycle(); +} -- cgit v1.2.1