aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar zinc <zinc@downsta.ir> 2026-10-14 10:30:45 +0000
committerGravatar zinc <zinc@downsta.ir> 2026-10-14 10:30:45 +0000
commit44e60a7afc4673c88890997d5779aa7c6c06f330 (patch)
treea7f950704ea90b6e63b42b70b52dade1b9b34375
parentx86-64 jit: translate ordinary vm blocks to x86-64 on linux, o3 regions on th... (diff)
x86-64 jit: zyi target, ZYNOO3, bench and test timeouts, docs; drop a stray file
-rw-r--r--Makefile6
-rw-r--r--README.md12
-rw-r--r--note1
-rw-r--r--src/core/core.c21
-rw-r--r--tests/bench/bench.sh6
-rwxr-xr-xtests/test.sh7
6 files changed, 33 insertions, 20 deletions
diff --git a/Makefile b/Makefile
index d5caa92..56894e4 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,7 @@
# make check the minimal suite, for iterating (tests/test.sh -q)
# make test JOBS=4 the same, four sections at a time
# make genome build/core/genome.bin from src/, with the fixpoint check
+# make build/bin/zyi the interpreter alone (zy has a jit on arm64 and on x86-64 linux)
# make lsp build/bin/zclsp, the language server
# make web build/web/: the page, its script and zc.wasm
# make js build/js/: the javascript vm, its layout and genome
@@ -87,9 +88,12 @@ $(B)/core/genome.bin: $(ZC_DEPS) $(BIN)/zc0$(EXE) $(BIN)/mkgenome$(EXE) src/core
cp $(B)/s1/genome.bin $@
@echo "fixpoint: $$(( $$(wc -c < $@) / 8 )) words, $@ updated"
-# the interpreter: the vm core with the compiler genome inside
+# the vm core with the compiler genome inside: a jit on arm64 and x86-64 linux, else the interpreter
$(BIN)/zy$(EXE): src/core/core.c $(IO) $(B)/core/genome.bin $(CORE_H) | $(BIN)
$(CC) $(CFLAGS) -DJTYPE=int src/core/core.c $(IO) $(COREINC) -o $@
+# the interpreter alone, even on a host with a jit: make build/bin/zyi, to compare against zy
+$(BIN)/zyi$(EXE): src/core/core.c $(IO) $(B)/core/genome.bin $(CORE_H) | $(BIN)
+ $(CC) $(CFLAGS) -DZYINTERP -DJTYPE=int src/core/core.c $(IO) $(COREINC) -o $@
# the portable microcore
$(BIN)/zym$(EXE): src/micro/micro.c src/micro/micro_port.h $(PORT) $(B)/core/genome.bin $(CORE_H) | $(BIN)
$(CC) $(CFLAGS) $(MICRO_THREADS) src/micro/micro.c $(PORT) $(COREINC) -Isrc/micro -o $@
diff --git a/README.md b/README.md
index 0b59a6b..bf46a01 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
# tinyvm zygote
-a small stack cpu began as a C89 code golf entry and became a self-hosting runtime. a native core runs vm code with an arm64 jit, through llvm on supported builds, or in the interpreter. the genome contains a compiler for zc, a C-like language with forth influences. the compiler resides in vm memory alongside the program; code can call its functions, modify its tables, add syntax, and compile new code at runtime. heap objects carry an ownership tag (u, m or d) checked by a static analyzer, with deterministic frees at scope ends and a mark-sweep collector underneath. types are a compiler primitive, with structs, generics, reflection and an optional typechecker. programs can install up to 512 custom opcodes (virtual enhancements), which the jits inline as native code. code marked `o3` bypasses the stack machine: the core compiles the region as a unit and keeps its variables in registers.
+a small stack cpu began as a C89 code golf entry and became a self-hosting runtime. a native core runs vm code with an arm64 or x86-64 jit, through llvm on supported builds, or in the interpreter. the genome contains a compiler for zc, a C-like language with forth influences. the compiler resides in vm memory alongside the program; code can call its functions, modify its tables, add syntax, and compile new code at runtime. heap objects carry an ownership tag (u, m or d) checked by a static analyzer, with deterministic frees at scope ends and a mark-sweep collector underneath. types are a compiler primitive, with structs, generics, reflection and an optional typechecker. programs can install up to 512 custom opcodes (virtual enhancements), which the jits inline as native code. code marked `o3` bypasses the stack machine: the core compiles the region as a unit and keeps its variables in registers.
this folder has the latest code. `docs/LEARN.md` is a tour of the language. `docs/history/` is the scrapbook: notes for every stage, plus frozen code from the stages that still have it, archived as `docs/history/code-snapshots.tar.gz`.
## files
-`src/core/core.c` is the native core. it contains the arm64 jit, the llvm backend (behind `-DZYLLVM`), the portable interpreter, the enhancement unit (recipe parser, vm-code lifter, optimizer and lowering), and the o3 region compiler. `src/runtime/rt.zc` provides i/o, native division enhancements, the heap, and the collector; `src/compiler/compiler.zc` is the compiler. both are written in zc and begin with an overview. together they make up the genome. `src/tools/zc0.c` is stage 0 of the bootstrap, a c compiler for the subset of zc used by `src/compiler/compiler.zc`. `src/tools/build.zc` has the resident compiler compile its own source into a fresh image, and `src/tools/mkgenome.c` checks that image against the memory map before it becomes `build/core/genome.bin`. an image is a file of little-endian 64-bit words, and every core takes its genome in at compile time with c23 `#embed` (`src/core/genome.h`). `examples/` contains small programs (hello, factorial, echo, self-patching code, an owning list, a repl, a typed program, and an enhancement demo) and larger proofs of concept:
+`src/core/core.c` is the native core. it contains the arm64 jit, the x86-64 jit, the llvm backend (behind `-DZYLLVM`), the portable interpreter, the enhancement unit (recipe parser, vm-code lifter, optimizer and lowering), and the o3 region compiler. `src/runtime/rt.zc` provides i/o, native division enhancements, the heap, and the collector; `src/compiler/compiler.zc` is the compiler. both are written in zc and begin with an overview. together they make up the genome. `src/tools/zc0.c` is stage 0 of the bootstrap, a c compiler for the subset of zc used by `src/compiler/compiler.zc`. `src/tools/build.zc` has the resident compiler compile its own source into a fresh image, and `src/tools/mkgenome.c` checks that image against the memory map before it becomes `build/core/genome.bin`. an image is a file of little-endian 64-bit words, and every core takes its genome in at compile time with c23 `#embed` (`src/core/genome.h`). `examples/` contains small programs (hello, factorial, echo, self-patching code, an owning list, a repl, a typed program, and an enhancement demo) and larger proofs of concept:
- `mandel.zc`: the mandelbrot set in 16.16 fixed point, with the fixed-point multiply wired in as a custom opcode.
- `bf.zc`: a brainfuck compiler. it turns brainfuck into zc source, has the resident compiler compile it, and runs it (native code on the jit). it takes a program after a backtick, or runs hello world.
@@ -71,7 +71,7 @@ windows needs clang or gcc and powershell for the tests. the batch file is the s
### native backends
-on arm64, `src/core/core.c` uses its jit by default. other hosts use the interpreter. `-DZYINTERP` forces interpretation; `-DZYLLVM` enables the llvm backend when its headers and libraries are available. direct builds from the root are:
+on arm64, `src/core/core.c` uses its arm64 jit by default, and on x86-64 linux its x86-64 jit. other hosts use the interpreter. `-DZYINTERP` forces interpretation (`make build/bin/zyi` builds that core beside `zy`); `-DZYLLVM` enables the llvm backend when its headers and libraries are available. direct builds from the root are:
```sh
make genome
@@ -433,7 +433,7 @@ going into o3 is free: o3 code reads normal objects the same way and ignores the
## the llvm backend
-a tiered jit for hosts the arm64 backend doesn't cover. the interpreter counts how often each block start gets reached, and a hot block is translated to llvm ir using the same block rules as the arm64 jit: it ends at call, jump, ret or halt, keeps going past a jz, and literal targets are fused. the ir goes through the O2 pipeline and gets compiled by orc's lljit. stores into compiled words drop the blocks covering them, like on arm64. it pays off for long-running code: a loop-and-recursion benchmark runs about 3.3x faster than the interpreter. short bursty work like the self-compile is slower, since compile time dominates there. `ZYHOT=1` compiles every block, which is a good stress test, and the test suite runs that way too.
+a tiered jit for hosts the native jits don't cover, or where it is asked for explicitly (`-DZYLLVM` replaces the x86-64 jit). the interpreter counts how often each block start gets reached, and a hot block is translated to llvm ir using the same block rules as the arm64 jit: it ends at call, jump, ret or halt, keeps going past a jz, and literal targets are fused. the ir goes through the O2 pipeline and gets compiled by orc's lljit. stores into compiled words drop the blocks covering them, like on arm64. it pays off for long-running code: a loop-and-recursion benchmark runs about 3.3x faster than the interpreter. short bursty work like the self-compile is slower, since compile time dominates there. `ZYHOT=1` compiles every block, which is a good stress test, and the test suite runs that way too.
## the heap
@@ -443,7 +443,7 @@ the heap is a buddy allocator over [HEAP_LO, top), where top starts at HEAP_HI a
## c abi
-zc can call native c functions and hand zc functions to native code. it is built on the syscall gate (ops 14 to 17), so it needs a host with `dlsym`: the interpreter, the llvm backend and the arm64 jit on linux and macos (the jit has to be a dynamic binary, since a static one has no `dlsym`). the microcore, the javascript vm, the web build and the windows scripts answer -1, and a native call there gives -1 and nothing else.
+zc can call native c functions and hand zc functions to native code. it is built on the syscall gate (ops 14 to 17), so it needs a host with `dlsym`: the interpreter, the llvm backend and the arm64 and x86-64 jits on linux and macos (the jit has to be a dynamic binary, since a static one has no `dlsym`). the microcore, the javascript vm, the web build and the windows scripts answer -1, and a native call there gives -1 and nothing else.
```
include rt;
@@ -568,6 +568,8 @@ calls push the arguments left to right and the callee's prologue pops them into
registers: x19 holds the memory base, x20 the data stack pointer, x21 the return stack pointer, x22 the translation table and x23 the state block (memory, s, r, table, coverage map, Q, G, &J, store hook, install hook, enhancement fallback). o3 regions also use x26..x28, which the entry stub saves.
+the x86-64 jit (linux hosts, `ZYJIT_X64`) keeps the same state in callee-saved registers: r12 is the memory base, r13 the data stack pointer, r14 the return stack pointer, r15 the translation table and rbx the state block, with rax, rcx, rdx, rsi, rdi and r8..r11 as scratch and rsp 16-byte aligned inside translated code so calls into c are plain system v calls. it shares everything but the emitter with the arm64 jit: the translation table, the coverage map, the store hook, the dropped-translation bookkeeping, the flush, the block rules below and the dispatch stubs' policy. inside a block it does not move r13 for every push and pop: the translator counts the words pushed and popped and addresses stack slots from r13 with that offset, and brings r13 up to date before a call into c and at every exit. it translates the base opcodes, the float and byte opcodes (the byte loads inline, the byte stores through a c helper), install, and the c abi call; every installed enhancement is a call to the interpreter's evaluator for its slot (`xrun`). o3 regions are not compiled to x86-64: a region entry is never chained to, so control reaches it through the c loop, which runs the region on the region interpreter exactly as the interpreter build does, and a store from either side into the other's code drops what it should (the regions mark their words in the same coverage map). the region interpreter is slower than the jit's own code for the same words (the `bench.sh` o3 kernels take 2 to 4 times longer than their plain versions on this jit, where they are 3 to 8 times faster on the interpreter), so `ZYNOO3=1` leaves regions alone and runs their vm code through the jit, as the microcore does, until a region compiler for x86-64 exists.
+
compilation is lazy. the first time control reaches an address, that block is translated from live memory. a block ends at call, jump, ret or halt. a `z` doesn't end it: the not-taken side keeps compiling, which makes superblocks. a literal followed by `c`, `j` or `z` is fused into a constant target. every exit goes through a dispatch stub that checks both stack pointers against their limits (kept in x24 and x25), bounds-checks the target, loads `tab[t]`, and branches straight to it when it's already compiled. otherwise it returns to c, which compiles the block and re-enters. enhancements are inlined: inputs load from the data stack into x0..x15, the optimized micro-ops run in registers with x16 and x17 as scratch, and the outputs go back. an install calls into c and invalidates translations and o3 regions that depend on its slot. invalidated code is rebuilt on its next entry. when the loop reaches an o3 entry point, it compiles the region (or reuses it) instead of translating a block.
self-modification is handled with a counted coverage map: one byte per word, the number of live translations that cover it, sticky at 255. a store checks that byte inline: an uncovered target, the usual case, gets a plain store. a covered target calls the hook, which looks the word up in a page index (64-word pages, each with a list of the blocks that touch it) and drops only the translations containing that word, decrementing the coverage of just their words. the jit code then returns to c at the next instruction, so even patches to the very next word take effect. writing back the same value is a no-op. installing an enhancement does not flush: every translation records which enhancement slots it inlined, and an install drops only those (and the o3 regions that depend on the slot). if the 16 mb code buffer or the block index fills, everything is flushed. on llvm each block has its own resource tracker, so dropped code is freed once no compiled code is running.
diff --git a/note b/note
deleted file mode 100644
index 42fe16c..0000000
--- a/note
+++ /dev/null
@@ -1 +0,0 @@
-zy \ No newline at end of file
diff --git a/src/core/core.c b/src/core/core.c
index 51d6b4f..b571981 100644
--- a/src/core/core.c
+++ b/src/core/core.c
@@ -1,10 +1,12 @@
/* zygote core: puts the genome (the zc compiler and its runtime, both vm code)
- into memory and runs it. on arm64, every block of vm code is translated to
- arm64 the first time it is reached, straight from live memory. stores that
- land on translated words throw those translations away, so code that
- rewrites itself, including the resident compiler, just gets retranslated
- on the next visit. on other hosts (or with -DZYINTERP) a plain interpreter
- with the same semantics runs instead. ZYTRACE=1 logs translations on stderr. */
+ into memory and runs it. on arm64 and on x86-64 linux, every block of vm code
+ is translated to native code the first time it is reached, straight from live
+ memory. stores that land on translated words throw those translations away,
+ so code that rewrites itself, including the resident compiler, just gets
+ retranslated on the next visit. the arm64 jit also compiles o3 regions; on
+ x86-64 they run on the region interpreter, and enhancements are called, not
+ inlined. on other hosts (or with -DZYINTERP) a plain interpreter with the same
+ semantics runs instead. ZYTRACE=1 logs translations on stderr. */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
@@ -5895,11 +5897,11 @@ static void rcompile(Rg *R, int eager)
c and every exit. o3 regions are not compiled here: a region entry is never
translated (the c loop sees it first), and the region interpreter runs it. */
static VM_TLS int vd;
+static VM_TLS int x64_noo3; /* ZYNOO3=1: leave o3 regions alone and run their vm code through the jit (what the microcore does) */
#define SL(k) (8 * (vd + (k)))
static void ldv(int r, int k) { mov_rm(r, rSP, SL(k)); }
static void stv(int k, int r) { mov_mr(rSP, SL(k), r); }
static void sp_keep(void) { if (vd) lea(rSP, rSP, -1, 1, 8 * vd); } /* r13 up to date; vd stays for the other path */
-static void sp_sync(void) { sp_keep(); vd = 0; }
static void exit_with(int v) { sp_keep(); mov_r32i(r_ax, (unsigned)v); jmp_to(x_exit); }
static void disp_imm(Cell t) { sp_keep(); mov_r32i(r_ax, (unsigned)(int)t); jmp_to(x_disp); } /* a constant target: the low 32 bits, like the interpreter's int pc */
static void disp_ax(void) { sp_keep(); jmp_to(x_disp); }
@@ -6015,7 +6017,7 @@ static void *translate(int p)
{
JW *start;
int i = p, n = 0, k, t, used[64], nused = 0, j2, ju;
- Cell w, x;
+ Cell w;
if (cp - code > CODEW * 4 - (1 << 19) || !ix_room()) flush();
start = cp;
vd = 0;
@@ -6199,7 +6201,7 @@ static int vm_until(int pc, int stop_eq, int stop_ge, Rg *re, Cell *ps, Cell *pr
if (stop_eq >= 0 && pc == stop_eq && S[2] - (stack_control ? stack_get(SC_RETURN_LOW) : RS0) == return_depth) break;
if (stop_ge >= 0 && pc >= stop_ge) break;
if (re && pc >= re->lo && pc < re->hi && o3ent[pc] == re->idx + 1) break;
- if (o3ent[pc]) { /* an o3 region: the region interpreter runs it */
+ if (o3ent[pc] && !x64_noo3) { /* an o3 region: the region interpreter runs it */
Rg *R = rat(pc);
if (R) {
int s = (int)S[1], r = (int)S[2];
@@ -6239,6 +6241,7 @@ static int x64_boot(Cell sp, Cell rp, void *stop)
{
code = mmap(0, CODEW * 4, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (code == MAP_FAILED) { perror("mmap"); return 1; }
+ x64_noo3 = getenv("ZYNOO3") != 0;
jitwrite(1); build_stubs(); jitwrite(0); syncicache(code, cp);
S[0] = (long)m; S[1] = sp; S[2] = rp; S[3] = (long)tab; S[4] = (long)cov;
S[5] = (long)Q; S[6] = (long)G; S[7] = (long)&J; S[8] = (long)hook;
diff --git a/tests/bench/bench.sh b/tests/bench/bench.sh
index 65f455e..4ef2ca5 100644
--- a/tests/bench/bench.sh
+++ b/tests/bench/bench.sh
@@ -4,8 +4,12 @@
cd "$(dirname "$0")/../.."; T=$(mktemp -d); trap 'rm -rf $T' EXIT
CC=${CC:-$(sh src/tools/findcc.sh)} || exit 1
make -s build/core/genome.bin || exit 1
-$CC -O2 -w -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core/map --embed-dir=build/core -o $T/zy
+$CC -O2 -w -DJTYPE=int -DZYINTERP src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core/map --embed-dir=build/core -o $T/zy
vms=("$T/zy"); names=("interp")
+# the host's own jit, where there is one (x86-64 linux: blocks are translated, o3 regions run on the region interpreter)
+if [ "$(uname -s)" = Linux ] && [ "$(uname -m)" = x86_64 ]; then
+ $CC -O2 -w -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core/map --embed-dir=build/core -o $T/zyn && { vms+=("$T/zyn"); names+=("x86-64 jit"); }
+fi
if command -v llvm-config >/dev/null; then
$CC -O2 -w -DJTYPE=int -DZYLLVM $(llvm-config --cflags) src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core/map --embed-dir=build/core -o $T/zyl $(llvm-config --ldflags --libs) && { vms+=("$T/zyl"); names+=("llvm"); }
fi
diff --git a/tests/test.sh b/tests/test.sh
index 709d09c..809da6e 100755
--- a/tests/test.sh
+++ b/tests/test.sh
@@ -50,9 +50,10 @@ ROOT=$T
CC=${CC:-$(sh src/tools/findcc.sh)} || exit 1
export CC
-# the language server runs on the interpreter unless the host has the arm64 jit, and a
-# check of a large document then takes many seconds: stretch the drivers' deadlines
-if [ -z "${ZC_TEST_TIMEOUT_SCALE:-}" ]; then case "$(uname -m)" in arm64|aarch64) ZC_TEST_TIMEOUT_SCALE=1;; *) ZC_TEST_TIMEOUT_SCALE=8;; esac; fi
+# the language server runs on the interpreter unless the host has a jit (arm64, or x86-64
+# linux), and a check of a large document then takes many seconds: stretch the drivers'
+# deadlines. the x86-64 jit gets a little room, since it does not compile o3 regions
+if [ -z "${ZC_TEST_TIMEOUT_SCALE:-}" ]; then case "$(uname -s)-$(uname -m)" in *-arm64|*-aarch64) ZC_TEST_TIMEOUT_SCALE=1;; Linux-x86_64) ZC_TEST_TIMEOUT_SCALE=2;; *) ZC_TEST_TIMEOUT_SCALE=8;; esac; fi
export ZC_TEST_TIMEOUT_SCALE
# the default map, generated as make does it, and the tools built against it
$CC -O2 -w -fwrapv src/tools/genmap.c -o $T/genmap || exit 1