diff --git a/build.py b/build.py new file mode 100644 index 0000000..df2c2b7 --- /dev/null +++ b/build.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python3 +"""SPL build system — .c → .o → exe, .spl → .sir, deps auto-resolved.""" + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +BUILD_DIR = ROOT / "build" +CACHE_FILE = BUILD_DIR / ".build_cache.json" +DESC_FILE = ROOT / "project_desc.py" +EXE_EXT = ".exe" if os.name == "nt" else "" + +CC = os.environ.get("CC", "gcc") +CFLAGS = os.environ.get("CFLAGS", "-Wall -Wextra -O0 -g").split() + +def load_desc(): + ns = {"__builtins__": __builtins__} + exec(DESC_FILE.read_text(), ns) + return { + "exe": ns.get("exe", {}), + "spl": ns.get("spl", {}), + "pipeline": ns.get("pipeline", {}), + } + +# ── Helpers ───────────────────────────────────────────────────────────── + +def obj_key(src): + return src.rsplit(".", 1)[0].replace("/", "_").replace("\\", "_") + +def obj_path(src): + return BUILD_DIR / (obj_key(src) + ".o") + +def exe_path(name): + return BUILD_DIR / (name + EXE_EXT) + +def sir_path(name): + return BUILD_DIR / (name + ".sir") + +# ── Cache ─────────────────────────────────────────────────────────────── + +def load_cache(): + if CACHE_FILE.is_file(): + try: + return json.loads(CACHE_FILE.read_text()) + except (json.JSONDecodeError, OSError): + pass + return {} + +def save_cache(cache): + BUILD_DIR.mkdir(parents=True, exist_ok=True) + CACHE_FILE.write_text(json.dumps(cache, indent=2)) + +def hash_file(p): + h = hashlib.sha256() + with p.open("rb") as f: + while chunk := f.read(65536): + h.update(chunk) + return h.hexdigest() + +# ── C compilation ─────────────────────────────────────────────────────── + +_built_objs = set() + +def compile_c(src, cache, force): + key = f"obj:{src}" + dst = obj_path(src) + src_path = ROOT / src + + if not src_path.is_file(): + print(f" ERR {src} not found"); return False + + h = hashlib.sha256() + h.update(hash_file(src_path).encode()) + h.update(" ".join(CFLAGS).encode()) + ch = h.hexdigest() + + cached = cache.get(key) + if not force and cached and cached["h"] == ch and dst.is_file(): + _built_objs.add(src); return True + + BUILD_DIR.mkdir(parents=True, exist_ok=True) + cmd = [CC] + CFLAGS + ["-c", str(src_path), "-o", str(dst)] + print(f" CC {src}") + if subprocess.run(cmd).returncode: return False + cache[key] = {"h": ch} + _built_objs.add(src) + return True + +def link_exe(name, sources, cache, force): + key = f"exe:{name}" + dst = exe_path(name) + + # hash = concat of all input obj hashes + name + h = hashlib.sha256(name.encode()) + for src in sources: + cached = cache.get(f"obj:{src}", {}) + h.update(cached.get("h", "?").encode()) + ch = h.hexdigest() + + cached = cache.get(key) + if not force and cached and cached["h"] == ch and dst.is_file(): + return True + + cmd = [CC] + CFLAGS + [str(obj_path(s)) for s in sources] + ["-o", str(dst)] + print(f" LINK {name}") + if subprocess.run(cmd).returncode: return False + cache[key] = {"h": ch} + return True + +# ── SPL compilation ───────────────────────────────────────────────────── + +def find_imports(src_path): + """Scan source for @import("path") directives, return resolved paths.""" + deps = [] + try: + text = src_path.read_text(errors="replace") + except OSError: + return deps + idx = 0 + while True: + idx = text.find('@import("', idx) + if idx < 0: + break + start = idx + 9 + end = text.find('")', start) + if end < 0: + break + imp_path = text[start:end] + imp_file = (src_path.parent / imp_path).resolve() + if imp_file.is_file(): + deps.append(imp_file) + idx = end + 2 + return deps + +def collect_deps(src_path): + """Collect all transitive @import dependencies.""" + seen = set() + result = [] + def walk(p): + p = p.resolve() + if p in seen: + return + seen.add(p) + for dep in find_imports(p): + walk(dep) + result.append(dep) + walk(src_path) + return result + +def compile_spl(name, src, compiler, desc, cache, force): + key = f"spl:{name}" + dst = sir_path(name) + src_path = ROOT / src + + if not src_path.is_file(): + print(f" ERR {src} not found"); return False + + h = hashlib.sha256() + h.update(hash_file(src_path).encode()) + + # Hash @import dependencies (transitive) + for dep in collect_deps(src_path): + h.update(hash_file(dep).encode()) + + # Determine how to run the compiler + if compiler in desc.get("spl", {}): + # SPL-based compiler: run via VM runner + cinfo = desc["spl"][compiler] + pl = desc.get("pipeline", {}).get(cinfo.get("pipeline", ""), {}) + runner = pl.get("runner") or cinfo.get("runner", "spl_cli") + runner_exe = exe_path(runner) + sir = sir_path(compiler) + if runner_exe.is_file(): + h.update(hash_file(runner_exe).encode()) + if os.path.isfile(sir): + h.update(hash_file(Path(sir)).encode()) + ch = h.hexdigest() + cached = cache.get(key) + if not force and cached and cached["h"] == ch and dst.is_file(): + return True + BUILD_DIR.mkdir(parents=True, exist_ok=True) + cmd = [str(runner_exe), str(sir), str(src_path), str(dst)] + print(f" SPL {name} ({compiler} via {runner})") + else: + # Native exe compiler + ce = exe_path(compiler) + if ce.is_file(): + h.update(hash_file(ce).encode()) + ch = h.hexdigest() + cached = cache.get(key) + if not force and cached and cached["h"] == ch and dst.is_file(): + return True + BUILD_DIR.mkdir(parents=True, exist_ok=True) + cmd = [str(ce), str(src_path), str(dst)] + print(f" SPL {name} ({compiler})") + + if subprocess.run(cmd).returncode: return False + cache[key] = {"h": ch} + return True + +# ── Dependency-aware build ────────────────────────────────────────────── + +_built = set() + +def build(name, desc, cache, force): + """Build target + all deps (auto-resolved DAG traversal).""" + if name in _built: + return True + + if name in desc.get("exe", {}): + sources = desc["exe"][name] + for src in sources: + if not compile_c(src, cache, force): + return False + if not link_exe(name, sources, cache, force): + return False + + elif name in desc.get("spl", {}): + info = desc["spl"][name] + pl = desc.get("pipeline", {}).get(info.get("pipeline", ""), {}) + compiler = pl.get("compiler") or info.get("compiler") + if not compiler: + print(f" ERR '{name}': no compiler in pipeline '{info.get('pipeline')}'") + return False + if not build(compiler, desc, cache, force): + return False + if not compile_spl(name, info["src"], compiler, desc, cache, force): + return False + + else: + print(f" ERR unknown target '{name}'") + return False + + _built.add(name) + return True + +def build_all(desc, cache, force): + targets = list(desc.get("exe", {})) + list(desc.get("spl", {})) + for t in targets: + if not build(t, desc, cache, force): + return False + return True + +# ── Run ───────────────────────────────────────────────────────────────── + +def run_target(name, desc, cache, force, extra_args): + if name in desc.get("exe", {}): + if not build(name, desc, cache, force): + return 1 + exe = exe_path(name) + print(f" RUN {name}") + return subprocess.run([str(exe)] + extra_args).returncode + + if name in desc.get("spl", {}): + info = desc["spl"][name] + pl = desc.get("pipeline", {}).get(info.get("pipeline", ""), {}) + runner = pl.get("runner") or info.get("runner", "spl_cli") + if not runner: + print(f" ERR '{name}': no runner"); return 1 + if not build(name, desc, cache, force): + return 1 + if not build(runner, desc, cache, force): + return 1 + print(f" RUN {name} (via {runner})") + return subprocess.run( + [str(exe_path(runner)), str(sir_path(name))] + extra_args + ).returncode + + if name in desc.get("pipeline", {}): + pl = desc["pipeline"][name] + compiler = pl.get("compiler") + runner = pl.get("runner") + if not compiler or not runner: + print(f" ERR pipeline '{name}' missing compiler or runner"); return 1 + if not extra_args: + print(" ERR usage: run [args]"); return 1 + src = extra_args[0] + + if not build(compiler, desc, cache, force): return 1 + if not build(runner, desc, cache, force): return 1 + + sir_name = "pipeline_" + hashlib.sha256(str(ROOT / src).encode()).hexdigest()[:8] + if not compile_spl(sir_name, src, compiler, desc, cache, force): + return 1 + + print(f" RUN pipeline {name} ({src})") + return subprocess.run( + [str(exe_path(runner)), str(sir_path(sir_name))] + extra_args[1:] + ).returncode + + print(f" ERR unknown target '{name}'"); return 1 + +# ── Clean / List ──────────────────────────────────────────────────────── + +def clean(): + if BUILD_DIR.is_dir(): + shutil.rmtree(BUILD_DIR) + print(f" CLEAN {BUILD_DIR}") + +def list_targets(desc): + print("Executables (.c → .o → exe):") + for name, sources in desc.get("exe", {}).items(): + print(f" {name}") + for s in sources: + print(f" {s}") + if desc.get("spl"): + print("\nSPL programs (.spl → .sir via pipeline):") + for name, info in desc["spl"].items(): + pl_name = info.get("pipeline", "-") + print(f" {name} pipeline={pl_name} src={info['src']}") + if desc.get("pipeline"): + print("\nPipelines:") + for name, pl in desc["pipeline"].items(): + print(f" {name} compiler={pl['compiler']} runner={pl.get('runner', '-')}") + +# ── CLI ───────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser(description="SPL build system") + sub = parser.add_subparsers(dest="command") + + bp = sub.add_parser("build", help="build targets (default: all)") + bp.add_argument("targets", nargs="*") + bp.add_argument("-f", "--force", action="store_true") + + rp = sub.add_parser("run", help="build and run a target, or run pipeline [args]") + rp.add_argument("target") + rp.add_argument("extra", nargs="*") + + sub.add_parser("test", help="build and run VM unit tests") + sub.add_parser("clean", help="remove build artifacts") + sub.add_parser("list", help="list all targets") + + args = parser.parse_args() + + if not DESC_FILE.is_file(): + print(f"ERR {DESC_FILE} not found"); return 1 + + # Default: build all + if args.command is None: + desc = load_desc(); cache = load_cache() + ok = build_all(desc, cache, False) + save_cache(cache) + return 0 if ok else 1 + + # Build + if args.command == "build": + desc = load_desc(); cache = load_cache() + if args.targets: + ok = all(build(t, desc, cache, args.force) for t in args.targets) + else: + ok = build_all(desc, cache, args.force) + save_cache(cache) + return 0 if ok else 1 + + # Run + if args.command == "run": + desc = load_desc(); cache = load_cache() + rc = run_target(args.target, desc, cache, False, args.extra) + save_cache(cache) + return rc + + # Pipeline: build compiler → compile .spl → run via runner + if args.command == "pipeline": + desc = load_desc(); cache = load_cache() + pl = desc.get("pipeline", {}).get(args.name) + if not pl: + print(f" ERR unknown pipeline '{args.name}'"); return 1 + compiler = pl.get("compiler") + runner = pl.get("runner") + if not compiler or not runner: + print(f" ERR pipeline '{args.name}' missing compiler or runner"); return 1 + src_path = ROOT / args.src + if not src_path.is_file(): + print(f" ERR source not found: {args.src}"); return 1 + + # Build compiler + runner + if not build(compiler, desc, cache, False): return 1 + if not build(runner, desc, cache, False): return 1 + + # Compile .spl → .sir (temp name = src path hash) + sir_name = "pipeline_" + hashlib.sha256(str(src_path).encode()).hexdigest()[:8] + if not compile_spl(sir_name, args.src, compiler, desc, cache, False): + return 1 + + # Run .sir via runner + print(f" PIPELINE {args.name} ({args.src})") + rc = subprocess.run( + [str(exe_path(runner)), str(sir_path(sir_name))] + args.extra + ).returncode + save_cache(cache) + return rc + + # Test + if args.command == "test": + desc = load_desc(); cache = load_cache() + ok = build("test", desc, cache, False) + save_cache(cache) + if not ok: return 1 + return subprocess.run([str(exe_path("test"))]).returncode + + # Clean / List + if args.command == "clean": + clean(); return 0 + if args.command == "list": + list_targets(load_desc()); return 0 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/project_desc.py b/project_desc.py new file mode 100644 index 0000000..ba92d9a --- /dev/null +++ b/project_desc.py @@ -0,0 +1,42 @@ +vm = [ + "stage0/spl_ir.c", + "stage0/spl_syscall.c", + "stage0/spl_vm.c", +] + +exe = { + "spl_cli": ["stage0/spl_cli.c"] + vm, + "splc_cli": ["stage1/splc_cli.c"] + vm + ["stage1/spl_comp.c", "stage1/spl_lexer.c", "stage1/spl_type.c", "stage1/spl_parser.c", "stage1/spl_lex_util.c", "stage1/spl_expr.c", "stage1/spl_stmt.c"], + "splc0": ["stage1/splc0.c"] + vm + ["stage1/spl_comp.c", "stage1/spl_lexer.c", "stage1/spl_type.c", "stage1/spl_parser.c", "stage1/spl_lex_util.c", "stage1/spl_expr.c", "stage1/spl_stmt.c"], + "test": ["stage0/test_spl_vm.c"] + vm, + "spl_disasm": ["stage0/spl_disasm.c"] + vm, +} + +pipeline = { + "splc0b": {"compiler": "splc0", "runner": "spl_cli"}, + "splc0r": {"compiler": "splc0", "runner": "splc_cli"}, + "splc0d": {"compiler": "splc0", "runner": "spl_disasm"}, + + "splc1b": {"compiler": "splc0", "runner": "splc_cli"}, + "splc1r": {"compiler": "splc1", "runner": "splc_cli"}, + "splc1d": {"compiler": "splc1", "runner": "spl_disasm"}, + + "splc2b": {"compiler": "splc1", "runner": "splc_cli"}, + "splc2r": {"compiler": "splc2", "runner": "splc_cli"}, + "splc2d": {"compiler": "splc2", "runner": "spl_disasm"}, + + "splc3b": {"compiler": "splc2", "runner": "splc_cli"}, + "splc3r": {"compiler": "splc3", "runner": "splc_cli"}, + "splc3d": {"compiler": "splc3", "runner": "spl_disasm"}, + + "splc4b": {"compiler": "splc3", "runner": "splc_cli"}, + "splc4r": {"compiler": "splc4", "runner": "splc_cli"}, + "splc4d": {"compiler": "splc4", "runner": "spl_disasm"}, +} + +spl = { + "splc1": {"src": "stage1/splc1.spl", "pipeline": "splc0r"}, + "splc2": {"src": "stage2/splc2.spl", "pipeline": "splc1r"}, + "splc3": {"src": "stage3/splc3.spl", "pipeline": "splc2r"}, + "splc4": {"src": "stage4/splc4.spl", "pipeline": "splc3r"}, +} diff --git a/stage0/include/acutest.h b/stage0/include/acutest.h new file mode 100644 index 0000000..5f9cb19 --- /dev/null +++ b/stage0/include/acutest.h @@ -0,0 +1,1994 @@ +/* + * Acutest -- Another C/C++ Unit Test facility + * + * + * Copyright 2013-2023 Martin Mitáš + * Copyright 2019 Garrett D'Amore + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef ACUTEST_H +#define ACUTEST_H + + +/* Try to auto-detect whether we need to disable C++ exception handling. + * If the detection fails, you may always define TEST_NO_EXCEPTIONS before + * including "acutest.h" manually. */ +#ifdef __cplusplus + #if (__cplusplus >= 199711L && !defined __cpp_exceptions) || \ + ((defined(__GNUC__) || defined(__clang__)) && !defined __EXCEPTIONS) + #ifndef TEST_NO_EXCEPTIONS + #define TEST_NO_EXCEPTIONS + #endif + #endif +#endif + + +/************************ + *** Public interface *** + ************************/ + +/* By default, "acutest.h" provides the main program entry point (function + * main()). However, if the test suite is composed of multiple source files + * which include "acutest.h", then this causes a problem of multiple main() + * definitions. To avoid this problem, #define macro TEST_NO_MAIN in all + * compilation units but one. + */ + +/* Macro to specify list of unit tests in the suite. + * The unit test implementation MUST provide list of unit tests it implements + * with this macro: + * + * TEST_LIST = { + * { "test1_name", test1_func_ptr }, + * { "test2_name", test2_func_ptr }, + * ... + * { NULL, NULL } // zeroed record marking the end of the list + * }; + * + * The list specifies names of each test (must be unique) and pointer to + * a function implementing it. The function does not take any arguments + * and has no return values, i.e. every test function has to be compatible + * with this prototype: + * + * void test_func(void); + * + * Note the list has to be ended with a zeroed record. + */ +#define TEST_LIST const struct acutest_test_ acutest_list_[] + + +/* Macros for testing whether an unit test succeeds or fails. These macros + * can be used arbitrarily in functions implementing the unit tests. + * + * If any condition fails throughout execution of a test, the test fails. + * + * TEST_CHECK takes only one argument (the condition), TEST_CHECK_ allows + * also to specify an error message to print out if the condition fails. + * (It expects printf-like format string and its parameters). The macros + * return non-zero (condition passes) or 0 (condition fails). + * + * That can be useful when more conditions should be checked only if some + * preceding condition passes, as illustrated in this code snippet: + * + * SomeStruct* ptr = allocate_some_struct(); + * if(TEST_CHECK(ptr != NULL)) { + * TEST_CHECK(ptr->member1 < 100); + * TEST_CHECK(ptr->member2 > 200); + * } + */ +#define TEST_CHECK_(cond,...) \ + acutest_check_(!!(cond), __FILE__, __LINE__, __VA_ARGS__) +#define TEST_CHECK(cond) \ + acutest_check_(!!(cond), __FILE__, __LINE__, "%s", #cond) + + +/* These macros are the same as TEST_CHECK_ and TEST_CHECK except that if the + * condition fails, the currently executed unit test is immediately aborted. + * + * That is done either by calling abort() if the unit test is executed as a + * child process; or via longjmp() if the unit test is executed within the + * main Acutest process. + * + * As a side effect of such abortion, your unit tests may cause memory leaks, + * unflushed file descriptors, and other phenomena caused by the abortion. + * + * Therefore you should not use these as a general replacement for TEST_CHECK. + * Use it with some caution, especially if your test causes some other side + * effects to the outside world (e.g. communicating with some server, inserting + * into a database etc.). + */ +#define TEST_ASSERT_(cond,...) \ + do { \ + if(!acutest_check_(!!(cond), __FILE__, __LINE__, __VA_ARGS__)) \ + acutest_abort_(); \ + } while(0) +#define TEST_ASSERT(cond) \ + do { \ + if(!acutest_check_(!!(cond), __FILE__, __LINE__, "%s", #cond)) \ + acutest_abort_(); \ + } while(0) + + +#ifdef __cplusplus +#ifndef TEST_NO_EXCEPTIONS +/* Macros to verify that the code (the 1st argument) throws exception of given + * type (the 2nd argument). (Note these macros are only available in C++.) + * + * TEST_EXCEPTION_ is like TEST_EXCEPTION but accepts custom printf-like + * message. + * + * For example: + * + * TEST_EXCEPTION(function_that_throw(), ExpectedExceptionType); + * + * If the function_that_throw() throws ExpectedExceptionType, the check passes. + * If the function throws anything incompatible with ExpectedExceptionType + * (or if it does not thrown an exception at all), the check fails. + */ +#define TEST_EXCEPTION(code, exctype) \ + do { \ + bool exc_ok_ = false; \ + const char *msg_ = NULL; \ + try { \ + code; \ + msg_ = "No exception thrown."; \ + } catch(exctype const&) { \ + exc_ok_= true; \ + } catch(...) { \ + msg_ = "Unexpected exception thrown."; \ + } \ + acutest_check_(exc_ok_, __FILE__, __LINE__, #code " throws " #exctype);\ + if(msg_ != NULL) \ + acutest_message_("%s", msg_); \ + } while(0) +#define TEST_EXCEPTION_(code, exctype, ...) \ + do { \ + bool exc_ok_ = false; \ + const char *msg_ = NULL; \ + try { \ + code; \ + msg_ = "No exception thrown."; \ + } catch(exctype const&) { \ + exc_ok_= true; \ + } catch(...) { \ + msg_ = "Unexpected exception thrown."; \ + } \ + acutest_check_(exc_ok_, __FILE__, __LINE__, __VA_ARGS__); \ + if(msg_ != NULL) \ + acutest_message_("%s", msg_); \ + } while(0) +#endif /* #ifndef TEST_NO_EXCEPTIONS */ +#endif /* #ifdef __cplusplus */ + + +/* Sometimes it is useful to split execution of more complex unit tests to some + * smaller parts and associate those parts with some names. + * + * This is especially handy if the given unit test is implemented as a loop + * over some vector of multiple testing inputs. Using these macros allow to use + * sort of subtitle for each iteration of the loop (e.g. outputting the input + * itself or a name associated to it), so that if any TEST_CHECK condition + * fails in the loop, it can be easily seen which iteration triggers the + * failure, without the need to manually output the iteration-specific data in + * every single TEST_CHECK inside the loop body. + * + * TEST_CASE allows to specify only single string as the name of the case, + * TEST_CASE_ provides all the power of printf-like string formatting. + * + * Note that the test cases cannot be nested. Starting a new test case ends + * implicitly the previous one. To end the test case explicitly (e.g. to end + * the last test case after exiting the loop), you may use TEST_CASE(NULL). + */ +#define TEST_CASE_(...) acutest_case_(__VA_ARGS__) +#define TEST_CASE(name) acutest_case_("%s", name) + + +/* Maximal output per TEST_CASE call. Longer messages are cut. + * You may define another limit prior including "acutest.h" + */ +#ifndef TEST_CASE_MAXSIZE + #define TEST_CASE_MAXSIZE 64 +#endif + + +/* printf-like macro for outputting an extra information about a failure. + * + * Intended use is to output some computed output versus the expected value, + * e.g. like this: + * + * if(!TEST_CHECK(produced == expected)) { + * TEST_MSG("Expected: %d", expected); + * TEST_MSG("Produced: %d", produced); + * } + * + * Note the message is only written down if the most recent use of any checking + * macro (like e.g. TEST_CHECK or TEST_EXCEPTION) in the current test failed. + * This means the above is equivalent to just this: + * + * TEST_CHECK(produced == expected); + * TEST_MSG("Expected: %d", expected); + * TEST_MSG("Produced: %d", produced); + * + * The macro can deal with multi-line output fairly well. It also automatically + * adds a final new-line if there is none present. + */ +#define TEST_MSG(...) acutest_message_(__VA_ARGS__) + + +/* Maximal output per TEST_MSG call. Longer messages are cut. + * You may define another limit prior including "acutest.h" + */ +#ifndef TEST_MSG_MAXSIZE + #define TEST_MSG_MAXSIZE 1024 +#endif + + +/* Macro for dumping a block of memory. + * + * Its intended use is very similar to what TEST_MSG is for, but instead of + * generating any printf-like message, this is for dumping raw block of a + * memory in a hexadecimal form: + * + * TEST_CHECK(size_produced == size_expected && + * memcmp(addr_produced, addr_expected, size_produced) == 0); + * TEST_DUMP("Expected:", addr_expected, size_expected); + * TEST_DUMP("Produced:", addr_produced, size_produced); + */ +#define TEST_DUMP(title, addr, size) acutest_dump_(title, addr, size) + +/* Maximal output per TEST_DUMP call (in bytes to dump). Longer blocks are cut. + * You may define another limit prior including "acutest.h" + */ +#ifndef TEST_DUMP_MAXSIZE + #define TEST_DUMP_MAXSIZE 1024 +#endif + + +/* Macros for marking the test as SKIPPED. + * Note it can only be used at the beginning of a test, before any other + * checking. + * + * Once used, the best practice is to return from the test routine as soon + * as possible. + */ +#define TEST_SKIP(...) acutest_skip_(__FILE__, __LINE__, __VA_ARGS__) + + +/* Common test initialisation/clean-up + * + * In some test suites, it may be needed to perform some sort of the same + * initialization and/or clean-up in all the tests. + * + * Such test suites may use macros TEST_INIT and/or TEST_FINI prior including + * this header. The expansion of the macro is then used as a body of helper + * function called just before executing every single (TEST_INIT) or just after + * it ends (TEST_FINI). + * + * Examples of various ways how to use the macro TEST_INIT: + * + * #define TEST_INIT my_init_func(); + * #define TEST_INIT my_init_func() // Works even without the semicolon + * #define TEST_INIT setlocale(LC_ALL, NULL); + * #define TEST_INIT { setlocale(LC_ALL, NULL); my_init_func(); } + * + * TEST_FINI is to be used in the same way. + */ + + +/********************** + *** Implementation *** + **********************/ + +/* The unit test files should not rely on anything below. */ + +#include + +/* Enable the use of the non-standard keyword __attribute__ to silence warnings under some compilers */ +#if defined(__GNUC__) || defined(__clang__) + #define ACUTEST_ATTRIBUTE_(attr) __attribute__((attr)) +#else + #define ACUTEST_ATTRIBUTE_(attr) +#endif + +#ifdef __cplusplus + extern "C" { +#endif + +enum acutest_state_ { + ACUTEST_STATE_INITIAL = -4, + ACUTEST_STATE_SELECTED = -3, + ACUTEST_STATE_NEEDTORUN = -2, + + /* By the end all tests should be in one of the following: */ + ACUTEST_STATE_EXCLUDED = -1, + ACUTEST_STATE_SUCCESS = 0, + ACUTEST_STATE_FAILED = 1, + ACUTEST_STATE_SKIPPED = 2 +}; + +int acutest_check_(int cond, const char* file, int line, const char* fmt, ...); +void acutest_case_(const char* fmt, ...); +void acutest_message_(const char* fmt, ...); +void acutest_dump_(const char* title, const void* addr, size_t size); +void acutest_abort_(void) ACUTEST_ATTRIBUTE_(noreturn); +#ifdef __cplusplus + } /* extern "C" */ +#endif + +#ifndef TEST_NO_MAIN + +#include +#include +#include +#include +#include + +#if defined(unix) || defined(__unix__) || defined(__unix) || defined(__APPLE__) + #define ACUTEST_UNIX_ 1 + #include + #include + #include + #include + #include + #include + #include + + #if defined CLOCK_PROCESS_CPUTIME_ID && defined CLOCK_MONOTONIC + #define ACUTEST_HAS_POSIX_TIMER_ 1 + #endif +#endif + +#if defined(_gnu_linux_) || defined(__linux__) + #define ACUTEST_LINUX_ 1 + #include + #include +#endif + +#if defined(_WIN32) || defined(__WIN32__) || defined(__WINDOWS__) + #define ACUTEST_WIN_ 1 + #include + #include +#endif + +#if defined(__APPLE__) + #define ACUTEST_MACOS_ + #include + #include + #include + #include + #include +#endif + +#ifdef __cplusplus +#ifndef TEST_NO_EXCEPTIONS + #include +#endif +#endif + +#ifdef __has_include + #if __has_include() + #include + #endif +#endif + +/* Note our global private identifiers end with '_' to mitigate risk of clash + * with the unit tests implementation. */ + +#ifdef __cplusplus + extern "C" { +#endif + +#ifdef _MSC_VER + /* In the multi-platform code like ours, we cannot use the non-standard + * "safe" functions from Microsoft C lib like e.g. sprintf_s() instead of + * standard sprintf(). Hence, lets disable the warning C4996. */ + #pragma warning(push) + #pragma warning(disable: 4996) +#endif + + +struct acutest_test_ { + const char* name; + void (*func)(void); +}; + +struct acutest_test_data_ { + enum acutest_state_ state; + double duration; +}; + + +extern const struct acutest_test_ acutest_list_[]; + + +static char* acutest_argv0_ = NULL; +static int acutest_list_size_ = 0; +static struct acutest_test_data_* acutest_test_data_ = NULL; +static int acutest_no_exec_ = -1; +static int acutest_no_summary_ = 0; +static int acutest_tap_ = 0; +static int acutest_exclude_mode_ = 0; +static int acutest_worker_ = 0; +static int acutest_worker_index_ = 0; +static int acutest_cond_failed_ = 0; +static FILE *acutest_xml_output_ = NULL; + +static const struct acutest_test_* acutest_current_test_ = NULL; +static int acutest_current_index_ = 0; +static char acutest_case_name_[TEST_CASE_MAXSIZE] = ""; +static int acutest_test_check_count_ = 0; +static int acutest_test_skip_count_ = 0; +static char acutest_test_skip_reason_[256] = ""; +static int acutest_test_already_logged_ = 0; +static int acutest_case_already_logged_ = 0; +static int acutest_verbose_level_ = 2; +static int acutest_test_failures_ = 0; +static int acutest_colorize_ = 0; +static int acutest_timer_ = 0; + +static int acutest_abort_has_jmp_buf_ = 0; +static jmp_buf acutest_abort_jmp_buf_; + +static int +acutest_count_(enum acutest_state_ state) +{ + int i, n; + + for(i = 0, n = 0; i < acutest_list_size_; i++) { + if(acutest_test_data_[i].state == state) + n++; + } + + return n; +} + +static void +acutest_cleanup_(void) +{ + free((void*) acutest_test_data_); +} + +static void ACUTEST_ATTRIBUTE_(noreturn) +acutest_exit_(int exit_code) +{ + acutest_cleanup_(); + exit(exit_code); +} + + +#if defined ACUTEST_WIN_ + typedef LARGE_INTEGER acutest_timer_type_; + static LARGE_INTEGER acutest_timer_freq_; + static acutest_timer_type_ acutest_timer_start_; + static acutest_timer_type_ acutest_timer_end_; + + static void + acutest_timer_init_(void) + { + QueryPerformanceFrequency(´st_timer_freq_); + } + + static void + acutest_timer_get_time_(LARGE_INTEGER* ts) + { + QueryPerformanceCounter(ts); + } + + static double + acutest_timer_diff_(LARGE_INTEGER start, LARGE_INTEGER end) + { + double duration = (double)(end.QuadPart - start.QuadPart); + duration /= (double)acutest_timer_freq_.QuadPart; + return duration; + } + + static void + acutest_timer_print_diff_(void) + { + printf("%.6lf secs", acutest_timer_diff_(acutest_timer_start_, acutest_timer_end_)); + } +#elif defined ACUTEST_HAS_POSIX_TIMER_ + static clockid_t acutest_timer_id_; + typedef struct timespec acutest_timer_type_; + static acutest_timer_type_ acutest_timer_start_; + static acutest_timer_type_ acutest_timer_end_; + + static void + acutest_timer_init_(void) + { + if(acutest_timer_ == 1) + acutest_timer_id_ = CLOCK_MONOTONIC; + else if(acutest_timer_ == 2) + acutest_timer_id_ = CLOCK_PROCESS_CPUTIME_ID; + } + + static void + acutest_timer_get_time_(struct timespec* ts) + { + clock_gettime(acutest_timer_id_, ts); + } + + static double + acutest_timer_diff_(struct timespec start, struct timespec end) + { + return (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) / 1e9; + } + + static void + acutest_timer_print_diff_(void) + { + printf("%.6lf secs", + acutest_timer_diff_(acutest_timer_start_, acutest_timer_end_)); + } +#else + typedef int acutest_timer_type_; + static acutest_timer_type_ acutest_timer_start_; + static acutest_timer_type_ acutest_timer_end_; + + void + acutest_timer_init_(void) + {} + + static void + acutest_timer_get_time_(int* ts) + { + (void) ts; + } + + static double + acutest_timer_diff_(int start, int end) + { + (void) start; + (void) end; + return 0.0; + } + + static void + acutest_timer_print_diff_(void) + {} +#endif + +#define ACUTEST_COLOR_DEFAULT_ 0 +#define ACUTEST_COLOR_RED_ 1 +#define ACUTEST_COLOR_GREEN_ 2 +#define ACUTEST_COLOR_YELLOW_ 3 +#define ACUTEST_COLOR_DEFAULT_INTENSIVE_ 10 +#define ACUTEST_COLOR_RED_INTENSIVE_ 11 +#define ACUTEST_COLOR_GREEN_INTENSIVE_ 12 +#define ACUTEST_COLOR_YELLOW_INTENSIVE_ 13 + +static int ACUTEST_ATTRIBUTE_(format (printf, 2, 3)) +acutest_colored_printf_(int color, const char* fmt, ...) +{ + va_list args; + char buffer[256]; + int n; + + va_start(args, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, args); + va_end(args); + buffer[sizeof(buffer)-1] = '\0'; + + if(!acutest_colorize_) { + return printf("%s", buffer); + } + +#if defined ACUTEST_UNIX_ + { + const char* col_str; + switch(color) { + case ACUTEST_COLOR_RED_: col_str = "\033[0;31m"; break; + case ACUTEST_COLOR_GREEN_: col_str = "\033[0;32m"; break; + case ACUTEST_COLOR_YELLOW_: col_str = "\033[0;33m"; break; + case ACUTEST_COLOR_RED_INTENSIVE_: col_str = "\033[1;31m"; break; + case ACUTEST_COLOR_GREEN_INTENSIVE_: col_str = "\033[1;32m"; break; + case ACUTEST_COLOR_YELLOW_INTENSIVE_: col_str = "\033[1;33m"; break; + case ACUTEST_COLOR_DEFAULT_INTENSIVE_: col_str = "\033[1m"; break; + default: col_str = "\033[0m"; break; + } + printf("%s", col_str); + n = printf("%s", buffer); + printf("\033[0m"); + return n; + } +#elif defined ACUTEST_WIN_ + { + HANDLE h; + CONSOLE_SCREEN_BUFFER_INFO info; + WORD attr; + + h = GetStdHandle(STD_OUTPUT_HANDLE); + GetConsoleScreenBufferInfo(h, &info); + + switch(color) { + case ACUTEST_COLOR_RED_: attr = FOREGROUND_RED; break; + case ACUTEST_COLOR_GREEN_: attr = FOREGROUND_GREEN; break; + case ACUTEST_COLOR_YELLOW_: attr = FOREGROUND_RED | FOREGROUND_GREEN; break; + case ACUTEST_COLOR_RED_INTENSIVE_: attr = FOREGROUND_RED | FOREGROUND_INTENSITY; break; + case ACUTEST_COLOR_GREEN_INTENSIVE_: attr = FOREGROUND_GREEN | FOREGROUND_INTENSITY; break; + case ACUTEST_COLOR_DEFAULT_INTENSIVE_: attr = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY; break; + case ACUTEST_COLOR_YELLOW_INTENSIVE_: attr = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; break; + default: attr = 0; break; + } + if(attr != 0) + SetConsoleTextAttribute(h, attr); + n = printf("%s", buffer); + SetConsoleTextAttribute(h, info.wAttributes); + return n; + } +#else + n = printf("%s", buffer); + return n; +#endif +} + +static const char* +acutest_basename_(const char* path) +{ + const char* name; + + name = strrchr(path, '/'); + if(name != NULL) + name++; + else + name = path; + +#ifdef ACUTEST_WIN_ + { + const char* alt_name; + + alt_name = strrchr(path, '\\'); + if(alt_name != NULL) + alt_name++; + else + alt_name = path; + + if(alt_name > name) + name = alt_name; + } +#endif + + return name; +} + +static void +acutest_begin_test_line_(const struct acutest_test_* test) +{ + if(!acutest_tap_) { + if(acutest_verbose_level_ >= 3) { + acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Test %s:\n", test->name); + acutest_test_already_logged_++; + } else if(acutest_verbose_level_ >= 1) { + int n; + char spaces[48]; + + n = acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Test %s... ", test->name); + memset(spaces, ' ', sizeof(spaces)); + if(n < (int) sizeof(spaces)) + printf("%.*s", (int) sizeof(spaces) - n, spaces); + } else { + acutest_test_already_logged_ = 1; + } + } +} + +static void +acutest_finish_test_line_(enum acutest_state_ state) +{ + if(acutest_tap_) { + printf("%s %d - %s%s\n", + (state == ACUTEST_STATE_SUCCESS || state == ACUTEST_STATE_SKIPPED) ? "ok" : "not ok", + acutest_current_index_ + 1, + acutest_current_test_->name, + (state == ACUTEST_STATE_SKIPPED) ? " # SKIP" : ""); + + if(state == ACUTEST_STATE_SUCCESS && acutest_timer_) { + printf("# Duration: "); + acutest_timer_print_diff_(); + printf("\n"); + } + } else { + int color; + const char* str; + + switch(state) { + case ACUTEST_STATE_SUCCESS: color = ACUTEST_COLOR_GREEN_INTENSIVE_; str = "OK"; break; + case ACUTEST_STATE_SKIPPED: color = ACUTEST_COLOR_YELLOW_INTENSIVE_; str = "SKIPPED"; break; + case ACUTEST_STATE_FAILED: /* Fall through. */ + default: color = ACUTEST_COLOR_RED_INTENSIVE_; str = "FAILED"; break; + } + + printf("[ "); + acutest_colored_printf_(color, "%s", str); + printf(" ]"); + + if(state == ACUTEST_STATE_SUCCESS && acutest_timer_) { + printf(" "); + acutest_timer_print_diff_(); + } + + printf("\n"); + } +} + +static void +acutest_line_indent_(int level) +{ + static const char spaces[] = " "; + int n = level * 2; + + if(acutest_tap_ && n > 0) { + n--; + printf("#"); + } + + while(n > 16) { + printf("%s", spaces); + n -= 16; + } + printf("%.*s", n, spaces); +} + +void ACUTEST_ATTRIBUTE_(format (printf, 3, 4)) +acutest_skip_(const char* file, int line, const char* fmt, ...) +{ + va_list args; + size_t reason_len; + + va_start(args, fmt); + vsnprintf(acutest_test_skip_reason_, sizeof(acutest_test_skip_reason_), fmt, args); + va_end(args); + acutest_test_skip_reason_[sizeof(acutest_test_skip_reason_)-1] = '\0'; + + /* Remove final dot, if provided; that collides with our other logic. */ + reason_len = strlen(acutest_test_skip_reason_); + if(acutest_test_skip_reason_[reason_len-1] == '.') + acutest_test_skip_reason_[reason_len-1] = '\0'; + + if(acutest_test_check_count_ > 0) { + acutest_check_(0, file, line, "Cannot skip, already performed some checks"); + return; + } + + if(acutest_verbose_level_ >= 2) { + const char *result_str = "skipped"; + int result_color = ACUTEST_COLOR_YELLOW_; + + if(!acutest_test_already_logged_ && acutest_current_test_ != NULL) + acutest_finish_test_line_(ACUTEST_STATE_SKIPPED); + acutest_test_already_logged_++; + + acutest_line_indent_(1); + + if(file != NULL) { + file = acutest_basename_(file); + printf("%s:%d: ", file, line); + } + + printf("%s... ", acutest_test_skip_reason_); + acutest_colored_printf_(result_color, "%s", result_str); + printf("\n"); + acutest_test_already_logged_++; + } + + acutest_test_skip_count_++; +} + +int ACUTEST_ATTRIBUTE_(format (printf, 4, 5)) +acutest_check_(int cond, const char* file, int line, const char* fmt, ...) +{ + const char *result_str; + int result_color; + int verbose_level; + + if(acutest_test_skip_count_) { + /* We've skipped the test. We shouldn't be here: The test implementation + * should have already return before. So lets suppress the following + * output. */ + cond = 1; + goto skip_check; + } + + if(cond) { + result_str = "ok"; + result_color = ACUTEST_COLOR_GREEN_; + verbose_level = 3; + } else { + if(!acutest_test_already_logged_ && acutest_current_test_ != NULL) + acutest_finish_test_line_(ACUTEST_STATE_FAILED); + + acutest_test_failures_++; + acutest_test_already_logged_++; + + result_str = "failed"; + result_color = ACUTEST_COLOR_RED_; + verbose_level = 2; + } + + if(acutest_verbose_level_ >= verbose_level) { + va_list args; + + if(!acutest_case_already_logged_ && acutest_case_name_[0]) { + acutest_line_indent_(1); + acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Case %s:\n", acutest_case_name_); + acutest_test_already_logged_++; + acutest_case_already_logged_++; + } + + acutest_line_indent_(acutest_case_name_[0] ? 2 : 1); + if(file != NULL) { + file = acutest_basename_(file); + printf("%s:%d: ", file, line); + } + + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + + printf("... "); + acutest_colored_printf_(result_color, "%s", result_str); + printf("\n"); + acutest_test_already_logged_++; + } + + acutest_test_check_count_++; + +skip_check: + acutest_cond_failed_ = (cond == 0); + return !acutest_cond_failed_; +} + +void ACUTEST_ATTRIBUTE_(format (printf, 1, 2)) +acutest_case_(const char* fmt, ...) +{ + va_list args; + + if(acutest_verbose_level_ < 2) + return; + + if(acutest_case_name_[0]) { + acutest_case_already_logged_ = 0; + acutest_case_name_[0] = '\0'; + } + + if(fmt == NULL) + return; + + va_start(args, fmt); + vsnprintf(acutest_case_name_, sizeof(acutest_case_name_) - 1, fmt, args); + va_end(args); + acutest_case_name_[sizeof(acutest_case_name_) - 1] = '\0'; + + if(acutest_verbose_level_ >= 3) { + acutest_line_indent_(1); + acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Case %s:\n", acutest_case_name_); + acutest_test_already_logged_++; + acutest_case_already_logged_++; + } +} + +void ACUTEST_ATTRIBUTE_(format (printf, 1, 2)) +acutest_message_(const char* fmt, ...) +{ + char buffer[TEST_MSG_MAXSIZE]; + char* line_beg; + char* line_end; + va_list args; + + if(acutest_verbose_level_ < 2) + return; + + /* We allow extra message only when something is already wrong in the + * current test. */ + if(acutest_current_test_ == NULL || !acutest_cond_failed_) + return; + + va_start(args, fmt); + vsnprintf(buffer, TEST_MSG_MAXSIZE, fmt, args); + va_end(args); + buffer[TEST_MSG_MAXSIZE-1] = '\0'; + + line_beg = buffer; + while(1) { + line_end = strchr(line_beg, '\n'); + if(line_end == NULL) + break; + acutest_line_indent_(acutest_case_name_[0] ? 3 : 2); + printf("%.*s\n", (int)(line_end - line_beg), line_beg); + line_beg = line_end + 1; + } + if(line_beg[0] != '\0') { + acutest_line_indent_(acutest_case_name_[0] ? 3 : 2); + printf("%s\n", line_beg); + } +} + +void +acutest_dump_(const char* title, const void* addr, size_t size) +{ + static const size_t BYTES_PER_LINE = 16; + size_t line_beg; + size_t truncate = 0; + + if(acutest_verbose_level_ < 2) + return; + + /* We allow extra message only when something is already wrong in the + * current test. */ + if(acutest_current_test_ == NULL || !acutest_cond_failed_) + return; + + if(size > TEST_DUMP_MAXSIZE) { + truncate = size - TEST_DUMP_MAXSIZE; + size = TEST_DUMP_MAXSIZE; + } + + acutest_line_indent_(acutest_case_name_[0] ? 3 : 2); + printf((title[strlen(title)-1] == ':') ? "%s\n" : "%s:\n", title); + + for(line_beg = 0; line_beg < size; line_beg += BYTES_PER_LINE) { + size_t line_end = line_beg + BYTES_PER_LINE; + size_t off; + + acutest_line_indent_(acutest_case_name_[0] ? 4 : 3); + printf("%08lx: ", (unsigned long)line_beg); + for(off = line_beg; off < line_end; off++) { + if(off < size) + printf(" %02x", ((const unsigned char*)addr)[off]); + else + printf(" "); + } + + printf(" "); + for(off = line_beg; off < line_end; off++) { + unsigned char byte = ((const unsigned char*)addr)[off]; + if(off < size) + printf("%c", (iscntrl(byte) ? '.' : byte)); + else + break; + } + + printf("\n"); + } + + if(truncate > 0) { + acutest_line_indent_(acutest_case_name_[0] ? 4 : 3); + printf(" ... (and more %u bytes)\n", (unsigned) truncate); + } +} + +/* This is called just before each test */ +static void +acutest_init_(const char *test_name) +{ +#ifdef TEST_INIT + TEST_INIT + ; /* Allow for a single unterminated function call */ +#endif + + /* Suppress any warnings about unused variable. */ + (void) test_name; +} + +/* This is called after each test */ +static void +acutest_fini_(const char *test_name) +{ +#ifdef TEST_FINI + TEST_FINI + ; /* Allow for a single unterminated function call */ +#endif + + /* Suppress any warnings about unused variable. */ + (void) test_name; +} + +void +acutest_abort_(void) +{ + if(acutest_abort_has_jmp_buf_) { + longjmp(acutest_abort_jmp_buf_, 1); + } else { + if(acutest_current_test_ != NULL) + acutest_fini_(acutest_current_test_->name); + fflush(stdout); + fflush(stderr); + acutest_exit_(ACUTEST_STATE_FAILED); + } +} + +static void +acutest_list_names_(void) +{ + const struct acutest_test_* test; + + printf("Unit tests:\n"); + for(test = ´st_list_[0]; test->func != NULL; test++) + printf(" %s\n", test->name); +} + +static int +acutest_name_contains_word_(const char* name, const char* pattern) +{ + static const char word_delim[] = " \t-_/.,:;"; + const char* substr; + size_t pattern_len; + + pattern_len = strlen(pattern); + + substr = strstr(name, pattern); + while(substr != NULL) { + int starts_on_word_boundary = (substr == name || strchr(word_delim, substr[-1]) != NULL); + int ends_on_word_boundary = (substr[pattern_len] == '\0' || strchr(word_delim, substr[pattern_len]) != NULL); + + if(starts_on_word_boundary && ends_on_word_boundary) + return 1; + + substr = strstr(substr+1, pattern); + } + + return 0; +} + +static int +acutest_select_(const char* pattern) +{ + int i; + int n = 0; + + /* Try exact match. */ + for(i = 0; i < acutest_list_size_; i++) { + if(strcmp(acutest_list_[i].name, pattern) == 0) { + acutest_test_data_[i].state = ACUTEST_STATE_SELECTED; + n++; + break; + } + } + if(n > 0) + return n; + + /* Try word match. */ + for(i = 0; i < acutest_list_size_; i++) { + if(acutest_name_contains_word_(acutest_list_[i].name, pattern)) { + acutest_test_data_[i].state = ACUTEST_STATE_SELECTED; + n++; + } + } + if(n > 0) + return n; + + /* Try relaxed match. */ + for(i = 0; i < acutest_list_size_; i++) { + if(strstr(acutest_list_[i].name, pattern) != NULL) { + acutest_test_data_[i].state = ACUTEST_STATE_SELECTED; + n++; + } + } + + return n; +} + + +/* Called if anything goes bad in Acutest, or if the unit test ends in other + * way then by normal returning from its function (e.g. exception or some + * abnormal child process termination). */ +static void ACUTEST_ATTRIBUTE_(format (printf, 1, 2)) +acutest_error_(const char* fmt, ...) +{ + if(acutest_verbose_level_ == 0) + return; + + if(acutest_verbose_level_ >= 2) { + va_list args; + + acutest_line_indent_(1); + if(acutest_verbose_level_ >= 3) + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "ERROR: "); + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + printf("\n"); + } + + if(acutest_verbose_level_ >= 3) { + printf("\n"); + } +} + +/* Call directly the given test unit function. */ +static enum acutest_state_ +acutest_do_run_(const struct acutest_test_* test, int index) +{ + enum acutest_state_ state = ACUTEST_STATE_FAILED; + + acutest_current_test_ = test; + acutest_current_index_ = index; + acutest_test_failures_ = 0; + acutest_test_already_logged_ = 0; + acutest_test_check_count_ = 0; + acutest_test_skip_count_ = 0; + acutest_cond_failed_ = 0; + +#ifdef __cplusplus +#ifndef TEST_NO_EXCEPTIONS + try { +#endif +#endif + acutest_init_(test->name); + acutest_begin_test_line_(test); + + /* This is good to do in case the test unit crashes. */ + fflush(stdout); + fflush(stderr); + + if(!acutest_worker_) { + acutest_abort_has_jmp_buf_ = 1; + if(setjmp(acutest_abort_jmp_buf_) != 0) + goto aborted; + } + + acutest_timer_get_time_(´st_timer_start_); + test->func(); + +aborted: + acutest_abort_has_jmp_buf_ = 0; + acutest_timer_get_time_(´st_timer_end_); + + if(acutest_test_failures_ > 0) + state = ACUTEST_STATE_FAILED; + else if(acutest_test_skip_count_ > 0) + state = ACUTEST_STATE_SKIPPED; + else + state = ACUTEST_STATE_SUCCESS; + + if(!acutest_test_already_logged_) + acutest_finish_test_line_(state); + + if(acutest_verbose_level_ >= 3) { + acutest_line_indent_(1); + switch(state) { + case ACUTEST_STATE_SUCCESS: + acutest_colored_printf_(ACUTEST_COLOR_GREEN_INTENSIVE_, "SUCCESS: "); + printf("All conditions have passed.\n"); + + if(acutest_timer_) { + acutest_line_indent_(1); + printf("Duration: "); + acutest_timer_print_diff_(); + printf("\n"); + } + break; + + case ACUTEST_STATE_SKIPPED: + acutest_colored_printf_(ACUTEST_COLOR_YELLOW_INTENSIVE_, "SKIPPED: "); + printf("%s.\n", acutest_test_skip_reason_); + break; + + default: + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "FAILED: "); + printf("%d condition%s %s failed.\n", + acutest_test_failures_, + (acutest_test_failures_ == 1) ? "" : "s", + (acutest_test_failures_ == 1) ? "has" : "have"); + break; + } + printf("\n"); + } + +#ifdef __cplusplus +#ifndef TEST_NO_EXCEPTIONS + } catch(std::exception& e) { + const char* what = e.what(); + acutest_check_(0, NULL, 0, "Threw std::exception"); + if(what != NULL) + acutest_message_("std::exception::what(): %s", what); + + if(acutest_verbose_level_ >= 3) { + acutest_line_indent_(1); + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "FAILED: "); + printf("C++ exception.\n\n"); + } + } catch(...) { + acutest_check_(0, NULL, 0, "Threw an exception"); + + if(acutest_verbose_level_ >= 3) { + acutest_line_indent_(1); + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "FAILED: "); + printf("C++ exception.\n\n"); + } + } +#endif +#endif + + acutest_fini_(test->name); + acutest_case_(NULL); + acutest_current_test_ = NULL; + + return state; +} + +/* Trigger the unit test. If possible (and not suppressed) it starts a child + * process who calls acutest_do_run_(), otherwise it calls acutest_do_run_() + * directly. */ +static void +acutest_run_(const struct acutest_test_* test, int index, int master_index) +{ + enum acutest_state_ state = ACUTEST_STATE_FAILED; + acutest_timer_type_ start, end; + + acutest_current_test_ = test; + acutest_test_already_logged_ = 0; + acutest_timer_get_time_(&start); + + if(!acutest_no_exec_) { + +#if defined(ACUTEST_UNIX_) + + pid_t pid; + int exit_code; + + /* Make sure the child starts with empty I/O buffers. */ + fflush(stdout); + fflush(stderr); + + pid = fork(); + if(pid == (pid_t)-1) { + acutest_error_("Cannot fork. %s [%d]", strerror(errno), errno); + } else if(pid == 0) { + /* Child: Do the test. */ + acutest_worker_ = 1; + state = acutest_do_run_(test, index); + acutest_exit_((int) state); + } else { + /* Parent: Wait until child terminates and analyze its exit code. */ + waitpid(pid, &exit_code, 0); + if(WIFEXITED(exit_code)) { + state = (enum acutest_state_) WEXITSTATUS(exit_code); + } else if(WIFSIGNALED(exit_code)) { + char tmp[32]; + const char* signame; + switch(WTERMSIG(exit_code)) { + case SIGINT: signame = "SIGINT"; break; + case SIGHUP: signame = "SIGHUP"; break; + case SIGQUIT: signame = "SIGQUIT"; break; + case SIGABRT: signame = "SIGABRT"; break; + case SIGKILL: signame = "SIGKILL"; break; + case SIGSEGV: signame = "SIGSEGV"; break; + case SIGILL: signame = "SIGILL"; break; + case SIGTERM: signame = "SIGTERM"; break; + default: snprintf(tmp, sizeof(tmp), "signal %d", WTERMSIG(exit_code)); signame = tmp; break; + } + acutest_error_("Test interrupted by %s.", signame); + } else { + acutest_error_("Test ended in an unexpected way [%d].", exit_code); + } + } + +#elif defined(ACUTEST_WIN_) + + char buffer[512] = {0}; + STARTUPINFOA startupInfo; + PROCESS_INFORMATION processInfo; + DWORD exitCode; + + /* Windows has no fork(). So we propagate all info into the child + * through a command line arguments. */ + snprintf(buffer, sizeof(buffer), + "%s --worker=%d %s --no-exec --no-summary %s --verbose=%d --color=%s -- \"%s\"", + acutest_argv0_, index, acutest_timer_ ? "--time" : "", + acutest_tap_ ? "--tap" : "", acutest_verbose_level_, + acutest_colorize_ ? "always" : "never", + test->name); + memset(&startupInfo, 0, sizeof(startupInfo)); + startupInfo.cb = sizeof(STARTUPINFO); + if(CreateProcessA(NULL, buffer, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo)) { + WaitForSingleObject(processInfo.hProcess, INFINITE); + GetExitCodeProcess(processInfo.hProcess, &exitCode); + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + switch(exitCode) { + case 0: state = ACUTEST_STATE_SUCCESS; break; + case 1: state = ACUTEST_STATE_FAILED; break; + case 2: state = ACUTEST_STATE_SKIPPED; break; + case 3: acutest_error_("Aborted."); break; + case 0xC0000005: acutest_error_("Access violation."); break; + default: acutest_error_("Test ended in an unexpected way [%lu].", exitCode); break; + } + } else { + acutest_error_("Cannot create unit test subprocess [%ld].", GetLastError()); + } + +#else + + /* A platform where we don't know how to run child process. */ + state = acutest_do_run_(test, index); + +#endif + + } else { + /* Child processes suppressed through --no-exec. */ + state = acutest_do_run_(test, index); + } + acutest_timer_get_time_(&end); + + acutest_current_test_ = NULL; + + acutest_test_data_[master_index].state = state; + acutest_test_data_[master_index].duration = acutest_timer_diff_(start, end); +} + +#if defined(ACUTEST_WIN_) +/* Callback for SEH events. */ +static LONG CALLBACK +acutest_seh_exception_filter_(EXCEPTION_POINTERS *ptrs) +{ + acutest_check_(0, NULL, 0, "Unhandled SEH exception"); + acutest_message_("Exception code: 0x%08lx", ptrs->ExceptionRecord->ExceptionCode); + acutest_message_("Exception address: 0x%p", ptrs->ExceptionRecord->ExceptionAddress); + + fflush(stdout); + fflush(stderr); + + return EXCEPTION_EXECUTE_HANDLER; +} +#endif + + +#define ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ 0x0001 +#define ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_ 0x0002 + +#define ACUTEST_CMDLINE_OPTID_NONE_ 0 +#define ACUTEST_CMDLINE_OPTID_UNKNOWN_ (-0x7fffffff + 0) +#define ACUTEST_CMDLINE_OPTID_MISSINGARG_ (-0x7fffffff + 1) +#define ACUTEST_CMDLINE_OPTID_BOGUSARG_ (-0x7fffffff + 2) + +typedef struct acutest_test_CMDLINE_OPTION_ { + char shortname; + const char* longname; + int id; + unsigned flags; +} ACUTEST_CMDLINE_OPTION_; + +static int +acutest_cmdline_handle_short_opt_group_(const ACUTEST_CMDLINE_OPTION_* options, + const char* arggroup, + int (*callback)(int /*optval*/, const char* /*arg*/)) +{ + const ACUTEST_CMDLINE_OPTION_* opt; + int i; + int ret = 0; + + for(i = 0; arggroup[i] != '\0'; i++) { + for(opt = options; opt->id != 0; opt++) { + if(arggroup[i] == opt->shortname) + break; + } + + if(opt->id != 0 && !(opt->flags & ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_)) { + ret = callback(opt->id, NULL); + } else { + /* Unknown option. */ + char badoptname[3]; + badoptname[0] = '-'; + badoptname[1] = arggroup[i]; + badoptname[2] = '\0'; + ret = callback((opt->id != 0 ? ACUTEST_CMDLINE_OPTID_MISSINGARG_ : ACUTEST_CMDLINE_OPTID_UNKNOWN_), + badoptname); + } + + if(ret != 0) + break; + } + + return ret; +} + +#define ACUTEST_CMDLINE_AUXBUF_SIZE_ 32 + +static int +acutest_cmdline_read_(const ACUTEST_CMDLINE_OPTION_* options, int argc, char** argv, + int (*callback)(int /*optval*/, const char* /*arg*/)) +{ + + const ACUTEST_CMDLINE_OPTION_* opt; + char auxbuf[ACUTEST_CMDLINE_AUXBUF_SIZE_+1]; + int after_doubledash = 0; + int i = 1; + int ret = 0; + + auxbuf[ACUTEST_CMDLINE_AUXBUF_SIZE_] = '\0'; + + while(i < argc) { + if(after_doubledash || strcmp(argv[i], "-") == 0) { + /* Non-option argument. */ + ret = callback(ACUTEST_CMDLINE_OPTID_NONE_, argv[i]); + } else if(strcmp(argv[i], "--") == 0) { + /* End of options. All the remaining members are non-option arguments. */ + after_doubledash = 1; + } else if(argv[i][0] != '-') { + /* Non-option argument. */ + ret = callback(ACUTEST_CMDLINE_OPTID_NONE_, argv[i]); + } else { + for(opt = options; opt->id != 0; opt++) { + if(opt->longname != NULL && strncmp(argv[i], "--", 2) == 0) { + size_t len = strlen(opt->longname); + if(strncmp(argv[i]+2, opt->longname, len) == 0) { + /* Regular long option. */ + if(argv[i][2+len] == '\0') { + /* with no argument provided. */ + if(!(opt->flags & ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_)) + ret = callback(opt->id, NULL); + else + ret = callback(ACUTEST_CMDLINE_OPTID_MISSINGARG_, argv[i]); + break; + } else if(argv[i][2+len] == '=') { + /* with an argument provided. */ + if(opt->flags & (ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ | ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_)) { + ret = callback(opt->id, argv[i]+2+len+1); + } else { + snprintf(auxbuf, sizeof(auxbuf), "--%s", opt->longname); + ret = callback(ACUTEST_CMDLINE_OPTID_BOGUSARG_, auxbuf); + } + break; + } else { + continue; + } + } + } else if(opt->shortname != '\0' && argv[i][0] == '-') { + if(argv[i][1] == opt->shortname) { + /* Regular short option. */ + if(opt->flags & ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_) { + if(argv[i][2] != '\0') + ret = callback(opt->id, argv[i]+2); + else if(i+1 < argc) + ret = callback(opt->id, argv[++i]); + else + ret = callback(ACUTEST_CMDLINE_OPTID_MISSINGARG_, argv[i]); + break; + } else { + ret = callback(opt->id, NULL); + + /* There might be more (argument-less) short options + * grouped together. */ + if(ret == 0 && argv[i][2] != '\0') + ret = acutest_cmdline_handle_short_opt_group_(options, argv[i]+2, callback); + break; + } + } + } + } + + if(opt->id == 0) { /* still not handled? */ + if(argv[i][0] != '-') { + /* Non-option argument. */ + ret = callback(ACUTEST_CMDLINE_OPTID_NONE_, argv[i]); + } else { + /* Unknown option. */ + char* badoptname = argv[i]; + + if(strncmp(badoptname, "--", 2) == 0) { + /* Strip any argument from the long option. */ + char* assignment = strchr(badoptname, '='); + if(assignment != NULL) { + size_t len = (size_t)(assignment - badoptname); + if(len > ACUTEST_CMDLINE_AUXBUF_SIZE_) + len = ACUTEST_CMDLINE_AUXBUF_SIZE_; + strncpy(auxbuf, badoptname, len); + auxbuf[len] = '\0'; + badoptname = auxbuf; + } + } + + ret = callback(ACUTEST_CMDLINE_OPTID_UNKNOWN_, badoptname); + } + } + } + + if(ret != 0) + return ret; + i++; + } + + return ret; +} + +static void +acutest_help_(void) +{ + printf("Usage: %s [options] [test...]\n", acutest_argv0_); + printf("\n"); + printf("Run the specified unit tests; or if the option '--exclude' is used, run all\n"); + printf("tests in the suite but those listed. By default, if no tests are specified\n"); + printf("on the command line, all unit tests in the suite are run.\n"); + printf("\n"); + printf("Options:\n"); + printf(" -X, --exclude Execute all unit tests but the listed ones\n"); + printf(" --exec[=WHEN] If supported, execute unit tests as child processes\n"); + printf(" (WHEN is one of 'auto', 'always', 'never')\n"); + printf(" -E, --no-exec Same as --exec=never\n"); +#if defined ACUTEST_WIN_ + printf(" -t, --time Measure test duration\n"); +#elif defined ACUTEST_HAS_POSIX_TIMER_ + printf(" -t, --time Measure test duration (real time)\n"); + printf(" --time=TIMER Measure test duration, using given timer\n"); + printf(" (TIMER is one of 'real', 'cpu')\n"); +#endif + printf(" --no-summary Suppress printing of test results summary\n"); + printf(" --tap Produce TAP-compliant output\n"); + printf(" (See https://testanything.org/)\n"); + printf(" -x, --xml-output=FILE Enable XUnit output to the given file\n"); + printf(" -l, --list List unit tests in the suite and exit\n"); + printf(" -v, --verbose Make output more verbose\n"); + printf(" --verbose=LEVEL Set verbose level to LEVEL:\n"); + printf(" 0 ... Be silent\n"); + printf(" 1 ... Output one line per test (and summary)\n"); + printf(" 2 ... As 1 and failed conditions (this is default)\n"); + printf(" 3 ... As 1 and all conditions (and extended summary)\n"); + printf(" -q, --quiet Same as --verbose=0\n"); + printf(" --color[=WHEN] Enable colorized output\n"); + printf(" (WHEN is one of 'auto', 'always', 'never')\n"); + printf(" --no-color Same as --color=never\n"); + printf(" -h, --help Display this help and exit\n"); + + if(acutest_list_size_ < 16) { + printf("\n"); + acutest_list_names_(); + } +} + +static const ACUTEST_CMDLINE_OPTION_ acutest_cmdline_options_[] = { + { 'X', "exclude", 'X', 0 }, + { 's', "skip", 'X', 0 }, /* kept for compatibility, use --exclude instead */ + { 0, "exec", 'e', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, + { 'E', "no-exec", 'E', 0 }, +#if defined ACUTEST_WIN_ + { 't', "time", 't', 0 }, + { 0, "timer", 't', 0 }, /* kept for compatibility */ +#elif defined ACUTEST_HAS_POSIX_TIMER_ + { 't', "time", 't', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, + { 0, "timer", 't', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, /* kept for compatibility */ +#endif + { 0, "no-summary", 'S', 0 }, + { 0, "tap", 'T', 0 }, + { 'l', "list", 'l', 0 }, + { 'v', "verbose", 'v', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, + { 'q', "quiet", 'q', 0 }, + { 0, "color", 'c', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, + { 0, "no-color", 'C', 0 }, + { 'h', "help", 'h', 0 }, + { 0, "worker", 'w', ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_ }, /* internal */ + { 'x', "xml-output", 'x', ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_ }, + { 0, NULL, 0, 0 } +}; + +static int +acutest_cmdline_callback_(int id, const char* arg) +{ + switch(id) { + case 'X': + acutest_exclude_mode_ = 1; + break; + + case 'e': + if(arg == NULL || strcmp(arg, "always") == 0) { + acutest_no_exec_ = 0; + } else if(strcmp(arg, "never") == 0) { + acutest_no_exec_ = 1; + } else if(strcmp(arg, "auto") == 0) { + /*noop*/ + } else { + fprintf(stderr, "%s: Unrecognized argument '%s' for option --exec.\n", acutest_argv0_, arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + } + break; + + case 'E': + acutest_no_exec_ = 1; + break; + + case 't': +#if defined ACUTEST_WIN_ || defined ACUTEST_HAS_POSIX_TIMER_ + if(arg == NULL || strcmp(arg, "real") == 0) { + acutest_timer_ = 1; + #ifndef ACUTEST_WIN_ + } else if(strcmp(arg, "cpu") == 0) { + acutest_timer_ = 2; + #endif + } else { + fprintf(stderr, "%s: Unrecognized argument '%s' for option --time.\n", acutest_argv0_, arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + } +#endif + break; + + case 'S': + acutest_no_summary_ = 1; + break; + + case 'T': + acutest_tap_ = 1; + break; + + case 'l': + acutest_list_names_(); + acutest_exit_(0); + break; + + case 'v': + acutest_verbose_level_ = (arg != NULL ? atoi(arg) : acutest_verbose_level_+1); + break; + + case 'q': + acutest_verbose_level_ = 0; + break; + + case 'c': + if(arg == NULL || strcmp(arg, "always") == 0) { + acutest_colorize_ = 1; + } else if(strcmp(arg, "never") == 0) { + acutest_colorize_ = 0; + } else if(strcmp(arg, "auto") == 0) { + /*noop*/ + } else { + fprintf(stderr, "%s: Unrecognized argument '%s' for option --color.\n", acutest_argv0_, arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + } + break; + + case 'C': + acutest_colorize_ = 0; + break; + + case 'h': + acutest_help_(); + acutest_exit_(0); + break; + + case 'w': + acutest_worker_ = 1; + acutest_worker_index_ = atoi(arg); + break; + case 'x': + acutest_xml_output_ = fopen(arg, "w"); + if (!acutest_xml_output_) { + fprintf(stderr, "Unable to open '%s': %s\n", arg, strerror(errno)); + acutest_exit_(2); + } + break; + + case 0: + if(acutest_select_(arg) == 0) { + fprintf(stderr, "%s: Unrecognized unit test '%s'\n", acutest_argv0_, arg); + fprintf(stderr, "Try '%s --list' for list of unit tests.\n", acutest_argv0_); + acutest_exit_(2); + } + break; + + case ACUTEST_CMDLINE_OPTID_UNKNOWN_: + fprintf(stderr, "Unrecognized command line option '%s'.\n", arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + break; + + case ACUTEST_CMDLINE_OPTID_MISSINGARG_: + fprintf(stderr, "The command line option '%s' requires an argument.\n", arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + break; + + case ACUTEST_CMDLINE_OPTID_BOGUSARG_: + fprintf(stderr, "The command line option '%s' does not expect an argument.\n", arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + break; + } + + return 0; +} + +static int +acutest_under_debugger_(void) +{ +#ifdef ACUTEST_LINUX_ + /* Scan /proc/self/status for line "TracerPid: [PID]". If such line exists + * and the PID is non-zero, we're being debugged. */ + { + static const int OVERLAP = 32; + int fd; + char buf[512]; + size_t n_read; + pid_t tracer_pid = 0; + + /* Little trick so that we can treat the 1st line the same as any other + * and detect line start easily. */ + buf[0] = '\n'; + n_read = 1; + + fd = open("/proc/self/status", O_RDONLY); + if(fd != -1) { + while(1) { + static const char pattern[] = "\nTracerPid:"; + const char* field; + + while(n_read < sizeof(buf) - 1) { + ssize_t n; + + n = read(fd, buf + n_read, sizeof(buf) - 1 - n_read); + if(n <= 0) + break; + n_read += (size_t)n; + } + buf[n_read] = '\0'; + + field = strstr(buf, pattern); + if(field != NULL && field < buf + sizeof(buf) - OVERLAP) { + tracer_pid = (pid_t) atoi(field + sizeof(pattern) - 1); + break; + } + + if(n_read == sizeof(buf) - 1) { + /* Move the tail with the potentially incomplete line we're + * be looking for to the beginning of the buffer. + * (The OVERLAP must be large enough so the searched line + * can fit in completely.) */ + memmove(buf, buf + sizeof(buf) - 1 - OVERLAP, OVERLAP); + n_read = OVERLAP; + } else { + break; + } + } + + close(fd); + + if(tracer_pid != 0) + return 1; + } + } +#endif + +#ifdef ACUTEST_MACOS_ + /* See https://developer.apple.com/library/archive/qa/qa1361/_index.html */ + { + int mib[4]; + struct kinfo_proc info; + size_t size; + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + + size = sizeof(info); + info.kp_proc.p_flag = 0; + sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + + if(info.kp_proc.p_flag & P_TRACED) + return 1; + } +#endif + +#ifdef ACUTEST_WIN_ + if(IsDebuggerPresent()) + return 1; +#endif + +#ifdef RUNNING_ON_VALGRIND + /* We treat Valgrind as a debugger of sorts. + * (Macro RUNNING_ON_VALGRIND is provided by , if available.) */ + if(RUNNING_ON_VALGRIND) + return 1; +#endif + + return 0; +} + +int +main(int argc, char** argv) +{ + int i, index; + int exit_code = 1; + + acutest_argv0_ = argv[0]; + +#if defined ACUTEST_UNIX_ + acutest_colorize_ = isatty(STDOUT_FILENO); +#elif defined ACUTEST_WIN_ + #if defined _BORLANDC_ + acutest_colorize_ = isatty(_fileno(stdout)); + #else + acutest_colorize_ = _isatty(_fileno(stdout)); + #endif +#else + acutest_colorize_ = 0; +#endif + + /* Count all test units */ + acutest_list_size_ = 0; + for(i = 0; acutest_list_[i].func != NULL; i++) + acutest_list_size_++; + + acutest_test_data_ = (struct acutest_test_data_*)calloc(acutest_list_size_, sizeof(struct acutest_test_data_)); + if(acutest_test_data_ == NULL) { + fprintf(stderr, "Out of memory.\n"); + acutest_exit_(2); + } + + /* Parse options */ + acutest_cmdline_read_(acutest_cmdline_options_, argc, argv, acutest_cmdline_callback_); + + /* Initialize the proper timer. */ + acutest_timer_init_(); + +#if defined(ACUTEST_WIN_) + SetUnhandledExceptionFilter(acutest_seh_exception_filter_); +#ifdef _MSC_VER + _set_abort_behavior(0, _WRITE_ABORT_MSG); +#endif +#endif + + /* Determine what to run. */ + if(acutest_count_(ACUTEST_STATE_SELECTED) > 0) { + enum acutest_state_ if_selected; + enum acutest_state_ if_unselected; + + if(!acutest_exclude_mode_) { + if_selected = ACUTEST_STATE_NEEDTORUN; + if_unselected = ACUTEST_STATE_EXCLUDED; + } else { + if_selected = ACUTEST_STATE_EXCLUDED; + if_unselected = ACUTEST_STATE_NEEDTORUN; + } + + for(i = 0; acutest_list_[i].func != NULL; i++) { + if(acutest_test_data_[i].state == ACUTEST_STATE_SELECTED) + acutest_test_data_[i].state = if_selected; + else + acutest_test_data_[i].state = if_unselected; + } + } else { + /* By default, we want to run all tests. */ + for(i = 0; acutest_list_[i].func != NULL; i++) + acutest_test_data_[i].state = ACUTEST_STATE_NEEDTORUN; + } + + /* By default, we want to suppress running tests as child processes if we + * run just one test, or if we're under debugger: Debugging tests is then + * so much easier. */ + if(acutest_no_exec_ < 0) { + if(acutest_count_(ACUTEST_STATE_NEEDTORUN) <= 1 || acutest_under_debugger_()) + acutest_no_exec_ = 1; + else + acutest_no_exec_ = 0; + } + + if(acutest_tap_) { + /* TAP requires we know test result ("ok", "not ok") before we output + * anything about the test, and this gets problematic for larger verbose + * levels. */ + if(acutest_verbose_level_ > 2) + acutest_verbose_level_ = 2; + + /* TAP harness should provide some summary. */ + acutest_no_summary_ = 1; + + if(!acutest_worker_) + printf("1..%d\n", acutest_count_(ACUTEST_STATE_NEEDTORUN)); + } + + index = acutest_worker_index_; + for(i = 0; acutest_list_[i].func != NULL; i++) { + if(acutest_test_data_[i].state == ACUTEST_STATE_NEEDTORUN) + acutest_run_(´st_list_[i], index++, i); + } + + /* Write a summary */ + if(!acutest_no_summary_ && acutest_verbose_level_ >= 1) { + int n_run, n_success, n_failed ; + + n_run = acutest_list_size_ - acutest_count_(ACUTEST_STATE_EXCLUDED); + n_success = acutest_count_(ACUTEST_STATE_SUCCESS); + n_failed = acutest_count_(ACUTEST_STATE_FAILED); + + if(acutest_verbose_level_ >= 3) { + acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Summary:\n"); + + printf(" Count of run unit tests: %4d\n", n_run); + printf(" Count of successful unit tests: %4d\n", n_success); + printf(" Count of failed unit tests: %4d\n", n_failed); + } + + if(n_failed == 0) { + acutest_colored_printf_(ACUTEST_COLOR_GREEN_INTENSIVE_, "SUCCESS:"); + printf(" No unit tests have failed.\n"); + } else { + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "FAILED:"); + printf(" %d of %d unit tests %s failed.\n", + n_failed, n_run, (n_failed == 1) ? "has" : "have"); + } + + if(acutest_verbose_level_ >= 3) + printf("\n"); + } + + if (acutest_xml_output_) { + const char* suite_name = acutest_basename_(argv[0]); + fprintf(acutest_xml_output_, "\n"); + fprintf(acutest_xml_output_, "\n", + suite_name, + (int)acutest_list_size_, + acutest_count_(ACUTEST_STATE_FAILED), + acutest_count_(ACUTEST_STATE_SKIPPED) + acutest_count_(ACUTEST_STATE_EXCLUDED)); + for(i = 0; acutest_list_[i].func != NULL; i++) { + struct acutest_test_data_ *details = ´st_test_data_[i]; + const char* str_state; + fprintf(acutest_xml_output_, " \n", acutest_list_[i].name, details->duration); + + switch(details->state) { + case ACUTEST_STATE_SUCCESS: str_state = NULL; break; + case ACUTEST_STATE_EXCLUDED: /* Fall through. */ + case ACUTEST_STATE_SKIPPED: str_state = ""; break; + case ACUTEST_STATE_FAILED: /* Fall through. */ + default: str_state = ""; break; + } + + if(str_state != NULL) + fprintf(acutest_xml_output_, " %s\n", str_state); + fprintf(acutest_xml_output_, " \n"); + } + fprintf(acutest_xml_output_, "\n"); + fclose(acutest_xml_output_); + } + + if(acutest_worker_ && acutest_count_(ACUTEST_STATE_EXCLUDED)+1 == acutest_list_size_) { + /* If we are the child process, we need to propagate the test state + * without any moderation. */ + for(i = 0; acutest_list_[i].func != NULL; i++) { + if(acutest_test_data_[i].state != ACUTEST_STATE_EXCLUDED) { + exit_code = (int) acutest_test_data_[i].state; + break; + } + } + } else { + if(acutest_count_(ACUTEST_STATE_FAILED) > 0) + exit_code = 1; + else + exit_code = 0; + } + + acutest_cleanup_(); + return exit_code; +} + + +#endif /* #ifndef TEST_NO_MAIN */ + +#ifdef _MSC_VER + #pragma warning(pop) +#endif + +#ifdef __cplusplus + } /* extern "C" */ +#endif + +#endif /* #ifndef ACUTEST_H */ diff --git a/stage0/include/core_map.h b/stage0/include/core_map.h new file mode 100644 index 0000000..84a2bca --- /dev/null +++ b/stage0/include/core_map.h @@ -0,0 +1,175 @@ +#ifndef __CORE_MAP_H__ +#define __CORE_MAP_H__ + +#include +#include +#include +#include + +#ifndef nullptr +#define nullptr NULL +#endif +typedef size_t usize; + +#define MAP_TYPEOF __typeof__ + +/* 状态常量 */ +#define __MAP_SLOT_EMPTY 0 +#define __MAP_SLOT_OCCUPIED 1 +#define __MAP_SLOT_DELETED 2 + +/* 默认负载因子 factor/128 */ +#define MAP_DEFAULT_LOAD_FACTOR 70 + +/* ---------- 默认哈希/比较函数 ---------- */ +#define MAP_HASH_INT(k) ((usize)((k) * 2654435761U)) +#define MAP_CMP_INT(a, b) ((a) != (b)) + +static inline usize map_hash_str(const char *s) { + usize h = 5381; + while (*s) + h = ((h << 5) + h) + (unsigned char)*s++; + return h; +} +#define MAP_HASH_STR map_hash_str +#define MAP_CMP_STR strcmp + +/* ---------- 数据结构宏 ---------- */ +#define MAP_SLOT(key_t, val_t) \ + struct { \ + key_t key; \ + val_t val; \ + char state; \ + } + +#define MAP(key_t, val_t) \ + struct { \ + usize size; \ + usize cap; \ + MAP_SLOT(key_t, val_t) * data; \ + usize (*hash)(key_t); \ + int (*cmp)(key_t, key_t); \ + } + +/* ---------- 操作宏 ---------- */ + +/** 初始化,必须提供哈希和比较函数 */ +#define map_init(map, hash_fn, cmp_fn) \ + do { \ + (map).size = 0; \ + (map).cap = 0; \ + (map).data = nullptr; \ + (map).hash = (hash_fn); \ + (map).cmp = (cmp_fn); \ + } while (0) + +/** 释放内部数组 */ +#define map_free(map) \ + do { \ + free((map).data); \ + (map).data = nullptr; \ + (map).size = (map).cap = 0; \ + } while (0) + +/** 遍历所有有效元素 */ +#define map_for(map, idx) \ + for (usize(idx) = 0; (idx) < (map).cap; ++(idx)) \ + if ((map).data[(idx)].state == __MAP_SLOT_OCCUPIED) + +/** + * 插入(若键已存在则更新值) + * 注意:扩容使用 realloc,失败会 abort(可自行修改错误处理) + */ +#define map_put(map, _key, _val) \ + do { \ + /* 扩容 */ \ + if ((map).cap == 0 || \ + (map).size * 128 / (map).cap >= MAP_DEFAULT_LOAD_FACTOR) { \ + usize new_cap = (map).cap == 0 ? 8 : (map).cap * 2; \ + MAP_SLOT(MAP_TYPEOF((map).data->key), \ + MAP_TYPEOF((map).data->val)) *new_data = \ + calloc(new_cap, sizeof(*new_data)); \ + if (!new_data) \ + abort(); \ + /* 重新插入旧元素 */ \ + for (usize _i = 0; _i < (map).cap; ++_i) { \ + if ((map).data[_i].state == __MAP_SLOT_OCCUPIED) { \ + usize _h = (map).hash((map).data[_i].key) & (new_cap - 1); \ + while (new_data[_h].state == __MAP_SLOT_OCCUPIED) \ + _h = (_h + 1) & (new_cap - 1); \ + new_data[_h].key = (map).data[_i].key; \ + new_data[_h].val = (map).data[_i].val; \ + new_data[_h].state = __MAP_SLOT_OCCUPIED; \ + } \ + } \ + free((map).data); \ + (map).data = (void *)new_data; \ + (map).cap = new_cap; \ + } \ + /* 查找或插入 */ \ + usize _mask = (map).cap - 1; \ + usize _idx = (map).hash(_key) & _mask; \ + usize _first_del = (usize) - 1; \ + while ((map).data[_idx].state != __MAP_SLOT_EMPTY) { \ + if ((map).data[_idx].state == __MAP_SLOT_OCCUPIED && \ + (map).cmp((map).data[_idx].key, _key) == 0) { \ + (map).data[_idx].val = _val; \ + break; \ + } \ + if ((map).data[_idx].state == __MAP_SLOT_DELETED && \ + _first_del == (usize) - 1) \ + _first_del = _idx; \ + _idx = (_idx + 1) & _mask; \ + } \ + if ((map).data[_idx].state == __MAP_SLOT_EMPTY) { \ + usize _target = (_first_del != (usize) - 1) ? _first_del : _idx; \ + (map).data[_target].key = _key; \ + (map).data[_target].val = _val; \ + (map).data[_target].state = __MAP_SLOT_OCCUPIED; \ + ++(map).size; \ + } \ + } while (0) + +/** + * 查询:若找到,*out_val 被赋值为对应值并返回 1;否则返回 0 + */ +#define map_get(map, _key, out_val) \ + (({ \ + int _found = 0; \ + if ((map).cap > 0) { \ + usize _mask = (map).cap - 1; \ + usize _idx = (map).hash(_key) & _mask; \ + while ((map).data[_idx].state != __MAP_SLOT_EMPTY) { \ + if ((map).data[_idx].state == __MAP_SLOT_OCCUPIED && \ + (map).cmp((map).data[_idx].key, _key) == 0) { \ + *out_val = (map).data[_idx].val; \ + _found = 1; \ + break; \ + } \ + _idx = (_idx + 1) & _mask; \ + } \ + } \ + _found; \ + })) + +/** + * 删除指定键 + */ +#define map_del(map, _key) \ + do { \ + if ((map).cap == 0) \ + break; \ + usize _mask = (map).cap - 1; \ + usize _idx = (map).hash(_key) & _mask; \ + while ((map).data[_idx].state != __MAP_SLOT_EMPTY) { \ + if ((map).data[_idx].state == __MAP_SLOT_OCCUPIED && \ + (map).cmp((map).data[_idx].key, _key) == 0) { \ + (map).data[_idx].state = __MAP_SLOT_DELETED; \ + --(map).size; \ + break; \ + } \ + _idx = (_idx + 1) & _mask; \ + } \ + } while (0) + +#endif /* __CORE_MAP_H__ */ diff --git a/stage0/include/core_vec.h b/stage0/include/core_vec.h new file mode 100644 index 0000000..a1d2cb2 --- /dev/null +++ b/stage0/include/core_vec.h @@ -0,0 +1,258 @@ +/** + * @file vec.h + * @brief 动态数组(Dynamic Array)实现 + * + * 提供类型安全的动态数组容器实现,支持自动扩容和基本操作 + */ + +#ifndef __CORE_VEC_H__ +#define __CORE_VEC_H__ + +#define __CORE_VEC_USE_STD__ +#ifndef __CORE_VEC_USE_STD__ +#include "core_log.h" + +#include "core_impl.h" +#include "core_type.h" +#define __vec_realloc realloc +#define __vec_free free +#define __vec_memcpy memcpy +#else +#include +#include +#include + +#ifndef nullptr +#define nullptr NULL +#endif +typedef size_t usize; +#define __vec_realloc realloc +#define __vec_free free +#define __vec_memcpy memcpy + +#ifndef LOG_FATAL +#include +#define LOG_FATAL(...) \ + do { \ + printf(__VA_ARGS__); \ + abort(); \ + } while (0) +#endif + +#ifndef Assert +#include +#define Assert(cond) assert(cond) +#endif +#endif + +/** @defgroup vec_struct 数据结构定义 */ + +/** + * @def VEC(type) + * @brief 声明向量结构体 + * @param type 存储的数据类型 + * + * 生成包含size/cap/data三个字段的结构体定义: + * - size: 当前元素数量 + * - cap: 数组容量 + * - data: 存储数组指针 + * @example + * VEC(char) string; <=> char[dynamic_array] string; + * struct people { VEC(char) name; int age; VEC(struct people) children; + * }; + */ +#define VEC(type) \ + struct { \ + usize size; \ + usize cap; \ + type *data; \ + } + +/** @defgroup vec_operations 动态数组操作宏 */ + +/** + * @def vec_init(vec) + * @brief 初始化向量结构体 + * @param vec 要初始化的向量结构体变量 + * + * @note 此宏不会分配内存,仅做零初始化 + */ +#define vec_init(vec) \ + do { \ + (vec).size = 0, (vec).cap = 0, (vec).data = 0; \ + } while (0) + +#define vec_realloc(vec, new_cap) \ + do { \ + void *data = __vec_realloc((vec).data, new_cap * sizeof(*(vec).data)); \ + if (!data) { \ + LOG_FATAL("vector_push: realloc failed\n"); \ + } \ + (vec).cap = new_cap; \ + (vec).data = data; \ + } while (0) + +#define vec_size(vec) ((vec).size) +#define vec_cap(vec) ((vec).cap) +#define vec_for(vec, idx) for (usize idx = 0; idx < vec_size(vec); idx += 1) + +/** + * @def vec_push(vec, value) + * @brief 添加元素到向量末尾 + * @param vec 目标向量结构体 + * @param value 要添加的值(需匹配存储类型) + * + * @note 当容量不足时自动扩容为2倍(初始容量为4) + * @warning 内存分配失败时会触发LOG_FATAL + */ +#define vec_push(vec, value) \ + do { \ + if ((vec).size >= (vec).cap) { \ + usize cap = (vec).cap ? (vec).cap * 2 : 4; \ + vec_realloc(vec, cap); \ + } \ + Assert((vec).data != nullptr); \ + (vec).data[(vec).size++] = value; \ + } while (0) + +/** + * @def vec_pop(vec) + * @brief 弹出最后一个元素 + * @param vec 目标向量结构体 + * @return 最后元素的引用 + * @warning 需确保size > 0时使用 + */ +#define vec_pop(vec) ((vec).data[--(vec).size]) + +/** + * @def vec_at(vec, idx) + * @brief 获取指定索引元素 + * @param vec 目标向量结构体 + * @param idx 元素索引(0 <= idx < size) + * @return 对应元素的引用 + */ +#define vec_at(vec, idx) (((vec).data)[idx]) + +/** + * @def vec_idx(vec, ptr) + * @brief 获取元素指针对应的索引 + * @param vec 目标向量结构体 + * @param ptr 元素指针(需在data数组范围内) + * @return 元素索引值 + */ +#define vec_idx(vec, ptr) ((ptr) - (vec).data) + +/** + * @def vec_free(vec) + * @brief 释放向量内存 + * @param vec 目标向量结构体 + * + * @note 释放后需重新初始化才能再次使用 + */ +#define vec_free(vec) \ + do { \ + if ((vec).data == nullptr) \ + break; \ + __vec_free((vec).data); \ + (vec).data = nullptr; \ + (vec).size = (vec).cap = 0; \ + } while (0) + +#define vec_unsafe_get_data(vec) ((vec).data) + +#define vec_unsafe_from_buffer(vec, buffer, buffer_size) \ + do { \ + (vec).size = buffer_size; \ + (vec).cap = (vec).size; \ + (vec).data = buffer; \ + } while (0) + +#define vec_unsafe_from_static_array(vec, array) \ + do { \ + (vec).size = sizeof(array) / sizeof((array)[0]); \ + (vec).cap = (vec).size; \ + (vec).data = array; \ + } while (0) + +/** + * @def vec_sized_realloc(vec, elem_size, new_cap) + * @brief 内部宏:按 elem_size 重新分配内存 + */ +#define vec_sized_realloc(vec, elem_size, new_cap) \ + do { \ + void *new_data = __vec_realloc((vec).data, (new_cap) * (elem_size)); \ + if (!new_data) \ + LOG_FATAL("vec_sized_realloc: failed\n"); \ + (vec).data = new_data; \ + (vec).cap = new_cap; \ + } while (0) + +/** + * @def vec_sized_push(vec, elem_size, src_ptr) + * @brief 添加一个元素(从 src_ptr 拷贝 elem_size 字节) + * @param vec VEC(type) 定义的向量变量(type 可为 char 或 void) + * @param elem_size 每个元素占用的字节数 + * @param src_ptr 源数据的指针 + * @param copy_size 要拷贝的字节数 + * + * @note 使用前需确保 vec.data 类型与 src_ptr 无关,内部会按字节拷贝。 + * 推荐声明时为 `VEC(char)` 或 `VEC(unsigned char)`。 + */ +#define vec_sized_push(vec, elem_size, src_ptr, copy_size) \ + do { \ + if ((vec).size >= (vec).cap) { \ + usize new_cap = (vec).cap ? (vec).cap * 2 : 4; \ + vec_sized_realloc(vec, elem_size, new_cap); \ + } \ + char *slot = (char *)(vec).data + (vec).size * (elem_size); \ + __vec_memcpy(slot, (src_ptr), (copy_size)); \ + (vec).size++; \ + } while (0) + +/** + * @def vec_sized_at_ptr(vec, elem_size, idx) + * @brief 获取第 idx 个元素的指针(void*) + * @return 指向元素的指针,需转换为具体类型使用 + */ +#define vec_sized_at_ptr(vec, elem_size, idx) \ + ((void *)((char *)(vec).data + (idx) * (elem_size))) + +/** + * @def vec_sized_foreach(vec, elem_size, elem_ptr_var, block) + * @brief 遍历所有元素 + * @param elem_ptr_var 循环内的变量名(void* 类型) + * @param block 循环体语句块 + */ +#define vec_sized_foreach(vec, elem_size, elem_ptr_var, block) \ + do { \ + for (usize __i = 0; __i < (vec).size; ++__i) { \ + void *elem_ptr_var = vec_sized_at_ptr(vec, elem_size, __i); \ + block; \ + } \ + } while (0) + +/** + * @def vec_sized_pop(vec, elem_size) + * @brief 弹出最后一个元素(仅减小 size,不返回数据) + */ +#define vec_sized_pop(vec, elem_size) \ + do { \ + if ((vec).size == 0) \ + LOG_FATAL("vec_sized_pop: empty\n"); \ + (vec).size--; \ + } while (0) + +/** + * @def vec_sized_clear(vec) + * @brief 清空向量(重置 size = 0,不释放内存) + */ +#define vec_sized_clear(vec) ((vec).size = 0) + +/** + * @def vec_sized_free(vec) + * @brief 释放向量内存(与原始 vec_free 相同,可复用) + * @note 注意:如果元素内部有堆资源,需在释放前自行遍历调用析构函数。 + */ +#define vec_sized_free(vec) vec_free(vec) + +#endif /* __CORE_VEC_H__ */ diff --git a/stage0/spl_cli.c b/stage0/spl_cli.c new file mode 100644 index 0000000..0d2384b --- /dev/null +++ b/stage0/spl_cli.c @@ -0,0 +1,55 @@ +/* spl_cli.c — SIR VM launcher + * + * Loads a compiled .sir binary and runs it via the SIR VM. + * Built-in syscalls are auto-registered via spl_syscall_register(). + * + * Usage: + * spl_cli [entry_point] + */ + +#include "spl_ir.h" +#include "spl_syscall.h" +#include "spl_vm.h" + +#include + +int main(int argc, const char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: spl_cli [entry_point]\n"); + return 1; + } + + const char *path = argv[1]; + const char *entry = argc >= 3 ? argv[2] : "main"; + + spl_prog_t prog; + if (spl_prog_load_from_file(path, &prog) != 0) { + fprintf(stderr, "spl_cli: cannot load '%s'\n", path); + return 1; + } + + spl_syscall_register(&prog); + + spl_vm_t vm; + spl_vm_init(&vm); + if (spl_vm_load_prog(&vm, &prog) != 0) { + fprintf(stderr, "vm: prog '%s' not found\n", entry); + spl_prog_drop(&prog); + return 1; + } + if (spl_vm_prepare(&vm, entry, argc, argv, NULL) != 0) { + fprintf(stderr, "vm: entry point '%s' not found\n", entry); + spl_prog_drop(&prog); + return 1; + } + + int ret = spl_vm_run_until(&vm, 0); + spl_vm_drop(&vm); + spl_prog_drop(&prog); + + if (ret < 0) { + fprintf(stderr, "spl_cli: VM error (exit_code=%d)\n", vm.exit_code); + return (int)vm.exit_code; + } + return (int)vm.exit_code; +} diff --git a/stage0/spl_disasm.c b/stage0/spl_disasm.c new file mode 100644 index 0000000..8716b7c --- /dev/null +++ b/stage0/spl_disasm.c @@ -0,0 +1,76 @@ +/* spl_disasm.c — SIR bytecode disassembler + * + * Usage: spl_disasm + */ + +#include "spl_ir.h" +#include + +int main(int argc, const char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: spl_disasm \n"); + return 1; + } + + spl_prog_t prog; + if (spl_prog_load_from_file(argv[1], &prog) != 0) { + fprintf(stderr, "spl_disasm: cannot load '%s'\n", argv[1]); + return 1; + } + + printf(";; SPL bytecode dump: %s\n", argv[1]); + printf(";; ninsns=%zu nfuncs=%zu nnatives=%zu nstrs=%zu\n\n", vec_size(prog.insns), + vec_size(prog.funcs), vec_size(prog.natives), vec_size(prog.strtab)); + + if (vec_size(prog.funcs) > 0) { + printf(";; --- functions ---\n"); + for (usize i = 0; i < vec_size(prog.funcs); i++) { + spl_func_t *f = &vec_at(prog.funcs, i); + printf(" %s nargs=%zu ninsns=%zu addr=%zu\n", f->name ? f->name : "(anon)", f->nargs, + f->ninsns, f->address); + } + printf("\n"); + } + + if (vec_size(prog.natives) > 0) { + printf(";; --- natives ---\n"); + for (int i = 0; i < (int)vec_size(prog.natives); i++) { + spl_native_t *n = &vec_at(prog.natives, i); + printf(" %s\n", n->name ? n->name : "(anon)"); + } + printf("\n"); + } + + if (vec_size(prog.strtab) > 0) { + printf(";; --- string table ---\n"); + for (int i = 0; i < (int)vec_size(prog.strtab); i++) { + printf(" %d: \"%s\"\n", i, vec_at(prog.strtab, i)); + } + printf("\n"); + } + + printf(";; --- instructions ---\n"); + for (usize i = 0; i < vec_size(prog.insns); i++) { + spl_ins_t *ins = &vec_at(prog.insns, i); + printf("%4zd: %s", i, spl_opcode_name((spl_opcode_t)ins->opcode)); + if (ins->type != SPL_VOID) + printf(" %s", spl_type_name((spl_type_t)ins->type)); + printf(" %zd", ins->imm); + + /* annotate jumps / calls */ + if (ins->opcode == SPL_JMP || ins->opcode == SPL_BZ || ins->opcode == SPL_BNZ) { + long long target = (long long)i + 1 + (long long)ins->imm; + printf(" ; -> %zd", target); + } else if (ins->opcode == SPL_CALL) { + printf(" ; nargs=%zd, from stack", (long long)ins->imm); + } else if (ins->opcode == SPL_CALLI) { + printf(" ; indirect call"); + } else if (ins->opcode == SPL_GADDR) { + printf(" ; gdata[%zd]", (long long)ins->imm); + } + printf("\n"); + } + + spl_prog_drop(&prog); + return 0; +} diff --git a/stage0/spl_ir.c b/stage0/spl_ir.c new file mode 100644 index 0000000..d8b5484 --- /dev/null +++ b/stage0/spl_ir.c @@ -0,0 +1,474 @@ +/* spl_ir.c — SIR binary serialization, deserialization, and utilities + * + * Binary format (all metadata fields are spl_val_t = uint64_t LE): + * [HEADER] magic(8) nfuncs(8) ninsns(8) nnatives(8) nstrs(8) ndata(8) + * [FUNCS] each: name_len(8) name(padded to 8) idx_of_strtab(8) + * nargs(8) ninsns(8) address(8) + * [INSTRS] each: opcode(2) type(2) imm(8) = 12 bytes + * [NATIVES] each: name_len(8) name(padded to 8) idx_of_strtab(8) + * [STRTAB] each: slen(8) str(slen bytes, padded to 8) + */ + +#include "spl_ir.h" + +void spl_prog_init(spl_prog_t *prog) { + if (!prog) + return; + vec_init(prog->insns); + vec_init(prog->funcs); + vec_init(prog->natives); + vec_init(prog->strtab); + vec_init(prog->gdata); + map_init(prog->symtab, MAP_HASH_STR, MAP_CMP_STR); +} + +void spl_prog_drop(spl_prog_t *prog) { + if (!prog) + return; + vec_free(prog->insns); + /* free each func name */ + vec_for(prog->funcs, i) { free(vec_at(prog->funcs, i).name); } + vec_free(prog->funcs); + /* free each native name */ + vec_for(prog->natives, i) { free(vec_at(prog->natives, i).name); } + vec_free(prog->natives); + /* free each gdata entry */ + vec_for(prog->gdata, i) { free(vec_at(prog->gdata, i).data); } + vec_free(prog->gdata); + /* free each strtab entry */ + vec_for(prog->strtab, i) { free((void *)vec_at(prog->strtab, i)); } + vec_free(prog->strtab); + map_free(prog->symtab); +} + +/* ---- LE read/write helpers ---- */ + +static inline spl_val_t rd64(const unsigned char **p) { + spl_val_t v = (spl_val_t)(*p)[0] | ((spl_val_t)(*p)[1] << 8) | ((spl_val_t)(*p)[2] << 16) | + ((spl_val_t)(*p)[3] << 24) | ((spl_val_t)(*p)[4] << 32) | + ((spl_val_t)(*p)[5] << 40) | ((spl_val_t)(*p)[6] << 48) | + ((spl_val_t)(*p)[7] << 56); + *p += 8; + return v; +} + +static inline void wr64(unsigned char **p, spl_val_t v) { + *(*p)++ = (unsigned char)(v); + *(*p)++ = (unsigned char)(v >> 8); + *(*p)++ = (unsigned char)(v >> 16); + *(*p)++ = (unsigned char)(v >> 24); + *(*p)++ = (unsigned char)(v >> 32); + *(*p)++ = (unsigned char)(v >> 40); + *(*p)++ = (unsigned char)(v >> 48); + *(*p)++ = (unsigned char)(v >> 56); +} + +/* Round n up to next multiple of 8 */ +#define ALIGN8(n) (((n) + 7) & ~7) + +int spl_prog_load_from_file(const char *fname, spl_prog_t *prog) { + FILE *f; + unsigned char *data; + long len; + const unsigned char *p; + spl_val_t nfuncs, ninsns, nnatives, nstrs, ndata; + + if (!fname || !prog) + return -1; + + f = fopen(fname, "rb"); + if (!f) + return -1; + fseek(f, 0, SEEK_END); + len = ftell(f); + fseek(f, 0, SEEK_SET); + if (len < 48) { + fclose(f); + return -1; + } + data = (unsigned char *)malloc((size_t)len); + if (!data) { + fclose(f); + return -1; + } + if (fread(data, 1, (size_t)len, f) != (size_t)len) { + free(data); + fclose(f); + return -1; + } + fclose(f); + + p = data; + + /* magic */ + if (p[0] != 'S' || p[1] != 'P' || p[2] != 'L' || p[3] != 'B' || p[4] != 'I' || p[5] != 'N' || + p[6] != '\0' || p[7] != '\0') { + free(data); + return -1; + } + p += 8; + + spl_prog_init(prog); + + /* header counts */ + nfuncs = rd64(&p); + ninsns = rd64(&p); + nnatives = rd64(&p); + nstrs = rd64(&p); + ndata = rd64(&p); + + /* ---- function table ---- */ + for (spl_val_t i = 0; i < nfuncs; i++) { + spl_func_t func = {0}; + spl_val_t nlen = rd64(&p); + usize pad = ALIGN8((usize)nlen) - (usize)nlen; + + func.name = (char *)malloc((usize)nlen); + if (!func.name) { + free(data); + return -1; + } + memcpy(func.name, p, (usize)nlen); + p += (usize)nlen + pad; + + func.idx_of_strtab = rd64(&p); + func.nargs = rd64(&p); + func.ninsns = rd64(&p); + func.address = rd64(&p); + vec_push(prog->funcs, func); + } + + /* ---- instructions ---- */ + for (spl_val_t i = 0; i < ninsns; i++) { + if ((size_t)(p - data) + 12 > (size_t)len) { + free(data); + return -1; + } + spl_ins_t ins; + ins.opcode = (uint16_t)p[0] | ((uint16_t)p[1] << 8); + ins.type = (uint16_t)p[2] | ((uint16_t)p[3] << 8); + p += 4; + ins.imm = rd64(&p); + vec_push(prog->insns, ins); + } + + /* ---- native table ---- */ + for (spl_val_t i = 0; i < nnatives; i++) { + spl_native_t nat = {0}; + spl_val_t nlen = rd64(&p); + usize pad = ALIGN8((usize)nlen) - (usize)nlen; + + nat.name = (char *)malloc((usize)nlen); + if (!nat.name) { + free(data); + return -1; + } + memcpy(nat.name, p, (usize)nlen); + p += (usize)nlen + pad; + + nat.idx_of_strtab = rd64(&p); + nat.impl_fn = NULL; /* function pointer can't be serialised */ + vec_push(prog->natives, nat); + } + + /* ---- string table ---- */ + for (spl_val_t i = 0; i < nstrs; i++) { + spl_val_t slen = rd64(&p); + usize pad = ALIGN8((usize)slen) - (usize)slen; + char *s = (char *)malloc((usize)slen + 1); + if (!s) { + free(data); + return -1; + } + memcpy(s, p, (usize)slen); + s[(usize)slen] = '\0'; + p += (usize)slen + pad; + vec_push(prog->strtab, s); + } + + /* ---- global data ---- */ + for (spl_val_t i = 0; i < ndata; i++) { + spl_gdata_t entry; + entry.size = rd64(&p); + usize pad = ALIGN8(entry.size) - entry.size; + entry.data = (unsigned char *)malloc(entry.size); + if (!entry.data) { + free(data); + return -1; + } + memcpy(entry.data, p, entry.size); + p += entry.size + pad; + vec_push(prog->gdata, entry); + } + + free(data); + return 0; +} + +int spl_prog_store_to_file(const char *fname, spl_prog_t *prog) { + unsigned char *buf, *p; + int i; + size_t total; + spl_val_t nfuncs, ninsns, nnatives, nstrs, ndata; + usize nlen, pad; + + if (!fname || !prog) + return -1; + + nfuncs = vec_size(prog->funcs); + ninsns = vec_size(prog->insns); + nnatives = vec_size(prog->natives); + nstrs = vec_size(prog->strtab); + ndata = vec_size(prog->gdata); + + /* Calculate total size */ + total = 8 /* magic */ + + 8 + 8 + 8 + 8 + 8; /* 5 counts (nfuncs+ninsns+nnatives+nstrs+ndata) */ + + /* funcs */ + for (i = 0; i < nfuncs; i++) { + nlen = strlen(vec_at(prog->funcs, i).name) + 1; /* include null */ + total += + 8 + ALIGN8(nlen) + 8 + 8 + 8 + 8; /* nlen + name(pad) + idx + nargs + ninsns + addr */ + } + + /* insns */ + total += ninsns * 12; /* opcode(2) + type(2) + imm(8) */ + + /* natives */ + for (i = 0; i < nnatives; i++) { + nlen = strlen(vec_at(prog->natives, i).name) + 1; + total += 8 + ALIGN8(nlen) + 8; /* nlen + name(pad) + idx_of_strtab */ + } + + /* strtab */ + for (i = 0; i < nstrs; i++) { + nlen = strlen(vec_at(prog->strtab, i)); + total += 8 + ALIGN8(nlen); /* slen + str(pad) */ + } + + /* gdata */ + for (i = 0; i < ndata; i++) { + usize dsize = vec_at(prog->gdata, i).size; + total += 8 + ALIGN8(dsize); /* dsize + data(pad) */ + } + + buf = (unsigned char *)malloc(total); + if (!buf) + return -1; + p = buf; + + /* magic */ + memcpy(p, "SPLBIN\0\0", 8); + p += 8; + + /* counts */ + wr64(&p, nfuncs); + wr64(&p, ninsns); + wr64(&p, nnatives); + wr64(&p, nstrs); + wr64(&p, ndata); + + /* ---- function table ---- */ + for (i = 0; i < nfuncs; i++) { + const char *name = vec_at(prog->funcs, i).name; + nlen = strlen(name) + 1; + pad = ALIGN8(nlen) - nlen; + wr64(&p, nlen); + memcpy(p, name, nlen); + p += nlen; + memset(p, 0, pad); + p += pad; + wr64(&p, vec_at(prog->funcs, i).idx_of_strtab); + wr64(&p, vec_at(prog->funcs, i).nargs); + wr64(&p, vec_at(prog->funcs, i).ninsns); + wr64(&p, vec_at(prog->funcs, i).address); + } + + /* ---- instructions ---- */ + for (i = 0; i < ninsns; i++) { + spl_ins_t *ins = &vec_at(prog->insns, i); + *p++ = (unsigned char)(ins->opcode); + *p++ = (unsigned char)(ins->opcode >> 8); + *p++ = (unsigned char)(ins->type); + *p++ = (unsigned char)(ins->type >> 8); + wr64(&p, ins->imm); + } + + /* ---- native table ---- */ + for (i = 0; i < nnatives; i++) { + const char *name = vec_at(prog->natives, i).name; + nlen = strlen(name) + 1; + pad = ALIGN8(nlen) - nlen; + wr64(&p, nlen); + memcpy(p, name, nlen); + p += nlen; + memset(p, 0, pad); + p += pad; + wr64(&p, vec_at(prog->natives, i).idx_of_strtab); + } + + /* ---- string table ---- */ + for (i = 0; i < nstrs; i++) { + const char *s = vec_at(prog->strtab, i); + nlen = strlen(s); + pad = ALIGN8(nlen) - nlen; + wr64(&p, nlen); + memcpy(p, s, nlen); + p += nlen; + memset(p, 0, pad); + p += pad; + } + + /* ---- global data ---- */ + for (i = 0; i < ndata; i++) { + spl_gdata_t *entry = &vec_at(prog->gdata, i); + pad = ALIGN8(entry->size) - entry->size; + wr64(&p, entry->size); + memcpy(p, entry->data, entry->size); + p += entry->size; + memset(p, 0, pad); + p += pad; + } + + /* Write file */ + FILE *f = fopen(fname, "wb"); + if (!f) { + free(buf); + return -1; + } + fwrite(buf, 1, total, f); + fclose(f); + free(buf); + return 0; +} + +int spl_prog_add_func(spl_prog_t *prog, spl_func_t *func) { + if (!prog || !func) + return 0; + vec_push(prog->funcs, *func); + return vec_size(prog->funcs); +} + +int spl_prog_add_native(spl_prog_t *prog, spl_native_t *native) { + if (!prog || !native) + return 0; + vec_push(prog->natives, *native); + return vec_size(prog->natives); +} + +int spl_prog_add_data(spl_prog_t *prog, void *ptr, usize size) { + spl_gdata_t entry; + if (!prog) + return 0; + entry.data = (unsigned char *)malloc(size); + if (!entry.data) + return 0; + memcpy(entry.data, ptr, size); + entry.size = size; + vec_push(prog->gdata, entry); + return vec_size(prog->gdata); +} + +spl_val_t spl_prog_emit(spl_prog_t *prog, uint16_t opcode, uint16_t type, spl_val_t imm) { + spl_ins_t ins = {opcode, type, imm}; + spl_val_t addr = vec_size(prog->insns); + vec_push(prog->insns, ins); + return addr; +} + +int spl_prog_add_func_simple(spl_prog_t *prog, const char *name, spl_val_t nargs) { + spl_func_t func = {0}; + if (!prog || !name) + return -1; + func.name = strdup(name); + func.idx_of_strtab = 0; + func.nargs = nargs; + func.ninsns = 0; + func.address = vec_size(prog->insns); + vec_push(prog->funcs, func); + return (int)vec_size(prog->funcs) - 1; +} + +void spl_prog_end_func(spl_prog_t *prog, int func_idx) { + if (!prog || func_idx < 0 || func_idx >= (int)vec_size(prog->funcs)) + return; + spl_func_t *func = &vec_at(prog->funcs, func_idx); + func->ninsns = vec_size(prog->insns) - func->address; +} + +int spl_prog_add_str(spl_prog_t *prog, const char *str) { + if (!prog || !str) + return -1; + vec_push(prog->strtab, strdup(str)); + return (int)vec_size(prog->strtab) - 1; +} + +spl_func_t *spl_prog_get_func(spl_prog_t *prog, const char *name) { + if (!prog || !name) + return NULL; + vec_for(prog->funcs, i) { + const char *match_name = vec_at(prog->funcs, i).name; + if (match_name && strcmp(match_name, name) == 0) { + return &vec_at(prog->funcs, i); + } + } + return NULL; +} + +spl_native_t *spl_prog_get_native(spl_prog_t *prog, const char *name) { + if (!prog || !name) + return NULL; + vec_for(prog->natives, i) { + const char *match_name = vec_at(prog->natives, i).name; + if (match_name && strcmp(match_name, name) == 0) { + return &vec_at(prog->natives, i); + } + } + return NULL; +} + +const char *opcode_name[] = { +#define X(opcode, name, argc, pop, push, desc) [opcode] = name, + SPL_OPCODES(X) +#undef X +}; +const char *spl_opcode_name(spl_opcode_t opcode) { return opcode_name[opcode]; } +const char *spl_type_name(spl_type_t type) { + switch (type) { + case SPL_VOID: + return "void"; + case SPL_I8: + return "i8"; + case SPL_U8: + return "u8"; + case SPL_I16: + return "i16"; + case SPL_U16: + return "u16"; + case SPL_I32: + return "i32"; + case SPL_U32: + return "u32"; + case SPL_I64: + return "i64"; + case SPL_U64: + return "u64"; + case SPL_F32: + return "f32"; + case SPL_F64: + return "f64"; + case SPL_PTR: + return "ptr"; + default: + return "???"; + } +} + +void spl_ins_dump(spl_ins_t *ins, spl_val_t addr) { + printf("%4zu: %s", addr, spl_opcode_name((spl_opcode_t)ins->opcode)); + if (ins->type != SPL_VOID) + printf(" %s", spl_type_name((spl_type_t)ins->type)); + printf(" %zd:%zx", ins->imm, ins->imm); + printf("\n"); +} diff --git a/stage0/spl_ir.h b/stage0/spl_ir.h new file mode 100644 index 0000000..3681098 --- /dev/null +++ b/stage0/spl_ir.h @@ -0,0 +1,221 @@ +/* spl_ir.h - SPL Intermediate Representation: instruction set and binary format + */ + +#ifndef __SPL_IR_H__ +#define __SPL_IR_H__ + +#include "include/core_map.h" +#include "include/core_vec.h" +#include +#include + +typedef uintptr_t usize; +typedef intptr_t isize; + +typedef enum { + SPL_VOID = 0, + SPL_I8, + SPL_U8, + SPL_I16, + SPL_U16, + SPL_I32, + SPL_U32, + SPL_I64, + SPL_U64, + SPL_ISIZE, + SPL_USIZE, + SPL_F32, + SPL_F64, + SPL_PTR, + SPL_TYPE_COUNT, +} spl_type_t; + +/* clang-format off */ +#define SPL_OPCODES(X) \ + /* opcode, name, argc, pop, push, desc */ \ + X(SPL_ERROR, "error", 0, 0, 0, "invalid opcode / internal error") \ + /* 栈操作 */ \ + X(SPL_PUSH, "push", 1, 0, 1, "push immediate") \ + X(SPL_DUP, "dup", 0, 0, 1, "duplicate top of stack") \ + X(SPL_DROP, "drop", 0, 1, 0, "discard top of stack") \ + X(SPL_SWAP, "swap", 0, 2, 2, "swap top two elements") \ + X(SPL_PICK, "pick", 1, 0, 1, "push copy of stack[imm] (0=TOS)") \ + /* 算术 */ \ + X(SPL_ADD, "add", 0, 2, 1, "integer addition") \ + X(SPL_SUB, "sub", 0, 2, 1, "integer subtraction") \ + X(SPL_MUL, "mul", 0, 2, 1, "integer multiplication") \ + X(SPL_DIV_S, "div_s", 0, 2, 1, "signed division") \ + X(SPL_DIV_U, "div_u", 0, 2, 1, "unsigned division") \ + X(SPL_REM_S, "rem_s", 0, 2, 1, "signed remainder") \ + X(SPL_REM_U, "rem_u", 0, 2, 1, "unsigned remainder") \ + X(SPL_NEG, "neg", 0, 1, 1, "two's complement negation") \ + /* 位运算 */ \ + X(SPL_AND, "and", 0, 2, 1, "bitwise AND") \ + X(SPL_OR, "or", 0, 2, 1, "bitwise OR") \ + X(SPL_XOR, "xor", 0, 2, 1, "bitwise XOR") \ + X(SPL_NOT, "not", 0, 1, 1, "bitwise NOT") \ + X(SPL_SHL, "shl", 0, 2, 1, "left shift") \ + X(SPL_SHR_U, "shr_u", 0, 2, 1, "logical right shift (zero-fill)") \ + X(SPL_SHR_S, "shr_s", 0, 2, 1, "arithmetic right shift (sign-fill)") \ + /* 比较 */ \ + X(SPL_EQ, "eq", 0, 2, 1, "equal") \ + X(SPL_NE, "ne", 0, 2, 1, "not equal") \ + X(SPL_SLT, "slt", 0, 2, 1, "signed less than") \ + X(SPL_SLE, "sle", 0, 2, 1, "signed less or equal") \ + X(SPL_ULT, "ult", 0, 2, 1, "unsigned less than") \ + X(SPL_ULE, "ule", 0, 2, 1, "unsigned less or equal") \ + X(SPL_SGT, "sgt", 0, 2, 1, "signed greater than") \ + X(SPL_SGE, "sge", 0, 2, 1, "signed greater or equal") \ + X(SPL_UGT, "ugt", 0, 2, 1, "unsigned greater than") \ + X(SPL_UGE, "uge", 0, 2, 1, "unsigned greater or equal") \ + /* 类型转换 */ \ + X(SPL_TRUNC, "trunc", 1, 1, 1, "truncate to low imm bits") \ + X(SPL_SEXT, "sext", 1, 1, 1, "sign-extend from bit imm") \ + X(SPL_ZEXT, "zext", 1, 1, 1, "zero-extend from bit imm") \ + /* 控制流 */ \ + X(SPL_JMP, "jmp", 1, 0, 0, "unconditional jump (offset isize)") \ + X(SPL_BZ, "bz", 1, 1, 0, "pop; jump if zero (offset isize)") \ + X(SPL_BNZ, "bnz", 1, 1, 0, "pop; jump if non-zero (offset isize)") \ + X(SPL_HALT, "halt", 0, 0, 0, "stop execution") \ + /* 函数调用 */ \ + X(SPL_CALL, "call", 1, 0, 0, "call function pop function offset, call it") \ + X(SPL_CALLI, "calli", 0, 1, 0, "indirect call: pop function address, call it") \ + X(SPL_RET, "ret", 0, 0, 0, "return from function") \ + /* 栈帧局部变量 */ \ + X(SPL_ALLOC, "alloc", 1, 0, 0, "allocate imm zero-slots on stack") \ + X(SPL_LADDR, "laddr", 1, 0, 1, "push address of local at fp+imm") \ + X(SPL_GADDR, "gaddr", 1, 0, 1, "push address of global at gp + imm") \ + /* 间接内存访问 */ \ + X(SPL_LOAD, "load", 0, 1, 1, "load sizeof(type)-bits zero-extended") \ + X(SPL_STORE, "store", 0, 2, 0, "store low sizeof(type)-bits") \ + /* 原生接口 */ \ + X(SPL_NCALL, "ncall", 1, 0, 1, "call native function by index") \ + X(SPL_NLIB, "nlib", 1, 0, 0, "dlopen library (name idx)") \ + /* 调试 */ \ + X(SPL_BK, "breakpoint", 0, 0, 0, "break point when exec will stop run") \ + X(SPL_DBG, "dbg", 0, 1, 1, "print top of stack as hex") + +/* clang-format on */ + +typedef enum { +#define X(opcode, name, argc, pop, push, desc) opcode, + SPL_OPCODES(X) +#undef X +} spl_opcode_t; + +/* + * Binary format (all metadata fields spl_val_t LE): + * magic[8] = "SPLBIN\0\0" + * nfuncs, ninsns, nnatives, nstrs, ndata + * [func table] each: name_len, name(pad8), idx_of_strtab, nargs, ninsns, address + * [insns] each: opcode(2) type(2) imm(8) = 12 bytes + * [natives] each: name_len, name(pad8), idx_of_strtab + * [strtab] each: slen, str(pad8) + * [gdata] each: dsize(8), data(dsize bytes, padded to 8) + * + * === Calling Convention === + * + * Before CALL: + * - args pushed left-to-right + * - target address pushed last + * + * CALL (imm = nargs): + * 1. pop target address + * 2. callstack[cp++] = {saved_fp, saved_ip, nargs} + * 3. fp = sp - nargs (fp points to arg0) + * 4. ip = target address + * + * ALLOC k: + * sp += k (slots zeroed; local[j] = stacks.data[fp + nargs + j]) + * + * LADDR imm: + * push &stacks.data[fp + imm] + * (imm < nargs accesses args; imm >= nargs accesses locals) + * + * GADDR imm: + * push prog->gdata[imm] (pointer to global data blob) + * + * RET: + * 1. pop retval if non-void type + * 2. sp = fp + * 3. pop frame; fp = saved_fp, ip = saved_ip + * 4. push retval if non-void type + * + * CALLI (indirect call): + * - stack before: ..., arg0, ..., argN-1, nargs, func_addr + * 1. pop func_addr, then pop nargs + * 2. same as CALL steps 2-4 + */ + +#define SPL_BINFMT_MAGIC "SPLBIN\0\0" +// All SIR stack values are ptr-bit unsigned integers +typedef usize spl_val_t; + +typedef struct spl_ins { + uint8_t opcode; + uint8_t type; + spl_val_t imm; +} spl_ins_t; +typedef VEC(spl_ins_t) spl_ins_vec_t; + +typedef struct spl_func { + char *name; + spl_val_t idx_of_strtab; + spl_val_t nargs; + spl_val_t ninsns; + spl_val_t address; +} spl_func_t; +typedef VEC(spl_func_t) spl_func_vec_t; + +/* Native function pointer type */ +typedef spl_val_t (*spl_fn_t)(int nargs, spl_val_t *args); +typedef struct spl_native { + char *name; + spl_val_t idx_of_strtab; + spl_fn_t impl_fn; +} spl_native_t; +typedef VEC(spl_native_t) spl_native_vec_t; + +typedef struct { + unsigned char *data; + usize size; +} spl_gdata_t; +typedef VEC(spl_gdata_t) spl_data_t; +typedef VEC(const char *) spl_strtab_t; +typedef MAP(const char *, const char *) spl_symtab_t; +/* Opaque handle for loaded program */ +typedef struct spl_prog { + spl_ins_vec_t insns; + spl_func_vec_t funcs; + spl_native_vec_t natives; + spl_data_t gdata; + spl_strtab_t strtab; + spl_symtab_t symtab; +} spl_prog_t; + +void spl_prog_init(spl_prog_t *prog); +void spl_prog_drop(spl_prog_t *prog); + +int spl_prog_load_from_file(const char *fname, spl_prog_t *prog); +int spl_prog_store_to_file(const char *fname, spl_prog_t *prog); + +int spl_prog_add_data(spl_prog_t *prog, void *ptr, usize size); +int spl_prog_add_func(spl_prog_t *prog, spl_func_t *func); +int spl_prog_add_native(spl_prog_t *prog, spl_native_t *native); + +spl_func_t *spl_prog_get_func(spl_prog_t *prog, const char *name); +spl_native_t *spl_prog_get_native(spl_prog_t *prog, const char *name); + +/* High-level program construction helpers */ +spl_val_t spl_prog_emit(spl_prog_t *prog, uint16_t opcode, uint16_t type, spl_val_t imm); +int spl_prog_add_func_simple(spl_prog_t *prog, const char *name, spl_val_t nargs); +void spl_prog_end_func(spl_prog_t *prog, int func_idx); +int spl_prog_add_str(spl_prog_t *prog, const char *str); + +/* Opcode name lookup for debugging/dumping */ +const char *spl_opcode_name(spl_opcode_t opcode); +const char *spl_type_name(spl_type_t type); + +void spl_ins_dump(spl_ins_t *ins, spl_val_t addr); + +#endif /* __SPL_IR_H__ */ diff --git a/stage0/spl_syscall.c b/stage0/spl_syscall.c new file mode 100644 index 0000000..d0e8779 --- /dev/null +++ b/stage0/spl_syscall.c @@ -0,0 +1,329 @@ +/* spl_syscall.c — built-in syscall implementations + registration + * + * All syscalls validate nargs, cast spl_val_t args to the expected C types, + * execute, and return the result as spl_val_t. + * + * Simple syscalls use the SYSCALL_N macros; complex ones are written + * manually but follow the same template. + */ + +#include "spl_syscall.h" +#include "spl_vm.h" + +#include +#include +#include + +/* ================================================================= + * Macro templates + * + * Each SYSCALL_N macro: + * 1. Validates nargs (prints error and returns -1 on mismatch) + * 2. Casts args[n] from spl_val_t to the specified C type via + * (type)(uintptr_t) — this handles both integer and pointer types + * 3. Evaluates the expression and returns the result as spl_val_t + * ================================================================= */ + +#define CHECK_NARGS(fname, expected) \ + do { \ + if (nargs != (expected)) { \ + fprintf(stderr, fname ": expected " #expected " args, got %d\n", nargs); \ + return -1; \ + } \ + } while (0) + +#define SYSCALL_0(name, ret_expr) \ + static spl_val_t name(int nargs, spl_val_t *args) { \ + CHECK_NARGS(#name, 0); \ + (void)args; \ + return (spl_val_t)(uintptr_t)(ret_expr); \ + } + +#define SYSCALL_1(name, t1, ret_expr) \ + static spl_val_t name(int nargs, spl_val_t *args) { \ + CHECK_NARGS(#name, 1); \ + t1 a1 = (t1)(uintptr_t)args[0]; \ + return (spl_val_t)(uintptr_t)(ret_expr); \ + } + +#define SYSCALL_2(name, t1, t2, ret_expr) \ + static spl_val_t name(int nargs, spl_val_t *args) { \ + CHECK_NARGS(#name, 2); \ + t1 a1 = (t1)(uintptr_t)args[0]; \ + t2 a2 = (t2)(uintptr_t)args[1]; \ + return (spl_val_t)(uintptr_t)(ret_expr); \ + } + +#define SYSCALL_3(name, t1, t2, t3, ret_expr) \ + static spl_val_t name(int nargs, spl_val_t *args) { \ + CHECK_NARGS(#name, 3); \ + t1 a1 = (t1)(uintptr_t)args[0]; \ + t2 a2 = (t2)(uintptr_t)args[1]; \ + t3 a3 = (t3)(uintptr_t)args[2]; \ + return (spl_val_t)(uintptr_t)(ret_expr); \ + } + +#define SYSCALL_4(name, t1, t2, t3, t4, ret_expr) \ + static spl_val_t name(int nargs, spl_val_t *args) { \ + CHECK_NARGS(#name, 4); \ + t1 a1 = (t1)(uintptr_t)args[0]; \ + t2 a2 = (t2)(uintptr_t)args[1]; \ + t3 a3 = (t3)(uintptr_t)args[2]; \ + t4 a4 = (t4)(uintptr_t)args[3]; \ + return (spl_val_t)(uintptr_t)(ret_expr); \ + } + +/* ================================================================= + * OS syscalls + * ================================================================= */ + +/* os_exit — terminate process with the given exit code */ +SYSCALL_1(os_exit, int, (exit(a1), (spl_val_t)0)) + +/* os_putchar — write a single character to stdout */ +SYSCALL_1(os_putchar, int, putchar(a1)) + +/* os_getchar — read a single character from stdin */ +SYSCALL_0(os_getchar, getchar()) + +/* os_putint — print an integer to stdout */ +SYSCALL_1(os_putint, int, (fprintf(stdout, "%d", a1), (spl_val_t)0)) + +/* os_putstr — print a string to stdout (no trailing newline) */ +SYSCALL_1(os_putstr, const char *, (fputs(a1, stdout), (spl_val_t)0)) + +/* os_fopen — open a file, returns FILE* as spl_val_t */ +SYSCALL_2(os_fopen, const char *, const char *, (uintptr_t)fopen(a1, a2)) + +/* os_fclose — close a file, returns 0 on success */ +SYSCALL_1(os_fclose, FILE *, fclose(a1)) + +/* os_fread — read from file, returns number of items read */ +SYSCALL_4(os_fread, void *, size_t, size_t, FILE *, fread(a1, a2, a3, a4)) + +/* os_fwrite — write to file, returns number of items written */ +SYSCALL_4(os_fwrite, const void *, size_t, size_t, FILE *, fwrite(a1, a2, a3, a4)) + +/* os_fsize — get file size in bytes */ +static spl_val_t os_fsize(int nargs, spl_val_t *args) { + CHECK_NARGS("os_fsize", 1); + FILE *f = (FILE *)(uintptr_t)args[0]; + long cur = ftell(f); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, cur, SEEK_SET); + return (spl_val_t)(uintptr_t)sz; +} + +/* os_read_file — read entire file into a malloc'd, null-terminated buffer */ +static spl_val_t os_read_file(int nargs, spl_val_t *args) { + CHECK_NARGS("os_read_file", 1); + const char *path = (const char *)(uintptr_t)args[0]; + FILE *f = fopen(path, "rb"); + if (!f) + return 0; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + char *buf = (char *)malloc((size_t)sz + 1); + if (!buf) { + fclose(f); + return 0; + } + size_t nread = fread(buf, 1, (size_t)sz, f); + fclose(f); + buf[nread] = '\0'; + return (spl_val_t)(uintptr_t)buf; +} + +/* os_alloc — allocate memory (malloc) */ +SYSCALL_1(os_alloc, size_t, (uintptr_t)malloc(a1)) + +/* os_free — free memory */ +SYSCALL_1(os_free, void *, (free(a1), (spl_val_t)0)) + +/* os_realloc — reallocate memory (realloc) */ +SYSCALL_2(os_realloc, void *, size_t, (uintptr_t)realloc(a1, a2)) + +/* os_strlen — get string length */ +SYSCALL_1(os_strlen, const char *, strlen(a1)) + +/* os_strcmp — compare two strings */ +SYSCALL_2(os_strcmp, const char *, const char *, strcmp(a1, a2)) + +/* os_memcpy — copy memory, returns dst */ +SYSCALL_3(os_memcpy, void *, const void *, size_t, (memcpy(a1, a2, a3), (uintptr_t)a1)) + +/* ================================================================= + * VM syscalls (for comptime — compile-time code execution) + * + * These let SPL code create isolated sub-VMs, load compiled .sir + * programs into them, push arguments, call functions, and run. + * ================================================================= */ + +/* vm_new — create a new sub-VM instance, returns spl_vm_t* */ +static spl_val_t vm_new(int nargs, spl_val_t *args) { + CHECK_NARGS("vm_new", 0); + (void)args; + spl_vm_t *vm = (spl_vm_t *)malloc(sizeof(spl_vm_t)); + if (!vm) + return 0; + spl_vm_init(vm); + return (spl_val_t)(uintptr_t)vm; +} + +/* vm_drop — destroy a sub-VM and its loaded program */ +static spl_val_t vm_drop(int nargs, spl_val_t *args) { + CHECK_NARGS("vm_drop", 1); + spl_vm_t *vm = (spl_vm_t *)(uintptr_t)args[0]; + if (!vm) + return 0; + if (vm->prog) { + spl_prog_drop(vm->prog); + free(vm->prog); + vm->prog = NULL; + } + spl_vm_drop(vm); + free(vm); + return 0; +} + +/* vm_load — load a .sir file, register syscalls, return spl_prog_t* */ +static spl_val_t vm_load(int nargs, spl_val_t *args) { + CHECK_NARGS("vm_load", 2); + spl_vm_t *vm = (spl_vm_t *)(uintptr_t)args[0]; + const char *path = (const char *)(uintptr_t)args[1]; + if (!vm || !path) + return -1; + + spl_prog_t *prog = (spl_prog_t *)malloc(sizeof(spl_prog_t)); + if (!prog) + return -1; + if (spl_prog_load_from_file(path, prog) != 0) { + free(prog); + return -1; + } + spl_syscall_register(prog); + if (spl_vm_load_prog(vm, prog) != 0) { + spl_prog_drop(prog); + free(prog); + return -1; + } + return (spl_val_t)(uintptr_t)prog; +} + +/* vm_push — push a value onto the sub-VM's stack (for passing arguments) */ +static spl_val_t vm_push(int nargs, spl_val_t *args) { + CHECK_NARGS("vm_push", 2); + spl_vm_t *vm = (spl_vm_t *)(uintptr_t)args[0]; + spl_val_t val = args[1]; + if (!vm) + return -1; + if (vm->sp >= vm->config.max_stack_depth) { + fprintf(stderr, "vm_push: stack overflow\n"); + return -1; + } + vm->stacks.data[vm->sp++] = val; + return 0; +} + +/* vm_call — call a function by name with pre-pushed arguments + * + * Args (3): vm, function_name, nargs + * The arguments should already be on the sub-VM's stack via vm_push. + * + * Mirrors the CALL instruction's frame setup: + * - Pushes a sentinel frame (saved_ip=-1) if cp==0 so RET halts properly + * - Pushes the actual call frame + * - Sets fp = sp - nargs (so arg0 = data[fp], arg1 = data[fp+1], ...) + * - Sets ip to the function address + */ +static spl_val_t vm_call(int nargs, spl_val_t *args) { + CHECK_NARGS("vm_call", 3); + spl_vm_t *vm = (spl_vm_t *)(uintptr_t)args[0]; + const char *name = (const char *)(uintptr_t)args[1]; + spl_val_t call_nargs = args[2]; + if (!vm || !name) + return -1; + + spl_func_t *func = spl_prog_get_func(vm->prog, name); + if (!func) { + fprintf(stderr, "vm_call: function '%s' not found\n", name); + return -1; + } + if (vm->cp >= vm->config.max_call_depth - 1) { + fprintf(stderr, "vm_call: call stack overflow\n"); + return -1; + } + + /* Sentinel frame so the entry function's RET halts cleanly */ + if (vm->cp == 0) { + vm->frames.data[vm->cp].saved_fp = 0; + vm->frames.data[vm->cp].saved_ip = (uintptr_t)-1; + vm->frames.data[vm->cp].nargs = 0; + vm->cp++; + } + + /* Actual call frame (same logic as the CALL instruction) */ + vm->frames.data[vm->cp].saved_fp = vm->fp; + vm->frames.data[vm->cp].saved_ip = vm->ip; + vm->frames.data[vm->cp].nargs = call_nargs; + vm->cp++; + vm->fp = vm->sp - call_nargs; + vm->ip = (uintptr_t)func->address; + return 0; +} + +/* vm_run — run a sub-VM to completion, returns exit_code */ +SYSCALL_1(vm_run, spl_vm_t *, spl_vm_run_until(a1, 0)) + +/* ================================================================= + * Registration + * + * Called after loading a .sir file. Matches native declarations in + * prog->natives against known syscall names and hooks up impl_fn. + * ================================================================= */ + +void spl_syscall_register(spl_prog_t *prog) { + static spl_native_t table[] = { + /* OS operations */ + {"os_exit", 0, os_exit}, + {"os_putchar", 0, os_putchar}, + {"os_getchar", 0, os_getchar}, + {"os_putint", 0, os_putint}, + {"os_putstr", 0, os_putstr}, + {"os_fopen", 0, os_fopen}, + {"os_fclose", 0, os_fclose}, + {"os_fread", 0, os_fread}, + {"os_fwrite", 0, os_fwrite}, + {"os_fsize", 0, os_fsize}, + {"os_read_file", 0, os_read_file}, + {"os_alloc", 0, os_alloc}, + {"os_free", 0, os_free}, + {"os_realloc", 0, os_realloc}, + {"os_strlen", 0, os_strlen}, + {"os_strcmp", 0, os_strcmp}, + {"os_memcpy", 0, os_memcpy}, + /* VM operations (comptime) */ + {"vm_new", 0, vm_new}, + {"vm_drop", 0, vm_drop}, + {"vm_load", 0, vm_load}, + {"vm_push", 0, vm_push}, + {"vm_call", 0, vm_call}, + {"vm_run", 0, vm_run}, + }; + int n = (int)(sizeof(table) / sizeof(table[0])); + if (!prog) + return; + vec_for(prog->natives, i) { + spl_native_t *nat = &vec_at(prog->natives, i); + if (!nat->name || nat->impl_fn) + continue; + for (int j = 0; j < n; j++) { + if (strcmp(nat->name, table[j].name) == 0) { + nat->impl_fn = table[j].impl_fn; + break; + } + } + } +} diff --git a/stage0/spl_syscall.h b/stage0/spl_syscall.h new file mode 100644 index 0000000..109dcca --- /dev/null +++ b/stage0/spl_syscall.h @@ -0,0 +1,22 @@ +/* spl_syscall.h — built-in syscall layer for SIR VM + * + * Provides I/O native functions (putchar, getchar, file ops, etc.) + * that compiled SPL programs can call via NCALL. + * + * spl_syscall_register() must be called AFTER spl_prog_load_from_file() + * and BEFORE spl_vm_run_until(). It matches native declarations in the + * loaded program against known syscall names and hooks up the C + * implementations. + */ + +#ifndef __SPL_SYSCALL_H__ +#define __SPL_SYSCALL_H__ + +#include "spl_ir.h" + +/* Register all known built-in syscalls into prog->natives[]. + * Entries whose name matches a known syscall get their impl_fn set; + * unmatched entries remain NULL (will error at runtime if called). */ +void spl_syscall_register(spl_prog_t *prog); + +#endif /* __SPL_SYSCALL_H__ */ diff --git a/stage0/spl_vm.c b/stage0/spl_vm.c new file mode 100644 index 0000000..e568274 --- /dev/null +++ b/stage0/spl_vm.c @@ -0,0 +1,1042 @@ +/* spl_vm.c — SIR step-by-step interpreter + * + * Implements the spl_vm.h API with macro-based type dispatch to + * eliminate repetitive per-type switch cases. + */ + +#include "spl_vm.h" +#include "spl_ir.h" + +#include +#include +#include + +#if defined(_WIN32) || defined(_WIN64) +#define WIN32_LEAN_AND_MEAN +#include +#define SPL_DLOPEN(name) ((void *)LoadLibraryA(name)) +#define SPL_DLSYM(lib, fn) ((void *)GetProcAddress((HMODULE)lib, fn)) +#define SPL_DLCLOSE(lib) FreeLibrary((HMODULE)lib) +#else +#include +#define SPL_DLOPEN(name) dlopen(name, RTLD_LAZY | RTLD_LOCAL) +#define SPL_DLSYM(lib, fn) dlsym(lib, fn) +#define SPL_DLCLOSE(lib) dlclose(lib) +#endif + +/* ================================================================ + * Type helpers + * ================================================================ */ + +static int spl_is_float(spl_type_t t) { return t == SPL_F32 || t == SPL_F64; } +static int spl_type_size(spl_type_t t) { + switch (t) { + case SPL_VOID: + return 0; + case SPL_I8: + case SPL_U8: + return 1; + case SPL_I16: + case SPL_U16: + return 2; + case SPL_I32: + case SPL_U32: + case SPL_F32: + return 4; + case SPL_I64: + case SPL_U64: + case SPL_F64: + return 8; + case SPL_ISIZE: + case SPL_USIZE: + case SPL_PTR: + return sizeof(void *) / 8; + case SPL_TYPE_COUNT: + return 0; + } + return 0; +} + +/* ================================================================ + * Error reporting macro + * ================================================================ */ + +#define VM_ERROR(msg) \ + do { \ + fprintf(stderr, "vm: error at ip=%zd: %s\n", vm->ip - 1, msg); \ + vm->exit_code = 1; \ + return -1; \ + } while (0) + +/* ================================================================ + * Stack push/pop (stacks.data is pre-allocated in init) + * ================================================================ */ + +#define PUSH(v) \ + do { \ + if (vm->sp >= vm->config.max_stack_depth) \ + VM_ERROR("stack overflow"); \ + vm->stacks.data[vm->sp++] = (spl_val_t)(v); \ + } while (0) + +#define POP() vm->stacks.data[--vm->sp] + +/* ================================================================ + * Type-dispatch macros for arithmetic / comparison + * + * ARITH_BINOP — ADD / SUB / MUL (two's complement: op same for + * signed and unsigned at the same bit-width) + * DIV_REM_S — signed division / remainder + * DIV_REM_U — unsigned division / remainder + * CMP_ALL — EQ / NE (bitwise compare, also handles floats) + * CMP_S — signed ordering (<, <=, >, >=) + * CMP_U — unsigned ordering + * ================================================================ */ + +#define ARITH_BINOP(OP) \ + do { \ + spl_val_t _b = POP(), _a = POP(); \ + spl_val_t _r = 0; \ + if (spl_is_float((spl_type_t)ins->type)) { \ + double _da, _db, _dr; \ + if (ins->type == SPL_F32) { \ + float _fa, _fb; \ + memcpy(&_fa, &_a, 4); \ + memcpy(&_fb, &_b, 4); \ + _da = _fa; \ + _db = _fb; \ + } else { \ + memcpy(&_da, &_a, 8); \ + memcpy(&_db, &_b, 8); \ + } \ + _dr = _da OP _db; \ + if (ins->type == SPL_F32) { \ + float _fr = (float)_dr; \ + memcpy(&_r, &_fr, 4); \ + } else { \ + memcpy(&_r, &_dr, 8); \ + } \ + } else { \ + switch (ins->type) { \ + case SPL_I8: \ + _r = (spl_val_t)((int8_t)_a OP(int8_t) _b); \ + break; \ + case SPL_U8: \ + _r = (spl_val_t)((uint8_t)_a OP(uint8_t) _b); \ + break; \ + case SPL_I16: \ + _r = (spl_val_t)((int16_t)_a OP(int16_t) _b); \ + break; \ + case SPL_U16: \ + _r = (spl_val_t)((uint16_t)_a OP(uint16_t) _b); \ + break; \ + case SPL_I32: \ + _r = (spl_val_t)((int32_t)_a OP(int32_t) _b); \ + break; \ + case SPL_U32: \ + _r = (spl_val_t)((uint32_t)_a OP(uint32_t) _b); \ + break; \ + case SPL_I64: \ + _r = (spl_val_t)((int64_t)_a OP(int64_t) _b); \ + break; \ + case SPL_U64: \ + _r = _a OP _b; \ + break; \ + default: \ + VM_ERROR("bad type for arithmetic"); \ + } \ + } \ + PUSH(_r); \ + } while (0) + +/* signed division / remainder — all types cast to signed */ +#define DIV_REM_S(OP) \ + do { \ + spl_val_t _b = POP(), _a = POP(); \ + if (_b == 0) \ + VM_ERROR("division by zero"); \ + spl_val_t _r = 0; \ + if (spl_is_float((spl_type_t)ins->type)) { \ + double _da, _db, _dr; \ + if (ins->type == SPL_F32) { \ + float _fa, _fb; \ + memcpy(&_fa, &_a, 4); \ + memcpy(&_fb, &_b, 4); \ + _da = _fa; \ + _db = _fb; \ + } else { \ + memcpy(&_da, &_a, 8); \ + memcpy(&_db, &_b, 8); \ + } \ + _dr = _da / _db; \ + if (ins->type == SPL_F32) { \ + float _fr = (float)_dr; \ + memcpy(&_r, &_fr, 4); \ + } else { \ + memcpy(&_r, &_dr, 8); \ + } \ + } else { \ + switch (ins->type) { \ + case SPL_I8: \ + _r = (spl_val_t)((int8_t)_a OP(int8_t) _b); \ + break; \ + case SPL_U8: \ + _r = (spl_val_t)((int8_t)_a OP(int8_t) _b); \ + break; \ + case SPL_I16: \ + _r = (spl_val_t)((int16_t)_a OP(int16_t) _b); \ + break; \ + case SPL_U16: \ + _r = (spl_val_t)((int16_t)_a OP(int16_t) _b); \ + break; \ + case SPL_I32: \ + _r = (spl_val_t)((int32_t)_a OP(int32_t) _b); \ + break; \ + case SPL_U32: \ + _r = (spl_val_t)((int32_t)_a OP(int32_t) _b); \ + break; \ + case SPL_I64: \ + _r = (spl_val_t)((int64_t)_a OP(int64_t) _b); \ + break; \ + case SPL_U64: \ + _r = (spl_val_t)((int64_t)_a OP(int64_t) _b); \ + break; \ + default: \ + VM_ERROR("bad type for signed division"); \ + } \ + } \ + PUSH(_r); \ + } while (0) + +/* unsigned division / remainder */ +#define DIV_REM_U(OP) \ + do { \ + spl_val_t _b = POP(), _a = POP(); \ + if (_b == 0) \ + VM_ERROR("division by zero"); \ + spl_val_t _r = 0; \ + switch (ins->type) { \ + case SPL_I8: \ + _r = (spl_val_t)((uint8_t)_a OP(uint8_t) _b); \ + break; \ + case SPL_U8: \ + _r = (spl_val_t)((uint8_t)_a OP(uint8_t) _b); \ + break; \ + case SPL_I16: \ + _r = (spl_val_t)((uint16_t)_a OP(uint16_t) _b); \ + break; \ + case SPL_U16: \ + _r = (spl_val_t)((uint16_t)_a OP(uint16_t) _b); \ + break; \ + case SPL_I32: \ + _r = (spl_val_t)((uint32_t)_a OP(uint32_t) _b); \ + break; \ + case SPL_U32: \ + _r = (spl_val_t)((uint32_t)_a OP(uint32_t) _b); \ + break; \ + case SPL_I64: \ + _r = (spl_val_t)((uint64_t)_a OP(uint64_t) _b); \ + break; \ + case SPL_U64: \ + _r = _a OP _b; \ + break; \ + default: \ + VM_ERROR("bad type for unsigned division"); \ + } \ + PUSH(_r); \ + } while (0) + +/* CMP_ALL — equality comparisons (all types, floats via memcpy) */ +#define CMP_ALL(OP) \ + do { \ + spl_val_t _b = POP(), _a = POP(); \ + intptr_t _r = 0; \ + switch (ins->type) { \ + case SPL_I8: \ + _r = (int8_t)_a OP(int8_t) _b; \ + break; \ + case SPL_U8: \ + _r = (uint8_t)_a OP(uint8_t) _b; \ + break; \ + case SPL_I16: \ + _r = (int16_t)_a OP(int16_t) _b; \ + break; \ + case SPL_U16: \ + _r = (uint16_t)_a OP(uint16_t) _b; \ + break; \ + case SPL_I32: \ + _r = (int32_t)_a OP(int32_t) _b; \ + break; \ + case SPL_U32: \ + _r = (uint32_t)_a OP(uint32_t) _b; \ + break; \ + case SPL_I64: \ + _r = (int64_t)_a OP(int64_t) _b; \ + break; \ + case SPL_U64: \ + _r = _a OP _b; \ + break; \ + case SPL_F32: { \ + float _fa, _fb; \ + memcpy(&_fa, &_a, 4); \ + memcpy(&_fb, &_b, 4); \ + _r = _fa OP _fb; \ + break; \ + } \ + case SPL_F64: { \ + double _da, _db; \ + memcpy(&_da, &_a, 8); \ + memcpy(&_db, &_b, 8); \ + _r = _da OP _db; \ + break; \ + } \ + default: \ + _r = 0; \ + break; \ + } \ + PUSH(_r); \ + } while (0) + +/* CMP_S — signed ordering (all ints cast to signed, floats OK) */ +#define CMP_S(OP) \ + do { \ + spl_val_t _b = POP(), _a = POP(); \ + intptr_t _r = 0; \ + switch (ins->type) { \ + case SPL_I8: \ + _r = (int8_t)_a OP(int8_t) _b; \ + break; \ + case SPL_U8: \ + _r = (int8_t)_a OP(int8_t) _b; \ + break; \ + case SPL_I16: \ + _r = (int16_t)_a OP(int16_t) _b; \ + break; \ + case SPL_U16: \ + _r = (int16_t)_a OP(int16_t) _b; \ + break; \ + case SPL_I32: \ + _r = (int32_t)_a OP(int32_t) _b; \ + break; \ + case SPL_U32: \ + _r = (int32_t)_a OP(int32_t) _b; \ + break; \ + case SPL_I64: \ + _r = (int64_t)_a OP(int64_t) _b; \ + break; \ + case SPL_U64: \ + _r = (int64_t)_a OP(int64_t) _b; \ + break; \ + case SPL_F32: { \ + float _fa, _fb; \ + memcpy(&_fa, &_a, 4); \ + memcpy(&_fb, &_b, 4); \ + _r = _fa OP _fb; \ + break; \ + } \ + case SPL_F64: { \ + double _da, _db; \ + memcpy(&_da, &_a, 8); \ + memcpy(&_db, &_b, 8); \ + _r = _da OP _db; \ + break; \ + } \ + default: \ + _r = 0; \ + break; \ + } \ + PUSH(_r); \ + } while (0) + +/* CMP_U — unsigned ordering (all ints cast to unsigned, no float) */ +#define CMP_U(OP) \ + do { \ + spl_val_t _b = POP(), _a = POP(); \ + intptr_t _r = 0; \ + switch (ins->type) { \ + case SPL_I8: \ + _r = (uint8_t)_a OP(uint8_t) _b; \ + break; \ + case SPL_U8: \ + _r = (uint8_t)_a OP(uint8_t) _b; \ + break; \ + case SPL_I16: \ + _r = (uint16_t)_a OP(uint16_t) _b; \ + break; \ + case SPL_U16: \ + _r = (uint16_t)_a OP(uint16_t) _b; \ + break; \ + case SPL_I32: \ + _r = (uint32_t)_a OP(uint32_t) _b; \ + break; \ + case SPL_U32: \ + _r = (uint32_t)_a OP(uint32_t) _b; \ + break; \ + case SPL_I64: \ + _r = (uint64_t)_a OP(uint64_t) _b; \ + break; \ + case SPL_U64: \ + _r = _a OP _b; \ + break; \ + default: \ + _r = 0; \ + break; \ + } \ + PUSH(_r); \ + } while (0) + +// 辅助函数:将异常代码转为可读字符串 +const char *ExceptionCodeToString(DWORD code) { + switch (code) { + case EXCEPTION_ACCESS_VIOLATION: + return "ACCESS_VIOLATION"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + return "ARRAY_BOUNDS_EXCEEDED"; + case EXCEPTION_BREAKPOINT: + return "BREAKPOINT"; + case EXCEPTION_DATATYPE_MISALIGNMENT: + return "DATATYPE_MISALIGNMENT"; + case EXCEPTION_FLT_DENORMAL_OPERAND: + return "FLT_DENORMAL_OPERAND"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + return "FLT_DIVIDE_BY_ZERO"; + case EXCEPTION_FLT_INEXACT_RESULT: + return "FLT_INEXACT_RESULT"; + case EXCEPTION_FLT_INVALID_OPERATION: + return "FLT_INVALID_OPERATION"; + case EXCEPTION_FLT_OVERFLOW: + return "FLT_OVERFLOW"; + case EXCEPTION_FLT_STACK_CHECK: + return "FLT_STACK_CHECK"; + case EXCEPTION_FLT_UNDERFLOW: + return "FLT_UNDERFLOW"; + case EXCEPTION_ILLEGAL_INSTRUCTION: + return "ILLEGAL_INSTRUCTION"; + case EXCEPTION_IN_PAGE_ERROR: + return "IN_PAGE_ERROR"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + return "INT_DIVIDE_BY_ZERO"; + case EXCEPTION_INT_OVERFLOW: + return "INT_OVERFLOW"; + case EXCEPTION_INVALID_DISPOSITION: + return "INVALID_DISPOSITION"; + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + return "NONCONTINUABLE_EXCEPTION"; + case EXCEPTION_PRIV_INSTRUCTION: + return "PRIV_INSTRUCTION"; + case EXCEPTION_SINGLE_STEP: + return "SINGLE_STEP"; + case EXCEPTION_STACK_OVERFLOW: + return "STACK_OVERFLOW"; + default: + return "UNKNOWN_EXCEPTION"; + } +} + +// 全局未处理异常过滤器 +LONG WINAPI UnhandledExceptionFilterImpl(EXCEPTION_POINTERS *pExceptionInfo) { + // 获取异常记录和上下文 + PEXCEPTION_RECORD record = pExceptionInfo->ExceptionRecord; + PCONTEXT context = pExceptionInfo->ContextRecord; + + // 打印基础信息 + fprintf(stderr, "========================================\n"); + fprintf(stderr, " Unhandled Exception Caught!\n"); + fprintf(stderr, " Exception Code: 0x%08lX (%s)\n", record->ExceptionCode, + ExceptionCodeToString(record->ExceptionCode)); + fprintf(stderr, " Exception Address: 0x%p\n", record->ExceptionAddress); + fprintf(stderr, " Exception Flags: %ld\n", record->ExceptionFlags); + + // 针对访问违例,打印更多细节 + if (record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) { + // ExceptionInformation[0]: 0=读, 1=写, 8=执行 + // ExceptionInformation[1]: 违例的目标地址 + if (record->NumberParameters >= 2) { + const char *operation; + switch (record->ExceptionInformation[0]) { + case 0: + operation = "Read"; + break; + case 1: + operation = "Write"; + break; + case 8: + operation = "Execute"; + break; + default: + operation = "Unknown"; + break; + } + fprintf(stderr, " Access Violation: %s at address 0x%p\n", operation, + (void *)(ULONG_PTR)record->ExceptionInformation[1]); + } + } + + // 可选:打印发生异常时的部分寄存器(例如 EIP/RIP, EAX/RAX 等) +#ifdef _M_X64 + fprintf(stderr, " Registers:\n"); + fprintf(stderr, " RIP: 0x%p RSP: 0x%p RAX: 0x%p\n", (void *)context->Rip, + (void *)context->Rsp, (void *)context->Rax); +#else + fprintf(stderr, " Registers:\n"); + fprintf(stderr, " EIP: 0x%p ESP: 0x%p EAX: 0x%p\n", (void *)context->Eip, + (void *)context->Esp, (void *)context->Eax); +#endif + + fprintf(stderr, "========================================\n"); + fflush(stderr); + + // 如果附加了调试器,同步输出到调试器窗口 + OutputDebugStringA("Unhandled exception occurred, check stderr.\n"); + + // 返回 EXCEPTION_EXECUTE_HANDLER 会终止进程 + // 你可以在这里调用 exit(1) 或直接返回,进程会被终止 + return EXCEPTION_EXECUTE_HANDLER; +} + +/* ================================================================ + * spl_vm_init / spl_vm_drop + * ================================================================ */ +void spl_vm_init_ex(spl_vm_t *vm, int stack_size, int call_depth) { +#ifdef _WIN32 + SetUnhandledExceptionFilter(UnhandledExceptionFilterImpl); +#endif + if (!vm) + return; + vm->config.max_stack_depth = stack_size > 0 ? stack_size : 1024 * 8; + vm->config.max_call_depth = call_depth > 0 ? call_depth : 128; + + vec_init(vm->stacks); + vec_realloc(vm->stacks, (usize)vm->config.max_stack_depth); + vec_init(vm->frames); + vec_realloc(vm->frames, (usize)vm->config.max_call_depth); + + vm->sp = vm->fp = vm->ip = vm->cp = vm->gp = 0; + vm->prog = NULL; + vm->trace = 0; + vm->debug = 1; + vm->exit_code = 0; +} + +void spl_vm_init(spl_vm_t *vm) { spl_vm_init_ex(vm, 0, 0); } + +void spl_vm_drop(spl_vm_t *vm) { + if (!vm) + return; + vec_free(vm->stacks); + vec_free(vm->frames); +} + +/* ================================================================ + * spl_vm_load_prog + * ================================================================ */ + +int spl_vm_load_prog(spl_vm_t *vm, spl_prog_t *prog) { + if (!vm || !prog) + return -1; + vm->prog = prog; + return 0; +} + +/* ================================================================ + * spl_vm_set_trace + * ================================================================ */ + +void spl_vm_set_trace(spl_vm_t *vm, int enabled) { + if (!vm) + return; + vm->trace = enabled ? 1 : 0; +} + +void spl_vm_set_debug(spl_vm_t *vm, int enabled) { + if (!vm) + return; + vm->debug = enabled ? 1 : 0; +} + +static inline int spl_vm_call(spl_vm_t *vm, spl_val_t addr, spl_val_t nargs) { + if (vm->cp >= vm->config.max_call_depth) + VM_ERROR("CALLI: call stack overflow"); + vm->frames.data[vm->cp].saved_fp = vm->fp; + vm->frames.data[vm->cp].saved_ip = vm->ip; + vm->frames.data[vm->cp].nargs = nargs; + vm->cp++; + vm->ip = addr; + vm->fp = vm->sp - nargs; + /* Transparent canary at fp-1 — invisible to normal LADDR/ST */ + if (vm->fp > 0) + vm->stacks.data[vm->fp - 1] = SPL_STACK_CANARY; + return 0; +} + +int spl_vm_prepare(spl_vm_t *vm, const char *entry, int argc, const char **argv, + const char **envp) { + if (!vm || !vm->prog) { + return -1; + } + spl_func_t *fn = spl_prog_get_func(vm->prog, entry ? entry : "main"); + if (!fn) { + fprintf(stderr, "vm: entry point '%s' not found\n", entry ? entry : "main"); + return -1; + } + vm->fp = 0; + vm->sp = 0; + vm->cp = 0; + vm->ip = -1; + vm->exit_code = 0; + + if (fn->nargs >= 1) { + PUSH(argc); + } + if (fn->nargs >= 2) { + PUSH(argv); + } + if (fn->nargs >= 3) { + PUSH(envp); + } + if (fn->nargs >= 4) { + printf("the start symbol can't using more than 3 args"); + return -1; + } + + /* push sentinel frame so RET knows this is the entry return */ + spl_vm_call(vm, fn->address, fn->nargs); + return 0; +} + +/* ================================================================ + * spl_vm_run_once — execute one instruction + * + * Returns: 0 = still running, 1 = halted, 2 = breakpoint (SPL_DBG), -1 = error + * ================================================================ */ + +int spl_vm_run_once(spl_vm_t *vm) { + const spl_ins_t *ins; + spl_prog_t *prog; + + if (!vm || !vm->prog) + return -1; + prog = vm->prog; + + if (vm->ip < 0 || vm->ip >= vec_size(prog->insns)) { + fprintf(stderr, "vm: ip=%zd out of bounds\n", vm->ip); + vm->exit_code = 1; + return -1; + } + + ins = &vec_at(prog->insns, vm->ip); + vm->ip++; + + if (vm->trace) { + fprintf(stderr, "vm: ip=%zd op=%s type=%s imm=%zu sp=%zd fp=%zd\n", vm->ip - 1, + spl_opcode_name(ins->opcode), spl_type_name(ins->type), ins->imm, vm->sp, vm->fp); + } + + switch (ins->opcode) { + + /* ========== Stack ========== */ + case SPL_PUSH: + PUSH(ins->imm); + break; + + case SPL_DUP: { + if (vm->sp < 1) + VM_ERROR("DUP: stack underflow"); + spl_val_t _v = vm->stacks.data[vm->sp - 1]; + PUSH(_v); + break; + } + + case SPL_DROP: + if (vm->sp < 1) + VM_ERROR("DROP: stack underflow"); + vm->sp--; + break; + + case SPL_SWAP: { + if (vm->sp < 2) + VM_ERROR("SWAP: stack underflow"); + spl_val_t _t = vm->stacks.data[vm->sp - 1]; + vm->stacks.data[vm->sp - 1] = vm->stacks.data[vm->sp - 2]; + vm->stacks.data[vm->sp - 2] = _t; + break; + } + + case SPL_PICK: { + isize _idx = ins->imm; + if (_idx >= vm->sp) + VM_ERROR("PICK: index out of range"); + PUSH(vm->stacks.data[vm->sp - 1 - _idx]); + break; + } + + /* ========== Arithmetic ========== */ + case SPL_ADD: + ARITH_BINOP(+); + break; + case SPL_SUB: + ARITH_BINOP(-); + break; + case SPL_MUL: + ARITH_BINOP(*); + break; + + case SPL_DIV_S: + DIV_REM_S(/); + break; + case SPL_DIV_U: + DIV_REM_U(/); + break; + case SPL_REM_S: + DIV_REM_S(%); + break; + case SPL_REM_U: + DIV_REM_U(%); + break; + + case SPL_NEG: { + spl_val_t _a = POP(); + if (spl_is_float((spl_type_t)ins->type)) { + double _d; + if (ins->type == SPL_F32) { + float _f; + memcpy(&_f, &_a, 4); + _f = -_f; + memcpy(&_a, &_f, 4); + } else { + memcpy(&_d, &_a, 8); + _d = -_d; + memcpy(&_a, &_d, 8); + } + PUSH(_a); + } else { + PUSH(-(int64_t)_a); + } + break; + } + + /* ========== Bitwise ========== */ + case SPL_AND: { + spl_val_t _b = POP(), _a = POP(); + PUSH(_a & _b); + break; + } + case SPL_OR: { + spl_val_t _b = POP(), _a = POP(); + PUSH(_a | _b); + break; + } + case SPL_XOR: { + spl_val_t _b = POP(), _a = POP(); + PUSH(_a ^ _b); + break; + } + case SPL_NOT: { + PUSH(~POP()); + break; + } + case SPL_SHL: { + int _s = (int)(POP() & 63); + PUSH(POP() << _s); + break; + } + case SPL_SHR_U: { + int _s = (int)(POP() & 63); + PUSH(POP() >> _s); + break; + } + case SPL_SHR_S: { + int _s = (int)(POP() & 63); + PUSH(POP() >> _s); + break; + } + + /* ========== Comparison ========== */ + case SPL_EQ: + CMP_ALL(==); + break; + case SPL_NE: + CMP_ALL(!=); + break; + case SPL_SLT: + CMP_S(<); + break; + case SPL_SLE: + CMP_S(<=); + break; + case SPL_SGT: + CMP_S(>); + break; + case SPL_SGE: + CMP_S(>=); + break; + case SPL_ULT: + CMP_U(<); + break; + case SPL_ULE: + CMP_U(<=); + break; + case SPL_UGT: + CMP_U(>); + break; + case SPL_UGE: + CMP_U(>=); + break; + + /* ========== Control Flow (relative offset) ========== */ + case SPL_JMP: + vm->ip = vm->ip + ins->imm; + break; + + case SPL_BZ: { + if (POP() == 0) + vm->ip = vm->ip + ins->imm; + break; + } + + case SPL_BNZ: { + if (POP() != 0) + vm->ip = vm->ip + ins->imm; + break; + } + + case SPL_CALL: { + spl_val_t _nargs = ins->imm; + spl_val_t _addr = POP(); + spl_vm_call(vm, _addr, _nargs); + break; + } + + case SPL_CALLI: { + spl_val_t _addr = POP(); + spl_val_t _nargs = POP(); + spl_vm_call(vm, _addr, _nargs); + break; + } + + case SPL_RET: { + spl_val_t _retval = 0; + if (ins->type != SPL_VOID) + _retval = POP(); + if (vm->cp <= 0) + VM_ERROR("RET: call stack underflow"); + vm->cp--; + intptr_t _saved_fp = vm->frames.data[vm->cp].saved_fp; + intptr_t _saved_ip = vm->frames.data[vm->cp].saved_ip; + /* entry return -> halt */ + if (_saved_ip < 0) { + vm->exit_code = (int)_retval; + return 1; + } + vm->sp = vm->fp; + vm->fp = _saved_fp; + vm->ip = _saved_ip; + if (ins->type != SPL_VOID) + PUSH(_retval); + break; + } + + case SPL_HALT: + return 1; + + /* ========== Stack / Frame Local Memory ========== */ + case SPL_ALLOC: { + spl_val_t _k = ins->imm; + uintptr_t _new_sp = vm->sp + _k; + if (_new_sp > vm->config.max_stack_depth) + VM_ERROR("ALLOC: stack overflow"); + for (uintptr_t _i = vm->sp; _i < _new_sp; _i++) + vm->stacks.data[_i] = 0; + vm->sp = _new_sp; + break; + } + + case SPL_LADDR: + PUSH(vm->stacks.data + vm->fp + ins->imm); + break; + + case SPL_GADDR: { + spl_val_t _idx = ins->imm; + if (_idx >= vec_size(prog->gdata)) + VM_ERROR("GADDR: global data index out of range"); + PUSH((spl_val_t)(uintptr_t)vec_at(prog->gdata, _idx).data); + break; + } + + /* ========== Indirect Memory (load/store with types) ========== */ + case SPL_LOAD: { + void *_addr = (void *)POP(); + spl_val_t _v = 0; + memcpy(&_v, _addr, spl_type_size(ins->type)); + PUSH(_v); + break; + } + case SPL_STORE: { + spl_val_t _v = POP(); + void *_addr = (void *)POP(); + memcpy(_addr, &_v, spl_type_size(ins->type)); + break; + } + + /* ========== Type Conversion ========== */ + case SPL_TRUNC: { + spl_val_t _v = POP(); + intptr_t _bits = ins->imm; + if (_bits < 1 || _bits > 64) + VM_ERROR("TRUNC: bad bit-width"); + if (_bits < 64) { + spl_val_t _mask = ((spl_val_t)1 << _bits) - 1; + _v &= _mask; + } + PUSH(_v); + break; + } + + case SPL_SEXT: { + spl_val_t _v = POP(); + intptr_t _bits = ins->imm; + if (_bits < 1 || _bits > 64) + VM_ERROR("SEXT: bad bit-width"); + if (_bits < 64) { + spl_val_t _sign = (spl_val_t)1 << (_bits - 1); + spl_val_t _mask = ((spl_val_t)1 << _bits) - 1; + _v &= _mask; + if (_v & _sign) + _v |= ~_mask; + } + PUSH(_v); + break; + } + + case SPL_ZEXT: { + spl_val_t _v = POP(); + intptr_t _bits = ins->imm; + if (_bits < 1 || _bits > 64) + VM_ERROR("ZEXT: bad bit-width"); + if (_bits < 64) + _v &= ((spl_val_t)1 << _bits) - 1; + PUSH(_v); + break; + } + + /* ========== Native Interface ========== */ + case SPL_NCALL: { + intptr_t _nargs = ins->imm; + spl_val_t _nat_idx = POP(); + spl_native_t *_nat; + if (_nat_idx >= vec_size(prog->natives)) + VM_ERROR("NCALL: native index out of range"); + _nat = &vec_at(prog->natives, _nat_idx); + if (!_nat->impl_fn) { + snprintf(vm->error_msg, sizeof(vm->error_msg), + "NCALL: NULL native function pointer expect %s", _nat->name); + VM_ERROR(vm->error_msg); + } + spl_val_t *_arg_base = vm->stacks.data + vm->sp - _nargs; + spl_val_t _result = _nat->impl_fn(_nargs, _arg_base); + vm->sp -= _nargs; + PUSH(_result); + break; + } + + case SPL_NLIB: { + spl_val_t _si = ins->imm; + const char *_lib; + if (_si >= vec_size(prog->strtab) || !vec_at(prog->strtab, _si)) + VM_ERROR("NLIB: invalid string index"); + _lib = vec_at(prog->strtab, _si); + void *_handle = SPL_DLOPEN(_lib); + if (!_handle) { + fprintf(stderr, "vm: NLIB: dlopen(%s) failed\n", _lib); + PUSH(0); + } else { + PUSH(_handle); + } + break; + } + + /* ========== Debug ========== */ + case SPL_DBG: { + spl_vm_backtrace(vm, vm->fp); + return 2; /* breakpoint: pause execution */ + } + + default: + fprintf(stderr, "vm: unknown opcode %d at ip=%zd\n", ins->opcode, vm->ip - 1); + vm->exit_code = 1; + return -1; + } + + /* canary check in debug mode (canary is at fp-1, invisible to compiled code) */ + if (vm->debug && vm->fp > 0) { + spl_val_t val = vm->stacks.data[vm->fp - 1]; + if (val != SPL_STACK_CANARY) { + snprintf(vm->error_msg, sizeof(vm->error_msg), + "STACK CANARY CORRUPTED at ip=%zd, fp=%zd\n", vm->ip - 1, vm->fp); + VM_ERROR(vm->error_msg); + } + } + + return 0; +} + +int spl_vm_run_until(spl_vm_t *vm, size_t step) { + size_t _count = 0; + int _ret; + if (!vm) + return -1; + while (1) { + if (step > 0 && _count >= step) + return 0; + _ret = spl_vm_run_once(vm); + if (_ret != 0) + return _ret; + _count++; + } +} + +static const char *func_name_by_ip(spl_prog_t *prog, spl_val_t ip) { + vec_for(prog->funcs, i) { + spl_func_t *f = &vec_at(prog->funcs, i); + if (ip >= f->address && ip < (f->address + f->ninsns)) + return f->name; + } + return "?"; +} + +void spl_vm_dump_instr(spl_vm_t *vm, spl_val_t ip) { + if (!vm || !vm->prog) + return; + if (ip >= vec_size(vm->prog->insns)) + return; + spl_ins_t *ins = &vec_at(vm->prog->insns, ip); + fprintf(stderr, " instr at ip=%zd: op=%s type=%s imm=%zu\n", ip, spl_opcode_name(ins->opcode), + spl_type_name(ins->type), ins->imm); +} + +void spl_vm_stackdump(spl_vm_t *vm, spl_val_t sp) { + if (!vm) + return; + fprintf(stderr, " stack (sp=%zd, fp=%zd):\n", sp, vm->fp); + spl_val_t start = sp > 16 ? sp - 16 : 0; + for (spl_val_t i = start; i < sp; i++) { + fprintf(stderr, " [%zd] = 0x%016zx (%zd)\n", i, vm->stacks.data[i], vm->stacks.data[i]); + } +} + +int spl_vm_backtrace(spl_vm_t *vm, spl_val_t fp) { + if (!vm || !vm->prog) + return -1; + (void)fp; + fprintf(stderr, "=== backtrace ===\n"); + for (isize i = vm->cp - 1; i >= 0; i--) { + spl_val_t _saved_ip = vm->frames.data[i].saved_ip; + spl_val_t _saved_fp = vm->frames.data[i].saved_fp; + const char *_fn = func_name_by_ip(vm->prog, _saved_ip - 1); + fprintf(stderr, " [%zd] %s (fp=%zd, ip=%zd, args=%zd)\n", i, _fn, _saved_fp, _saved_ip, + vm->frames.data[i].nargs); + } + const char *_cur = func_name_by_ip(vm->prog, vm->ip); + fprintf(stderr, " => %s (fp=%zd, ip=%zd, sp=%zd)\n", _cur, vm->fp, vm->ip, vm->sp); + return 0; +} diff --git a/stage0/spl_vm.h b/stage0/spl_vm.h new file mode 100644 index 0000000..f83e007 --- /dev/null +++ b/stage0/spl_vm.h @@ -0,0 +1,66 @@ +/* spl_vm.h — SIR interpreter */ + +#ifndef __SPL_VM_H__ +#define __SPL_VM_H__ + +#include "include/core_vec.h" +#include "spl_ir.h" +#include + +#define SPL_STACK_CANARY ((spl_val_t)0xDEADBEEFCAFEBABEull) + +typedef struct { + uintptr_t saved_fp; + uintptr_t saved_ip; + spl_val_t nargs; +} spl_callframe_t; + +typedef VEC(spl_val_t) spl_stack_vec_t; +typedef VEC(spl_callframe_t) spl_frame_vec_t; + +typedef struct { + spl_stack_vec_t stacks; + spl_frame_vec_t frames; + uintptr_t gp; // global pointer + uintptr_t sp; // stack pointer + uintptr_t fp; // frame pointer + uintptr_t cp; // call pointer + uintptr_t ip; // instr pointer + int exit_code; + int trace; /* non-zero to print each instruction */ + int debug; /* non-zero to enable canary checks */ + spl_prog_t *prog; + char error_msg[1024]; + struct { + uintptr_t max_stack_depth; + uintptr_t max_call_depth; + } config; +} spl_vm_t; + +/* Initialize VM with default sizes (SPL_DEFAULT_STACK_SIZE / + * SPL_DEFAULT_CALL_DEPTH) */ +void spl_vm_init(spl_vm_t *vm); + +/* Initialize VM with custom sizes (pass 0 to use defaults) */ +void spl_vm_init_ex(spl_vm_t *vm, int stack_size, int call_depth); + +/* Free dynamically allocated memory in VM */ +void spl_vm_drop(spl_vm_t *vm); + +int spl_vm_load_prog(spl_vm_t *vm, spl_prog_t *prog); +int spl_vm_prepare(spl_vm_t *vm, const char *entry, int argc, const char **argv, const char **envp); + +/* Enable/disable instruction-level tracing */ +void spl_vm_set_trace(spl_vm_t *vm, int enabled); + +/* Enable/disable debug mode (stack canary protection) */ +void spl_vm_set_debug(spl_vm_t *vm, int enabled); + +int spl_vm_run_once(spl_vm_t *vm); +int spl_vm_run_until(spl_vm_t *vm, size_t step); + +void spl_vm_dump_instr(spl_vm_t *vm, spl_val_t ip); +void spl_vm_stackdump(spl_vm_t *vm, spl_val_t sp); +int spl_vm_backtrace(spl_vm_t *vm, spl_val_t fp); + +#endif /* __SPL_VM_H__ */ diff --git a/stage0/test_spl_vm.c b/stage0/test_spl_vm.c new file mode 100644 index 0000000..27e9705 --- /dev/null +++ b/stage0/test_spl_vm.c @@ -0,0 +1,876 @@ +/* test_spl_vm.c – unit tests for SPL VM using acutest.h + * + * Constructs SIR binaries, writes to temp files, loads via + * spl_prog_load_from_file, and runs via the spl_vm_t API. + */ + +#include "include/acutest.h" +#include "spl_ir.h" +#include "spl_vm.h" + +#include +#include +#include + +/* ================================================================ + * Binary-writing helpers (mirrors spl_ir.c internal format) + * ================================================================ */ + +/* Write spl_val_t (8 LE bytes), advance pointer */ +#define LE64(p, v) \ + do { \ + unsigned char *_p = (p); \ + spl_val_t _v = (spl_val_t)(v); \ + *_p++ = (unsigned char)(_v); \ + *_p++ = (unsigned char)(_v >> 8); \ + *_p++ = (unsigned char)(_v >> 16); \ + *_p++ = (unsigned char)(_v >> 24); \ + *_p++ = (unsigned char)(_v >> 32); \ + *_p++ = (unsigned char)(_v >> 40); \ + *_p++ = (unsigned char)(_v >> 48); \ + *_p++ = (unsigned char)(_v >> 56); \ + (p) = _p; \ + } while (0) + +/* Convenience: build spl_ins_t from raw fields */ +static spl_ins_t ins(uint16_t op, uint16_t type, spl_val_t imm) { + spl_ins_t x; + x.opcode = op; + x.type = type; + x.imm = imm; + return x; +} + +/* Build a single-function SIR binary in malloc'd memory. + * Returns buffer that must be freed by caller. */ +static unsigned char *build_binary(const char *fname, spl_val_t nargs, const spl_ins_t *instns, + int ninsns, size_t *out_len) { + size_t nlen = strlen(fname) + 1; + size_t npad = ((nlen + 7) / 8) * 8 - nlen; + size_t sz = 8 + 8 + 8 + 8 + 8 + 8 /* magic + 5 counts */ + + 8 + nlen + npad + 8 + 8 + 8 + 8 /* func entry */ + + (size_t)ninsns * 12; /* instructions */ + unsigned char *buf = (unsigned char *)malloc(sz); + unsigned char *p = buf; + + memcpy(p, "SPLBIN\0\0", 8); + p += 8; /* magic */ + LE64(p, 1); /* nfuncs */ + LE64(p, (spl_val_t)ninsns); /* ninsns */ + LE64(p, 0); /* nnatives */ + LE64(p, 0); /* nstrs */ + LE64(p, 0); /* ndata */ + + /* func entry */ + LE64(p, (spl_val_t)nlen); /* name_len */ + memcpy(p, fname, nlen); + p += nlen; + memset(p, 0, npad); + p += npad; + LE64(p, 0); /* idx_of_strtab */ + LE64(p, nargs); + LE64(p, (spl_val_t)ninsns); + LE64(p, 0); /* address = 0 */ + + for (int i = 0; i < ninsns; i++) { + *p++ = (unsigned char)(instns[i].opcode); + *p++ = (unsigned char)(instns[i].opcode >> 8); + *p++ = (unsigned char)(instns[i].type); + *p++ = (unsigned char)(instns[i].type >> 8); + LE64(p, instns[i].imm); + } + + *out_len = sz; + return buf; +} + +/* ================================================================ + * VM test helpers (file-based prog load) + * ================================================================ */ + +static const char *TMPFILE = "test_spl_vm_tmp.bin"; + +/* Write binary buffer to temp file */ +static int write_temp(const unsigned char *bin, size_t len) { + FILE *f = fopen(TMPFILE, "wb"); + if (!f) + return -1; + fwrite(bin, 1, len, f); + fclose(f); + return 0; +} + +/* Run a single-function program, return exit code (or -1 on failure). */ +static int run(const spl_ins_t *instns, int ninsns) { + size_t len; + unsigned char *bin = build_binary("main", 0, instns, ninsns, &len); + spl_prog_t prog; + spl_vm_t vm; + int rc = -1; + + spl_vm_init(&vm); + if (write_temp(bin, len) == 0 && spl_prog_load_from_file(TMPFILE, &prog) == 0) { + if (spl_vm_load_prog(&vm, &prog) == 0) { + if (spl_vm_prepare(&vm, "main", 0, NULL, NULL) == 0) { + int ret = spl_vm_run_until(&vm, 0); + if (ret == 1) /* halted normally */ + rc = (int)vm.exit_code; + else if (ret == 0) /* still running (shouldn't happen with halt) */ + rc = -2; + } + } + } + spl_vm_drop(&vm); + remove(TMPFILE); + free(bin); + return rc; +} + +/* Run with native functions registered. */ +static int run_with_natives(const spl_ins_t *instns, int ninsns, spl_native_t *natives, + int nnatives) { + size_t len; + unsigned char *bin = build_binary("main", 0, instns, ninsns, &len); + spl_prog_t prog; + spl_vm_t vm; + int rc = -1; + + spl_vm_init(&vm); + if (write_temp(bin, len) == 0 && spl_prog_load_from_file(TMPFILE, &prog) == 0) { + for (int i = 0; i < nnatives; i++) + spl_prog_add_native(&prog, &natives[i]); + if (spl_vm_load_prog(&vm, &prog) == 0) { + if (spl_vm_prepare(&vm, "main", 0, NULL, NULL) == 0) { + int ret = spl_vm_run_until(&vm, 0); + if (ret == 1) + rc = (int)vm.exit_code; + else if (ret == 0) + rc = -2; + } + } + } + spl_vm_drop(&vm); + remove(TMPFILE); + free(bin); + return rc; +} + +/* ================================================================ + * Native function for NCALL tests + * ================================================================ */ + +static spl_val_t native_add_impl(int nargs, spl_val_t *args) { + return (nargs >= 2) ? args[0] + args[1] : 0; +} + +/* ================================================================ + * Stack tests + * ================================================================ */ + +void test_push_imm(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 42), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 2) == 42); +} + +void test_dup(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 99), ins(SPL_DUP, SPL_VOID, 0), + ins(SPL_ADD, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 198); +} + +void test_drop(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 1), ins(SPL_PUSH, SPL_I32, 2), + ins(SPL_DROP, SPL_VOID, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +void test_swap(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 1), ins(SPL_PUSH, SPL_I32, 2), + ins(SPL_SWAP, SPL_VOID, 0), ins(SPL_DROP, SPL_VOID, 0), + ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 5) == 2); +} + +void test_pick(void) { + /* push 10, 20, 30, pick 1 -> copy 20, add -> 50 */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 10), ins(SPL_PUSH, SPL_I32, 20), + ins(SPL_PUSH, SPL_I32, 30), ins(SPL_PICK, SPL_VOID, 1), + ins(SPL_ADD, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 6) == 50); +} + +/* ================================================================ + * Arithmetic tests + * ================================================================ */ + +void test_add(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 2), ins(SPL_PUSH, SPL_I32, 3), ins(SPL_ADD, SPL_I32, 0), + ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 5); +} + +void test_sub(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 10), ins(SPL_PUSH, SPL_I32, 3), + ins(SPL_SUB, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 7); +} + +void test_mul(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 6), ins(SPL_PUSH, SPL_I32, 7), ins(SPL_MUL, SPL_I32, 0), + ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 42); +} + +void test_div(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 100), ins(SPL_PUSH, SPL_I32, 3), + ins(SPL_DIV_S, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 33); +} + +void test_rem(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 100), ins(SPL_PUSH, SPL_I32, 3), + ins(SPL_REM_S, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +void test_neg(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 42), ins(SPL_NEG, SPL_I32, 0), + ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 3) == -42); +} + +void test_i64_arith(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I64, 42), ins(SPL_RET, SPL_I64, 0)}; + TEST_CHECK(run(p, 2) == 42); +} + +/* ================================================================ + * Bitwise tests + * ================================================================ */ + +void test_and(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xFF00), ins(SPL_PUSH, SPL_U32, 0x0FF0), + ins(SPL_AND, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 0x0F00); +} + +void test_or(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xFF00), ins(SPL_PUSH, SPL_U32, 0x00FF), + ins(SPL_OR, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 0xFFFF); +} + +void test_xor(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xFFFF), ins(SPL_PUSH, SPL_U32, 0x0FF0), + ins(SPL_XOR, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 0xF00F); +} + +void test_not(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xFFFF0000), ins(SPL_NOT, SPL_U32, 0), + ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 3) == (unsigned int)0x0000FFFF); +} + +void test_shl(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 1), ins(SPL_PUSH, SPL_U32, 10), + ins(SPL_SHL, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 1024); +} + +void test_shr(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 1024), ins(SPL_PUSH, SPL_U32, 10), + ins(SPL_SHR_U, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +void test_shr_s(void) { + /* arithmetic right shift: -1024 >> 5 sign-extends */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, (spl_val_t)(int32_t)-1024), ins(SPL_PUSH, SPL_U32, 5), + ins(SPL_SHR_S, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == -32); +} + +/* ================================================================ + * Comparison tests + * ================================================================ */ + +void test_eq(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 42), ins(SPL_PUSH, SPL_I32, 42), + ins(SPL_EQ, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +void test_neq(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 42), ins(SPL_PUSH, SPL_I32, 99), + ins(SPL_NE, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +void test_lt(void) { + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 10), ins(SPL_PUSH, SPL_I32, 20), + ins(SPL_SLT, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +void test_gt_signed(void) { + /* -1 > 1 should be 0 (false) for signed compare */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 0xFFFFFFFF), ins(SPL_PUSH, SPL_I32, 1), + ins(SPL_SGT, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 0); +} + +void test_sle(void) { + /* -1 <= 1 -> true (1) for signed */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 0xFFFFFFFF), ins(SPL_PUSH, SPL_I32, 1), + ins(SPL_SLE, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +void test_sge(void) { + /* -1 >= 1 -> false (0) for signed */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 0xFFFFFFFF), ins(SPL_PUSH, SPL_I32, 1), + ins(SPL_SGE, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 4) == 0); +} + +void test_ult(void) { + /* 0xFFFFFFFF < 1 -> false (0) for unsigned */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xFFFFFFFF), ins(SPL_PUSH, SPL_U32, 1), + ins(SPL_ULT, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 0); +} + +void test_ule(void) { + /* 0xFFFFFFFF <= 0xFFFFFFFF -> true (1) */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xFFFFFFFF), ins(SPL_PUSH, SPL_U32, 0xFFFFFFFF), + ins(SPL_ULE, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +void test_ugt(void) { + /* 0xFFFFFFFF > 1 -> true (1) for unsigned */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xFFFFFFFF), ins(SPL_PUSH, SPL_U32, 1), + ins(SPL_UGT, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +void test_uge(void) { + /* 0xFFFFFFFF >= 1 -> true (1) */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xFFFFFFFF), ins(SPL_PUSH, SPL_U32, 1), + ins(SPL_UGE, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +/* ================================================================ + * Control flow tests (relative offset) + * ================================================================ */ + +void test_jmp(void) { + /* push 1, jmp +3 (skip next 2 insns), push 2 (skipped), push 3, add, ret -> 4 */ + /* jmp at ip=1, after fetch ip=2, target ip=3 (push 3) => offset = 1 */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 1), ins(SPL_JMP, SPL_VOID, 1), + ins(SPL_PUSH, SPL_I32, 2), ins(SPL_PUSH, SPL_I32, 3), + ins(SPL_ADD, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 6) == 4); +} + +void test_bz_bnz(void) { + /* push 0, bz +2 (skip to ret with 1) -> return 1 */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 0), ins(SPL_BZ, SPL_VOID, 2), + ins(SPL_PUSH, SPL_I32, 99), ins(SPL_RET, SPL_I32, 0), + ins(SPL_PUSH, SPL_I32, 1), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 6) == 1); + + /* push 1, bnz +2 (skip to ret with 42) -> return 42 */ + spl_ins_t q[] = {ins(SPL_PUSH, SPL_I32, 1), ins(SPL_BNZ, SPL_VOID, 2), + ins(SPL_PUSH, SPL_I32, 99), ins(SPL_RET, SPL_I32, 0), + ins(SPL_PUSH, SPL_I32, 42), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(q, 6) == 42); +} + +/* ================================================================ + * Memory tests (ALLOC, LADDR, LD, ST) + * ================================================================ */ + +void test_alloc_laddr_st64_ld64(void) { + /* alloc 1 local, laddr + st64 42, laddr + ld64 back -> 42 */ + spl_ins_t p[] = { + ins(SPL_ALLOC, SPL_VOID, 1), /* 0: alloc 1 local */ + ins(SPL_LADDR, SPL_VOID, 0), /* 1: addr of local[0] */ + ins(SPL_PUSH, SPL_I64, 42), /* 2: value */ + ins(SPL_STORE, SPL_I64, 0), /* 3: store */ + ins(SPL_LADDR, SPL_VOID, 0), /* 4: addr of local[0] */ + ins(SPL_LOAD, SPL_I64, 0), /* 5: load */ + ins(SPL_RET, SPL_I64, 0) /* 6: ret */ + }; + TEST_CHECK(run(p, 7) == 42); +} + +void test_alloc_zeroed(void) { + /* alloc 1 local, laddr + ld64 -> should be 0 (zero-initialized) */ + spl_ins_t p[] = {ins(SPL_ALLOC, SPL_VOID, 1), ins(SPL_LADDR, SPL_VOID, 0), + ins(SPL_LOAD, SPL_I64, 0), ins(SPL_RET, SPL_I64, 0)}; + TEST_CHECK(run(p, 4) == 0); +} + +/* ================================================================ + * Indirect memory load/store tests (stack-based, no heap) + * ================================================================ */ + +void test_ld_st64(void) { + /* alloc 8 slots, st64 42, ld64 back -> 42 */ + spl_ins_t p[] = {ins(SPL_ALLOC, SPL_VOID, 8), ins(SPL_LADDR, SPL_VOID, 0), + ins(SPL_DUP, SPL_VOID, 0), ins(SPL_PUSH, SPL_I64, 42), + ins(SPL_STORE, SPL_I64, 0), ins(SPL_LOAD, SPL_I64, 0), + ins(SPL_RET, SPL_I64, 0)}; + TEST_CHECK(run(p, 7) == 42); +} + +void test_ld_st32(void) { + spl_ins_t p[] = {ins(SPL_ALLOC, SPL_VOID, 8), ins(SPL_LADDR, SPL_VOID, 0), + ins(SPL_DUP, SPL_VOID, 0), ins(SPL_PUSH, SPL_I64, 0xAABBCCDD), + ins(SPL_STORE, SPL_U32, 0), ins(SPL_LOAD, SPL_U32, 0), + ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 7) == (int)0xAABBCCDD); +} + +void test_ld_st16(void) { + spl_ins_t p[] = {ins(SPL_ALLOC, SPL_VOID, 8), ins(SPL_LADDR, SPL_VOID, 0), + ins(SPL_DUP, SPL_VOID, 0), ins(SPL_PUSH, SPL_I64, 0xBEEF), + ins(SPL_STORE, SPL_U16, 0), ins(SPL_LOAD, SPL_U16, 0), + ins(SPL_RET, SPL_U16, 0)}; + TEST_CHECK(run(p, 7) == 0xBEEF); +} + +void test_ld_st8(void) { + spl_ins_t p[] = {ins(SPL_ALLOC, SPL_VOID, 8), ins(SPL_LADDR, SPL_VOID, 0), + ins(SPL_DUP, SPL_VOID, 0), ins(SPL_PUSH, SPL_I64, 0xAB), + ins(SPL_STORE, SPL_U8, 0), ins(SPL_LOAD, SPL_U8, 0), + ins(SPL_RET, SPL_U8, 0)}; + TEST_CHECK(run(p, 7) == 0xAB); +} + +/* ================================================================ + * Function call tests (relative offset CALL) + * ================================================================ */ + +void test_call_add(void) { + /* + * add(a, b) address 0, 6 insns, 2 args + * main() address 6, 5 insns, 0 args + * + * add (ip=0): + * laddr 0 ip=0 arg0 addr + * ld64 ip=1 load arg0 + * laddr 1 ip=2 arg1 addr + * ld64 ip=3 load arg1 + * add i64 ip=4 + * ret i64 ip=5 + * + * main (ip=6): + * push 10 ip=6 + * push 20 ip=7 + * push 0 ip=8 target address = add at 0 + * call 2 ip=9 nargs=2 + * ret i64 ip=10 + */ + spl_ins_t add_insns[] = {ins(SPL_LADDR, SPL_VOID, 0), ins(SPL_LOAD, SPL_I64, 0), + ins(SPL_LADDR, SPL_VOID, 1), ins(SPL_LOAD, SPL_I64, 0), + ins(SPL_ADD, SPL_I64, 0), ins(SPL_RET, SPL_I64, 0)}; + spl_ins_t main_insns[] = {ins(SPL_PUSH, SPL_I32, 10), ins(SPL_PUSH, SPL_I32, 20), + ins(SPL_PUSH, SPL_VOID, 0), ins(SPL_CALL, SPL_VOID, 2), + ins(SPL_RET, SPL_I64, 0)}; + + int nadd = (int)(sizeof(add_insns) / sizeof(add_insns[0])); + int nmain = (int)(sizeof(main_insns) / sizeof(main_insns[0])); + int ninsns_t = nadd + nmain; + size_t nlen0 = strlen("add") + 1; + size_t npad0 = ((nlen0 + 7) / 8) * 8 - nlen0; + size_t nlen1 = strlen("main") + 1; + size_t npad1 = ((nlen1 + 7) / 8) * 8 - nlen1; + size_t sz = 8 + 8 + 8 + 8 + 8 + 8 /* magic + 5 counts */ + + 8 + nlen0 + npad0 + 8 + 8 + 8 + 8 /* func 0 */ + + 8 + nlen1 + npad1 + 8 + 8 + 8 + 8 /* func 1 */ + + (size_t)ninsns_t * 12; + unsigned char *bin = (unsigned char *)malloc(sz); + unsigned char *p = bin; + + memcpy(p, "SPLBIN\0\0", 8); + p += 8; + LE64(p, 2); + LE64(p, (spl_val_t)ninsns_t); + LE64(p, 0); + LE64(p, 0); + LE64(p, 0); + + /* func[0]: "add" */ + LE64(p, (spl_val_t)nlen0); + memcpy(p, "add", nlen0); + p += nlen0; + memset(p, 0, npad0); + p += npad0; + LE64(p, 0); + LE64(p, 2); + LE64(p, (spl_val_t)nadd); + LE64(p, 0); + + /* func[1]: "main" */ + LE64(p, (spl_val_t)nlen1); + memcpy(p, "main", nlen1); + p += nlen1; + memset(p, 0, npad1); + p += npad1; + LE64(p, 0); + LE64(p, 0); + LE64(p, (spl_val_t)nmain); + LE64(p, (spl_val_t)nadd); /* address */ + + for (int i = 0; i < nadd; i++) { + *p++ = (unsigned char)(add_insns[i].opcode); + *p++ = (unsigned char)(add_insns[i].opcode >> 8); + *p++ = (unsigned char)(add_insns[i].type); + *p++ = (unsigned char)(add_insns[i].type >> 8); + LE64(p, add_insns[i].imm); + } + for (int i = 0; i < nmain; i++) { + *p++ = (unsigned char)(main_insns[i].opcode); + *p++ = (unsigned char)(main_insns[i].opcode >> 8); + *p++ = (unsigned char)(main_insns[i].type); + *p++ = (unsigned char)(main_insns[i].type >> 8); + LE64(p, main_insns[i].imm); + } + + spl_prog_t prog; + spl_vm_t vm; + int rc = -1; + + spl_vm_init(&vm); + if (write_temp(bin, sz) == 0 && spl_prog_load_from_file(TMPFILE, &prog) == 0) { + spl_vm_load_prog(&vm, &prog); + if (spl_vm_prepare(&vm, "main", 0, NULL, NULL) == 0) { + int ret = spl_vm_run_until(&vm, 0); + if (ret == 1) + rc = (int)vm.exit_code; + } + spl_vm_drop(&vm); + } + remove(TMPFILE); + free(bin); + TEST_CHECK(rc == 30); +} + +/* ================================================================ + * CALLI test + * ================================================================ */ + +void test_calli(void) { + /* + * add (ip=0, 2 args): + * laddr 0 ip=0 + * ld64 ip=1 + * laddr 1 ip=2 + * ld64 ip=3 + * add i64 ip=4 + * ret i64 ip=5 + * + * main (ip=6, 0 args): + * push 10 ip=6 + * push 20 ip=7 + * push 2 ip=8 (nargs) + * push 0 ip=9 (address of add) + * calli ip=10 + * ret i64 ip=11 + */ + spl_ins_t add_insns[] = {ins(SPL_LADDR, SPL_VOID, 0), ins(SPL_LOAD, SPL_I64, 0), + ins(SPL_LADDR, SPL_VOID, 1), ins(SPL_LOAD, SPL_I64, 0), + ins(SPL_ADD, SPL_I64, 0), ins(SPL_RET, SPL_I64, 0)}; + spl_ins_t main_insns[] = {ins(SPL_PUSH, SPL_I32, 10), ins(SPL_PUSH, SPL_I32, 20), + ins(SPL_PUSH, SPL_VOID, 2), ins(SPL_PUSH, SPL_VOID, 0), + ins(SPL_CALLI, SPL_VOID, 0), ins(SPL_RET, SPL_I64, 0)}; + + int ninsns_t = 6 + 6; + size_t nlen0 = strlen("add") + 1; + size_t npad0 = ((nlen0 + 7) / 8) * 8 - nlen0; + size_t nlen1 = strlen("main") + 1; + size_t npad1 = ((nlen1 + 7) / 8) * 8 - nlen1; + size_t sz = 8 + 8 + 8 + 8 + 8 + 8 + 8 + nlen0 + npad0 + 8 + 8 + 8 + 8 + 8 + nlen1 + npad1 + 8 + + 8 + 8 + 8 + (size_t)ninsns_t * 12; + unsigned char *bin = (unsigned char *)malloc(sz); + unsigned char *p = bin; + + memcpy(p, "SPLBIN\0\0", 8); + p += 8; + LE64(p, 2); + LE64(p, (spl_val_t)ninsns_t); + LE64(p, 0); + LE64(p, 0); + LE64(p, 0); + + /* func[0]: add */ + LE64(p, (spl_val_t)nlen0); + memcpy(p, "add", nlen0); + p += nlen0; + memset(p, 0, npad0); + p += npad0; + LE64(p, 0); + LE64(p, 2); + LE64(p, 6); + LE64(p, 0); + + /* func[1]: main */ + LE64(p, (spl_val_t)nlen1); + memcpy(p, "main", nlen1); + p += nlen1; + memset(p, 0, npad1); + p += npad1; + LE64(p, 0); + LE64(p, 0); + LE64(p, 6); + LE64(p, 6); + + for (int i = 0; i < 6; i++) { + *p++ = (unsigned char)(add_insns[i].opcode); + *p++ = (unsigned char)(add_insns[i].opcode >> 8); + *p++ = (unsigned char)(add_insns[i].type); + *p++ = (unsigned char)(add_insns[i].type >> 8); + LE64(p, add_insns[i].imm); + } + for (int i = 0; i < 6; i++) { + *p++ = (unsigned char)(main_insns[i].opcode); + *p++ = (unsigned char)(main_insns[i].opcode >> 8); + *p++ = (unsigned char)(main_insns[i].type); + *p++ = (unsigned char)(main_insns[i].type >> 8); + LE64(p, main_insns[i].imm); + } + + spl_prog_t prog; + spl_vm_t vm; + int rc = -1; + + spl_vm_init(&vm); + if (write_temp(bin, sz) == 0 && spl_prog_load_from_file(TMPFILE, &prog) == 0) { + spl_vm_load_prog(&vm, &prog); + if (spl_vm_prepare(&vm, "main", 0, NULL, NULL) == 0) { + int ret = spl_vm_run_until(&vm, 0); + if (ret == 1) + rc = (int)vm.exit_code; + } + spl_vm_drop(&vm); + } + remove(TMPFILE); + free(bin); + TEST_CHECK(rc == 30); +} + +/* ================================================================ + * Global data (GADDR) test + * ================================================================ */ + +void test_gaddr_ld32(void) { + /* Load program, add global data, GADDR + LD32 to read it back */ + spl_ins_t p[] = {ins(SPL_GADDR, SPL_VOID, 0), ins(SPL_LOAD, SPL_U32, 0), + ins(SPL_RET, SPL_U32, 0)}; + size_t len; + unsigned char *bin = build_binary("main", 0, p, 3, &len); + spl_prog_t prog; + spl_vm_t vm; + int rc = -1; + + spl_vm_init(&vm); + if (write_temp(bin, len) == 0 && spl_prog_load_from_file(TMPFILE, &prog) == 0) { + uint32_t val = 0xDEADBEEF; + spl_prog_add_data(&prog, &val, sizeof(val)); + + spl_vm_load_prog(&vm, &prog); + if (spl_vm_prepare(&vm, "main", 0, NULL, NULL) == 0) { + int ret = spl_vm_run_until(&vm, 0); + if (ret == 1) + rc = (int)vm.exit_code; + } + spl_vm_drop(&vm); + } + remove(TMPFILE); + free(bin); + TEST_CHECK(rc == (int)0xDEADBEEF); +} + +void test_gaddr_multi(void) { + /* Two global data entries: read second one via GADDR 1 */ + spl_ins_t p[] = {ins(SPL_GADDR, SPL_VOID, 1), ins(SPL_LOAD, SPL_U32, 0), + ins(SPL_RET, SPL_U32, 0)}; + size_t len; + unsigned char *bin = build_binary("main", 0, p, 3, &len); + spl_prog_t prog; + spl_vm_t vm; + int rc = -1; + + spl_vm_init(&vm); + if (write_temp(bin, len) == 0 && spl_prog_load_from_file(TMPFILE, &prog) == 0) { + uint32_t a = 0xAAAAAAAA, b = 0xBBBBBBBB; + spl_prog_add_data(&prog, &a, sizeof(a)); + spl_prog_add_data(&prog, &b, sizeof(b)); + + spl_vm_load_prog(&vm, &prog); + if (spl_vm_prepare(&vm, "main", 0, NULL, NULL) == 0) { + int ret = spl_vm_run_until(&vm, 0); + if (ret == 1) + rc = (int)vm.exit_code; + } + spl_vm_drop(&vm); + } + remove(TMPFILE); + free(bin); + TEST_CHECK(rc == (int)0xBBBBBBBB); +} + +/* ================================================================ + * Native call test + * ================================================================ */ + +void test_ncall(void) { + /* main() -> i32 { return native_add(30, 12); } */ + /* NCALL imm = nargs, native index is pushed separately */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 30), ins(SPL_PUSH, SPL_I32, 12), + ins(SPL_PUSH, SPL_VOID, 0), ins(SPL_NCALL, SPL_I32, 2), + ins(SPL_RET, SPL_I32, 0)}; + spl_native_t nat[] = {{"native_add", 0, native_add_impl}}; + TEST_CHECK(run_with_natives(p, 5, nat, 1) == 42); +} + +/* ================================================================ + * Type conversion tests + * ================================================================ */ + +void test_trunc(void) { + /* push 0xABCD, trunc to 8 bits -> 0xCD */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xABCD), ins(SPL_TRUNC, SPL_U8, 8), + ins(SPL_RET, SPL_U8, 0)}; + TEST_CHECK(run(p, 3) == 0xCD); +} + +void test_sext(void) { + /* push 0x80, sext from 8 bits -> 0xFFFFFF80 */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 0x80), ins(SPL_SEXT, SPL_I32, 8), + ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 3) == (int)(int8_t)(0x80)); +} + +void test_zext(void) { + /* push 0xFFFF, zext from 8 bits -> 0xFF */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 0xFFFF), ins(SPL_ZEXT, SPL_U32, 8), + ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 3) == 0xFF); +} + +/* ================================================================ + * Extended arithmetic tests (unsigned div/rem) + * ================================================================ */ + +void test_div_u(void) { + /* unsigned: 100 / 3 = 33 */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 100), ins(SPL_PUSH, SPL_U32, 3), + ins(SPL_DIV_U, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 33); +} + +void test_rem_u(void) { + /* unsigned: 100 % 3 = 1 */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_U32, 100), ins(SPL_PUSH, SPL_U32, 3), + ins(SPL_REM_U, SPL_U32, 0), ins(SPL_RET, SPL_U32, 0)}; + TEST_CHECK(run(p, 4) == 1); +} + +/* ================================================================ + * Edge case tests + * ================================================================ */ + +void test_many_ops(void) { + /* (1+2) * (3+4) = 21 */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 1), ins(SPL_PUSH, SPL_I32, 2), ins(SPL_ADD, SPL_I32, 0), + ins(SPL_PUSH, SPL_I32, 3), ins(SPL_PUSH, SPL_I32, 4), ins(SPL_ADD, SPL_I32, 0), + ins(SPL_MUL, SPL_I32, 0), ins(SPL_RET, SPL_I32, 0)}; + TEST_CHECK(run(p, 8) == 21); +} + +void test_halt(void) { + /* push 77, halt -> exit code 0 (HALT doesn't pop) */ + spl_ins_t p[] = {ins(SPL_PUSH, SPL_I32, 77), ins(SPL_HALT, SPL_VOID, 0)}; + TEST_CHECK(run(p, 2) == 0); +} + +/* ================================================================ + * Bad program tests + * ================================================================ */ + +void test_bad_magic(void) { + unsigned char bad[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + spl_prog_t prog; + if (write_temp(bad, 8) == 0) { + TEST_CHECK(spl_prog_load_from_file(TMPFILE, &prog) != 0); + remove(TMPFILE); + } +} + +void test_empty(void) { + TEST_CHECK(spl_prog_load_from_file("nonexistent_file_xyz.bin", NULL) != 0); +} + +/* ================================================================ + * Test list + * ================================================================ */ + +TEST_LIST = { + {"push", test_push_imm}, + {"dup", test_dup}, + {"drop", test_drop}, + {"swap", test_swap}, + {"pick", test_pick}, + {"add", test_add}, + {"sub", test_sub}, + {"mul", test_mul}, + {"div", test_div}, + {"rem", test_rem}, + {"neg", test_neg}, + {"i64_arith", test_i64_arith}, + {"and", test_and}, + {"or", test_or}, + {"xor", test_xor}, + {"not", test_not}, + {"shl", test_shl}, + {"shr", test_shr}, + {"shr_s", test_shr_s}, + {"eq", test_eq}, + {"neq", test_neq}, + {"lt", test_lt}, + {"gt_signed", test_gt_signed}, + {"sle", test_sle}, + {"sge", test_sge}, + {"ult", test_ult}, + {"ule", test_ule}, + {"ugt", test_ugt}, + {"uge", test_uge}, + {"jmp", test_jmp}, + {"bz_bnz", test_bz_bnz}, + {"alloc_laddr_ld_st", test_alloc_laddr_st64_ld64}, + {"alloc_zeroed", test_alloc_zeroed}, + {"call_add", test_call_add}, + {"calli", test_calli}, + {"gaddr_ld32", test_gaddr_ld32}, + {"gaddr_multi", test_gaddr_multi}, + {"ncall", test_ncall}, + {"div_u", test_div_u}, + {"rem_u", test_rem_u}, + {"trunc", test_trunc}, + {"sext", test_sext}, + {"zext", test_zext}, + {"ld_st8", test_ld_st8}, + {"ld_st16", test_ld_st16}, + {"ld_st32", test_ld_st32}, + {"ld_st64", test_ld_st64}, + {"many_ops", test_many_ops}, + {"halt", test_halt}, + {"bad_magic", test_bad_magic}, + {"empty", test_empty}, + {NULL, NULL}, +};