diff options
Diffstat (limited to 'salis.py')
| -rwxr-xr-x | salis.py | 371 |
1 files changed, 168 insertions, 203 deletions
@@ -1,11 +1,4 @@ -#!/usr/bin/env -S PYTHONDONTWRITEBYTECODE=1 python3 - -# Author: Paul Oliver <contact@pauloliver.dev> -# Project: salis-v3 - -# Salis simulator launcher script -# Emits a single C source file, builds it into a binary and launches it. -# JIT compilation allows quick switching between all available executable configurations. +#!/usr/bin/env -S PYTHONDONTWRITEBYTECODE=1 python import os import random @@ -14,34 +7,33 @@ import subprocess import sys from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, ArgumentTypeError, RawTextHelpFormatter -from jinja2 import Environment, FileSystemLoader, StrictUndefined from tempfile import TemporaryDirectory # ------------------------------------------------------------------------------ # Parse CLI arguments # ------------------------------------------------------------------------------ -headline = "Salis: Simple A-Life Simulator" -script = sys.argv[0] -epilog = f"Use '-h' to list arguments for each command.\nExample: '{script} bench -h'" +description = "Salis: Simple A-Life Simulator" +prog = sys.argv[0] +epilog = f"Use '-h' to list arguments for each command.\nExample: '{prog} bench -h'" main_parser = ArgumentParser( - description = headline, - epilog = epilog, - formatter_class = RawTextHelpFormatter, - prog = script, + description=description, + epilog=epilog, + formatter_class=RawTextHelpFormatter, + prog=prog, ) parsers = main_parser.add_subparsers(dest="command", required=True) -fclass = ArgumentDefaultsHelpFormatter +formatter_class = lambda prog: ArgumentDefaultsHelpFormatter(prog, max_help_position=32) -bench = parsers.add_parser("bench", formatter_class=fclass, help="run benchmark") -load = parsers.add_parser("load", formatter_class=fclass, help="load saved simulation") -new = parsers.add_parser("new", formatter_class=fclass, help="create new simulation") +bench = parsers.add_parser("bench", formatter_class=formatter_class, help="run benchmark") +load = parsers.add_parser("load", formatter_class=formatter_class, help="load saved simulation") +new = parsers.add_parser("new", formatter_class=formatter_class, help="create new simulation") -archs = os.listdir("./arch") -uis = os.listdir("./ui") +architectures = os.listdir("./arch") +uis = os.listdir("./ui") -def iseed(i): +def seed(i): ival = int(i, 0) if ival < -1: raise ArgumentTypeError("invalid seed value") return ival @@ -57,135 +49,118 @@ def inat(i): return ival option_keys = ["short", "long", "metavar", "description", "default", "required", "type", "parsers"] - -# fmt: off -option_conf = [ - ["A", "anc", "ANC", "ancestor file name without extension, to be compiled on " - "all cores (ANC points to 'ancs/<ARCH>/<ANC>.asm')", None, True, str, [bench, new]], - ["a", "arch", archs, "VM architecture", "dummy", False, str, [bench, new]], - ["b", "steps", "N", "number of steps to run in benchmark", 0x1000000, False, ipos, [bench]], - ["C", "clones", "N", "number of ancestor clones on each core", 1, False, inat, [bench, new]], - ["c", "cores", "N", "number of simulator cores", 2, False, inat, [bench, new]], - ["d", "data-push-pow", "POW", "data aggregation interval exponent (interval == 2^POW >= " - "thread sync interval); a value of 0 disables data " - "aggregation (requires 'sqlite')", 28, False, ipos, [new]], - ["f", "force", None, "overwrite existing simulation of given name", False, False, bool, [new]], - ["F", "muta-flip", None, "cosmic rays flip bits instead of randomizing whole bytes", False, False, bool, [bench, new]], - ["M", "muta-pow", "POW", "mutator range exponent (range == 2^POW)", 32, False, ipos, [bench, new]], - ["m", "mvec-pow", "POW", "memory vector size exponent (size == 2^POW)", 20, False, ipos, [bench, new]], - ["n", "name", "NAME", "name of new or loaded simulation", "def.sim", False, str, [load, new]], - ["o", "optimized", None, "builds salis binary with optimizations", False, False, bool, [bench, load, new]], - ["p", "pre-cmd", "CMD", "shell command to wrap call to executable (e.g. gdb, " - "valgrind, etc.)", None, False, str, [bench, load, new]], - ["s", "seed", "SEED", "seed value for new simulation; a value of 0 disables " - "cosmic rays; a value of -1 creates a random seed", 0, False, iseed, [bench, new]], - ["S", "print-source", None, "print generated C source to stdout and exit", False, False, bool, [bench, load, new]], - ["T", "delete-temp-dir", None, "delete temporary directory on exit", True, False, bool, [bench, load, new]], - ["t", "thread-gap", "N", "memory gap between cores in bytes (may help reduce cache " - "misses?)", 0x100, False, inat, [bench, load, new]], - ["u", "ui", uis, "user interface", "curses", False, str, [load, new]], - ["x", "compress", None, "compress save files (requires 'zlib')", True, False, bool, [new]], - ["y", "sync-pow", "POW", "core sync interval exponent (interval == 2^POW)", 20, False, ipos, [bench, new]], - ["z", "auto-save-pow", "POW", "auto-save interval exponent (interval == 2^POW)", 36, False, ipos, [new]], +option_list = [ + ["A", "anc", "ANC", "ancestor file name without extension, to be compiled on all cores (ANC points to 'anc/{arch}/{ANC}.asm')", None, True, str, [bench, new]], + ["a", "arch", architectures, "VM architecture", "dummy", False, str, [bench, new]], + ["b", "steps", "N", "number of steps to run in benchmark", 0x1000000, False, ipos, [bench]], + ["C", "clones", "N", "number of ancestor clones on each core", 1, False, inat, [bench, new]], + ["c", "cores", "N", "number of simulator cores", 2, False, inat, [bench, new]], + ["d", "data-push-pow", "POW", "data aggregation interval exponent (interval == 2^{POW} >= {sync-pow}); a value of 0 disables data aggregation (requires 'sqlite')", 28, False, ipos, [new]], + ["f", "force", None, "overwrite existing simulation of given name", False, False, bool, [new]], + ["F", "muta-flip", None, "cosmic rays flip bits instead of randomizing whole bytes", False, False, bool, [bench, new]], + ["g", "compiler", "CC", "C compiler to use", "gcc", False, str, [bench, load, new]], + ["M", "muta-pow", "POW", "mutator range exponent (range == 2^{POW})", 32, False, ipos, [bench, new]], + ["m", "mvec-pow", "POW", "memory vector size exponent (size == 2^{POW})", 20, False, ipos, [bench, new]], + ["n", "name", "NAME", "name of new or loaded simulation", "def.sim", False, str, [load, new]], + ["o", "optimized", None, "builds salis binary with optimizations", False, False, bool, [bench, load, new]], + ["p", "pre-cmd", "CMD", "shell command to wrap call to executable (e.g. gdb, time, valgrind, etc.)", None, False, str, [bench, load, new]], + ["s", "seed", "SEED", "seed value for new simulation; a value of 0 disables cosmic rays; a value of -1 creates a random seed", 0, False, seed, [bench, new]], + ["T", "keep-temp-dir", None, "delete temporary directory on exit", False, False, bool, [bench, load, new]], + ["t", "thread-gap", "N", "memory gap between cores in bytes (may help reduce cache misses)", 0x100, False, inat, [bench, load, new]], + ["u", "ui", uis, "user interface", "curses", False, str, [load, new]], + ["x", "no-compress", None, "do not compress save files (useful if 'zlib' is unavailable)", True, False, bool, [new]], + ["y", "sync-pow", "POW", "core sync interval exponent (interval == 2^{POW})", 20, False, ipos, [bench, new]], + ["z", "auto-save-pow", "POW", "auto-save interval exponent (interval == 2^{POW})", 36, False, ipos, [new]], ] -# fmt: on -# Map arguments to subparsers that use them -options = list(map(lambda option: dict(zip(option_keys, option)), option_conf)) +options = list(map(lambda option: dict(zip(option_keys, option)), option_list)) parser_map = ((parser, option) for option in options for parser in option["parsers"]) for parser, option in parser_map: arg_kwargs = {} - def push_same(key): - arg_kwargs[key] = option[key] - - def push_diff(tgt_key, src_key): - arg_kwargs[tgt_key] = option[src_key] - - def push_val(key, val): - arg_kwargs[key] = val + def push_same(key): arg_kwargs[key] = option[key] + def push_diff(tgt_key, src_key): arg_kwargs[tgt_key] = option[src_key] + def push_val(key, val): arg_kwargs[key] = val push_diff("help", "description") push_same("required") - # No metavar means this argument is a flag if option["metavar"] is None: - push_val("action", "store_false" if option["default"] else "store_true") + push_val("action", "store_true") else: push_same("default") push_same("type") - if type(option["metavar"]) is list: - push_diff("choices", "metavar") - - if type(option["metavar"]) is str: - push_same("metavar") + if type(option["metavar"]) is list: push_diff("choices", "metavar") + if type(option["metavar"]) is str: push_same("metavar") - parser.add_argument( - f"-{option["short"]}", - f"--{option["long"]}", - **arg_kwargs, - ) + parser.add_argument(f"-{option["short"]}", f"--{option["long"]}", **arg_kwargs) args = main_parser.parse_args() +# ------------------------------------------------------------------------------ +# Logging +# ------------------------------------------------------------------------------ def info(msg, val=""): - print(f"\033[1;34mINFO:\033[0m {msg}", val) + print(f"\033[1;34m[INFO]\033[0m {msg}", val) def warn(msg, val=""): - print(f"\033[1;31mWARN:\033[0m {msg}", val) + print(f"\033[1;33m[WARN]\033[0m {msg}", val) def error(msg, val=""): - print(f"\033[1;31mERROR:\033[0m {msg}", val) + print(f"\033[1;31m[ERROR]\033[0m {msg}", val) sys.exit(1) # ------------------------------------------------------------------------------ # Load configuration # ------------------------------------------------------------------------------ -info(headline) -info(f"Called '{script}' with the following options:") +info(description) +info(f"Called '{prog}' with the following options:") for key, val in vars(args).items(): print(f"{key} = {repr(val)}") if args.command in ["load", "new"]: - sim_dir = f"{os.environ["HOME"]}/.salis/{args.name}" - sim_opts = f"{sim_dir}/opts.py" - sim_path = f"{sim_dir}/{args.name}" + sim_dir = os.path.join(os.environ["HOME"], ".salis", args.name) + sim_opts = os.path.join(sim_dir, "opts.py") + sim_path = os.path.join(sim_dir, args.name) if args.command in ["load"]: if not os.path.isdir(sim_dir): error("No simulation found named:", args.name) - info(f"Sourcing configuration from '{sim_opts}':") + info(f"Sourcing configuration from: '{sim_opts}':") sys.path.append(sim_dir) - import opts as opts_module + import opts - # Copy all fields in configuration file into the 'args' object - opts = (opt for opt in dir(opts_module) if not opt.startswith("__")) + opt_vars = (opt for opt in dir(opts) if not opt.startswith("__")) - for opt in opts: - opt_attr = getattr(opts_module, opt) - print(f"{opt} = {repr(opt_attr)}") - setattr(args, opt, opt_attr) + for opt_var in opt_vars: + opt_attr = getattr(opts, opt_var) + print(f"{opt_var} = {repr(opt_attr)}") + setattr(args, opt_var, opt_attr) if args.command in ["new"]: - if args.data_push_pow != 0 and args.data_push_pow < args.sync_pow: + if args.data_push_pow and args.data_push_pow < args.sync_pow: error("Data push power must be equal or greater than thread sync power") if os.path.isdir(sim_dir) and args.force: - warn("Force flag used - wiping old simulation at:", sim_dir) + warn("Force flag used! Wiping old simulation at:", sim_dir) shutil.rmtree(sim_dir) if os.path.isdir(sim_dir): error("Simulation directory found at:", sim_dir) - info("Creating new simulation directory at:", sim_dir) - os.mkdir(sim_dir) + if args.seed == -1: + args.seed = random.getrandbits(64) + info("Using random seed:", args.seed) + info("Creating new simulation directory at:", sim_dir) info("Creating configuration file at:", sim_opts) + os.mkdir(sim_dir) + opts = ( option["long"].replace("-", "_") for option in options @@ -199,82 +174,24 @@ if args.command in ["new"]: # ------------------------------------------------------------------------------ # Load architecture and UI variables # ------------------------------------------------------------------------------ -arch_path = f"arch/{args.arch}" -info("Loading architecture specific variables from:", f"{arch_path}/arch_vars.py") +arch_path = os.path.join("arch", args.arch) +info("Loading architecture variables from:", os.path.join(arch_path, "arch_vars.py")) sys.path.append(arch_path) -from arch_vars import gen_arch_vars -arch_vars = gen_arch_vars(args) +from arch_vars import ArchVars +arch_vars = ArchVars(args) if args.command in ["load", "new"]: - ui_path = f"ui/{args.ui}" - info("Loading UI specific variables from:", f"{ui_path}/ui_vars.py") + ui_path = os.path.join("ui", args.ui) + info("Loading UI variables from:", os.path.join(ui_path, "ui_vars.py")) sys.path.append(ui_path) - from ui_vars import gen_ui_vars - ui_vars = gen_ui_vars(args) - -# ------------------------------------------------------------------------------ -# Fill in template variables -# ------------------------------------------------------------------------------ -ul_val = lambda val: f"{hex(val)}ul" -ul_pow = lambda val: f"{hex(2 ** val)}ul" - -includes = [ - "assert.h", - "stdarg.h", - "stdbool.h", - "stddef.h", - "stdint.h", - "stdlib.h", - "string.h", - "threads.h", -] - -inst_cap = "0x80" -inst_mask = "0x7f" -ipc_flag = "0x80" -mall_flag = "0x80" -muta_range = ul_pow(args.muta_pow) -mvec_size = ul_pow(args.mvec_pow) -sync_interval = ul_pow(args.sync_pow) -thread_gap = ul_val(args.thread_gap) -uint64_half = ul_val(0x8000000000000000) - -if args.seed == -1: - args.seed = ul_val(random.getrandbits(64)) - info("Using random seed", args.seed) -else: - args.seed = ul_val(args.seed) - -if args.command in ["bench"]: - includes.append("stdio.h") - args.steps = ul_val(args.steps) - -if args.command in ["load", "new"]: - auto_save_interval = ul_pow(args.auto_save_pow) - auto_save_name_len = f"{len(sim_path) + 20}" - - if args.data_push_pow != 0: - data_push_path = f"{sim_dir}/{args.name}.sqlite3" - data_push_interval = ul_pow(args.data_push_pow) - data_push_busy_timeout = 600000 - includes.append("sqlite3.h") - info("Data will be aggregated at:", data_push_path) - else: - warn("Data aggregation disabled") - - if args.compress: - includes.append("zlib.h") - info("Save file compression enabled") - else: - warn("Save file compression disabled") - - includes.extend(ui_vars["includes"]) + from ui_vars import UIVars + ui_vars = UIVars(args) # ------------------------------------------------------------------------------ -# Assemble ancestor organism into byte array +# Compile ancestor organism # ------------------------------------------------------------------------------ if args.command in ["bench", "new"] and args.anc is not None: - anc_path = f"ancs/{args.arch}/{args.anc}.asm" + anc_path = os.path.join("anc", args.arch, f"{args.anc}.asm") if not os.path.isfile(anc_path): error("Could not find ancestor file:", anc_path) @@ -287,71 +204,120 @@ if args.command in ["bench", "new"] and args.anc is not None: lines = filter(lambda line: line, lines) lines = map(lambda line: line.split(), lines) - # A very simple assembler that compares lines in input ASM file against - # all entries in the instruction set table provided by each architecture. - # The resulting bytes equate to each instruction's index on the table. anc_bytes = [] for line in lines: found = False - for byte, tup in enumerate(arch_vars["inst_set"]): + for byte, tup in enumerate(arch_vars.inst_set): if line == tup[0]: anc_bytes.append(byte) found = True - continue + break if not found: error("Unrecognized instruction in ancestor file:", line) - anc_repr = f"[{','.join(map(str, anc_bytes))}]" - info(f"Compiled ancestor file '{anc_path}' into byte array:", anc_repr) + anc_bytes_repr = ",".join(map(str, anc_bytes)) + info(f"Compiled ancestor file '{anc_path}' into byte array:", f"{{{anc_bytes_repr}}}") # ------------------------------------------------------------------------------ -# Emit C source +# Populate compiler flags # ------------------------------------------------------------------------------ -tempdir = TemporaryDirectory(prefix="salis_", delete=args.delete_temp_dir) -info("Created a temporary salis directory at:", tempdir.name) +flags = set() +includes = set() +defines = set() +links = set() -salis_src = f"{tempdir.name}/salis.c" -info("Emitting C source at:", salis_src) +flags.update({"-Wall", "-Wextra", "-Werror", f"-Iarch/{args.arch}"}) -jinja_env = Environment( - loader = FileSystemLoader("."), - lstrip_blocks = True, - trim_blocks = True, - undefined = StrictUndefined, -) +defines.add(f"-DARCH=\"{args.arch}\"") +defines.add(f"-DCOMMAND_{args.command.upper()}") +defines.add(f"-DCORES={args.cores}") +defines.add(f"-DMUTA_RANGE={2 ** args.muta_pow}ul") +defines.add(f"-DMVEC_SIZE={2 ** args.mvec_pow}ul") +defines.add(f"-DSEED={args.seed}ul") +defines.add(f"-DSYNC_INTERVAL={2 ** args.sync_pow}ul") +defines.add(f"-DTHREAD_GAP={args.thread_gap}") -source_str = jinja_env.get_template("core.j2.c").render(**locals()) +defines.add(f"-DCORE_FIELDS={" ".join(f"CORE_FIELD({", ".join(field)})" for field in arch_vars.core_fields)}") +defines.add(f"-DPROC_FIELDS={" ".join(f"PROC_FIELD({", ".join(field)})" for field in arch_vars.proc_fields)}") +defines.add(f"-DINST_SET={" ".join(f"INST({index}, {"_".join(inst[0])}, \"{" ".join(inst[0])}\", L'{inst[1]}')" for index, inst in enumerate(arch_vars.inst_set))}") +defines.add(f"-DCORE_FIELD_COUNT={len(arch_vars.core_fields)}") +defines.add(f"-DPROC_FIELD_COUNT={len(arch_vars.proc_fields)}") +defines.add(f"-DINST_COUNT={len(arch_vars.inst_set)}") +defines.add(f"-DFOR_CORES={" ".join(f"FOR_CORE({i})" for i in range(args.cores))}") -if args.print_source: - info("Printing C source and exiting...") - print(source_str) - exit(0) +if args.muta_flip: defines.add("-DMUTA_FLIP") +if arch_vars.mvec_loop: defines.add("-DMVEC_LOOP") -with open(salis_src, "w") as file: - file.write(source_str) +if args.optimized: + flags.add("-O3") + defines.add("-DNDEBUG") +else: + flags.add("-ggdb") + +if args.command in ["bench"]: + includes.add("stdio.h") + defines.add(f"-DSTEPS={args.steps}ul") + +if args.command in ["bench", "new"]: + defines.add(f"-DCLONES={args.clones}") + + if args.anc is not None: + defines.add(f"-DANC_BYTES={{{anc_bytes_repr}}}") + defines.add(f"-DANC_SIZE={len(anc_bytes)}") + +if args.command in ["load", "new"]: + flags.add(f"-Iui/{args.ui}") + includes.update(ui_vars.includes) + defines.update(ui_vars.defines) + defines.add(f"-DAUTOSAVE_INTERVAL={2 ** args.auto_save_pow}ul") + defines.add(f"-DAUTOSAVE_NAME_LEN={len(sim_path) + 20}") + defines.add(f"-DNAME=\"{args.name}\"") + defines.add(f"-DSIM_PATH=\"{sim_path}\"") + links.update(ui_vars.links) + + if args.data_push_pow: + includes.add("sqlite3.h") + data_push_path = os.path.join(sim_dir, f"{args.name}.sqlite3") + defines.add(f"-DDATA_PUSH_INTERVAL={2 ** args.data_push_pow}ul") + defines.add(f"-DDATA_PUSH_PATH=\"{data_push_path}\"") + links.add("-lsqlite3") + info("Data will be aggregated at:", data_push_path) + + if arch_vars.data_is_compressed: + includes.add("zlib.h") + links.add("-lz") + info("Data aggregation requires compression") + else: + warn("Data aggregation disabled") + + if not args.no_compress: + includes.add("zlib.h") + defines.add("-D_POSIX_C_SOURCE=200809L") + defines.add("-DCOMPRESS") + links.add("-lz") + info("Save file compression enabled") + else: + warn("Save file compression disabled") # ------------------------------------------------------------------------------ # Build executable # ------------------------------------------------------------------------------ -salis_bin = f"{tempdir.name}/salis_bin" -info("Building salis binary at:", salis_bin) - -build_cmd = ["gcc", salis_src, "-o", salis_bin, "-Wall", "-Wextra", "-Werror", "-Wno-overlength-strings", "-pedantic", "-std=c11"] -build_cmd.extend(["-O3", "-DNDEBUG"] if args.optimized else ["-ggdb"]) +tempdir = TemporaryDirectory(prefix="salis_", delete=not args.keep_temp_dir) +info("Created a temporary salis directory at:", tempdir.name) -if args.command in ["load", "new"]: - build_cmd.extend(ui_vars["flags"]) +salis_bin = os.path.join(tempdir.name, "salis_bin") +info("Building salis binary at:", salis_bin) - # Enable POSIX extensions (open_memstream) if compression is enabled - # This makes it easy to generate compressed data arrays for lzip using - # C's native FILE interface. - build_cmd.extend(["-lz", "-D_POSIX_C_SOURCE=200809L"] if args.compress else []) - build_cmd.extend(["-lsqlite3"] if args.data_push_pow != 0 else []) +build_cmd = [args.compiler, "core.c", "-o", salis_bin] +build_cmd.extend(flags) +build_cmd.extend(sum(map(lambda include: [f"-include", include], includes), [])) +build_cmd.extend(defines) +build_cmd.extend(links) -info("Using build command:", " ".join(build_cmd)) +info("Using build command:", build_cmd) subprocess.run(build_cmd, check=True) # ------------------------------------------------------------------------------ @@ -365,7 +331,6 @@ run_cmd.append(salis_bin) info("Using run command:", " ".join(run_cmd)) salis_sp = subprocess.Popen(run_cmd, stdout=sys.stdout, stderr=sys.stderr) -# Ctrl-C terminates the simulator gracefully. # When using signals (e.g. SIGTERM), they must be sent to the entire process group # to make sure both the simulator and the interpreter get shut down. try: |
