aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Oliver <contact@pauloliver.dev>2024-02-29 02:29:13 +0100
committerPaul Oliver <contact@pauloliver.dev>2024-02-29 02:29:13 +0100
commitca118555214a176728b9aab87849391344306d6d (patch)
tree833cffdd4066a7114b1d79d6eeaa2e0152408fc8
Initial commit.
-rw-r--r--.gitignore8
-rw-r--r--Makefile29
-rw-r--r--README.md52
-rw-r--r--bin/common/.keep0
-rw-r--r--bin/genomes/.keep0
-rw-r--r--bin/genomes/86.anc1
-rw-r--r--bin/handler.py403
-rw-r--r--bin/lib/.keep0
-rw-r--r--bin/printer.py833
-rwxr-xr-xbin/salis.py346
-rw-r--r--bin/sims/.keep0
-rw-r--r--bin/sims/auto/.keep0
-rw-r--r--bin/world.py277
-rw-r--r--build/.keep0
-rw-r--r--include/common.h19
-rw-r--r--include/evolver.h38
-rw-r--r--include/getter.h20
-rw-r--r--include/instset.h71
-rw-r--r--include/memory.h134
-rw-r--r--include/process.h97
-rw-r--r--include/salis.h67
-rw-r--r--include/types.h45
-rw-r--r--src/common.c72
-rw-r--r--src/evolver.c132
-rw-r--r--src/instset.c39
-rw-r--r--src/memory.c325
-rw-r--r--src/process.c1488
-rw-r--r--src/salis.c109
28 files changed, 4605 insertions, 0 deletions
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
--- /dev/null
+++ b/bin/common/.keep
diff --git a/bin/genomes/.keep b/bin/genomes/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/bin/genomes/.keep
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
--- /dev/null
+++ b/bin/lib/.keep
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/<file-name>.<sim-epoch>.<sim-cycle>.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
--- /dev/null
+++ b/bin/sims/.keep
diff --git a/bin/sims/auto/.keep b/bin/sims/auto/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/bin/sims/auto/.keep
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
--- /dev/null
+++ b/build/.keep
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 <types.h>
+#include <instset.h>
+#include <memory.h>
+#include <evolver.h>
+#include <common.h>
+#include <process.h>
+
+/** 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 <limits.h>
+
+#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 <assert.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#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 <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#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 <assert.h>
+#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 <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#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 <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#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, &reg, 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, &reg, 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, &reg, 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, &reg, 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, &reg, 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, &reg, 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, &reg, 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 <assert.h>
+#include <stdio.h>
+#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();
+}